From 780084a36ca840c35b3dbe2fa0b44ab0fa2cd247 Mon Sep 17 00:00:00 2001
From: Gareth George
Date: Wed, 27 Nov 2024 17:53:28 -0800
Subject: [PATCH 1/6] typesystem enforces paths exist in protos
---
webui/src/components/form/TypedForm.tsx | 154 ++++++++++++++++++++++++
1 file changed, 154 insertions(+)
create mode 100644 webui/src/components/form/TypedForm.tsx
diff --git a/webui/src/components/form/TypedForm.tsx b/webui/src/components/form/TypedForm.tsx
new file mode 100644
index 00000000..613aa73c
--- /dev/null
+++ b/webui/src/components/form/TypedForm.tsx
@@ -0,0 +1,154 @@
+import React, { useContext, useEffect, useMemo } from "react";
+import { Form } from "antd";
+import { clone, create, DescMessage, Message, MessageShape } from "@bufbuild/protobuf";
+import { RepoSchema } from "../../../gen/ts/v1/config_pb";
+
+type Paths = T extends object
+ ? {
+ [K in keyof T]: `${Exclude}${"" | `.${Paths}`}`;
+ }[keyof T]
+ : never;
+
+type DeepIndex = T extends object
+ ? K extends `${infer F}.${infer R}`
+ ? DeepIndex, R>
+ : Idx
+ : never;
+
+type Idx = K extends keyof T ? T[K] : never;
+
+const getValue = >(
+ obj: T,
+ path: K
+): DeepIndex => {
+ let curr: any = obj;
+ const parts = path.split(".");
+ for (let i = 0; i < parts.length; i++) {
+ curr = curr[parts[i]];
+ }
+ return curr as DeepIndex;
+};
+
+const setValue = >(
+ obj: T,
+ path: K,
+ value: DeepIndex
+): void => {
+ let curr: any = obj;
+ const parts = path.split(".");
+ for (let i = 0; i < parts.length - 1; i++) {
+ curr = curr[parts[i]];
+ }
+ curr[parts[parts.length - 1]] = value;
+};
+
+interface typedFormCtx {
+ formData: any;
+ setFormData: (value: any) => any;
+}
+
+const TypedFormCtx = React.createContext({
+ formData: {},
+ setFormData: () => {},
+});
+
+export const TypedForm = ({
+ schema,
+ initialValue,
+ props,
+ children,
+ onChange,
+}: {
+ props: any;
+ schema: Desc;
+ initialValue: MessageShape;
+ children: React.ReactNode;
+ onChange?: (value: MessageShape) => void;
+}) => {
+ const [form] = Form.useForm();
+ const [formData, setFormData] =
+ React.useState>(initialValue);
+
+ return (
+
+
+
+ );
+};
+
+export const TypedFormItem = ({
+ name,
+ children,
+ ...props,
+}: {
+ name: Paths;
+ children: React.ReactNode;
+ props: any;
+}): React.ReactElement => {
+ const { formData } = useContext(TypedFormCtx);
+
+ return (
+
+ {children}
+
+ );
+};
+
+export const TypedFormOneof = ({
+ name,
+ children,
+ ...props,
+}: {
+ name: Paths;
+ children: React.ReactNode;
+ props: any;
+}): React.ReactElement => {
+ return <>>;
+};
+
+const const TypedFormOneofCase = ({
+ name,
+ children,
+ ...props,
+}: {
+ name: Paths;
+ children: React.ReactNode;
+ props: any;
+}): React.ReactElement => {
+ const { formData, setFormData } = useContext(TypedFormCtx);
+
+ return <>>
+}
+
+const TestElement = () => {
+ return (
+
+
+
+
+
+
+
+
+ );
+}
\ No newline at end of file
From 526b26e17ceeb43982e177991622513b97452bfc Mon Sep 17 00:00:00 2001
From: Gareth George
Date: Wed, 27 Nov 2024 23:31:49 -0800
Subject: [PATCH 2/6] basically working type constraints
---
webui/src/components/form/TypedForm.tsx | 113 ++++++++++++------------
1 file changed, 58 insertions(+), 55 deletions(-)
diff --git a/webui/src/components/form/TypedForm.tsx b/webui/src/components/form/TypedForm.tsx
index 613aa73c..9b1899b0 100644
--- a/webui/src/components/form/TypedForm.tsx
+++ b/webui/src/components/form/TypedForm.tsx
@@ -1,15 +1,28 @@
import React, { useContext, useEffect, useMemo } from "react";
import { Form } from "antd";
-import { clone, create, DescMessage, Message, MessageShape } from "@bufbuild/protobuf";
-import { RepoSchema } from "../../../gen/ts/v1/config_pb";
+import {
+ clone,
+ create,
+ DescMessage,
+ Message,
+ MessageShape,
+} from "@bufbuild/protobuf";
+import {
+ Plan,
+ PlanSchema,
+ Repo,
+ RepoSchema,
+} from "../../../gen/ts/v1/config_pb";
-type Paths = T extends object
+export type Paths = T extends object
? {
- [K in keyof T]: `${Exclude}${"" | `.${Paths}`}`;
- }[keyof T]
+ [K in Exclude]: `${K}${Paths extends never
+ ? ""
+ : `.${Paths}`}`;
+ }[Exclude]
: never;
-type DeepIndex = T extends object
+export type DeepIndex = T extends object
? K extends `${infer F}.${infer R}`
? DeepIndex, R>
: Idx
@@ -17,7 +30,7 @@ type DeepIndex = T extends object
type Idx = K extends keyof T ? T[K] : never;
-const getValue = >(
+const getValue = (
obj: T,
path: K
): DeepIndex => {
@@ -55,15 +68,15 @@ const TypedFormCtx = React.createContext({
export const TypedForm = ({
schema,
initialValue,
- props,
children,
onChange,
+ ...props
}: {
- props: any;
schema: Desc;
initialValue: MessageShape;
- children: React.ReactNode;
+ children?: React.ReactNode;
onChange?: (value: MessageShape) => void;
+ props?: any;
}) => {
const [form] = Form.useForm();
const [formData, setFormData] =
@@ -92,63 +105,53 @@ export const TypedForm = ({
);
};
-export const TypedFormItem = ({
- name,
+export const TypedFormItem = ({
+ field,
children,
- ...props,
}: {
- name: Paths;
- children: React.ReactNode;
- props: any;
+ field: Paths;
+ children?: React.ReactNode;
}): React.ReactElement => {
const { formData } = useContext(TypedFormCtx);
return (
-
+
{children}
);
};
-export const TypedFormOneof = ({
- name,
- children,
- ...props,
+export const TypedFormOneof = >({
+ field,
+ items,
}: {
- name: Paths;
- children: React.ReactNode;
- props: any;
-}): React.ReactElement => {
- return <>>;
+ field: K;
+ items: { [K2 in DeepIndex]: React.ReactNode };
+}): React.ReactNode => {
+ const { formData } = useContext(TypedFormCtx);
+ const c = getValue(formData as T, field);
+ const val = items[c];
+ if (!val) {
+ return <>>;
+ }
+ return val;
};
-const const TypedFormOneofCase = ({
- name,
- children,
- ...props,
-}: {
- name: Paths;
- children: React.ReactNode;
- props: any;
-}): React.ReactElement => {
- const { formData, setFormData } = useContext(TypedFormCtx);
-
- return <>>
-}
+// const TestElement = () => {
+// const a = create(PlanSchema, {});
+// const b: Paths = "retention.policy.case";
-const TestElement = () => {
- return (
-
-
-
-
-
-
-
-
- );
-}
\ No newline at end of file
+// return (
+//
+// field={"id"}>
+//
+// field="retention.policy.case"
+// items={{
+// policyKeepAll: <>>,
+// policyKeepLastN: <>>,
+// policyTimeBucketed: <>>,
+// }}
+// />
+//
+// );
+// };
From f36a7994107c9ca12f458dd4a65aa6a5cf04dc1a Mon Sep 17 00:00:00 2001
From: Gareth George
Date: Thu, 28 Nov 2024 00:00:10 -0800
Subject: [PATCH 3/6] start migrating forms
---
webui/src/components/form/TypedForm.tsx | 30 +++++++------------------
webui/src/views/AddRepoModal.tsx | 10 ++++-----
2 files changed, 13 insertions(+), 27 deletions(-)
diff --git a/webui/src/components/form/TypedForm.tsx b/webui/src/components/form/TypedForm.tsx
index 9b1899b0..1c045a5f 100644
--- a/webui/src/components/form/TypedForm.tsx
+++ b/webui/src/components/form/TypedForm.tsx
@@ -68,15 +68,14 @@ const TypedFormCtx = React.createContext({
export const TypedForm = ({
schema,
initialValue,
- children,
onChange,
+ children,
...props
}: {
schema: Desc;
initialValue: MessageShape;
children?: React.ReactNode;
onChange?: (value: MessageShape) => void;
- props?: any;
}) => {
const [form] = Form.useForm();
const [formData, setFormData] =
@@ -108,14 +107,20 @@ export const TypedForm = ({
export const TypedFormItem = ({
field,
children,
+ ...props
}: {
field: Paths;
children?: React.ReactNode;
+ props?: any;
}): React.ReactElement => {
const { formData } = useContext(TypedFormCtx);
return (
-
+
{children}
);
@@ -136,22 +141,3 @@ export const TypedFormOneof = >({
}
return val;
};
-
-// const TestElement = () => {
-// const a = create(PlanSchema, {});
-// const b: Paths = "retention.policy.case";
-
-// return (
-//
-// field={"id"}>
-//
-// field="retention.policy.case"
-// items={{
-// policyKeepAll: <>>,
-// policyKeepLastN: <>>,
-// policyTimeBucketed: <>>,
-// }}
-// />
-//
-// );
-// };
diff --git a/webui/src/views/AddRepoModal.tsx b/webui/src/views/AddRepoModal.tsx
index 1cfb7198..83b1d3af 100644
--- a/webui/src/views/AddRepoModal.tsx
+++ b/webui/src/views/AddRepoModal.tsx
@@ -37,13 +37,13 @@ import {
} from "../components/HooksFormList";
import { ConfirmButton } from "../components/SpinButton";
import { useConfig } from "../components/ConfigProvider";
-import Cron from "react-js-cron";
import {
ScheduleDefaultsInfrequent,
ScheduleFormItem,
} from "../components/ScheduleFormItem";
import { isWindows } from "../state/buildcfg";
import { create, fromJson, toJson } from "@bufbuild/protobuf";
+import { TypedForm, TypedFormItem } from "../components/form/TypedForm";
const repoDefaults = create(RepoSchema, {
prunePolicy: {
@@ -221,9 +221,10 @@ export const AddRepoModal = ({ template }: { template: Repo | null }) => {
for more details about repositories.
-
+
hasFeedback
- name="id"
label="Repo Name"
validateTrigger={["onChange", "onBlur"]}
rules={[
From 8f7fae85f46cd9f42c79425f4287c3caedabb13c Mon Sep 17 00:00:00 2001
From: Gareth George
Date: Fri, 29 Nov 2024 15:54:57 -0800
Subject: [PATCH 4/6] more work on typed forms
---
webui/src/components/form/TypedForm.tsx | 86 +++++++++++++++----------
webui/src/views/AddRepoModal.tsx | 1 +
2 files changed, 53 insertions(+), 34 deletions(-)
diff --git a/webui/src/components/form/TypedForm.tsx b/webui/src/components/form/TypedForm.tsx
index 1c045a5f..46122855 100644
--- a/webui/src/components/form/TypedForm.tsx
+++ b/webui/src/components/form/TypedForm.tsx
@@ -7,12 +7,7 @@ import {
Message,
MessageShape,
} from "@bufbuild/protobuf";
-import {
- Plan,
- PlanSchema,
- Repo,
- RepoSchema,
-} from "../../../gen/ts/v1/config_pb";
+import { useWatch } from "antd/es/form/Form";
export type Paths = T extends object
? {
@@ -56,14 +51,12 @@ const setValue = >(
};
interface typedFormCtx {
- formData: any;
+ formData: Message;
+ schema: DescMessage;
setFormData: (value: any) => any;
}
-const TypedFormCtx = React.createContext({
- formData: {},
- setFormData: () => {},
-});
+const TypedFormCtx = React.createContext(null);
export const TypedForm = ({
schema,
@@ -76,26 +69,18 @@ export const TypedForm = ({
initialValue: MessageShape;
children?: React.ReactNode;
onChange?: (value: MessageShape) => void;
+} & {
+ [key: string]: any;
}) => {
const [form] = Form.useForm();
const [formData, setFormData] =
React.useState>(initialValue);
return (
-
+
{/* Repo.uri */}
@@ -292,9 +302,9 @@ export const AddRepoModal = ({ template }: { template: Repo | null }) => {
>
}
>
-
+
hasFeedback
- name="uri"
+ field="uri"
label="Repository URI"
validateTrigger={["onChange", "onBlur"]}
rules={[
@@ -305,7 +315,7 @@ export const AddRepoModal = ({ template }: { template: Repo | null }) => {
]}
>
-
+
{/* Repo.password */}
@@ -334,13 +344,13 @@ export const AddRepoModal = ({ template }: { template: Repo | null }) => {
-
+
hasFeedback
- name="password"
+ field="password"
validateTrigger={["onChange", "onBlur"]}
>
-
+
{
{/* Repo.env */}
- {
)}
-
+ */}
{/* Repo.flags */}
-
+ {/*
{(fields, { add, remove }, { errors }) => (
<>
@@ -472,10 +482,10 @@ export const AddRepoModal = ({ template }: { template: Repo | null }) => {
>
)}
-
+ */}
{/* Repo.prunePolicy */}
- {
name={["prunePolicy", "schedule"]}
defaults={ScheduleDefaultsInfrequent}
/>
-
+ */}
{/* Repo.checkPolicy */}
- {
name={["checkPolicy", "schedule"]}
defaults={ScheduleDefaultsInfrequent}
/>
-
+ */}
{/* Repo.commandPrefix */}
- {!isWindows && (
+ {/* {!isWindows && (
{
- )}
+ )} */}
-
label={
{
Auto Unlock
}
- name="autoUnlock"
+ field="autoUnlock"
valuePropName="checked"
>
-
+
Hooks}
@@ -688,7 +698,7 @@ export const AddRepoModal = ({ template }: { template: Repo | null }) => {
children: (
- {JSON.stringify(form.getFieldsValue(), undefined, 2)}
+ {toJsonString(RepoSchema, repo, { prettySpaces: 2 })}
),
@@ -697,7 +707,7 @@ export const AddRepoModal = ({ template }: { template: Repo | null }) => {
/>
)}
-
+
>
);
From b026aaaeb51997bc7904d3bd86e81ddb4ec88fae Mon Sep 17 00:00:00 2001
From: Gareth George
Date: Thu, 5 Dec 2024 23:22:14 -0800
Subject: [PATCH 6/6] stash progress
---
webui/src/components/form/TypedForm.tsx | 156 +++++++++++-------------
webui/src/views/AddRepoModal.tsx | 25 ++--
2 files changed, 83 insertions(+), 98 deletions(-)
diff --git a/webui/src/components/form/TypedForm.tsx b/webui/src/components/form/TypedForm.tsx
index 295f810e..2e505e9c 100644
--- a/webui/src/components/form/TypedForm.tsx
+++ b/webui/src/components/form/TypedForm.tsx
@@ -1,4 +1,4 @@
-import React, { useContext, useEffect, useMemo } from "react";
+import React, { useContext, useEffect, useMemo, useState } from "react";
import { Form } from "antd";
import {
clone,
@@ -8,6 +8,7 @@ import {
MessageShape,
} from "@bufbuild/protobuf";
import { useWatch } from "antd/es/form/Form";
+import useFormInstance from "antd/es/form/hooks/useFormInstance";
export type Paths = T extends object
? {
@@ -25,27 +26,28 @@ export type DeepIndex = T extends object
type Idx = K extends keyof T ? T[K] : never;
-const getValue = (
- obj: T,
- path: K
-): DeepIndex => {
+const getValue = (obj: any, path: string): any => {
let curr: any = obj;
const parts = path.split(".");
for (let i = 0; i < parts.length; i++) {
- curr = curr[parts[i]];
+ if (Array.isArray(curr)) {
+ curr = curr[parseInt(parts[i])];
+ } else {
+ curr = curr[parts[i]];
+ }
}
- return curr as DeepIndex;
+ return curr as any;
};
-const setValue = >(
- obj: T,
- path: K,
- value: DeepIndex
-): void => {
+const setValue = (obj: any, path: string, value: any): void => {
let curr: any = obj;
const parts = path.split(".");
for (let i = 0; i < parts.length - 1; i++) {
- curr = curr[parts[i]];
+ if (Array.isArray(curr)) {
+ curr = curr[parseInt(parts[i])];
+ } else {
+ curr = curr[parts[i]];
+ }
}
curr[parts[parts.length - 1]] = value;
};
@@ -53,42 +55,12 @@ const setValue = >(
interface typedFormCtx {
formData: Message;
schema: DescMessage;
-<<<<<<< Updated upstream
- setFormData: (value: any) => any;
-=======
setFormData: (fn: (prev: any) => any) => void;
->>>>>>> Stashed changes
}
const TypedFormCtx = React.createContext(null);
+const FieldPrefixCtx = React.createContext("");
-<<<<<<< Updated upstream
-export const TypedForm = ({
- schema,
- initialValue,
- onChange,
- children,
- ...props
-}: {
- schema: Desc;
- initialValue: MessageShape;
- children?: React.ReactNode;
- onChange?: (value: MessageShape) => void;
-} & {
- [key: string]: any;
-}) => {
- const [form] = Form.useForm();
- const [formData, setFormData] =
- React.useState>(initialValue);
-
- return (
-
-