From 759ad2b78d10a9f95b919cf1b7f116d55c8a3c71 Mon Sep 17 00:00:00 2001 From: saadi Date: Mon, 21 Apr 2025 17:03:07 -0700 Subject: [PATCH 1/3] Allow empty payloads in test runs --- .../app/components/code/codeMirrorSetup.ts | 18 +++++- .../route.tsx | 1 - apps/webapp/app/v3/testTask.ts | 56 +++++++++---------- 3 files changed, 42 insertions(+), 33 deletions(-) diff --git a/apps/webapp/app/components/code/codeMirrorSetup.ts b/apps/webapp/app/components/code/codeMirrorSetup.ts index 89988c3d9bc..eb12f14f891 100644 --- a/apps/webapp/app/components/code/codeMirrorSetup.ts +++ b/apps/webapp/app/components/code/codeMirrorSetup.ts @@ -2,12 +2,13 @@ import { closeBrackets } from "@codemirror/autocomplete"; import { indentWithTab } from "@codemirror/commands"; import { jsonParseLinter } from "@codemirror/lang-json"; import { bracketMatching } from "@codemirror/language"; -import { lintGutter, lintKeymap, linter } from "@codemirror/lint"; +import { type Diagnostic, linter, lintGutter, lintKeymap } from "@codemirror/lint"; import { highlightSelectionMatches } from "@codemirror/search"; import { Prec, type Extension } from "@codemirror/state"; import { drawSelection, dropCursor, + type EditorView, highlightActiveLine, highlightActiveLineGutter, highlightSpecialChars, @@ -15,6 +16,19 @@ import { lineNumbers, } from "@codemirror/view"; +function emptyAwareJsonLinter() { + return (view: EditorView): Diagnostic[] => { + const content = view.state.doc.toString().trim(); + + // return no errors if content is empty + if (!content) { + return []; + } + + return jsonParseLinter()(view); + }; +} + export function getEditorSetup(showLineNumbers = true, showHighlights = true): Array { const options = [ drawSelection(), @@ -22,7 +36,7 @@ export function getEditorSetup(showLineNumbers = true, showHighlights = true): A bracketMatching(), closeBrackets(), lintGutter(), - linter(jsonParseLinter()), + linter(emptyAwareJsonLinter()), Prec.highest( keymap.of([ { diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.test.tasks.$taskParam/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.test.tasks.$taskParam/route.tsx index 6205336d75b..abdfcda4c38 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.test.tasks.$taskParam/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.test.tasks.$taskParam/route.tsx @@ -291,7 +291,6 @@ function StandardTaskForm({ task, runs }: { task: TestTask["task"]; runs: Standa }} height="100%" autoFocus={!tab || tab === "payload"} - placeholder="{ }" className={cn("h-full overflow-auto", tab === "metadata" && "hidden")} /> { - try { - const data = JSON.parse(payload); - return data as any; - } catch (e) { - console.log("parsing error", e); + payload: z + .string() + .optional() + .transform((val, ctx) => { + if (!val) { + return {}; + } - if (e instanceof Error) { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - message: e.message, - }); - } else { + try { + return JSON.parse(val); + } catch { ctx.addIssue({ code: z.ZodIssueCode.custom, - message: "This is invalid JSON", + message: "Payload must be a valid JSON string", }); + return z.NEVER; + } + }), + metadata: z + .string() + .optional() + .transform((val, ctx) => { + if (!val) { + return {}; } - } - }), - metadata: z.string().transform((metadata, ctx) => { - try { - const data = JSON.parse(metadata); - return data as any; - } catch (e) { - console.log("parsing error", e); - if (e instanceof Error) { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - message: e.message, - }); - } else { + try { + return JSON.parse(val); + } catch { ctx.addIssue({ code: z.ZodIssueCode.custom, - message: "This is invalid JSON", + message: "Metadata must be a valid JSON string", }); + return z.NEVER; } - } - }), + }), }), z.object({ triggerSource: z.literal("SCHEDULED"), From 09ecc225b9105d6929c66a22e7949f1bb5d7f266 Mon Sep 17 00:00:00 2001 From: saadi Date: Mon, 21 Apr 2025 17:14:27 -0700 Subject: [PATCH 2/3] Expose a basic linter configs in the code editor component --- .../webapp/app/components/code/JSONEditor.tsx | 37 ++++++++++++++++++- .../app/components/code/codeMirrorSetup.ts | 19 +--------- 2 files changed, 36 insertions(+), 20 deletions(-) diff --git a/apps/webapp/app/components/code/JSONEditor.tsx b/apps/webapp/app/components/code/JSONEditor.tsx index 0313120b140..db85a9cd9f5 100644 --- a/apps/webapp/app/components/code/JSONEditor.tsx +++ b/apps/webapp/app/components/code/JSONEditor.tsx @@ -1,5 +1,5 @@ -import { json as jsonLang } from "@codemirror/lang-json"; -import type { ViewUpdate } from "@codemirror/view"; +import { json as jsonLang, jsonParseLinter } from "@codemirror/lang-json"; +import type { EditorView, ViewUpdate } from "@codemirror/view"; import { CheckIcon, ClipboardIcon, TrashIcon } from "@heroicons/react/20/solid"; import type { ReactCodeMirrorProps, UseCodeMirror } from "@uiw/react-codemirror"; import { useCodeMirror } from "@uiw/react-codemirror"; @@ -8,6 +8,7 @@ import { cn } from "~/utils/cn"; import { Button } from "../primitives/Buttons"; import { getEditorSetup } from "./codeMirrorSetup"; import { darkTheme } from "./codeMirrorTheme"; +import { linter, lintGutter, type Diagnostic } from "@codemirror/lint"; export interface JSONEditorProps extends Omit { defaultValue?: string; @@ -18,18 +19,35 @@ export interface JSONEditorProps extends Omit { onBlur?: (code: string) => void; showCopyButton?: boolean; showClearButton?: boolean; + linterEnabled?: boolean; + allowEmpty?: boolean; } const languages = { json: jsonLang, }; +function emptyAwareJsonLinter() { + return (view: EditorView): Diagnostic[] => { + const content = view.state.doc.toString().trim(); + + // return no errors if content is empty + if (!content) { + return []; + } + + return jsonParseLinter()(view); + }; +} + type JSONEditorDefaultProps = Partial; const defaultProps: JSONEditorDefaultProps = { language: "json", readOnly: true, basicSetup: false, + linterEnabled: true, + allowEmpty: true, }; export function JSONEditor(opts: JSONEditorProps) { @@ -44,6 +62,8 @@ export function JSONEditor(opts: JSONEditorProps) { autoFocus, showCopyButton = true, showClearButton = true, + linterEnabled, + allowEmpty, } = { ...defaultProps, ...opts, @@ -56,6 +76,19 @@ export function JSONEditor(opts: JSONEditorProps) { extensions.push(languageExtension()); + if (linterEnabled) { + extensions.push(lintGutter()); + + switch (language) { + case "json": { + extensions.push(allowEmpty ? linter(emptyAwareJsonLinter()) : linter(jsonParseLinter())); + break; + } + default: + language satisfies never; + } + } + const editor = useRef(null); const settings: Omit = { ...opts, diff --git a/apps/webapp/app/components/code/codeMirrorSetup.ts b/apps/webapp/app/components/code/codeMirrorSetup.ts index eb12f14f891..811a6ebc298 100644 --- a/apps/webapp/app/components/code/codeMirrorSetup.ts +++ b/apps/webapp/app/components/code/codeMirrorSetup.ts @@ -1,14 +1,12 @@ import { closeBrackets } from "@codemirror/autocomplete"; import { indentWithTab } from "@codemirror/commands"; -import { jsonParseLinter } from "@codemirror/lang-json"; import { bracketMatching } from "@codemirror/language"; -import { type Diagnostic, linter, lintGutter, lintKeymap } from "@codemirror/lint"; +import { lintKeymap } from "@codemirror/lint"; import { highlightSelectionMatches } from "@codemirror/search"; import { Prec, type Extension } from "@codemirror/state"; import { drawSelection, dropCursor, - type EditorView, highlightActiveLine, highlightActiveLineGutter, highlightSpecialChars, @@ -16,27 +14,12 @@ import { lineNumbers, } from "@codemirror/view"; -function emptyAwareJsonLinter() { - return (view: EditorView): Diagnostic[] => { - const content = view.state.doc.toString().trim(); - - // return no errors if content is empty - if (!content) { - return []; - } - - return jsonParseLinter()(view); - }; -} - export function getEditorSetup(showLineNumbers = true, showHighlights = true): Array { const options = [ drawSelection(), dropCursor(), bracketMatching(), closeBrackets(), - lintGutter(), - linter(emptyAwareJsonLinter()), Prec.highest( keymap.of([ { From 134e486fe3ab1676147de1b2b219cf15073067b2 Mon Sep 17 00:00:00 2001 From: saadi Date: Mon, 21 Apr 2025 17:16:34 -0700 Subject: [PATCH 3/3] Touch up the linting error elements in the code editor component --- .../app/components/code/codeMirrorTheme.ts | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/apps/webapp/app/components/code/codeMirrorTheme.ts b/apps/webapp/app/components/code/codeMirrorTheme.ts index 74d074b3cd7..25167051340 100644 --- a/apps/webapp/app/components/code/codeMirrorTheme.ts +++ b/apps/webapp/app/components/code/codeMirrorTheme.ts @@ -39,6 +39,32 @@ export function darkTheme(): Extension { fontSize: "14px", }, + ".cm-tooltip.cm-tooltip-lint": { + backgroundColor: tooltipBackground, + }, + + ".cm-diagnostic": { + padding: "4px 8px", + color: ivory, + fontFamily: "Geist Mono Variable", + fontSize: "12px", + }, + + ".cm-diagnostic-error": { + borderLeft: "2px solid #e11d48", + }, + + ".cm-lint-marker-error": { + content: "none", + backgroundColor: "#e11d48", + height: "100%", + width: "2px", + }, + + ".cm-lintPoint:after": { + borderBottom: "4px solid #e11d48", + }, + ".cm-cursor, .cm-dropCursor": { borderLeftColor: cursor }, "&.cm-focused .cm-selectionBackground, .cm-selectionBackground, .cm-content ::selection": { backgroundColor: selection, @@ -82,6 +108,7 @@ export function darkTheme(): Extension { ".cm-tooltip": { border: "none", + marginTop: "6px", backgroundColor: tooltipBackground, }, ".cm-tooltip .cm-tooltip-arrow:before": {