@checkstack/ui 1.10.0 → 1.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/.storybook/main.ts +43 -0
  2. package/CHANGELOG.md +565 -0
  3. package/package.json +15 -7
  4. package/scripts/generate-stdlib-types.ts +25 -2
  5. package/src/components/ActionCard.tsx +309 -0
  6. package/src/components/CodeEditor/CodeEditor.tsx +132 -9
  7. package/src/components/CodeEditor/TypefoxEditor.tsx +1024 -0
  8. package/src/components/CodeEditor/bracketKeyGroups.test.ts +120 -0
  9. package/src/components/CodeEditor/bracketKeyGroups.ts +205 -0
  10. package/src/components/CodeEditor/generateTypeDefinitions.ts +4 -4
  11. package/src/components/CodeEditor/generated/builtin-modules.json +1 -0
  12. package/src/components/CodeEditor/importSpecifiers.test.ts +286 -0
  13. package/src/components/CodeEditor/importSpecifiers.ts +267 -0
  14. package/src/components/CodeEditor/index.ts +26 -0
  15. package/src/components/CodeEditor/monacoTsService.ts +217 -0
  16. package/src/components/CodeEditor/popoutTitle.test.ts +37 -0
  17. package/src/components/CodeEditor/popoutTitle.ts +31 -0
  18. package/src/components/CodeEditor/scriptContext.test.ts +41 -0
  19. package/src/components/CodeEditor/scriptContext.ts +76 -1
  20. package/src/components/CodeEditor/scriptDiagnostics.test.ts +135 -0
  21. package/src/components/CodeEditor/scriptDiagnostics.ts +172 -0
  22. package/src/components/CodeEditor/templateValidation.ts +51 -0
  23. package/src/components/CodeEditor/types.ts +168 -0
  24. package/src/components/CodeEditor/validateJsonTemplate.test.ts +61 -0
  25. package/src/components/CodeEditor/validateJsonTemplate.ts +26 -0
  26. package/src/components/CodeEditor/validateScripts.ts +132 -0
  27. package/src/components/CodeEditor/validateXmlTemplate.test.ts +34 -0
  28. package/src/components/CodeEditor/validateXmlTemplate.ts +35 -0
  29. package/src/components/CodeEditor/validateYamlTemplate.test.ts +39 -0
  30. package/src/components/CodeEditor/validateYamlTemplate.ts +28 -0
  31. package/src/components/Dialog.tsx +32 -11
  32. package/src/components/DurationInput.tsx +121 -0
  33. package/src/components/DynamicForm/DynamicForm.tsx +27 -1
  34. package/src/components/DynamicForm/FormField.tsx +138 -10
  35. package/src/components/DynamicForm/KeyValueEditor.tsx +2 -169
  36. package/src/components/DynamicForm/MultiTypeEditorField.tsx +83 -9
  37. package/src/components/DynamicForm/SecretEnvEditor.tsx +315 -0
  38. package/src/components/DynamicForm/index.ts +6 -0
  39. package/src/components/DynamicForm/secretEnv.logic.test.ts +126 -0
  40. package/src/components/DynamicForm/secretEnv.logic.ts +87 -0
  41. package/src/components/DynamicForm/types.ts +83 -1
  42. package/src/components/DynamicForm/utils.ts +32 -0
  43. package/src/components/Popover.tsx +6 -1
  44. package/src/components/ScriptTestPanel.logic.test.ts +139 -0
  45. package/src/components/ScriptTestPanel.logic.ts +137 -0
  46. package/src/components/ScriptTestPanel.tsx +394 -0
  47. package/src/components/Sheet.tsx +21 -6
  48. package/src/components/TemplateInput.tsx +104 -0
  49. package/src/components/TemplateInputToggle.tsx +111 -0
  50. package/src/components/TemplateValueInput.test.ts +98 -0
  51. package/src/components/TemplateValueInput.tsx +470 -0
  52. package/src/components/TimeOfDayInput.tsx +116 -0
  53. package/src/components/VariablePicker.tsx +271 -0
  54. package/src/components/comboboxInteraction.ts +39 -0
  55. package/src/components/portalContainer.ts +24 -0
  56. package/src/hooks/useInitOnceForKey.test.ts +27 -0
  57. package/src/hooks/useInitOnceForKey.ts +21 -18
  58. package/src/index.ts +9 -0
  59. package/stories/ActionCard.stories.tsx +122 -0
  60. package/stories/Alert.stories.tsx +5 -5
  61. package/stories/CodeEditor.stories.tsx +47 -2
  62. package/stories/DurationInput.stories.tsx +59 -0
  63. package/stories/ScriptTestPanel.stories.tsx +106 -0
  64. package/stories/SecretEnvEditor.stories.tsx +80 -0
  65. package/stories/TemplateInputToggle.stories.tsx +77 -0
  66. package/stories/TemplateValueInput.stories.tsx +65 -0
  67. package/stories/TimeOfDayInput.stories.tsx +34 -0
  68. package/stories/VariablePicker.stories.tsx +109 -0
  69. package/tsconfig.json +1 -0
  70. package/src/components/CodeEditor/MonacoEditor.tsx +0 -616
  71. package/src/components/CodeEditor/monacoStdlib.ts +0 -62
  72. package/src/components/CodeEditor/monacoWorkers.ts +0 -118
@@ -0,0 +1,59 @@
1
+ import type { Meta, StoryObj } from "@storybook/react";
2
+ import { useState } from "react";
3
+ import {
4
+ DurationInput,
5
+ type DurationValue,
6
+ } from "../src/components/DurationInput";
7
+ import { Label } from "../src/components/Label";
8
+
9
+ const meta: Meta<typeof DurationInput> = {
10
+ title: "Components/Inputs/DurationInput",
11
+ component: DurationInput,
12
+ tags: ["autodocs"],
13
+ };
14
+
15
+ export default meta;
16
+ type Story = StoryObj<typeof DurationInput>;
17
+
18
+ const Demo = ({
19
+ initial,
20
+ defaultUnit,
21
+ }: {
22
+ initial?: DurationValue;
23
+ defaultUnit?: "seconds" | "minutes" | "hours";
24
+ }) => {
25
+ const [value, setValue] = useState<DurationValue | undefined>(initial);
26
+ return (
27
+ <div className="max-w-sm space-y-2">
28
+ <Label>Fire only if state holds for</Label>
29
+ <DurationInput
30
+ value={value}
31
+ onChange={setValue}
32
+ defaultUnit={defaultUnit}
33
+ />
34
+ <pre className="text-xs text-muted-foreground">
35
+ {JSON.stringify(value ?? null)}
36
+ </pre>
37
+ </div>
38
+ );
39
+ };
40
+
41
+ export const Empty: Story = { render: () => <Demo /> };
42
+
43
+ export const Minutes: Story = {
44
+ render: () => <Demo initial={{ minutes: 30 }} />,
45
+ };
46
+
47
+ export const Hours: Story = {
48
+ render: () => <Demo initial={{ hours: 2 }} />,
49
+ };
50
+
51
+ export const SecondsDefault: Story = {
52
+ render: () => <Demo defaultUnit="seconds" />,
53
+ };
54
+
55
+ export const Disabled: Story = {
56
+ render: () => (
57
+ <DurationInput value={{ minutes: 10 }} onChange={() => {}} disabled />
58
+ ),
59
+ };
@@ -0,0 +1,106 @@
1
+ import type { Meta, StoryObj } from "@storybook/react";
2
+ import React from "react";
3
+ import {
4
+ ScriptTestPanel,
5
+ ContextSampleEditor,
6
+ type ScriptTestPanelResult,
7
+ } from "../src/components/ScriptTestPanel";
8
+
9
+ const meta: Meta<typeof ScriptTestPanel> = {
10
+ title: "Components/Editors/ScriptTestPanel",
11
+ component: ScriptTestPanel,
12
+ tags: ["autodocs"],
13
+ };
14
+
15
+ export default meta;
16
+ type Story = StoryObj<typeof ScriptTestPanel>;
17
+
18
+ function delay<T>(value: T, ms = 400): Promise<T> {
19
+ return new Promise((resolve) => setTimeout(() => resolve(value), ms));
20
+ }
21
+
22
+ const successResult: ScriptTestPanelResult = {
23
+ result: { id: "INC-42", ok: true },
24
+ stdout: "Resolved incident INC-42\nDone.",
25
+ stderr: "",
26
+ durationMs: 128,
27
+ timedOut: false,
28
+ };
29
+
30
+ const shellSuccessResult: ScriptTestPanelResult = {
31
+ stdout: "INC-42",
32
+ stderr: "",
33
+ exitCode: 0,
34
+ durationMs: 42,
35
+ timedOut: false,
36
+ };
37
+
38
+ const failureResult: ScriptTestPanelResult = {
39
+ stdout: "",
40
+ stderr: "TypeError: Cannot read properties of undefined (reading 'id')",
41
+ durationMs: 31,
42
+ timedOut: false,
43
+ error: "Cannot read properties of undefined (reading 'id')",
44
+ };
45
+
46
+ export const CollapsedByDefault: Story = {
47
+ render: () => (
48
+ <div className="max-w-xl">
49
+ <ScriptTestPanel onRun={() => delay(successResult)} />
50
+ </div>
51
+ ),
52
+ };
53
+
54
+ export const TypeScriptSuccess: Story = {
55
+ render: () => (
56
+ <div className="max-w-xl">
57
+ <ScriptTestPanel onRun={() => delay(successResult)} defaultOpen />
58
+ </div>
59
+ ),
60
+ };
61
+
62
+ export const ShellSuccess: Story = {
63
+ render: () => (
64
+ <div className="max-w-xl">
65
+ <ScriptTestPanel onRun={() => delay(shellSuccessResult)} defaultOpen />
66
+ </div>
67
+ ),
68
+ };
69
+
70
+ export const Failure: Story = {
71
+ render: () => (
72
+ <div className="max-w-xl">
73
+ <ScriptTestPanel onRun={() => delay(failureResult)} defaultOpen />
74
+ </div>
75
+ ),
76
+ };
77
+
78
+ export const WithSampleContextEditor: Story = {
79
+ render: () => {
80
+ const Wrapper: React.FC = () => {
81
+ const [context, setContext] = React.useState(
82
+ '{\n "trigger": {\n "event": "incident.created",\n "payload": { "id": "INC-42" }\n }\n}',
83
+ );
84
+ return (
85
+ <div className="max-w-xl">
86
+ <ScriptTestPanel
87
+ onRun={() => delay(successResult)}
88
+ defaultOpen
89
+ contextEditor={
90
+ <ContextSampleEditor value={context} onChange={setContext} />
91
+ }
92
+ />
93
+ </div>
94
+ );
95
+ };
96
+ return <Wrapper />;
97
+ },
98
+ };
99
+
100
+ export const Disabled: Story = {
101
+ render: () => (
102
+ <div className="max-w-xl">
103
+ <ScriptTestPanel onRun={() => delay(successResult)} defaultOpen disabled />
104
+ </div>
105
+ ),
106
+ };
@@ -0,0 +1,80 @@
1
+ import type { Meta, StoryObj } from "@storybook/react";
2
+ import { useState } from "react";
3
+ import { SecretEnvEditor } from "../src/components/DynamicForm/SecretEnvEditor";
4
+ import { Label } from "../src/components/Label";
5
+
6
+ const meta: Meta<typeof SecretEnvEditor> = {
7
+ title: "Components/Inputs/SecretEnvEditor",
8
+ component: SecretEnvEditor,
9
+ tags: ["autodocs"],
10
+ };
11
+
12
+ export default meta;
13
+ type Story = StoryObj<typeof SecretEnvEditor>;
14
+
15
+ const Demo = ({
16
+ initial,
17
+ secretNames,
18
+ }: {
19
+ initial?: Record<string, string>;
20
+ secretNames?: string[];
21
+ }) => {
22
+ const [value, setValue] = useState<Record<string, string>>(initial ?? {});
23
+ return (
24
+ <div className="max-w-xl space-y-2">
25
+ <Label>Secret environment variables</Label>
26
+ <SecretEnvEditor
27
+ id="demo-secret-env"
28
+ value={value}
29
+ onChange={setValue}
30
+ secretNames={secretNames}
31
+ />
32
+ <pre className="text-xs text-muted-foreground">
33
+ {JSON.stringify(value, null, 2)}
34
+ </pre>
35
+ </div>
36
+ );
37
+ };
38
+
39
+ /** Empty editor with secret-name suggestions available. */
40
+ export const Empty: Story = {
41
+ render: () => (
42
+ <Demo secretNames={["jira_token", "db_password", "smtp_api_key"]} />
43
+ ),
44
+ };
45
+
46
+ /** Pre-populated mapping; the stored value is the `${{ secrets.NAME }}` template. */
47
+ export const WithValues: Story = {
48
+ render: () => (
49
+ <Demo
50
+ initial={{
51
+ API_TOKEN: "${{ secrets.jira_token }}",
52
+ DB_PASSWORD: "${{ secrets.db_password }}",
53
+ }}
54
+ secretNames={["jira_token", "db_password", "smtp_api_key"]}
55
+ />
56
+ ),
57
+ };
58
+
59
+ /** No name suggestions available — the secret field is a plain free-text
60
+ * combobox and no existence warning is shown (the list is unknown). */
61
+ export const NoSuggestions: Story = {
62
+ render: () => <Demo initial={{ TOKEN: "${{ secrets.some_secret }}" }} />,
63
+ };
64
+
65
+ /**
66
+ * A row references a secret that the loaded list doesn't contain — the row
67
+ * shows a non-blocking warning (red border + message). The secret may have
68
+ * been deleted/renamed or be created later, so the value still round-trips.
69
+ */
70
+ export const UnknownSecret: Story = {
71
+ render: () => (
72
+ <Demo
73
+ initial={{
74
+ API_TOKEN: "${{ secrets.jira_token }}",
75
+ GHOST: "${{ secrets.deleted_secret }}",
76
+ }}
77
+ secretNames={["jira_token", "db_password"]}
78
+ />
79
+ ),
80
+ };
@@ -0,0 +1,77 @@
1
+ import type { Meta, StoryObj } from "@storybook/react";
2
+ import { useState } from "react";
3
+ import { TemplateInputToggle } from "../src/components/TemplateInputToggle";
4
+ import { Input } from "../src/components/Input";
5
+
6
+ const meta: Meta<typeof TemplateInputToggle> = {
7
+ title: "Components/Automation/TemplateInputToggle",
8
+ component: TemplateInputToggle,
9
+ tags: ["autodocs"],
10
+ parameters: {
11
+ docs: {
12
+ description: {
13
+ component:
14
+ 'Wraps a typed input with an "fx" button. Click it to switch into template mode and back.',
15
+ },
16
+ },
17
+ },
18
+ };
19
+
20
+ export default meta;
21
+ type Story = StoryObj<typeof TemplateInputToggle>;
22
+
23
+ const NumberFieldDemo = () => {
24
+ const [value, setValue] = useState("30");
25
+ return (
26
+ <div className="w-96 p-4">
27
+ <TemplateInputToggle
28
+ value={value}
29
+ onChange={setValue}
30
+ renderTyped={({ disabled }) => (
31
+ <Input
32
+ type="number"
33
+ value={value}
34
+ onChange={(e) => setValue(e.target.value)}
35
+ disabled={disabled}
36
+ />
37
+ )}
38
+ templateProperties={[
39
+ { path: "trigger.payload.timeoutSeconds", type: "number" },
40
+ { path: "var.delay", type: "number" },
41
+ ]}
42
+ templatePlaceholder="{{ trigger.payload.timeoutSeconds }}"
43
+ />
44
+ </div>
45
+ );
46
+ };
47
+
48
+ const StartsInTemplateModeDemo = () => {
49
+ const [value, setValue] = useState("{{ trigger.payload.timeoutSeconds }}");
50
+ return (
51
+ <div className="w-96 p-4">
52
+ <TemplateInputToggle
53
+ value={value}
54
+ onChange={setValue}
55
+ renderTyped={({ disabled }) => (
56
+ <Input
57
+ type="number"
58
+ value={value}
59
+ onChange={(e) => setValue(e.target.value)}
60
+ disabled={disabled}
61
+ />
62
+ )}
63
+ templateProperties={[
64
+ { path: "trigger.payload.timeoutSeconds", type: "number" },
65
+ ]}
66
+ />
67
+ </div>
68
+ );
69
+ };
70
+
71
+ export const NumberField: Story = {
72
+ render: () => <NumberFieldDemo />,
73
+ };
74
+
75
+ export const StartsInTemplateMode: Story = {
76
+ render: () => <StartsInTemplateModeDemo />,
77
+ };
@@ -0,0 +1,65 @@
1
+ import type { Meta, StoryObj } from "@storybook/react";
2
+ import { useState } from "react";
3
+ import { TemplateValueInput } from "../src/components/TemplateValueInput";
4
+ import type { TemplateProperty } from "../src/components/CodeEditor";
5
+
6
+ const meta: Meta<typeof TemplateValueInput> = {
7
+ title: "Components/Automation/TemplateValueInput",
8
+ component: TemplateValueInput,
9
+ tags: ["autodocs"],
10
+ parameters: {
11
+ docs: {
12
+ description: {
13
+ component:
14
+ "Single-line input with `{{` template autocomplete. Type `{{` to open the picker.",
15
+ },
16
+ },
17
+ },
18
+ };
19
+
20
+ export default meta;
21
+ type Story = StoryObj<typeof TemplateValueInput>;
22
+
23
+ const sampleProperties: TemplateProperty[] = [
24
+ { path: "trigger.event", type: "string", description: "Event id." },
25
+ { path: "trigger.payload.incidentId", type: "string" },
26
+ { path: "trigger.payload.title", type: "string" },
27
+ { path: "trigger.payload.severity", type: '"low" | "high"' },
28
+ { path: "var.outer", type: "string" },
29
+ { path: "artifact.jira.issue.key", type: "string" },
30
+ ];
31
+
32
+ const WithPropertiesDemo = () => {
33
+ const [value, setValue] = useState("Title: ");
34
+ return (
35
+ <div className="w-96 p-4">
36
+ <TemplateValueInput
37
+ value={value}
38
+ onChange={setValue}
39
+ placeholder="Type {{ to insert a variable…"
40
+ templateProperties={sampleProperties}
41
+ />
42
+ </div>
43
+ );
44
+ };
45
+
46
+ const NoPropertiesDemo = () => {
47
+ const [value, setValue] = useState("");
48
+ return (
49
+ <div className="w-96 p-4">
50
+ <TemplateValueInput
51
+ value={value}
52
+ onChange={setValue}
53
+ placeholder="No template autocomplete here"
54
+ />
55
+ </div>
56
+ );
57
+ };
58
+
59
+ export const WithProperties: Story = {
60
+ render: () => <WithPropertiesDemo />,
61
+ };
62
+
63
+ export const NoProperties: Story = {
64
+ render: () => <NoPropertiesDemo />,
65
+ };
@@ -0,0 +1,34 @@
1
+ import type { Meta, StoryObj } from "@storybook/react";
2
+ import { useState } from "react";
3
+ import { TimeOfDayInput } from "../src/components/TimeOfDayInput";
4
+ import { Label } from "../src/components/Label";
5
+
6
+ const meta: Meta<typeof TimeOfDayInput> = {
7
+ title: "Components/Inputs/TimeOfDayInput",
8
+ component: TimeOfDayInput,
9
+ tags: ["autodocs"],
10
+ };
11
+
12
+ export default meta;
13
+ type Story = StoryObj<typeof TimeOfDayInput>;
14
+
15
+ const Demo = ({ initial }: { initial?: string }) => {
16
+ const [value, setValue] = useState<string | undefined>(initial);
17
+ return (
18
+ <div className="max-w-sm space-y-2">
19
+ <Label>On-call window starts at</Label>
20
+ <TimeOfDayInput value={value} onChange={setValue} />
21
+ <pre className="text-xs text-muted-foreground">
22
+ {JSON.stringify(value ?? null)}
23
+ </pre>
24
+ </div>
25
+ );
26
+ };
27
+
28
+ export const Empty: Story = { render: () => <Demo /> };
29
+
30
+ export const Set: Story = { render: () => <Demo initial="22:00" /> };
31
+
32
+ export const Disabled: Story = {
33
+ render: () => <TimeOfDayInput value="09:30" onChange={() => {}} disabled />,
34
+ };
@@ -0,0 +1,109 @@
1
+ import type { Meta, StoryObj } from "@storybook/react";
2
+ import { useState } from "react";
3
+ import {
4
+ VariablePicker,
5
+ type VariableNode,
6
+ } from "../src/components/VariablePicker";
7
+
8
+ const meta: Meta<typeof VariablePicker> = {
9
+ title: "Components/Automation/VariablePicker",
10
+ component: VariablePicker,
11
+ tags: ["autodocs"],
12
+ parameters: {
13
+ docs: {
14
+ description: {
15
+ component:
16
+ "Hierarchical picker — click the trigger button to open a tree of in-scope automation variables.",
17
+ },
18
+ },
19
+ },
20
+ };
21
+
22
+ export default meta;
23
+ type Story = StoryObj<typeof VariablePicker>;
24
+
25
+ const sampleScope: VariableNode[] = [
26
+ {
27
+ path: "trigger",
28
+ type: "object",
29
+ description: "Trigger that fired this run.",
30
+ children: [
31
+ {
32
+ path: "trigger.event",
33
+ type: '"incident.created" | "incident.resolved"',
34
+ description: "Discriminator.",
35
+ },
36
+ {
37
+ path: "trigger.payload",
38
+ type: "object",
39
+ children: [
40
+ { path: "trigger.payload.incidentId", type: "string" },
41
+ {
42
+ path: "trigger.payload.title",
43
+ type: "string",
44
+ conditionalOnTriggers: ["incident.created"],
45
+ },
46
+ {
47
+ path: "trigger.payload.resolvedAt",
48
+ type: "string",
49
+ conditionalOnTriggers: ["incident.resolved"],
50
+ },
51
+ ],
52
+ },
53
+ ],
54
+ },
55
+ {
56
+ path: "var",
57
+ templateRef: "variables",
58
+ type: "object",
59
+ description: "Operator-defined variables.",
60
+ children: [
61
+ { path: "var.outer", templateRef: "variables.outer", type: "string" },
62
+ ],
63
+ },
64
+ {
65
+ path: "artifact",
66
+ templateRef: "artifacts",
67
+ type: "object",
68
+ description: "Artifacts produced upstream.",
69
+ children: [
70
+ {
71
+ // Hyphenated/dotted artifact id → templateRef uses bracket notation
72
+ // so the inserted `{{ }}` text is runtime-parseable.
73
+ path: "artifact.integration-jira.issue",
74
+ templateRef: 'artifacts["integration-jira.issue"]',
75
+ type: "object",
76
+ children: [
77
+ {
78
+ path: "artifact.integration-jira.issue.key",
79
+ templateRef: 'artifacts["integration-jira.issue"].key',
80
+ type: "string",
81
+ },
82
+ {
83
+ path: "artifact.integration-jira.issue.url",
84
+ templateRef: 'artifacts["integration-jira.issue"].url',
85
+ type: "string",
86
+ },
87
+ ],
88
+ },
89
+ ],
90
+ },
91
+ ];
92
+
93
+ const DefaultDemo = () => {
94
+ const [inserted, setInserted] = useState<string | null>(null);
95
+ return (
96
+ <div className="p-12 flex flex-col items-start gap-4">
97
+ <VariablePicker scope={sampleScope} onSelect={setInserted} />
98
+ {inserted && (
99
+ <p className="text-xs text-muted-foreground">
100
+ Inserted: <code className="font-mono">{`{{${inserted}}}`}</code>
101
+ </p>
102
+ )}
103
+ </div>
104
+ );
105
+ };
106
+
107
+ export const Default: Story = {
108
+ render: () => <DefaultDemo />,
109
+ };
package/tsconfig.json CHANGED
@@ -3,6 +3,7 @@
3
3
  "include": [
4
4
  "src",
5
5
  "src/components/CodeEditor/generated/stdlib-types.json",
6
+ "src/components/CodeEditor/generated/builtin-modules.json",
6
7
  "scripts"
7
8
  ],
8
9
  "references": [