Skip to content

Commit

Permalink
Setup framework for new form fields
Browse files Browse the repository at this point in the history
  • Loading branch information
garronej committed Sep 20, 2024
1 parent b7a04c3 commit 1876782
Show file tree
Hide file tree
Showing 14 changed files with 279 additions and 0 deletions.
5 changes: 5 additions & 0 deletions web/src/ui/i18n/resources/de.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -672,6 +672,11 @@ Fühlen Sie sich frei, Ihre Kubernetes-Bereitstellungen zu erkunden und die Kont
</>
)
},
"YamlCodeBlockFormField": {
"not an array": "Ein Array wird erwartet",
"not an object": "Ein Objekt wird erwartet",
"not valid yaml": "Ungültiges YAML/JSON"
},
"NoLongerBookmarkedDialog": {
"no longer bookmarked dialog title": "Nicht gespeicherte Änderungen",
"no longer bookmarked dialog body":
Expand Down
5 changes: 5 additions & 0 deletions web/src/ui/i18n/resources/en.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -660,6 +660,11 @@ Feel free to explore and take charge of your Kubernetes deployments!
"Click on the bookmark icon again to update your saved configuration",
"ok": "Ok"
},
"YamlCodeBlockFormField": {
"not an array": "An array is expected",
"not an object": "An object is expected",
"not valid yaml": "Invalid YAML/JSON"
},
"MyService": {
"page title": ({ helmReleaseFriendlyName }) =>
`${helmReleaseFriendlyName} Monitoring`
Expand Down
7 changes: 7 additions & 0 deletions web/src/ui/i18n/resources/es.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { capitalize } from "tsafe/capitalize";
import { MaybeLink } from "ui/shared/MaybeLink";

export const translations: Translations<"en"> = {
/* spell-checker: disable */
"Account": {
"infos": "Información de la cuenta",
"git": "Git",
Expand Down Expand Up @@ -666,6 +667,11 @@ export const translations: Translations<"en"> = {
),
"ok": "Ok"
},
"YamlCodeBlockFormField": {
"not an array": "Se espera un arreglo",
"not an object": "Se espera un objeto",
"not valid yaml": "YAML/JSON no válido"
},
"NoLongerBookmarkedDialog": {
"no longer bookmarked dialog title": "Tus cambios no se guardarán",
"no longer bookmarked dialog body":
Expand Down Expand Up @@ -1007,4 +1013,5 @@ export const translations: Translations<"en"> = {
"copied to clipboard": "¡Copiado!",
"copy to clipboard": "Copiar al portapapeles"
}
/* spell-checker: enable */
};
5 changes: 5 additions & 0 deletions web/src/ui/i18n/resources/fi.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -657,6 +657,11 @@ Tutustu vapaasti ja ota hallintaan Kubernetes-julkaisusi!
),
"ok": "Ok"
},
"YamlCodeBlockFormField": {
"not an array": "Taulukkoa odotetaan",
"not an object": "Oliota odotetaan",
"not valid yaml": "Virheellinen YAML/JSON"
},
"NoLongerBookmarkedDialog": {
"no longer bookmarked dialog title": "Muutokset eivät tallennu",
"no longer bookmarked dialog body":
Expand Down
5 changes: 5 additions & 0 deletions web/src/ui/i18n/resources/fr.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -672,6 +672,11 @@ N'hésitez pas à explorer et à prendre en main vos déploiements Kubernetes !
),
"ok": "Ok"
},
"YamlCodeBlockFormField": {
"not an array": "Un tableau est attendu",
"not an object": "Un objet est attendu",
"not valid yaml": "YAML/JSON invalide"
},
"NoLongerBookmarkedDialog": {
"no longer bookmarked dialog title": "Changements non enregistrés",
"no longer bookmarked dialog body":
Expand Down
5 changes: 5 additions & 0 deletions web/src/ui/i18n/resources/it.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -666,6 +666,11 @@ Sentiti libero di esplorare e prendere il controllo dei tuoi deployment Kubernet
</>
)
},
"YamlCodeBlockFormField": {
"not an array": "È previsto un array",
"not an object": "È previsto un oggetto",
"not valid yaml": "YAML/JSON non valido"
},
"NoLongerBookmarkedDialog": {
"no longer bookmarked dialog title": "Modifiche non salvate",
"no longer bookmarked dialog body":
Expand Down
5 changes: 5 additions & 0 deletions web/src/ui/i18n/resources/nl.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -668,6 +668,11 @@ Voel je vrij om te verkennen en de controle over je Kubernetes-implementaties te
</>
)
},
"YamlCodeBlockFormField": {
"not an array": "Een array wordt verwacht",
"not an object": "Een object wordt verwacht",
"not valid yaml": "Ongeldige YAML/JSON"
},
"NoLongerBookmarkedDialog": {
"no longer bookmarked dialog title": "Niet opgeslagen wijzigingen",
"no longer bookmarked dialog body":
Expand Down
5 changes: 5 additions & 0 deletions web/src/ui/i18n/resources/no.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -667,6 +667,11 @@ Utforsk gjerne og ta kontroll over tjenestene du kjører på Kubernetes!
</>
)
},
"YamlCodeBlockFormField": {
"not an array": "En matrise forventes",
"not an object": "Et objekt forventes",
"not valid yaml": "Ugyldig YAML/JSON"
},
"NoLongerBookmarkedDialog": {
"no longer bookmarked dialog title": "Endringene dine vil ikke bli lagret",
"no longer bookmarked dialog body":
Expand Down
5 changes: 5 additions & 0 deletions web/src/ui/i18n/resources/zh-CN.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -618,6 +618,11 @@ ${
</>
)
},
"YamlCodeBlockFormField": {
"not an array": "需要是数组",
"not an object": "需要是对象",
"not valid yaml": "无效的 YAML/JSON"
},
"NoLongerBookmarkedDialog": {
"no longer bookmarked dialog title": "更改未保存",
"no longer bookmarked dialog body": "再次单击书签符号以更新您保存的配置.",
Expand Down
1 change: 1 addition & 0 deletions web/src/ui/i18n/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ export type ComponentKey =
| import("ui/pages/launcher/LauncherDialogs/AcknowledgeSharingOfConfigConfirmDialog").I18n
| import("ui/pages/launcher/LauncherDialogs/AutoLaunchDisabledDialog").I18n
| import("ui/pages/launcher/LauncherDialogs/NoLongerBookmarkedDialog").I18n
| import("ui/pages/launcher/formFields/YamlCodeBlockFormField").I18n
| import("ui/pages/myService/MyService").I18n
| import("ui/pages/myService/PodLogsTab").I18n
| import("ui/pages/myService/MyServiceButtonBar").I18n
Expand Down
37 changes: 37 additions & 0 deletions web/src/ui/pages/launcher/formFields/FormFieldWrapper.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { tss } from "tss";

type Props = {
className?: string;
title: string;
description: string | undefined;
onResetToDefault: () => void;
error: JSX.Element | string | undefined;
children: JSX.Element;
};

export function FormFieldWrapper(props: Props) {
const { className, title, description, onResetToDefault, error, children } = props;

const { cx, classes } = useStyles({ "isErrored": error !== undefined });

return (
<div className={cx(classes.root, className)}>
<h3 lang="und">{title}</h3>
<button onClick={onResetToDefault}>Reset to default</button>
{description !== undefined && <p lang="und">{description}</p>}
{children}
{error !== undefined && error}
</div>
);
}

const useStyles = tss
.withName({ FormFieldWrapper })
.withParams<{ isErrored: boolean }>()
.create(({ theme, isErrored }) => ({
"root": {
"color": !isErrored
? undefined
: theme.colors.useCases.alertSeverity.error.main
}
}));
25 changes: 25 additions & 0 deletions web/src/ui/pages/launcher/formFields/YamlCodeBlock.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import type { Meta, StoryObj } from "@storybook/react";
import { YamlCodeBlockFormField } from "./YamlCodeBlockFormField";
import { action } from "@storybook/addon-actions";

const meta = {
title: "pages/Launcher/formFields/YamlCodeBlock",
component: YamlCodeBlockFormField
} satisfies Meta<typeof YamlCodeBlockFormField>;

export default meta;

type Story = StoryObj<typeof meta>;

export const Default: Story = {
args: {
"title": "This is the title",
"description": "This is the description",
"expectedDataType": "object",
"value": {
"key1": "value1",
"key2": "value2"
},
"onChange": action("Value changed")
}
};
99 changes: 99 additions & 0 deletions web/src/ui/pages/launcher/formFields/YamlCodeBlockFormField.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import { memo, Suspense } from "react";
import type { Stringifyable } from "core/tools/Stringifyable";
import { FormFieldWrapper } from "./FormFieldWrapper";
import { tss } from "tss";
import { useFormField } from "./useFormField";
import YAML from "yaml";
import { declareComponentKeys, useTranslation } from "ui/i18n";

type Props = {
className?: string;
title: string;
description: string | undefined;
expectedDataType: "object" | "array";
value: Record<string, Stringifyable> | Stringifyable[];
onChange: (newValue: Record<string, Stringifyable> | Stringifyable[]) => void;
};

export const YamlCodeBlockFormField = memo((props: Props) => {
const { className, title, description, expectedDataType, value, onChange } = props;

const { t } = useTranslation({ YamlCodeBlockFormField });

const { cx, classes } = useStyles();

const { serializedValue, setSerializedValue, errorMessageKey, resetToDefault } =
useFormField<
Record<string, Stringifyable> | Stringifyable[],
string,
"not valid yaml" | "not an array" | "not an object"
>({
"serializedValue": JSON.stringify(value),
onChange,
"parse": serializedValue => {
let value: Record<string, Stringifyable> | Stringifyable[];

try {
value = YAML.parse(serializedValue);
} catch {
return {
"isValid": false,
"errorMessageKey": "not valid yaml"
};
}

switch (expectedDataType) {
case "array":
if (!(value instanceof Array)) {
return {
"isValid": false,
"errorMessageKey": "not an array"
};
}
break;
case "object":
if (!(value instanceof Object) || value instanceof Array) {
return {
"isValid": false,
"errorMessageKey": "not an object"
};
}
break;
}

return {
"isValid": true,
value
};
}
});

return (
<FormFieldWrapper
className={cx(classes.root, className)}
title={title}
description={description}
error={errorMessageKey === undefined ? undefined : t(errorMessageKey)}
onResetToDefault={resetToDefault}
>
<Suspense fallback={null}>
<input
className={cx(classes.input)}
value={serializedValue}
onChange={e => setSerializedValue(e.target.value)}
/>
</Suspense>
</FormFieldWrapper>
);
});

const useStyles = tss.withName({ YamlCodeBlockFormField }).create({
"root": {},
"input": {}
});

const { i18n } = declareComponentKeys<
"not valid yaml" | "not an array" | "not an object"
>()({ YamlCodeBlockFormField });

export type I18n = typeof i18n;
70 changes: 70 additions & 0 deletions web/src/ui/pages/launcher/formFields/useFormField.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { useEffect, useState } from "react";
import { useConstCallback } from "powerhooks/useConstCallback";
import { useConst } from "powerhooks/useConst";
import { waitForDebounceFactory } from "powerhooks/tools/waitForDebounce";

export function useFormField<
TValue,
TSerializedValue extends string | number | boolean,
ErrorMessageKey extends string
>(params: {
serializedValue: TSerializedValue;
onChange: (newValue: TValue) => void;
parse: (
serializedValue: TSerializedValue
) =>
| { isValid: true; value: TValue }
| { isValid: false; errorMessageKey: ErrorMessageKey };
}): {
serializedValue: TSerializedValue;
setSerializedValue: (newValue: TSerializedValue) => void;
errorMessageKey: ErrorMessageKey | undefined;
resetToDefault: () => void;
} {
const { serializedValue: serializedValue_params, onChange, parse } = params;

const serializedValue_default = useConst(() => serializedValue_params);

const [serializedValue, setSerializedValue] = useState(serializedValue_params);
const [errorMessageKey, setErrorMessageKey] = useState<ErrorMessageKey | undefined>(
undefined
);

useEffect(() => {
setSerializedValue(serializedValue_params);
}, [serializedValue_params]);

const onChange_const = useConstCallback(onChange);

const onChangeWithDebounce = useConst(() => {
const { waitForDebounce } = waitForDebounceFactory({ "delay": 500 });

async function onChangeWithDebounce(newValue: TValue) {
await waitForDebounce();

onChange_const(newValue);
}

return onChangeWithDebounce;
});

useEffect(() => {
const resultOfParse = parse(serializedValue);

if (!resultOfParse.isValid) {
setErrorMessageKey(resultOfParse.errorMessageKey);
return;
}

setErrorMessageKey(undefined);

onChangeWithDebounce(resultOfParse.value);
}, [serializedValue]);

return {
serializedValue,
setSerializedValue,
errorMessageKey,
"resetToDefault": () => setSerializedValue(serializedValue_default)
};
}

0 comments on commit 1876782

Please sign in to comment.