Skip to content

Commit

Permalink
feat: monaco editor for console (#2040)
Browse files Browse the repository at this point in the history
* feat: monaco editor

* feat: handle json error in request console; change theme

* chore: fix build

* chore: refactor

* chore: rm vite config stuff

* chore: add back default editor worker
  • Loading branch information
markphelps authored Aug 28, 2023
1 parent 1a495e1 commit 0266072
Show file tree
Hide file tree
Showing 9 changed files with 171 additions and 43 deletions.
38 changes: 38 additions & 0 deletions ui/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@
"highlight.js": "^11.8.0",
"lodash": "^4.17.21",
"moment": "^2.29.4",
"monaco-editor": "^0.40.0",
"monaco-themes": "^0.4.4",
"nightwind": "^1.1.13",
"playwright": "^1.37.1",
"react": "^18.2.0",
Expand Down
80 changes: 42 additions & 38 deletions ui/src/app/console/Console.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,19 @@
import { ArrowPathIcon } from '@heroicons/react/20/solid';
import { Form, Formik, useFormikContext } from 'formik';
import hljs from 'highlight.js';
import javascript from 'highlight.js/lib/languages/json';
import 'highlight.js/styles/tokyo-night-dark.css';
import 'highlight.js/styles/tomorrow-night-bright.css';
import React, { useCallback, useEffect, useState } from 'react';
import { useSelector } from 'react-redux';
import { useNavigate } from 'react-router-dom';
import { v4 as uuidv4 } from 'uuid';
import * as Yup from 'yup';
import { selectCurrentNamespace } from '~/app/namespaces/namespacesSlice';
import { ContextEditor } from '~/components/console/ContextEditor';
import EmptyState from '~/components/EmptyState';
import Button from '~/components/forms/buttons/Button';
import Combobox from '~/components/forms/Combobox';
import Input from '~/components/forms/Input';
import TextArea from '~/components/forms/TextArea';
import { evaluateV2, listFlags } from '~/data/api';
import { useError } from '~/data/hooks/error';
import {
Expand All @@ -27,7 +28,7 @@ import {
IFlagList
} from '~/types/Flag';
import { INamespace } from '~/types/Namespace';
import { classNames } from '~/utils/helpers';
import { classNames, getErrorMessage } from '~/utils/helpers';

hljs.registerLanguage('json', javascript);

Expand All @@ -44,12 +45,12 @@ function ResetOnNamespaceChange({ namespace }: { namespace: INamespace }) {
interface ConsoleFormValues {
flagKey: string;
entityId: string;
context: string | undefined;
}

export default function Console() {
const [flags, setFlags] = useState<FilterableFlag[]>([]);
const [selectedFlag, setSelectedFlag] = useState<FilterableFlag | null>(null);
const [context, setContext] = useState<string | null>(null);
const [response, setResponse] = useState<string | null>(null);
const [hasEvaluationError, setHasEvaluationError] = useState<boolean>(false);

Expand Down Expand Up @@ -77,14 +78,21 @@ export default function Console() {
}, [namespace.key]);

const handleSubmit = (flag: IFlag | null, values: ConsoleFormValues) => {
const { entityId, context } = values;
const { entityId } = values;

if (!flag) {
return;
}

// need to unescape the context string
const parsed = context ? JSON.parse(context) : undefined;
let parsed = null;
try {
// need to unescape the context string
parsed = context ? JSON.parse(context) : undefined;
} catch (err) {
setHasEvaluationError(true);
setResponse('error: ' + getErrorMessage(err));
return;
}

const rest = {
entityId,
Expand Down Expand Up @@ -116,8 +124,7 @@ export default function Console() {

const initialvalues: ConsoleFormValues = {
flagKey: selectedFlag?.key || '',
entityId: uuidv4(),
context: undefined
entityId: uuidv4()
};

return (
Expand All @@ -144,11 +151,6 @@ export default function Console() {
onSubmit={(values) => {
handleSubmit(selectedFlag, values);
}}
onReset={() => {
setResponse(null);
setHasEvaluationError(false);
setSelectedFlag(null);
}}
>
{(formik) => (
<Form className="px-1 sm:overflow-hidden sm:rounded-md">
Expand Down Expand Up @@ -178,12 +180,29 @@ export default function Console() {
>
Entity ID
</label>
<Input
className="mt-1"
name="entityId"
id="entityId"
type="text"
/>
<div className="flex items-center justify-between">
<Input
className="mr-2 mt-1"
name="entityId"
id="entityId"
type="text"
/>
<button
aria-label="New Entity ID"
title="New Entity ID"
className="hidden md:block"
onClick={(e) => {
e.preventDefault();
formik.setFieldValue('entityId', uuidv4());
}}
>
<ArrowPathIcon
className={classNames(
'text-gray-400 m-auto h-5 w-5 justify-center align-middle transition-opacity duration-300 ease-in-out hover:text-white dark:hover:text-gray-500'
)}
/>
</button>
</div>
</div>
<div className="col-span-3">
<label
Expand All @@ -192,27 +211,12 @@ export default function Console() {
>
Request Context
</label>
<TextArea
rows={10}
name="context"
id="context"
className="mt-1"
placeholder="{}"
/>
<div className="nightwind-prevent mt-1">
<ContextEditor id="context" setValue={setContext} />
</div>
</div>
</div>
<div className="flex justify-end">
<Button
type="reset"
onClick={(e) => {
e.preventDefault();
formik.resetForm();
formik.setFieldValue('entityId', uuidv4());
formik.setFieldValue('context', '');
}}
>
Reset
</Button>
<Button
primary
className="ml-3"
Expand Down
4 changes: 4 additions & 0 deletions ui/src/components/console/ContextEditor.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
.ContextEditor {
width: 100vw;
height: 50vh;
}
53 changes: 53 additions & 0 deletions ui/src/components/console/ContextEditor.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import * as monaco from 'monaco-editor/esm/vs/editor/editor.api';
import tmrw from 'monaco-themes/themes/Tomorrow-Night-Bright.json';
import { useEffect, useRef } from 'react';
import styles from './ContextEditor.module.css';
import './worker';

type ContextEditorProps = {
id: string;
setValue(value: string): void;
};

export const ContextEditor: React.FC<ContextEditorProps> = (
props: ContextEditorProps
) => {
const { id, setValue } = props;

const editor = useRef<undefined | monaco.editor.IStandaloneCodeEditor>();
const monacoEl = useRef(null);
const subscription = useRef<monaco.IDisposable | null>(null);

monaco.editor.defineTheme('tmrw', tmrw as monaco.editor.IStandaloneThemeData);

useEffect(() => {
if (monacoEl.current) {
editor.current = monaco.editor.create(monacoEl.current!, {
value: '{}',
language: 'json',
fontSize: 14,
lineNumbers: 'off',
renderLineHighlight: 'none',
minimap: {
enabled: false
},
folding: false,
autoDetectHighContrast: false,
autoClosingBrackets: 'always',
scrollBeyondLastLine: false,
theme: 'tmrw'
});

// After initializing monaco editor
if (editor?.current) {
subscription.current = editor.current.onDidChangeModelContent(() => {
if (editor?.current) setValue(editor.current.getValue());
});
}

return () => editor.current && editor.current.dispose();
}
}, []);

Check warning on line 50 in ui/src/components/console/ContextEditor.tsx

View workflow job for this annotation

GitHub Actions / Lint UI

React Hook useEffect has a missing dependency: 'setValue'. Either include it or remove the dependency array

return <div id={id} className={styles.ContextEditor} ref={monacoEl}></div>;
};
21 changes: 21 additions & 0 deletions ui/src/components/console/worker.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// @ts-ignore
self.MonacoEnvironment = {
getWorker(_, label) {
switch (label) {
case 'json':
return new Worker(
new URL(
'monaco-editor/esm/vs/language/json/json.worker.js',
import.meta.url
)
);
default:
return new Worker(
new URL(
'monaco-editor/esm/vs/editor/editor.worker.js',
import.meta.url
)
);
}
}
};
5 changes: 3 additions & 2 deletions ui/src/components/flags/FlagForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,7 @@ export default function FlagForm(props: FlagFormProps) {
{!isNew && (
<button
aria-label="Copy"
title="Copy to Clipboard"
className="hidden md:block"
onClick={(e) => {
e.preventDefault();
Expand All @@ -178,15 +179,15 @@ export default function FlagForm(props: FlagFormProps) {
>
<CheckIcon
className={classNames(
'nightwind-prevent text-green-400 absolute m-auto h-6 w-6 justify-center align-middle transition-opacity duration-300 ease-in-out hover:text-white',
'nightwind-prevent text-green-400 absolute m-auto h-5 w-5 justify-center align-middle transition-opacity duration-300 ease-in-out hover:text-white',
keyCopied
? 'visible opacity-100'
: 'invisible opacity-0'
)}
/>
<ClipboardDocumentIcon
className={classNames(
'text-gray-400 m-auto h-6 w-6 justify-center align-middle transition-opacity duration-300 ease-in-out hover:text-white dark:hover:text-gray-500',
'text-gray-400 m-auto h-5 w-5 justify-center align-middle transition-opacity duration-300 ease-in-out hover:text-white dark:hover:text-gray-500',
keyCopied
? 'invisible opacity-0'
: 'visible opacity-100'
Expand Down
5 changes: 3 additions & 2 deletions ui/src/components/segments/SegmentForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,7 @@ export default function SegmentForm(props: SegmentFormProps) {
{!isNew && (
<button
aria-label="Copy"
title="Copy to Clipboard"
className="hidden md:block"
onClick={(e) => {
e.preventDefault();
Expand All @@ -160,15 +161,15 @@ export default function SegmentForm(props: SegmentFormProps) {
>
<CheckIcon
className={classNames(
'nightwind-prevent text-green-400 absolute m-auto h-6 w-6 justify-center align-middle transition-opacity duration-300 ease-in-out hover:text-white',
'nightwind-prevent text-green-400 absolute m-auto h-5 w-5 justify-center align-middle transition-opacity duration-300 ease-in-out hover:text-white',
keyCopied
? 'visible opacity-100'
: 'invisible opacity-0'
)}
/>
<ClipboardDocumentIcon
className={classNames(
'text-gray-400 m-auto h-6 w-6 justify-center align-middle transition-opacity duration-300 ease-in-out hover:text-white dark:hover:text-gray-500',
'text-gray-400 m-auto h-5 w-5 justify-center align-middle transition-opacity duration-300 ease-in-out hover:text-white dark:hover:text-gray-500',
keyCopied
? 'invisible opacity-0'
: 'visible opacity-100'
Expand Down
6 changes: 5 additions & 1 deletion ui/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,5 +21,9 @@
}
},
"include": ["src", "tests"],
"references": [{ "path": "./tsconfig.node.json" }]
"references": [
{
"path": "./tsconfig.node.json"
}
]
}

0 comments on commit 0266072

Please sign in to comment.