diff --git a/apps/webapp/app/components/code/JSONEditor.tsx b/apps/webapp/app/components/code/JSONEditor.tsx index 4b635904a1..7d5ef0c237 100644 --- a/apps/webapp/app/components/code/JSONEditor.tsx +++ b/apps/webapp/app/components/code/JSONEditor.tsx @@ -1,11 +1,13 @@ import { json as jsonLang } from "@codemirror/lang-json"; import type { ViewUpdate } from "@codemirror/view"; +import { CheckIcon, ClipboardIcon } from "@heroicons/react/20/solid"; import type { ReactCodeMirrorProps, UseCodeMirror } from "@uiw/react-codemirror"; import { useCodeMirror } from "@uiw/react-codemirror"; -import { useRef, useEffect } from "react"; +import { useCallback, useEffect, useRef, useState } from "react"; +import { cn } from "~/utils/cn"; +import { Button } from "../primitives/Buttons"; import { getEditorSetup } from "./codeMirrorSetup"; import { darkTheme } from "./codeMirrorTheme"; -import { cn } from "~/utils/cn"; export interface JSONEditorProps extends Omit { defaultValue?: string; @@ -14,6 +16,8 @@ export interface JSONEditorProps extends Omit { onChange?: (value: string) => void; onUpdate?: (update: ViewUpdate) => void; onBlur?: (code: string) => void; + showCopyButton?: boolean; + showClearButton?: boolean; } const languages = { @@ -38,6 +42,8 @@ export function JSONEditor(opts: JSONEditorProps) { onBlur, basicSetup, autoFocus, + showCopyButton = true, + showClearButton = true, } = { ...defaultProps, ...opts, @@ -65,7 +71,8 @@ export function JSONEditor(opts: JSONEditorProps) { onChange, onUpdate, }; - const { setContainer, state } = useCodeMirror(settings); + const { setContainer, view } = useCodeMirror(settings); + const [copied, setCopied] = useState(false); useEffect(() => { if (editor.current) { @@ -75,24 +82,71 @@ export function JSONEditor(opts: JSONEditorProps) { //if the defaultValue changes update the editor useEffect(() => { - if (state !== undefined) { - state.update({ - changes: { from: 0, to: state.doc.length, insert: defaultValue }, + if (view !== undefined) { + if (view.state.doc.toString() === defaultValue) return; + view.dispatch({ + changes: { from: 0, to: view.state.doc.length, insert: defaultValue }, }); } - }, [defaultValue, state]); + }, [defaultValue, view]); + + const clear = useCallback(() => { + if (view === undefined) return; + view.dispatch({ + changes: { from: 0, to: view.state.doc.length, insert: undefined }, + }); + onChange?.(""); + }, [view]); + + const copy = useCallback(() => { + if (view === undefined) return; + navigator.clipboard.writeText(view.state.doc.toString()); + setCopied(true); + setTimeout(() => { + setCopied(false); + }, 1500); + }, [view]); return ( -
{ - if (!onBlur) return; - onBlur(editor.current?.textContent ?? ""); - }} - /> +
+
{ + if (!onBlur) return; + onBlur(editor.current?.textContent ?? ""); + }} + /> +
+ {showClearButton && ( + + )} + {showCopyButton && ( + + )} +
+
); } diff --git a/apps/webapp/app/components/code/codeMirrorSetup.ts b/apps/webapp/app/components/code/codeMirrorSetup.ts index 9e7ff711df..4931967276 100644 --- a/apps/webapp/app/components/code/codeMirrorSetup.ts +++ b/apps/webapp/app/components/code/codeMirrorSetup.ts @@ -1,34 +1,19 @@ +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 { highlightSelectionMatches } from "@codemirror/search"; +import { Prec, type Extension } from "@codemirror/state"; import { - highlightSpecialChars, drawSelection, - highlightActiveLine, dropCursor, - lineNumbers, + highlightActiveLine, highlightActiveLineGutter, + highlightSpecialChars, keymap, + lineNumbers, } from "@codemirror/view"; -import type { Extension } from "@codemirror/state"; -import { highlightSelectionMatches } from "@codemirror/search"; -import { json as jsonLang } from "@codemirror/lang-json"; -import { closeBrackets } from "@codemirror/autocomplete"; -import { bracketMatching } from "@codemirror/language"; -import { indentWithTab } from "@codemirror/commands"; - -export function getPreviewSetup(): Array { - return [ - jsonLang(), - highlightSpecialChars(), - drawSelection(), - dropCursor(), - bracketMatching(), - highlightSelectionMatches(), - lineNumbers(), - ]; -} - -export function getViewerSetup(): Array { - return [drawSelection(), dropCursor(), bracketMatching(), lineNumbers()]; -} export function getEditorSetup(showLineNumbers = true, showHighlights = true): Array { const options = [ @@ -36,7 +21,21 @@ export function getEditorSetup(showLineNumbers = true, showHighlights = true): A dropCursor(), bracketMatching(), closeBrackets(), - keymap.of([indentWithTab]), + lintGutter(), + linter(jsonParseLinter()), + Prec.highest( + keymap.of([ + { + key: "Mod-Enter", + run: () => { + console.log("Mod-Enter"); + return true; + }, + preventDefault: false, + }, + ]) + ), + keymap.of([indentWithTab, ...lintKeymap]), ]; if (showLineNumbers) { diff --git a/apps/webapp/app/components/code/codeMirrorTheme.ts b/apps/webapp/app/components/code/codeMirrorTheme.ts index 0492b80b93..babcccf09e 100644 --- a/apps/webapp/app/components/code/codeMirrorTheme.ts +++ b/apps/webapp/app/components/code/codeMirrorTheme.ts @@ -17,10 +17,15 @@ export function darkTheme(): Extension { violet = "#c678dd", darkBackground = "#21252b", highlightBackground = "rgba(71,85,105,0.2)", - background = "#0f172a", + background = "rgba(11, 16, 24 ,100)", tooltipBackground = "#353a42", selection = "rgb(71 85 105)", - cursor = "#528bff"; + cursor = "#528bff", + scrollbarTrack = "#0E1521", + scrollbarTrackActive = "#131B2B", + scrollbarThumb = "#293649", + scrollbarThumbActive = "#3C4B62", + scrollbarBg = "#0E1521"; const jsonHeroEditorTheme = EditorView.theme( { @@ -94,6 +99,45 @@ export function darkTheme(): Extension { color: ivory, }, }, + ".cm-scroller": { + scrollbarWidth: "thin", + scrollbarColor: `${scrollbarThumb} ${scrollbarTrack}`, + }, + ".cm-scroller::-webkit-scrollbar": { + display: "block", + width: "8px", + height: "8px", + }, + ".cm-scroller::-webkit-scrollbar-track": { + backgroundColor: scrollbarTrack, + borderRadius: "0", + }, + ".cm-scroller::-webkit-scrollbar-track:hover": { + backgroundColor: scrollbarTrackActive, + }, + ".cm-scroller::-webkit-scrollbar-track:active": { + backgroundColor: scrollbarTrackActive, + }, + ".cm-scroller::-webkit-scrollbar-thumb": { + backgroundColor: scrollbarThumb, + borderRadius: "0", + }, + ".cm-scroller::-webkit-scrollbar-thumb:hover": { + backgroundColor: scrollbarThumbActive, + }, + ".cm-scroller::-webkit-scrollbar-thumb:active": { + backgroundColor: scrollbarThumbActive, + }, + ".cm-scroller::-webkit-scrollbar-corner": { + backgroundColor: scrollbarBg, + borderRadius: "0", + }, + ".cm-scroller::-webkit-scrollbar-corner:hover": { + backgroundColor: scrollbarBg, + }, + ".cm-scroller::-webkit-scrollbar-corner:active": { + backgroundColor: scrollbarBg, + }, }, { dark: true } ); @@ -155,157 +199,3 @@ export function darkTheme(): Extension { return [jsonHeroEditorTheme, syntaxHighlighting(jsonHeroHighlightStyle)]; } - -export function lightTheme(): Extension[] { - const stringColor = "text-[#53a053]", - numberColor = "text-[#447bef]", - variableColor = "text-[#a42ea2]", - booleanColor = "text-[#e2574e]", - coral = "text-[#e06c75]", - invalid = "text-[#ffffff]", - ivory = "text-[#abb2bf]", - stone = "text-[#7d8799]", - malibu = "text-[#61afef]", - whiskey = "text-[#d19a66]", - violet = "text-[#c678dd]", - darkBackground = "text-[#21252b]", - highlightBackground = "text-[#D0D0D0]", - background = "text-[#ffffff]", - tooltipBackground = "text-[#353a42]", - selection = "text-[#D0D0D0]", - cursor = "text-[#528bff]"; - - const jsonHeroEditorTheme = EditorView.theme( - { - "&": { - color: ivory, - backgroundColor: background, - }, - - ".cm-content": { - caretColor: cursor, - fontFamily: "monospace", - fontSize: "14px", - }, - - ".cm-cursor, .cm-dropCursor": { borderLeftColor: cursor }, - "&.cm-focused .cm-selectionBackground, .cm-selectionBackground, .cm-content ::selection": { - backgroundColor: selection, - }, - - ".cm-panels": { backgroundColor: darkBackground, color: ivory }, - ".cm-panels.cm-panels-top": { borderBottom: "2px solid black" }, - ".cm-panels.cm-panels-bottom": { borderTop: "2px solid black" }, - - ".cm-searchMatch": { - backgroundColor: "#72a1ff59", - outline: "1px solid #457dff", - }, - ".cm-searchMatch.cm-searchMatch-selected": { - backgroundColor: "#6199ff2f", - }, - - ".cm-activeLine": { backgroundColor: highlightBackground }, - ".cm-selectionMatch": { backgroundColor: "#aafe661a" }, - - "&.cm-focused .cm-matchingBracket, &.cm-focused .cm-nonmatchingBracket": { - backgroundColor: "#bad0f847", - outline: "1px solid #515a6b", - }, - - ".cm-gutters": { - backgroundColor: background, - color: stone, - border: "none", - }, - - ".cm-activeLineGutter": { - backgroundColor: highlightBackground, - }, - - ".cm-foldPlaceholder": { - backgroundColor: "transparent", - border: "none", - color: "#ddd", - }, - - ".cm-tooltip": { - border: "none", - backgroundColor: tooltipBackground, - }, - ".cm-tooltip .cm-tooltip-arrow:before": { - borderTopColor: "transparent", - borderBottomColor: "transparent", - }, - ".cm-tooltip .cm-tooltip-arrow:after": { - borderTopColor: tooltipBackground, - borderBottomColor: tooltipBackground, - }, - ".cm-tooltip-autocomplete": { - "& > ul > li[aria-selected]": { - backgroundColor: highlightBackground, - color: ivory, - }, - }, - }, - { dark: false } - ); - - /// The highlighting style for code in the JSON Hero theme. - const jsonHeroHighlightStyle = tagHighlighter([ - { tag: tags.keyword, class: violet }, - { - tag: [tags.name, tags.deleted, tags.character, tags.propertyName, tags.macroName], - class: variableColor, - }, - { - tag: [tags.function(tags.variableName), tags.labelName], - class: malibu, - }, - { - tag: [tags.color, tags.constant(tags.name), tags.standard(tags.name)], - class: whiskey, - }, - { tag: [tags.definition(tags.name), tags.separator], class: ivory }, - { - tag: [ - tags.typeName, - tags.className, - tags.number, - tags.changed, - tags.annotation, - tags.modifier, - tags.self, - tags.namespace, - ], - class: numberColor, - }, - { - tag: [ - tags.operator, - tags.operatorKeyword, - tags.url, - tags.escape, - tags.regexp, - tags.link, - tags.special(tags.string), - ], - class: stringColor, - }, - { tag: [tags.meta, tags.comment], class: stone }, - - { tag: tags.link, class: stone }, - { tag: tags.heading, class: coral }, - { - tag: [tags.atom, tags.bool, tags.special(tags.variableName)], - class: booleanColor, - }, - { - tag: [tags.processingInstruction, tags.string, tags.inserted], - class: stringColor, - }, - { tag: tags.invalid, class: invalid }, - ]); - - return [jsonHeroEditorTheme, syntaxHighlighting(jsonHeroHighlightStyle)]; -} diff --git a/apps/webapp/app/components/helpContent/HelpContentText.tsx b/apps/webapp/app/components/helpContent/HelpContentText.tsx index 8e9a45871c..ce057dae48 100644 --- a/apps/webapp/app/components/helpContent/HelpContentText.tsx +++ b/apps/webapp/app/components/helpContent/HelpContentText.tsx @@ -79,37 +79,6 @@ export function HowToRunYourJob() { ); } -export function HowToRunATest() { - return ( - <> - - - Select the environment you’d like the test to run against. - - - - - - Write your own payload specific to your Job. Some Triggers also provide example payloads - that you can select from. This will populate the code editor below. - - - - - - When you’re happy with the payload, click Run test. - - - Learn more about running tests. - - - ); -} - export function HowToConnectAnIntegration() { return ( <> diff --git a/apps/webapp/app/components/primitives/Buttons.tsx b/apps/webapp/app/components/primitives/Buttons.tsx index 532d368367..e469c2561b 100644 --- a/apps/webapp/app/components/primitives/Buttons.tsx +++ b/apps/webapp/app/components/primitives/Buttons.tsx @@ -144,7 +144,7 @@ export function ButtonContent(props: ButtonContentPropsType) { const textColorClassName = variation.textColor; return ( -
+
{ }; function formatDateTimeAccurate(date: Date, timeZone: string, locales: string[]): string { - const milliseconds = `00${date.getMilliseconds()}`.slice(-3); - const formattedDateTime = new Intl.DateTimeFormat(locales, { + year: "numeric", month: "short", - day: "2-digit", + day: "numeric", hour: "numeric", - minute: "2-digit", - second: "2-digit", + minute: "numeric", + second: "numeric", timeZone, + // @ts-ignore this works in 92.5% of browsers https://caniuse.com/mdn-javascript_builtins_intl_datetimeformat_datetimeformat_options_parameter_options_fractionalseconddigits_parameter + fractionalSecondDigits: 3, }).format(date); - return `${formatDateTime}.${milliseconds}`; + return formattedDateTime; } diff --git a/apps/webapp/app/components/primitives/DetailCell.tsx b/apps/webapp/app/components/primitives/DetailCell.tsx new file mode 100644 index 0000000000..6047cc150f --- /dev/null +++ b/apps/webapp/app/components/primitives/DetailCell.tsx @@ -0,0 +1,95 @@ +import { cn } from "~/utils/cn"; +import { Icon, IconInBox, RenderIcon } from "./Icon"; +import { Paragraph } from "./Paragraph"; + +const variations = { + small: { + label: { + variant: "small" as const, + className: "m-0 leading-[1.1rem]", + }, + description: { + variant: "extra-small" as const, + className: "m-0", + }, + }, + base: { + label: { + variant: "base" as const, + className: "m-0 leading-[1.1rem] ", + }, + description: { + variant: "small" as const, + className: "m-0", + }, + }, +}; + +type DetailCellProps = { + leadingIcon?: RenderIcon; + leadingIconClassName?: string; + trailingIcon?: RenderIcon; + trailingIconClassName?: string; + label: string | React.ReactNode; + description?: string | React.ReactNode; + className?: string; + variant?: keyof typeof variations; +}; + +export function DetailCell({ + leadingIcon, + leadingIconClassName, + trailingIcon, + trailingIconClassName, + label, + description, + className, + variant = "small", +}: DetailCellProps) { + const variation = variations[variant]; + + return ( +
+ +
+ + {label} + + {description && ( + + {description} + + )} +
+
+ +
+
+ ); +} diff --git a/apps/webapp/app/components/primitives/Icon.tsx b/apps/webapp/app/components/primitives/Icon.tsx new file mode 100644 index 0000000000..470d4cc809 --- /dev/null +++ b/apps/webapp/app/components/primitives/Icon.tsx @@ -0,0 +1,37 @@ +import { IconNamesOrString, NamedIcon } from "./NamedIcon"; +import { cn } from "~/utils/cn"; + +export type RenderIcon = IconNamesOrString | React.ComponentType; + +type IconProps = { + icon?: RenderIcon; + className?: string; +}; + +/** Use this icon to either render a passed in React component, or a NamedIcon/CompanyIcon */ +export function Icon(props: IconProps) { + if (typeof props.icon === "string") { + return } />; + } + + const Icon = props.icon; + + if (!Icon) { + return <>; + } + + return ; +} + +export function IconInBox({ boxClassName, ...props }: IconProps & { boxClassName?: string }) { + return ( +
+ +
+ ); +} diff --git a/apps/webapp/app/components/primitives/ShortcutKey.tsx b/apps/webapp/app/components/primitives/ShortcutKey.tsx index ff5d4f5f70..8be993c12e 100644 --- a/apps/webapp/app/components/primitives/ShortcutKey.tsx +++ b/apps/webapp/app/components/primitives/ShortcutKey.tsx @@ -23,7 +23,7 @@ export function ShortcutKey({ shortcut, variant, className }: ShortcutKeyProps) const isMac = platform === "mac"; let relevantShortcut = "mac" in shortcut ? (isMac ? shortcut.mac : shortcut.windows) : shortcut; const modifiers = relevantShortcut.modifiers ?? []; - const character = relevantShortcut.key; + const character = keyString(relevantShortcut.key, isMac); return ( @@ -35,6 +35,15 @@ export function ShortcutKey({ shortcut, variant, className }: ShortcutKeyProps) ); } +function keyString(key: String, isMac: boolean) { + switch (key) { + case "enter": + return isMac ? "↵" : key; + default: + return key; + } +} + function modifierString(modifier: Modifier, isMac: boolean) { switch (modifier) { case "alt": @@ -42,8 +51,10 @@ function modifierString(modifier: Modifier, isMac: boolean) { case "ctrl": return isMac ? "⌃" : "Ctrl+"; case "meta": - return isMac ? "⌘" : "⊞"; + return isMac ? "⌘" : "⊞+"; case "shift": return isMac ? "⇧" : "Shift+"; + case "mod": + return isMac ? "⌘" : "Ctrl+"; } } diff --git a/apps/webapp/app/components/stories/DetailCell.stories.tsx b/apps/webapp/app/components/stories/DetailCell.stories.tsx new file mode 100644 index 0000000000..7f70e01a16 --- /dev/null +++ b/apps/webapp/app/components/stories/DetailCell.stories.tsx @@ -0,0 +1,46 @@ +import type { Meta, StoryObj } from "@storybook/react"; +import { DetailCell } from "../primitives/DetailCell"; +import { ClockIcon, CodeBracketIcon } from "@heroicons/react/24/outline"; +import { DateTime, DateTimeAccurate } from "../primitives/DateTime"; + +const meta: Meta = { + title: "Primitives/DetailCells", +}; + +export default meta; + +type Story = StoryObj; + +export const Basic: Story = { + render: () => , +}; + +function Examples() { + return ( +
+ + + } + description="Run #42 complete" + trailingIcon="plus" + trailingIconClassName="text-slate-500 group-hover:text-bright" + /> +
+ ); +} diff --git a/apps/webapp/app/components/stories/Shortcuts.stories.tsx b/apps/webapp/app/components/stories/Shortcuts.stories.tsx index a5ed28866a..dc2f381a75 100644 --- a/apps/webapp/app/components/stories/Shortcuts.stories.tsx +++ b/apps/webapp/app/components/stories/Shortcuts.stories.tsx @@ -24,6 +24,8 @@ const shortcuts: ShortcutDefinition[] = [ { key: "f", modifiers: ["meta"] }, { key: "k", modifiers: ["meta"] }, { key: "del", modifiers: ["ctrl", "alt"] }, + { key: "enter", modifiers: ["meta"] }, + { key: "enter", modifiers: ["mod"] }, ]; function Collection() { @@ -67,6 +69,9 @@ function Set({ platform }: { platform: "mac" | "windows" }) { +
))} diff --git a/apps/webapp/app/hooks/useShortcutKeys.tsx b/apps/webapp/app/hooks/useShortcutKeys.tsx index ccbebf618e..092e163967 100644 --- a/apps/webapp/app/hooks/useShortcutKeys.tsx +++ b/apps/webapp/app/hooks/useShortcutKeys.tsx @@ -1,12 +1,12 @@ -import { useEffect, useState } from "react"; import { useHotkeys } from "react-hotkeys-hook"; import { useOperatingSystem } from "~/components/primitives/OperatingSystemProvider"; -export type Modifier = "alt" | "ctrl" | "meta" | "shift"; +export type Modifier = "alt" | "ctrl" | "meta" | "shift" | "mod"; export type Shortcut = { key: string; modifiers?: Modifier[]; + enabledOnInputElements?: boolean; }; export type ShortcutDefinition = @@ -20,19 +20,31 @@ type useShortcutKeysProps = { shortcut: ShortcutDefinition; action: (event: KeyboardEvent) => void; disabled?: boolean; + enabledOnInputElements?: boolean; }; export function useShortcutKeys({ shortcut, action, disabled = false }: useShortcutKeysProps) { - const keys = createKeysFromShortcut(shortcut); - useHotkeys(keys, action, { enabled: !disabled }); -} - -function createKeysFromShortcut(shortcut: ShortcutDefinition) { const { platform } = useOperatingSystem(); const isMac = platform === "mac"; - let relevantShortcut = "mac" in shortcut ? (isMac ? shortcut.mac : shortcut.windows) : shortcut; - const modifiers = relevantShortcut.modifiers; - const character = relevantShortcut.key; + const relevantShortcut = "mac" in shortcut ? (isMac ? shortcut.mac : shortcut.windows) : shortcut; + + const keys = createKeysFromShortcut(relevantShortcut); + useHotkeys( + keys, + (event, hotkeysEvent) => { + action(event); + }, + { + enabled: !disabled, + enableOnFormTags: relevantShortcut.enabledOnInputElements, + enableOnContentEditable: relevantShortcut.enabledOnInputElements, + } + ); +} + +function createKeysFromShortcut(shortcut: Shortcut) { + const modifiers = shortcut.modifiers; + const character = shortcut.key; - return modifiers ? modifiers.map((k) => k).join("+") + "+" : "" + character; + return modifiers ? modifiers.map((k) => k).join("+") + "+" + character : character; } diff --git a/apps/webapp/app/presenters/TestJobPresenter.server.ts b/apps/webapp/app/presenters/TestJobPresenter.server.ts index 3028199e2b..c87b035c8d 100644 --- a/apps/webapp/app/presenters/TestJobPresenter.server.ts +++ b/apps/webapp/app/presenters/TestJobPresenter.server.ts @@ -4,6 +4,7 @@ import { PrismaClient, prisma } from "~/db.server"; import { Job } from "~/models/job.server"; import { Organization } from "~/models/organization.server"; import { Project } from "~/models/project.server"; +import { EventExample } from "@trigger.dev/core"; export class TestJobPresenter { #prismaClient: PrismaClient; @@ -67,14 +68,22 @@ export class TestJobPresenter { name: "latest", }, }, - _count: { + runs: { select: { - runs: { - where: { - isTest: true, + id: true, + createdAt: true, + number: true, + status: true, + event: { + select: { + payload: true, }, }, }, + orderBy: { + createdAt: "desc", + }, + take: 5, }, }, where: { @@ -97,6 +106,15 @@ export class TestJobPresenter { throw new Error("Job not found"); } + //collect together the examples, we don't care about the environments + const examples = job.aliases.flatMap((alias) => + alias.version.examples.map((example) => ({ + ...example, + icon: example.icon ?? undefined, + payload: example.payload ? JSON.stringify(example.payload, exampleReplacer, 2) : undefined, + })) + ); + return { environments: job.aliases.map((alias) => ({ id: alias.environment.id, @@ -104,15 +122,18 @@ export class TestJobPresenter { slug: alias.environment.slug, userId: alias.environment.orgMember?.userId, versionId: alias.version.id, - examples: alias.version.examples.map((example) => ({ - ...example, - payload: JSON.stringify(example.payload, exampleReplacer, 2), - })), hasAuthResolver: alias.version.integrations.some( (i) => i.integration.authSource === "RESOLVER" ), })), - hasTestRuns: job._count.runs > 0, + examples, + runs: job.runs.map((r) => ({ + id: r.id, + number: r.number, + status: r.status, + created: r.createdAt, + payload: r.event.payload ? JSON.stringify(r.event.payload, null, 2) : undefined, + })), }; } } diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.integrations/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.integrations/route.tsx index 1c653370a0..8a2535a2b1 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.integrations/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.integrations/route.tsx @@ -13,6 +13,7 @@ import { BreadcrumbLink } from "~/components/navigation/NavBar"; import { LinkButton } from "~/components/primitives/Buttons"; import { Callout } from "~/components/primitives/Callout"; import { DateTime } from "~/components/primitives/DateTime"; +import { DetailCell } from "~/components/primitives/DetailCell"; import { Header2 } from "~/components/primitives/Headers"; import { Help, HelpContent, HelpTrigger } from "~/components/primitives/Help"; import { Input } from "~/components/primitives/Input"; @@ -209,10 +210,12 @@ function PossibleIntegrationsList({ - } @@ -221,10 +224,12 @@ function PossibleIntegrationsList({ Create an Integration -
@@ -482,77 +487,16 @@ function AddIntegrationConnection({ icon?: string; }) { return ( -
- - - {name} - -
- {isIntegration && } - -
-
- ); -} - -function ExternalIntegrationLink({ - name, - label, - trailingIcon, -}: { - name: string; - label: string; - trailingIcon: string; -}) { - return ( - - - - {label} - -
- -
-
+ ); } export function IntegrationIcon() { return ; } - -function InfoLink({ text }: { text: string }) { - return ( -
- - - {text} - -
- -
-
- ); -} diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.jobs.$jobParam.test/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.jobs.$jobParam.test/route.tsx index 21917e7bfe..f3dc9a36f0 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.jobs.$jobParam.test/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.jobs.$jobParam.test/route.tsx @@ -1,6 +1,7 @@ -import { conform, useForm } from "@conform-to/react"; +import { useForm } from "@conform-to/react"; import { parse } from "@conform-to/zod"; -import { PopoverTrigger } from "@radix-ui/react-popover"; +import { ClipboardIcon } from "@heroicons/react/20/solid"; +import { ClockIcon, CodeBracketIcon } from "@heroicons/react/24/outline"; import { Form, useActionData, useSubmit } from "@remix-run/react"; import { ActionFunction, LoaderArgs, json } from "@remix-run/server-runtime"; import { useCallback, useRef, useState } from "react"; @@ -8,16 +9,16 @@ import { typedjson, useTypedLoaderData } from "remix-typedjson"; import { z } from "zod"; import { JSONEditor } from "~/components/code/JSONEditor"; import { EnvironmentLabel } from "~/components/environments/EnvironmentLabel"; -import { HowToRunATest } from "~/components/helpContent/HelpContentText"; import { BreadcrumbLink } from "~/components/navigation/NavBar"; -import { Button, ButtonContent } from "~/components/primitives/Buttons"; +import { Button, LinkButton } from "~/components/primitives/Buttons"; import { Callout } from "~/components/primitives/Callout"; +import { DateTime } from "~/components/primitives/DateTime"; +import { DetailCell } from "~/components/primitives/DetailCell"; import { FormError } from "~/components/primitives/FormError"; -import { Help, HelpContent, HelpTrigger } from "~/components/primitives/Help"; +import { Header2 } from "~/components/primitives/Headers"; +import { Hint } from "~/components/primitives/Hint"; import { Input } from "~/components/primitives/Input"; import { InputGroup } from "~/components/primitives/InputGroup"; -import { Label } from "~/components/primitives/Label"; -import { Popover, PopoverContent } from "~/components/primitives/Popover"; import { Select, SelectContent, @@ -26,6 +27,8 @@ import { SelectTrigger, SelectValue, } from "~/components/primitives/Select"; +import { TextLink } from "~/components/primitives/TextLink"; +import { runStatusClassNameColor, runStatusTitle } from "~/components/runs/RunStatuses"; import { redirectBackWithErrorMessage, redirectWithSuccessMessage } from "~/models/message.server"; import { TestJobPresenter } from "~/presenters/TestJobPresenter.server"; import { TestJobService } from "~/services/jobs/testJob.server"; @@ -39,14 +42,14 @@ export const loader = async ({ request, params }: LoaderArgs) => { const { organizationSlug, projectParam, jobParam } = JobParamsSchema.parse(params); const presenter = new TestJobPresenter(); - const { environments, hasTestRuns } = await presenter.call({ + const { environments, runs, examples } = await presenter.call({ userId, organizationSlug, projectSlug: projectParam, jobSlug: jobParam, }); - return typedjson({ environments, hasTestRuns }); + return typedjson({ environments, runs, examples }); }; const schema = z.object({ @@ -116,23 +119,31 @@ export const handle: Handle = { const startingJson = "{\n\n}"; export default function Page() { + const { environments, runs, examples } = useTypedLoaderData(); + + //form submission const submit = useSubmit(); const lastSubmission = useActionData(); - const [isExamplePopoverOpen, setIsExamplePopoverOpen] = useState(false); - const { environments, hasTestRuns } = useTypedLoaderData(); - const [defaultJson, setDefaultJson] = useState(startingJson); - const currentJson = useRef(defaultJson); - const [selectedEnvironmentId, setSelectedEnvironmentId] = useState(environments[0].id); - const [currentAccountId, setCurrentAccountId] = useState(undefined); - - const selectedEnvironment = environments.find((e) => e.id === selectedEnvironmentId); + //examples + const [selectedCodeSampleId, setSelectedCodeSampleId] = useState( + examples.at(0)?.id ?? runs.at(0)?.id + ); + const selectedCodeSample = + examples.find((e) => e.id === selectedCodeSampleId)?.payload ?? + runs.find((r) => r.id === selectedCodeSampleId)?.payload; - const insertCode = useCallback((code: string) => { + const [defaultJson, setDefaultJson] = useState(selectedCodeSample ?? startingJson); + const setCode = useCallback((code: string) => { setDefaultJson(code); - setIsExamplePopoverOpen(false); }, []); + const [selectedEnvironmentId, setSelectedEnvironmentId] = useState(environments[0].id); + const selectedEnvironment = environments.find((e) => e.id === selectedEnvironmentId); + + const currentJson = useRef(defaultJson); + const [currentAccountId, setCurrentAccountId] = useState(undefined); + const submitForm = useCallback( (e: React.FormEvent) => { submit( @@ -170,120 +181,178 @@ export default function Page() { } return ( - - {(open) => ( -
-
-
submitForm(e)} - > -
-
- - - +
+
+ submitForm(e)} + > +
+
+ { + currentJson.current = v; - {selectedEnvironment && selectedEnvironment.examples.length > 0 && ( - setIsExamplePopoverOpen(open)} + //deselect the example if it's been edited + if (selectedCodeSampleId) { + if (v !== selectedCodeSample) { + setDefaultJson(v); + setSelectedCodeSampleId(undefined); + } + } + }} + height="100%" + min-height="100%" + max-height="100%" + autoFocus + placeholder="Use your schema to enter valid JSON or add one of the example payloads then click 'Run test'" + className="h-full" + /> +
+
+ {examples.length > 0 && ( +
+ Example payloads + {examples.map((example) => ( + - ))} - - - )} + + + ))}
- + )} +
+ Recent payloads + {runs.length === 0 ? ( + + Recent payloads will show here once you've completed a Run. + + ) : ( +
+ {runs.map((run) => ( + + ))} +
+ )}
- - -
- (currentJson.current = v)} - minHeight="150px" - /> -
-
{selectedEnvironment?.hasAuthResolver && ( - - - setCurrentAccountId(e.target.value)} - /> - {accountId.error} - +
+ Account ID + + setCurrentAccountId(e.target.value)} + /> + {accountId.error} + + Learn about testing Jobs with an Account ID in our{" "} + + BYOAuth docs + + + +
)} -
- {payload.error ? ( - {payload.error} - ) : ( -
- )} -
+
+ + Learn more about running tests + +
+ {payload.error ? ( + {payload.error} + ) : ( +
+ )} + + + + +
- - - -
- )} - + +
+
); } diff --git a/apps/webapp/package.json b/apps/webapp/package.json index 2e90e4b22e..a7576143e7 100644 --- a/apps/webapp/package.json +++ b/apps/webapp/package.json @@ -34,6 +34,7 @@ "@codemirror/lang-javascript": "^6.1.1", "@codemirror/lang-json": "^6.0.1", "@codemirror/language": "^6.3.1", + "@codemirror/lint": "^6.4.2", "@codemirror/search": "^6.2.3", "@codemirror/state": "^6.1.3", "@codemirror/view": "^6.5.0", @@ -93,7 +94,7 @@ "react": "^18.2.0", "react-dom": "^18.2.0", "react-hot-toast": "^2.4.0", - "react-hotkeys-hook": "^3.4.7", + "react-hotkeys-hook": "^4.4.1", "react-use": "^17.4.0", "recharts": "^2.8.0", "remix-auth": "^3.2.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 100a4df48c..1fc5501a66 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -59,6 +59,7 @@ importers: '@codemirror/lang-javascript': ^6.1.1 '@codemirror/lang-json': ^6.0.1 '@codemirror/language': ^6.3.1 + '@codemirror/lint': ^6.4.2 '@codemirror/search': ^6.2.3 '@codemirror/state': ^6.1.3 '@codemirror/view': ^6.5.0 @@ -168,7 +169,7 @@ importers: react: ^18.2.0 react-dom: ^18.2.0 react-hot-toast: ^2.4.0 - react-hotkeys-hook: ^3.4.7 + react-hotkeys-hook: ^4.4.1 react-use: ^17.4.0 recharts: ^2.8.0 remix-auth: ^3.2.2 @@ -202,6 +203,7 @@ importers: '@codemirror/lang-javascript': 6.1.2 '@codemirror/lang-json': 6.0.1 '@codemirror/language': 6.3.2 + '@codemirror/lint': 6.4.2 '@codemirror/search': 6.2.3 '@codemirror/state': 6.2.0 '@codemirror/view': 6.7.2 @@ -232,7 +234,7 @@ importers: '@trigger.dev/core': link:../../packages/core '@trigger.dev/database': link:../../packages/database '@trigger.dev/sdk': link:../../packages/trigger-sdk - '@uiw/react-codemirror': 4.19.5_k4ec5g7vuuzzonc3d6xbjnmmle + '@uiw/react-codemirror': 4.19.5_th22fcplkuhrqjnlojwclcaim4 class-variance-authority: 0.5.2_typescript@4.9.4 clsx: 1.2.1 compression: 1.7.4 @@ -261,7 +263,7 @@ importers: react: 18.2.0 react-dom: 18.2.0_react@18.2.0 react-hot-toast: 2.4.0_biqbaboplfbrettd7655fr4n2y - react-hotkeys-hook: 3.4.7_biqbaboplfbrettd7655fr4n2y + react-hotkeys-hook: 4.4.1_biqbaboplfbrettd7655fr4n2y react-use: 17.4.0_biqbaboplfbrettd7655fr4n2y recharts: 2.8.0_v2m5e27vhdewzwhryxwfaorcca remix-auth: 3.4.0_mrckq3wlqfipa3hs7ezq3k3x3y @@ -5332,7 +5334,7 @@ packages: dependencies: '@codemirror/autocomplete': 6.4.0_czcfkg2f66rxeiodoti7r2gulu '@codemirror/language': 6.3.2 - '@codemirror/lint': 6.1.0 + '@codemirror/lint': 6.4.2 '@codemirror/state': 6.2.0 '@codemirror/view': 6.7.2 '@lezer/common': 1.0.2 @@ -5357,8 +5359,8 @@ packages: style-mod: 4.0.0 dev: false - /@codemirror/lint/6.1.0: - resolution: {integrity: sha512-mdvDQrjRmYPvQ3WrzF6Ewaao+NWERYtpthJvoQ3tK3t/44Ynhk8ZGjTSL9jMEv8CgSMogmt75X8ceOZRDSXHtQ==} + /@codemirror/lint/6.4.2: + resolution: {integrity: sha512-wzRkluWb1ptPKdzlsrbwwjYCPLgzU6N88YBAmlZi8WFyuiEduSd05MnJYNogzyc8rPK7pj6m95ptUApc8sHKVA==} dependencies: '@codemirror/state': 6.2.0 '@codemirror/view': 6.7.2 @@ -13653,12 +13655,13 @@ packages: '@typescript-eslint/types': 5.59.6 eslint-visitor-keys: 3.4.2 - /@uiw/codemirror-extensions-basic-setup/4.19.5_wd2tsis3in55bkaiwnc2c46tom: + /@uiw/codemirror-extensions-basic-setup/4.19.5_o3n2erwajrogdzfxqd6wu4qkza: resolution: {integrity: sha512-1zt7ZPJ01xKkSW/KDy0FZNga0bngN1fC594wCVG7FBi60ehfcAucpooQ+JSPScKXopxcb+ugPKZvVLzr9/OfzA==} peerDependencies: '@codemirror/autocomplete': '>=6.0.0' '@codemirror/commands': '>=6.0.0' '@codemirror/language': '>=6.0.0' + '@codemirror/lint': '>=6.0.0' '@codemirror/search': '>=6.0.0' '@codemirror/state': '>=6.0.0' '@codemirror/view': '>=6.0.0' @@ -13666,13 +13669,13 @@ packages: '@codemirror/autocomplete': 6.4.0_czcfkg2f66rxeiodoti7r2gulu '@codemirror/commands': 6.1.3 '@codemirror/language': 6.3.2 - '@codemirror/lint': 6.1.0 + '@codemirror/lint': 6.4.2 '@codemirror/search': 6.2.3 '@codemirror/state': 6.2.0 '@codemirror/view': 6.7.2 dev: false - /@uiw/react-codemirror/4.19.5_k4ec5g7vuuzzonc3d6xbjnmmle: + /@uiw/react-codemirror/4.19.5_th22fcplkuhrqjnlojwclcaim4: resolution: {integrity: sha512-ZCHh8d7beXbF8/t7F1+yHht6A9Y6CdKeOkZq4A09lxJEnyTQrj1FMf2zvfaqc7K23KNjkTCtSlbqKKbVDgrWaw==} peerDependencies: '@codemirror/state': '>=6.0.0' @@ -13685,13 +13688,14 @@ packages: '@codemirror/state': 6.2.0 '@codemirror/theme-one-dark': 6.1.0 '@codemirror/view': 6.7.2 - '@uiw/codemirror-extensions-basic-setup': 4.19.5_wd2tsis3in55bkaiwnc2c46tom + '@uiw/codemirror-extensions-basic-setup': 4.19.5_o3n2erwajrogdzfxqd6wu4qkza codemirror: 6.0.1 react: 18.2.0 react-dom: 18.2.0_react@18.2.0 transitivePeerDependencies: - '@codemirror/autocomplete' - '@codemirror/language' + - '@codemirror/lint' - '@codemirror/search' dev: false @@ -15754,7 +15758,7 @@ packages: '@codemirror/autocomplete': 6.4.0_czcfkg2f66rxeiodoti7r2gulu '@codemirror/commands': 6.1.3 '@codemirror/language': 6.3.2 - '@codemirror/lint': 6.1.0 + '@codemirror/lint': 6.4.2 '@codemirror/search': 6.2.3 '@codemirror/state': 6.2.0 '@codemirror/view': 6.7.2 @@ -20182,10 +20186,6 @@ packages: lru-cache: 7.18.3 dev: false - /hotkeys-js/3.9.4: - resolution: {integrity: sha512-2zuLt85Ta+gIyvs4N88pCYskNrxf1TFv3LR9t5mdAZIX8BcgQQ48F2opUptvHa6m8zsy5v/a0i9mWzTrlNWU0Q==} - dev: false - /hpagent/0.1.2: resolution: {integrity: sha512-ePqFXHtSQWAFXYmj+JtOTHr84iNrII4/QRlAAPPE+zqnKy4xJo7Ie1Y4kC7AdB+LxLxSTTzBMASsEcy0q8YyvQ==} requiresBuild: true @@ -25774,13 +25774,12 @@ packages: - csstype dev: false - /react-hotkeys-hook/3.4.7_biqbaboplfbrettd7655fr4n2y: - resolution: {integrity: sha512-+bbPmhPAl6ns9VkXkNNyxlmCAIyDAcWbB76O4I0ntr3uWCRuIQf/aRLartUahe9chVMPj+OEzzfk3CQSjclUEQ==} + /react-hotkeys-hook/4.4.1_biqbaboplfbrettd7655fr4n2y: + resolution: {integrity: sha512-sClBMBioFEgFGYLTWWRKvhxcCx1DRznd+wkFHwQZspnRBkHTgruKIHptlK/U/2DPX8BhHoRGzpMVWUXMmdZlmw==} peerDependencies: react: '>=16.8.1' react-dom: '>=16.8.1' dependencies: - hotkeys-js: 3.9.4 react: 18.2.0 react-dom: 18.2.0_react@18.2.0 dev: false diff --git a/references/job-catalog/src/byo-auth.ts b/references/job-catalog/src/byo-auth.ts index e77786bb13..b739f69127 100644 --- a/references/job-catalog/src/byo-auth.ts +++ b/references/job-catalog/src/byo-auth.ts @@ -23,7 +23,7 @@ const stripe = new Stripe({ }); const slack = new Slack({ id: "slack" }); const openai = new OpenAI({ id: "openai" }); -const github = new Github({ id: "github" }); +const github = new Github({ id: "github-byoa" }); client.defineAuthResolver(resend, async (ctx, integration) => { return {