Skip to content

Commit

Permalink
Add YamlCodeBlockFormField
Browse files Browse the repository at this point in the history
  • Loading branch information
garronej committed Sep 21, 2024
1 parent dd57886 commit a97dfe4
Show file tree
Hide file tree
Showing 8 changed files with 371 additions and 31 deletions.
14 changes: 14 additions & 0 deletions web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,20 +22,34 @@
"@aws-sdk/client-s3": "^3.513.0",
"@aws-sdk/lib-storage": "^3.513.0",
"@aws-sdk/s3-request-presigner": "^3.513.0",
"@babel/runtime": "7.25.6",
"@codemirror/autocomplete": "6.18.1",
"@codemirror/lang-yaml": "6.1.1",
"@codemirror/language": "6.10.3",
"@codemirror/lint": "6.8.1",
"@codemirror/search": "6.5.6",
"@codemirror/state": "6.4.1",
"@codemirror/theme-one-dark": "6.1.2",
"@codemirror/view": "6.33.0",
"@duckdb/duckdb-wasm": "^1.13.1-dev230.0",
"@duckdb/duckdb-wasm-shell": "^1.13.1-dev230.0",
"@emotion/cache": "^11.11.0",
"@emotion/react": "^11.11.3",
"@emotion/styled": "^11.11.0",
"@lezer/common": "1.2.1",
"@lezer/highlight": "1.2.1",
"@mui/icons-material": "5.14.15",
"@mui/material": "^5.15.10",
"@mui/system": "^5.15.9",
"@mui/x-data-grid": "^6.19.4",
"@uiw/codemirror-themes": "4.23.2",
"@uiw/react-codemirror": "4.23.2",
"@ungap/structured-clone": "^1.2.0",
"async-mutex": "^0.4.0",
"axios": "^0.26.0",
"bytes": "^3.1.2",
"clean-architecture": "^4.3.8",
"codemirror": "6.0.1",
"color": "^4.2.3",
"compare-versions": "^5.0.1",
"evt": "^2.5.7",
Expand Down
2 changes: 1 addition & 1 deletion web/src/ui/i18n/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +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/launcher/formFields/YamlCodeBlockFormField/YamlCodeBlockFormField").I18n
| import("ui/pages/myService/MyService").I18n
| import("ui/pages/myService/PodLogsTab").I18n
| import("ui/pages/myService/MyServiceButtonBar").I18n
Expand Down
56 changes: 43 additions & 13 deletions web/src/ui/pages/launcher/formFields/YamlCodeBlock.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,25 +1,55 @@
import type { Meta, StoryObj } from "@storybook/react";
import { YamlCodeBlockFormField } from "./YamlCodeBlockFormField";
import { action } from "@storybook/addon-actions";
import { css } from "ui/theme";
import { useState } from "react";
import type { Stringifyable } from "core/tools/Stringifyable";

const meta = {
title: "pages/Launcher/formFields/YamlCodeBlock",
component: YamlCodeBlockFormField
component: StoryWrapper
} 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")
}
};
const onChangeAction = action("onChange");

function StoryWrapper() {
const [value, setValue] = useState<Stringifyable[] | Record<string, Stringifyable>>({
"key1": "value1",
"key2": 42,
"arr": ["a", "b", "c"],
"obj": {
"isSomething": true,
"name": "John",
"nested": {
"a": 1,
"b": 2
}
}
});

return (
<>
<YamlCodeBlockFormField
className={css({ "width": 400, "maxHeight": 400 })}
title="This is the title"
description="This is the description"
expectedDataType="object"
value={value}
onChange={newValue => {
onChangeAction(newValue);
setValue(newValue);
}}
/>
<br />
<br />
<p>Value: </p>
<pre>{JSON.stringify(value, null, 4)}</pre>
</>
);
}

export const Default: Story = {};
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { memo, Suspense } from "react";
import { memo, Suspense, lazy } from "react";
import type { Stringifyable } from "core/tools/Stringifyable";
import { FormFieldWrapper } from "./shared/FormFieldWrapper";
import { FormFieldWrapper } from "../shared/FormFieldWrapper";
import { tss } from "tss";
import { useFormField } from "./shared/useFormField";
import { useFormField } from "../shared/useFormField";
import YAML from "yaml";
import { declareComponentKeys, useTranslation } from "ui/i18n";
import { CircularProgress } from "onyxia-ui/CircularProgress";
const YamlCodeEditor = lazy(() => import("./YamlCodeEditor"));

type Props = {
className?: string;
Expand All @@ -15,6 +17,8 @@ type Props = {
onChange: (newValue: Record<string, Stringifyable> | Stringifyable[]) => void;
};

const DEFAULT_HEIGHT = 300;

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

Expand All @@ -28,7 +32,7 @@ export const YamlCodeBlockFormField = memo((props: Props) => {
string,
"not valid yaml" | "not an array" | "not an object"
>({
"serializedValue": JSON.stringify(value),
"serializedValue": YAML.stringify(value),
onChange,
"parse": serializedValue => {
let value: Record<string, Stringifyable> | Stringifyable[];
Expand Down Expand Up @@ -76,11 +80,18 @@ export const YamlCodeBlockFormField = memo((props: Props) => {
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
fallback={
<div className={cx(classes.suspenseFallback)}>
<CircularProgress />
</div>
}
>
<YamlCodeEditor
className={classes.yamlCodeEditor}
yamlCode={serializedValue}
onYamlCodeChange={setSerializedValue}
defaultHeight={DEFAULT_HEIGHT}
/>
</Suspense>
</FormFieldWrapper>
Expand All @@ -89,7 +100,13 @@ export const YamlCodeBlockFormField = memo((props: Props) => {

const useStyles = tss.withName({ YamlCodeBlockFormField }).create({
"root": {},
"input": {}
"yamlCodeEditor": {},
"suspenseFallback": {
"display": "flex",
"justifyContent": "center",
"alignItems": "center",
"height": DEFAULT_HEIGHT
}
});

const { i18n } = declareComponentKeys<
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import CodeMirror from "@uiw/react-codemirror";
import { yaml } from "@codemirror/lang-yaml";
import { createTheme } from "@uiw/codemirror-themes";
import { tags } from "@lezer/highlight";
import { useMemo } from "react";
import { alpha } from "@mui/system";
import { tss } from "tss";

type Props = {
className?: string;
yamlCode: string;
onYamlCodeChange: (newCode: string) => void;
defaultHeight: number;
};

export default function YamlCodeEditor(props: Props) {
const { className, yamlCode, defaultHeight, onYamlCodeChange } = props;

const { cx, classes, theme } = useStyles();

const codeMirrorTheme = useMemo(
() =>
createTheme({
theme: theme.isDarkModeEnabled ? "dark" : "light",
settings: {
background: theme.colors.useCases.surfaces.surface1,
foreground: theme.colors.useCases.typography.textPrimary,
caret: theme.colors.useCases.typography.textFocus,
selection: alpha(theme.colors.useCases.typography.textFocus, 0.6),
selectionMatch: alpha(
theme.colors.useCases.typography.textFocus,
0.3
),
lineHighlight: alpha(
theme.colors.useCases.typography.textFocus,
0.05
),
gutterBackground: theme.colors.useCases.surfaces.surface2,
gutterForeground: theme.colors.useCases.typography.textTertiary
},
styles: [
{
tag: [tags.comment],
color: theme.colors.useCases.typography.textDisabled,
fontStyle: "italic"
},
{
tag: [
tags.name,
tags.deleted,
tags.character,
tags.propertyName,
tags.macroName
],
color: theme.colors.useCases.typography.textSecondary
},
{
tag: [tags.definition(tags.name), tags.separator],
color: theme.colors.useCases.typography.textTertiary
}
]
}),
[theme.isDarkModeEnabled]
);

return (
<CodeMirror
className={cx(classes.root, className)}
value={yamlCode}
theme={codeMirrorTheme}
height={`${defaultHeight}px`}
extensions={[yaml()]}
onChange={onYamlCodeChange}
/>
);
}

const useStyles = tss.withName({ YamlCodeEditor }).create(({ theme }) => ({
"root": {
"borderRadius": theme.spacing(1),
"overflow": "hidden"
}
}));
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./YamlCodeBlockFormField";
19 changes: 19 additions & 0 deletions web/src/ui/pages/launcher/formFields/shared/useFormField.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import { useEffect, useState } from "react";
import { useConstCallback } from "powerhooks/useConstCallback";
import { useConst } from "powerhooks/useConst";
import { waitForDebounceFactory } from "powerhooks/tools/waitForDebounce";
import { assert } from "tsafe/assert";
import { same } from "evt/tools/inDepth/same";

export function useFormField<
TValue,
Expand Down Expand Up @@ -31,6 +33,23 @@ export function useFormField<
);

useEffect(() => {
if (serializedValue_params === serializedValue) {
return;
}

const resultOfParse_params = parse(serializedValue_params);

assert(resultOfParse_params.isValid);

const resultOfParse = parse(serializedValue);

if (
resultOfParse.isValid &&
same(resultOfParse.value, resultOfParse_params.value)
) {
return;
}

setSerializedValue(serializedValue_params);
}, [serializedValue_params]);

Expand Down
Loading

0 comments on commit a97dfe4

Please sign in to comment.