From c400acbd1870a11b44b14efc48f99b193b6e3b97 Mon Sep 17 00:00:00 2001 From: Yury Molodov Date: Fri, 21 Jul 2023 02:15:00 +0200 Subject: [PATCH] vmui: enhancements multiline field editing (#4294) * fix: change textarea for relabel page * feat: add comment for monaco theme * fix: change behavior of multiline fields * vmui: merge master --- app/vmui/packages/vmui/package-lock.json | 38 ++++++++- app/vmui/packages/vmui/package.json | 1 + .../vmui/src/api/expand-with-exprs.ts | 2 +- .../Configurators/QueryEditor/QueryEditor.tsx | 16 +++- .../Main/Autocomplete/Autocomplete.tsx | 8 +- .../components/Main/TextField/TextField.tsx | 11 ++- .../src/components/Main/TextField/style.scss | 9 ++ .../components/MonacoEditor/MonacoEditor.tsx | 52 ++++++++++++ .../MonacoEditor/hooks/useKeybindings.ts | 22 +++++ .../MonacoEditor/hooks/useLabelsSyntax.ts | 84 +++++++++++++++++++ .../MonacoEditor/hooks/useMonacoTheme.ts | 25 ++++++ .../src/components/MonacoEditor/style.scss | 27 ++++++ .../packages/vmui/src/pages/Relabel/index.tsx | 31 +++---- .../vmui/src/pages/Relabel/style.scss | 16 +--- .../src/pages/TracePage/JsonForm/JsonForm.tsx | 1 + .../WithTemplate/hooks/useExpandWithExprs.ts | 4 + .../vmui/src/pages/WithTemplate/index.tsx | 12 ++- .../vmui/src/pages/WithTemplate/style.scss | 1 + 18 files changed, 323 insertions(+), 37 deletions(-) create mode 100644 app/vmui/packages/vmui/src/components/MonacoEditor/MonacoEditor.tsx create mode 100644 app/vmui/packages/vmui/src/components/MonacoEditor/hooks/useKeybindings.ts create mode 100644 app/vmui/packages/vmui/src/components/MonacoEditor/hooks/useLabelsSyntax.ts create mode 100644 app/vmui/packages/vmui/src/components/MonacoEditor/hooks/useMonacoTheme.ts create mode 100644 app/vmui/packages/vmui/src/components/MonacoEditor/style.scss diff --git a/app/vmui/packages/vmui/package-lock.json b/app/vmui/packages/vmui/package-lock.json index ea99020c0..402e850f4 100644 --- a/app/vmui/packages/vmui/package-lock.json +++ b/app/vmui/packages/vmui/package-lock.json @@ -8,6 +8,7 @@ "name": "vmui", "version": "0.1.0", "dependencies": { + "@monaco-editor/react": "^4.5.1", "@types/lodash.debounce": "^4.0.6", "@types/lodash.get": "^4.4.6", "@types/lodash.throttle": "^4.1.6", @@ -26,7 +27,7 @@ "preact": "^10.7.1", "qs": "^6.10.3", "react-input-mask": "^2.0.4", - "react-router-dom": "^6.3.0", + "react-router-dom": "^6.10.0", "sass": "^1.56.0", "source-map-explorer": "^2.5.3", "typescript": "~4.6.2", @@ -3432,6 +3433,30 @@ "dev": true, "peer": true }, + "node_modules/@monaco-editor/loader": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@monaco-editor/loader/-/loader-1.3.3.tgz", + "integrity": "sha512-6KKF4CTzcJiS8BJwtxtfyYt9shBiEv32ateQ9T4UVogwn4HM/uPo9iJd2Dmbkpz8CM6Y0PDUpjnZzCwC+eYo2Q==", + "dependencies": { + "state-local": "^1.0.6" + }, + "peerDependencies": { + "monaco-editor": ">= 0.21.0 < 1" + } + }, + "node_modules/@monaco-editor/react": { + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/@monaco-editor/react/-/react-4.5.1.tgz", + "integrity": "sha512-NNDFdP+2HojtNhCkRfE6/D6ro6pBNihaOzMbGK84lNWzRu+CfBjwzGt4jmnqimLuqp5yE5viHS2vi+QOAnD5FQ==", + "dependencies": { + "@monaco-editor/loader": "^1.3.3" + }, + "peerDependencies": { + "monaco-editor": ">= 0.25.0 < 1", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/@nicolo-ribaudo/eslint-scope-5-internals": { "version": "5.1.1-v1", "resolved": "https://registry.npmjs.org/@nicolo-ribaudo/eslint-scope-5-internals/-/eslint-scope-5-internals-5.1.1-v1.tgz", @@ -13171,6 +13196,12 @@ "mkdirp": "bin/cmd.js" } }, + "node_modules/monaco-editor": { + "version": "0.40.0", + "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.40.0.tgz", + "integrity": "sha512-1wymccLEuFSMBvCk/jT1YDW/GuxMLYwnFwF9CDyYCxoTw2Pt379J3FUhwy9c43j51JdcxVPjwk0jm0EVDsBS2g==", + "peer": true + }, "node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", @@ -17066,6 +17097,11 @@ "dev": true, "peer": true }, + "node_modules/state-local": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/state-local/-/state-local-1.0.7.tgz", + "integrity": "sha512-HTEHMNieakEnoe33shBYcZ7NX83ACUjCu8c40iOGEZsngj9zRnkqS9j1pqQPXwobB0ZcVTk27REb7COQ0UR59w==" + }, "node_modules/statuses": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", diff --git a/app/vmui/packages/vmui/package.json b/app/vmui/packages/vmui/package.json index ff05078f7..12463f74f 100644 --- a/app/vmui/packages/vmui/package.json +++ b/app/vmui/packages/vmui/package.json @@ -4,6 +4,7 @@ "private": true, "homepage": "./", "dependencies": { + "@monaco-editor/react": "^4.5.1", "@types/lodash.debounce": "^4.0.6", "@types/lodash.get": "^4.4.6", "@types/lodash.throttle": "^4.1.6", diff --git a/app/vmui/packages/vmui/src/api/expand-with-exprs.ts b/app/vmui/packages/vmui/src/api/expand-with-exprs.ts index 8b0da7497..beb433a9c 100644 --- a/app/vmui/packages/vmui/src/api/expand-with-exprs.ts +++ b/app/vmui/packages/vmui/src/api/expand-with-exprs.ts @@ -1,2 +1,2 @@ export const getExpandWithExprUrl = (server: string, query: string): string => - `${server}/expand-with-exprs?query=${query}&format=json`; + `${server}/expand-with-exprs?query=${encodeURIComponent(query)}&format=json`; diff --git a/app/vmui/packages/vmui/src/components/Configurators/QueryEditor/QueryEditor.tsx b/app/vmui/packages/vmui/src/components/Configurators/QueryEditor/QueryEditor.tsx index cd77cc059..f0ef16c60 100644 --- a/app/vmui/packages/vmui/src/components/Configurators/QueryEditor/QueryEditor.tsx +++ b/app/vmui/packages/vmui/src/components/Configurators/QueryEditor/QueryEditor.tsx @@ -48,6 +48,9 @@ const QueryEditor: FC = ({ const handleKeyDown = (e: KeyboardEvent) => { const { key, ctrlKey, metaKey, shiftKey } = e; + const value = (e.target as HTMLTextAreaElement).value || ""; + const isMultiline = value.split("\n").length > 1; + const ctrlMetaKey = ctrlKey || metaKey; const arrowUp = key === "ArrowUp"; const arrowDown = key === "ArrowDown"; @@ -65,12 +68,21 @@ const QueryEditor: FC = ({ onArrowDown(); } + if (enter && openAutocomplete) { + e.preventDefault(); + } + // execute query - if (enter && !shiftKey && !openAutocomplete) { + if (enter && !shiftKey && (!isMultiline || ctrlMetaKey) && !openAutocomplete) { + e.preventDefault(); onEnter(); } }; + const handleChangeFoundOptions = (val: string[]) => { + setOpenAutocomplete(!!val.length); + }; + return
= ({ options={options} anchor={autocompleteAnchorEl} onSelect={handleSelect} - onOpenAutocomplete={setOpenAutocomplete} + onFoundOptions={handleChangeFoundOptions} /> )} {showSeriesFetchedWarning && ( diff --git a/app/vmui/packages/vmui/src/components/Main/Autocomplete/Autocomplete.tsx b/app/vmui/packages/vmui/src/components/Main/Autocomplete/Autocomplete.tsx index a53feacf1..5f9a7d45f 100644 --- a/app/vmui/packages/vmui/src/components/Main/Autocomplete/Autocomplete.tsx +++ b/app/vmui/packages/vmui/src/components/Main/Autocomplete/Autocomplete.tsx @@ -21,6 +21,7 @@ interface AutocompleteProps { disabledFullScreen?: boolean onSelect: (val: string) => void onOpenAutocomplete?: (val: boolean) => void + onFoundOptions?: (val: string[]) => void } const Autocomplete: FC = ({ @@ -36,7 +37,8 @@ const Autocomplete: FC = ({ label, disabledFullScreen, onSelect, - onOpenAutocomplete + onOpenAutocomplete, + onFoundOptions }) => { const { isMobile } = useDeviceDetect(); const wrapperEl = useRef(null); @@ -120,6 +122,10 @@ const Autocomplete: FC = ({ onOpenAutocomplete && onOpenAutocomplete(openAutocomplete); }, [openAutocomplete]); + useEffect(() => { + onFoundOptions && onFoundOptions(foundOptions); + }, [foundOptions]); + return ( = ({ const handleKeyDown = (e: KeyboardEvent) => { onKeyDown && onKeyDown(e); - if (e.key === "Enter" && !e.shiftKey) { + + const { key, ctrlKey, metaKey } = e; + const isEnter = key === "Enter"; + const runByEnter = type !== "textarea" ? isEnter : isEnter && (metaKey || ctrlKey); + if (runByEnter && onEnter) { e.preventDefault(); - onEnter && onEnter(); + onEnter(); } }; @@ -139,7 +143,8 @@ const TextField: FC = ({ {helperText} )} - ; + + ; }; export default TextField; diff --git a/app/vmui/packages/vmui/src/components/Main/TextField/style.scss b/app/vmui/packages/vmui/src/components/Main/TextField/style.scss index fe686e015..70b463645 100644 --- a/app/vmui/packages/vmui/src/components/Main/TextField/style.scss +++ b/app/vmui/packages/vmui/src/components/Main/TextField/style.scss @@ -128,4 +128,13 @@ left: auto; right: $padding-small; } + + &__controls-info { + position: absolute; + bottom: $padding-small; + right: $padding-global; + color: $color-text-secondary; + font-size: $font-size-small; + opacity: 0.8; + } } diff --git a/app/vmui/packages/vmui/src/components/MonacoEditor/MonacoEditor.tsx b/app/vmui/packages/vmui/src/components/MonacoEditor/MonacoEditor.tsx new file mode 100644 index 000000000..cee3c1b84 --- /dev/null +++ b/app/vmui/packages/vmui/src/components/MonacoEditor/MonacoEditor.tsx @@ -0,0 +1,52 @@ +import React, { FC } from "preact/compat"; +import Editor, { useMonaco } from "@monaco-editor/react"; +import useMonacoTheme from "./hooks/useMonacoTheme"; +import useLabelsSyntax from "./hooks/useLabelsSyntax"; +import useKeybindings from "./hooks/useKeybindings"; +import "./style.scss"; +import classNames from "classnames"; + +interface MonacoEditorProps { + value: string; + label?: string; + language?: string; + disabled?: boolean; + resize?: "vertical" | "horizontal" | "both" | "none"; + onChange: (val: string | undefined) => void; + onEnter?: (val: string) => void; +} + +const MonacoEditor: FC = ({ value, label, language, disabled, resize = "none", onChange, onEnter }) => { + const monaco = useMonaco(); + useMonacoTheme(monaco); + useLabelsSyntax(monaco); + useKeybindings(monaco, onEnter); + + return ( +
+ + {label && {label}} +
+ ); +}; + +export default MonacoEditor; diff --git a/app/vmui/packages/vmui/src/components/MonacoEditor/hooks/useKeybindings.ts b/app/vmui/packages/vmui/src/components/MonacoEditor/hooks/useKeybindings.ts new file mode 100644 index 000000000..9a7a2fd40 --- /dev/null +++ b/app/vmui/packages/vmui/src/components/MonacoEditor/hooks/useKeybindings.ts @@ -0,0 +1,22 @@ +import { Monaco } from "@monaco-editor/react"; +import { useEffect } from "preact/compat"; +import * as monaco from "monaco-editor"; + +const useKeybindings = (monaco: Monaco | null, onEnter?: (val: string) => void) => { + + const handleRunEnter = (e: monaco.editor.ICodeEditor) => { + onEnter && onEnter(e.getValue() || ""); + }; + + useEffect(() => { + if (!monaco) return; + monaco.editor.addEditorAction({ + id: "execute-ctrl-enter", + label: "Execute", + keybindings: [monaco.KeyMod.CtrlCmd | monaco.KeyCode.Enter], + run: handleRunEnter + }); + }, [monaco, onEnter]); +}; + +export default useKeybindings; diff --git a/app/vmui/packages/vmui/src/components/MonacoEditor/hooks/useLabelsSyntax.ts b/app/vmui/packages/vmui/src/components/MonacoEditor/hooks/useLabelsSyntax.ts new file mode 100644 index 000000000..967d65fdf --- /dev/null +++ b/app/vmui/packages/vmui/src/components/MonacoEditor/hooks/useLabelsSyntax.ts @@ -0,0 +1,84 @@ +import { useEffect } from "preact/compat"; +import { Monaco } from "@monaco-editor/react"; +import * as monaco from "monaco-editor"; + +const languageId = "vm-labels"; + +export const language = { + ignoreCase: false, + defaultToken: "", + tokenizer: { + root: [ + // labels + [ /[a-z_]\w*(?=\s*(=|!=|=~|!~))/, "tag" ], + + // strings + [ /"([^"\\]|\\.)*$/, "string.invalid" ], + [ /'([^'\\]|\\.)*$/, "string.invalid" ], + [ /"/, "string", "@string_double" ], + [ /'/, "string", "@string_single" ], + [ /`/, "string", "@string_backtick" ], + + // delimiters and operators + [ /[{}()[]]/, "@brackets" ], + ], + + string_double: [ + [ /[^\\"]+/, "string" ], + [ /\\./, "string.escape.invalid" ], + [ /"/, "string", "@pop" ] + ], + + string_single: [ + [ /[^\\']+/, "string" ], + [ /\\./, "string.escape.invalid" ], + [ /'/, "string", "@pop" ] + ], + + string_backtick: [ + [ /[^\\`$]+/, "string" ], + [ /\\./, "string.escape.invalid" ], + [ /`/, "string", "@pop" ] + ], + }, +} as monaco.languages.IMonarchLanguage; + +export const languageConfiguration: monaco.languages.LanguageConfiguration = { + wordPattern: /(-?\d*\.\d\w*)|([^`~!#%^&*()\-=+[{\]}\\|;:'",.<>/?\s]+)/g, + comments: { + lineComment: "#", + }, + brackets: [ + [ "{", "}" ], + [ "[", "]" ], + [ "(", ")" ], + ], + autoClosingPairs: [ + { open: "{", close: "}" }, + { open: "[", close: "]" }, + { open: "(", close: ")" }, + { open: "\"", close: "\"" }, + { open: "'", close: "'" }, + ], + surroundingPairs: [ + { open: "{", close: "}" }, + { open: "[", close: "]" }, + { open: "(", close: ")" }, + { open: "\"", close: "\"" }, + { open: "'", close: "'" }, + { open: "<", close: ">" }, + ], + folding: {} +}; + +const useLabelsSyntax = (monaco: Monaco | null) => { + + useEffect(() => { + if (!monaco) return; + monaco.languages.register({ id: languageId }); + monaco.languages.setMonarchTokensProvider(languageId, language); + monaco.languages.setLanguageConfiguration(languageId, languageConfiguration); + }, [monaco]); +}; + +export default useLabelsSyntax; diff --git a/app/vmui/packages/vmui/src/components/MonacoEditor/hooks/useMonacoTheme.ts b/app/vmui/packages/vmui/src/components/MonacoEditor/hooks/useMonacoTheme.ts new file mode 100644 index 000000000..e09101371 --- /dev/null +++ b/app/vmui/packages/vmui/src/components/MonacoEditor/hooks/useMonacoTheme.ts @@ -0,0 +1,25 @@ +import { useEffect } from "preact/compat"; +import { useAppState } from "../../../state/common/StateContext"; +import { Monaco } from "@monaco-editor/react"; + +const useMonacoTheme = (monaco: Monaco | null) => { + const { isDarkTheme } = useAppState(); + + useEffect(() => { + if (!monaco) return; + monaco.editor.defineTheme("vm-theme", { + base: isDarkTheme ? "vs-dark" : "vs", + inherit: true, + rules: [], + colors: { + // #00000000 - for transparent + "editor.background": "#00000000", + "editor.lineHighlightBackground": "#00000000", + "editor.lineHighlightBorder": "#00000000" + } + }); + monaco.editor.setTheme("vm-theme"); + }, [monaco, isDarkTheme]); +}; + +export default useMonacoTheme; diff --git a/app/vmui/packages/vmui/src/components/MonacoEditor/style.scss b/app/vmui/packages/vmui/src/components/MonacoEditor/style.scss new file mode 100644 index 000000000..2297189be --- /dev/null +++ b/app/vmui/packages/vmui/src/components/MonacoEditor/style.scss @@ -0,0 +1,27 @@ +@use "src/styles/variables" as *; + +.vm-monaco-editor { + display: grid; + height: 100%; + min-height: inherit; + + &__input { + height: 100%; + padding: $padding-small 0; + resize: none; + + &_resize { + &-vertical { + resize: vertical; + } + + &-horizontal { + resize: horizontal; + } + + &-both { + resize: both; + } + } + } +} diff --git a/app/vmui/packages/vmui/src/pages/Relabel/index.tsx b/app/vmui/packages/vmui/src/pages/Relabel/index.tsx index 620fd3714..595db152d 100644 --- a/app/vmui/packages/vmui/src/pages/Relabel/index.tsx +++ b/app/vmui/packages/vmui/src/pages/Relabel/index.tsx @@ -1,6 +1,5 @@ -import React, { FC, useEffect } from "preact/compat"; +import React, { FC, useCallback, useEffect } from "preact/compat"; import "./style.scss"; -import TextField from "../../components/Main/TextField/TextField"; import Button from "../../components/Main/Button/Button"; import { InfoIcon, PlayIcon, WikiIcon } from "../../components/Main/Icons"; import "./style.scss"; @@ -9,6 +8,7 @@ import Spinner from "../../components/Main/Spinner/Spinner"; import Alert from "../../components/Main/Alert/Alert"; import { useSearchParams } from "react-router-dom"; import useStateSearchParams from "../../hooks/useStateSearchParams"; +import MonacoEditor from "../../components/MonacoEditor/MonacoEditor"; const example = { config: `- if: '{bar_label=~"b.*"}' @@ -30,20 +30,20 @@ const Relabel: FC = () => { const [config, setConfig] = useStateSearchParams("", "config"); const [labels, setLabels] = useStateSearchParams("", "labels"); - const handleChangeConfig = (val: string) => { - setConfig(val); + const handleChangeConfig = (val?: string) => { + setConfig(val || ""); }; - const handleChangeLabels = (val: string) => { - setLabels(val); + const handleChangeLabels = (val?: string) => { + setLabels(val || ""); }; - const handleRunQuery = () => { + const handleRunQuery = useCallback(() => { fetchData(config, labels); searchParams.set("config", config); searchParams.set("labels", labels); setSearchParams(searchParams); - }; + }, [config, labels]); const handleRunExample = () => { const { config, labels } = example; @@ -69,21 +69,24 @@ const Relabel: FC = () => {
{loading && }
-
- +
-
diff --git a/app/vmui/packages/vmui/src/pages/Relabel/style.scss b/app/vmui/packages/vmui/src/pages/Relabel/style.scss index 893454bbb..5a14aaf40 100644 --- a/app/vmui/packages/vmui/src/pages/Relabel/style.scss +++ b/app/vmui/packages/vmui/src/pages/Relabel/style.scss @@ -10,22 +10,12 @@ align-items: flex-start; width: 100%; - &__configs { - textarea { - min-height: 200px; - } + &-configs { + min-height: 200px; } &__labels { - textarea { - min-height: 60px; - } - } - - textarea { - overflow: auto; - width: 100%; - height: 100%; + min-height: 60px; } &-bottom { diff --git a/app/vmui/packages/vmui/src/pages/TracePage/JsonForm/JsonForm.tsx b/app/vmui/packages/vmui/src/pages/TracePage/JsonForm/JsonForm.tsx index b34764138..9c3933d71 100644 --- a/app/vmui/packages/vmui/src/pages/TracePage/JsonForm/JsonForm.tsx +++ b/app/vmui/packages/vmui/src/pages/TracePage/JsonForm/JsonForm.tsx @@ -99,6 +99,7 @@ const JsonForm: FC = ({ error={error} autofocus onChange={handleChangeJson} + onEnter={handleApply} disabled={!editable} />
diff --git a/app/vmui/packages/vmui/src/pages/WithTemplate/hooks/useExpandWithExprs.ts b/app/vmui/packages/vmui/src/pages/WithTemplate/hooks/useExpandWithExprs.ts index 28e13db94..f13e52e36 100644 --- a/app/vmui/packages/vmui/src/pages/WithTemplate/hooks/useExpandWithExprs.ts +++ b/app/vmui/packages/vmui/src/pages/WithTemplate/hooks/useExpandWithExprs.ts @@ -2,15 +2,19 @@ import { useAppState } from "../../../state/common/StateContext"; import { useState } from "react"; import { ErrorTypes } from "../../../types"; import { getExpandWithExprUrl } from "../../../api/expand-with-exprs"; +import { useSearchParams } from "react-router-dom"; export const useExpandWithExprs = () => { const { serverUrl } = useAppState(); + const [searchParams, setSearchParams] = useSearchParams(); const [data, setData] = useState(""); const [loading, setLoading] = useState(false); const [error, setError] = useState(); const fetchData = async (query: string) => { + searchParams.set("expr", query); + setSearchParams(searchParams); const fetchUrl = getExpandWithExprUrl(serverUrl, query); setLoading(true); try { diff --git a/app/vmui/packages/vmui/src/pages/WithTemplate/index.tsx b/app/vmui/packages/vmui/src/pages/WithTemplate/index.tsx index 27097a174..602df8362 100644 --- a/app/vmui/packages/vmui/src/pages/WithTemplate/index.tsx +++ b/app/vmui/packages/vmui/src/pages/WithTemplate/index.tsx @@ -1,16 +1,19 @@ import React, { FC } from "preact/compat"; import "./style.scss"; import TextField from "../../components/Main/TextField/TextField"; -import { useState } from "react"; +import { useEffect, useState } from "react"; import Button from "../../components/Main/Button/Button"; import { PlayIcon } from "../../components/Main/Icons"; import WithTemplateTutorial from "./WithTemplateTutorial/WithTemplateTutorial"; import { useExpandWithExprs } from "./hooks/useExpandWithExprs"; import Spinner from "../../components/Main/Spinner/Spinner"; +import { useSearchParams } from "react-router-dom"; const WithTemplate: FC = () => { + const [searchParams] = useSearchParams(); + const { data, loading, error, expand } = useExpandWithExprs(); - const [expr, setExpr] = useState(""); + const [expr, setExpr] = useState(searchParams.get("expr") || ""); const handleChangeInput = (val: string) => { setExpr(val); @@ -20,6 +23,10 @@ const WithTemplate: FC = () => { expand(expr); }; + useEffect(() => { + if (expr) expand(expr); + }, []); + return (
{loading && } @@ -32,6 +39,7 @@ const WithTemplate: FC = () => { value={expr} error={error} autofocus + onEnter={handleRunQuery} onChange={handleChangeInput} />
diff --git a/app/vmui/packages/vmui/src/pages/WithTemplate/style.scss b/app/vmui/packages/vmui/src/pages/WithTemplate/style.scss index 71a79e785..323d02ffd 100644 --- a/app/vmui/packages/vmui/src/pages/WithTemplate/style.scss +++ b/app/vmui/packages/vmui/src/pages/WithTemplate/style.scss @@ -26,6 +26,7 @@ } textarea { + font-family: $font-family-monospace; overflow: auto; width: 100%; height: 100%;