@checkstack/ui 1.8.3 → 1.10.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.
- package/CHANGELOG.md +83 -0
- package/package.json +6 -2
- package/scripts/generate-stdlib-types.ts +90 -0
- package/src/components/CodeEditor/CodeEditor.tsx +7 -0
- package/src/components/CodeEditor/MonacoEditor.tsx +203 -117
- package/src/components/CodeEditor/generateTypeDefinitions.ts +19 -26
- package/src/components/CodeEditor/generated/stdlib-types.json +1 -0
- package/src/components/CodeEditor/index.ts +7 -0
- package/src/components/CodeEditor/monacoStdlib.ts +62 -0
- package/src/components/CodeEditor/monacoWorkers.ts +118 -0
- package/src/components/CodeEditor/scriptContext.test.ts +280 -0
- package/src/components/CodeEditor/scriptContext.ts +467 -0
- package/src/components/CodeEditor/shellEnvVarMatcher.test.ts +95 -0
- package/src/components/CodeEditor/shellEnvVarMatcher.ts +70 -0
- package/src/components/DynamicForm/DynamicForm.tsx +6 -0
- package/src/components/DynamicForm/FormField.tsx +15 -0
- package/src/components/DynamicForm/MultiTypeEditorField.tsx +111 -6
- package/src/components/DynamicForm/index.ts +2 -0
- package/src/components/DynamicForm/starterTemplateSelector.test.ts +96 -0
- package/src/components/DynamicForm/starterTemplateSelector.ts +32 -0
- package/src/components/DynamicForm/types.ts +34 -1
- package/src/components/ListEmptyState.tsx +51 -0
- package/src/components/QueryErrorState.tsx +64 -0
- package/src/components/ResponsiveTable.tsx +92 -0
- package/src/components/Skeleton.tsx +39 -0
- package/src/hooks/useInitOnceForKey.test.ts +127 -0
- package/src/hooks/useInitOnceForKey.ts +87 -0
- package/src/index.ts +6 -0
- package/src/utils/toastTemplates.test.ts +82 -0
- package/src/utils/toastTemplates.ts +47 -0
- package/stories/ListEmptyState.stories.tsx +48 -0
- package/stories/QueryErrorState.stories.tsx +40 -0
- package/stories/ResponsiveTable.stories.tsx +93 -0
- package/stories/Skeleton.stories.tsx +53 -0
- package/stories/toastTemplates.stories.tsx +60 -0
- package/tsconfig.json +3 -1
|
@@ -33,6 +33,9 @@ export const FormField: React.FC<FormFieldProps> = ({
|
|
|
33
33
|
formValues,
|
|
34
34
|
optionsResolvers,
|
|
35
35
|
templateProperties,
|
|
36
|
+
typeDefinitions,
|
|
37
|
+
shellEnvVars,
|
|
38
|
+
starterTemplates,
|
|
36
39
|
onChange,
|
|
37
40
|
}) => {
|
|
38
41
|
const description = propSchema.description || "";
|
|
@@ -129,6 +132,9 @@ export const FormField: React.FC<FormFieldProps> = ({
|
|
|
129
132
|
isRequired={isRequired}
|
|
130
133
|
editorTypes={editorTypes}
|
|
131
134
|
templateProperties={templateProperties}
|
|
135
|
+
typeDefinitions={typeDefinitions}
|
|
136
|
+
shellEnvVars={shellEnvVars}
|
|
137
|
+
starterTemplates={starterTemplates}
|
|
132
138
|
onChange={onChange as (val: string | undefined) => void}
|
|
133
139
|
/>
|
|
134
140
|
);
|
|
@@ -346,6 +352,9 @@ export const FormField: React.FC<FormFieldProps> = ({
|
|
|
346
352
|
formValues={formValues}
|
|
347
353
|
optionsResolvers={optionsResolvers}
|
|
348
354
|
templateProperties={templateProperties}
|
|
355
|
+
typeDefinitions={typeDefinitions}
|
|
356
|
+
shellEnvVars={shellEnvVars}
|
|
357
|
+
starterTemplates={starterTemplates}
|
|
349
358
|
onChange={(val) =>
|
|
350
359
|
onChange({ ...(value as Record<string, unknown>), [key]: val })
|
|
351
360
|
}
|
|
@@ -454,6 +463,9 @@ export const FormField: React.FC<FormFieldProps> = ({
|
|
|
454
463
|
formValues={formValues}
|
|
455
464
|
optionsResolvers={optionsResolvers}
|
|
456
465
|
templateProperties={templateProperties}
|
|
466
|
+
typeDefinitions={typeDefinitions}
|
|
467
|
+
shellEnvVars={shellEnvVars}
|
|
468
|
+
starterTemplates={starterTemplates}
|
|
457
469
|
onChange={(val) => {
|
|
458
470
|
const next = [...(items as unknown[])];
|
|
459
471
|
next[index] = val;
|
|
@@ -582,6 +594,9 @@ export const FormField: React.FC<FormFieldProps> = ({
|
|
|
582
594
|
formValues={formValues}
|
|
583
595
|
optionsResolvers={optionsResolvers}
|
|
584
596
|
templateProperties={templateProperties}
|
|
597
|
+
typeDefinitions={typeDefinitions}
|
|
598
|
+
shellEnvVars={shellEnvVars}
|
|
599
|
+
starterTemplates={starterTemplates}
|
|
585
600
|
onChange={(val) => onChange({ ...currentValue, [key]: val })}
|
|
586
601
|
/>
|
|
587
602
|
))}
|
|
@@ -17,6 +17,8 @@ import {
|
|
|
17
17
|
parseFormData,
|
|
18
18
|
EDITOR_TYPE_LABELS,
|
|
19
19
|
} from "./utils";
|
|
20
|
+
import type { EditorStarterTemplates, ShellEnvVar } from "./types";
|
|
21
|
+
import { pickStarterForEmptyField } from "./starterTemplateSelector";
|
|
20
22
|
|
|
21
23
|
export interface MultiTypeEditorFieldProps {
|
|
22
24
|
/** Unique identifier for the field */
|
|
@@ -33,6 +35,25 @@ export interface MultiTypeEditorFieldProps {
|
|
|
33
35
|
editorTypes: EditorType[];
|
|
34
36
|
/** Optional template properties for autocomplete */
|
|
35
37
|
templateProperties?: TemplateProperty[];
|
|
38
|
+
/**
|
|
39
|
+
* Optional TypeScript declarations injected into Monaco for TS/JS modes —
|
|
40
|
+
* powers IntelliSense + return-shape error reporting against the runtime
|
|
41
|
+
* context the user will see (`context.config`, `context.event`, ...).
|
|
42
|
+
*/
|
|
43
|
+
typeDefinitions?: string;
|
|
44
|
+
/**
|
|
45
|
+
* Optional shell env-var hints surfaced as completion items after `$`
|
|
46
|
+
* (and `${`) in shell mode. Typically populated by the platform with
|
|
47
|
+
* names like `EVENT_ID` / `PAYLOAD_*` so users don't have to memorise.
|
|
48
|
+
*/
|
|
49
|
+
shellEnvVars?: ShellEnvVar[];
|
|
50
|
+
/**
|
|
51
|
+
* Optional starter templates per editor language. When the field is
|
|
52
|
+
* empty (and the user hasn't typed yet), switching to an editor with a
|
|
53
|
+
* starter for that language pre-populates the editor so users see a
|
|
54
|
+
* working example instead of a blank canvas.
|
|
55
|
+
*/
|
|
56
|
+
starterTemplates?: EditorStarterTemplates;
|
|
36
57
|
/** Callback when value changes */
|
|
37
58
|
onChange: (value: string | undefined) => void;
|
|
38
59
|
}
|
|
@@ -50,6 +71,9 @@ export const MultiTypeEditorField: React.FC<MultiTypeEditorFieldProps> = ({
|
|
|
50
71
|
isRequired,
|
|
51
72
|
editorTypes,
|
|
52
73
|
templateProperties,
|
|
74
|
+
typeDefinitions,
|
|
75
|
+
shellEnvVars,
|
|
76
|
+
starterTemplates,
|
|
53
77
|
onChange,
|
|
54
78
|
}) => {
|
|
55
79
|
// Detect initial editor type from value
|
|
@@ -57,16 +81,81 @@ export const MultiTypeEditorField: React.FC<MultiTypeEditorFieldProps> = ({
|
|
|
57
81
|
detectEditorType(value, editorTypes),
|
|
58
82
|
);
|
|
59
83
|
|
|
60
|
-
//
|
|
61
|
-
|
|
84
|
+
// Latched once the field has had non-empty content at least once for
|
|
85
|
+
// this mount. Used by the seed effects below to decide whether to
|
|
86
|
+
// (re-)install the starter template.
|
|
87
|
+
const hasSeededRef = React.useRef(false);
|
|
88
|
+
|
|
89
|
+
// React reuses this `MultiTypeEditorField` instance across collector
|
|
90
|
+
// switches when its parent `FormField` keeps the same key (e.g. both
|
|
91
|
+
// collectors have a property called `script`). The `useState`
|
|
92
|
+
// initializer above only runs on the first mount, so without this
|
|
93
|
+
// effect a switch from a shell-only collector to a typescript-only
|
|
94
|
+
// collector leaves `selectedType` stuck on "shell" — Monaco then
|
|
95
|
+
// tokenises the new collector's TS content as shell (no
|
|
96
|
+
// highlighting), and the reverse direction tokenises shell content as
|
|
97
|
+
// TS (nonsense "Cannot find name" errors).
|
|
98
|
+
//
|
|
99
|
+
// Whenever `editorTypes` changes and the current selection is no
|
|
100
|
+
// longer offered, re-detect from the new value. We deliberately do
|
|
101
|
+
// NOT reset on every `value` change — that would clobber the user's
|
|
102
|
+
// manual dropdown choice mid-edit.
|
|
62
103
|
React.useEffect(() => {
|
|
63
|
-
if (
|
|
64
|
-
|
|
65
|
-
|
|
104
|
+
if (!editorTypes.includes(selectedType)) {
|
|
105
|
+
const next = detectEditorType(value, editorTypes);
|
|
106
|
+
setSelectedType(next);
|
|
107
|
+
// Reset the seed latch too — this is effectively a fresh field
|
|
108
|
+
// for a different collector, and the new language's starter
|
|
109
|
+
// template should be eligible again.
|
|
110
|
+
hasSeededRef.current = false;
|
|
111
|
+
}
|
|
112
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps -- intentional: only re-derive when the offered types change
|
|
113
|
+
}, [editorTypes]);
|
|
114
|
+
|
|
115
|
+
// Seed an empty field with the starter template for its detected
|
|
116
|
+
// language so users see a working example instead of a blank canvas.
|
|
117
|
+
//
|
|
118
|
+
// Why this isn't just `useEffect(..., [])`:
|
|
119
|
+
// React fires CHILD effects before PARENT effects. `DynamicForm`'s
|
|
120
|
+
// own init effect (which applies schema defaults) runs AFTER this one
|
|
121
|
+
// and, on initial mount, calls `onChange(defaults)` — which clobbers
|
|
122
|
+
// a freshly-seeded `script: starter` back to `script: ""` because the
|
|
123
|
+
// defaults effect's closure-captured `value` was the original `{}`.
|
|
124
|
+
// A naive `[]`-dep seed would therefore appear to do nothing.
|
|
125
|
+
//
|
|
126
|
+
// Two-effect design fixes this without depending on cross-component
|
|
127
|
+
// effect ordering:
|
|
128
|
+
// 1. The "observed" effect (declared FIRST) flips `hasSeededRef` to
|
|
129
|
+
// true the moment we see non-empty content in `value` — whether
|
|
130
|
+
// that came from our seed surviving, a parent default landing
|
|
131
|
+
// with content, or the user typing.
|
|
132
|
+
// 2. The "seed" effect re-runs on every value/starter change and
|
|
133
|
+
// installs the starter while `hasSeededRef` is still false. If
|
|
134
|
+
// the parent clobbers us back to "", we just re-seed on the
|
|
135
|
+
// next pass — until the value sticks and effect (1) latches.
|
|
136
|
+
//
|
|
137
|
+
// The latch also means we DON'T re-seed if the user deliberately
|
|
138
|
+
// deletes their content later, or on any subsequent refetch / parent
|
|
139
|
+
// re-render: once non-empty has been observed at least once, the
|
|
140
|
+
// field is the user's territory.
|
|
141
|
+
React.useEffect(() => {
|
|
142
|
+
if (!hasSeededRef.current && (value ?? "").trim().length > 0) {
|
|
143
|
+
hasSeededRef.current = true;
|
|
66
144
|
}
|
|
67
|
-
// Don't re-detect type when value changes after initial mount
|
|
68
145
|
}, [value]);
|
|
69
146
|
|
|
147
|
+
React.useEffect(() => {
|
|
148
|
+
if (hasSeededRef.current) return;
|
|
149
|
+
const starter = pickStarterForEmptyField({
|
|
150
|
+
value,
|
|
151
|
+
selectedType,
|
|
152
|
+
starterTemplates,
|
|
153
|
+
});
|
|
154
|
+
if (starter !== undefined) {
|
|
155
|
+
onChange(starter);
|
|
156
|
+
}
|
|
157
|
+
}, [value, selectedType, starterTemplates, onChange]);
|
|
158
|
+
|
|
70
159
|
const handleTypeChange = (newType: EditorType) => {
|
|
71
160
|
setSelectedType(newType);
|
|
72
161
|
|
|
@@ -76,6 +165,19 @@ export const MultiTypeEditorField: React.FC<MultiTypeEditorFieldProps> = ({
|
|
|
76
165
|
return;
|
|
77
166
|
}
|
|
78
167
|
|
|
168
|
+
// If the field is empty after the switch and we have a starter template
|
|
169
|
+
// for the new language, seed it. (Switching languages on a non-empty
|
|
170
|
+
// field preserves the existing content, just like before.)
|
|
171
|
+
const starter = pickStarterForEmptyField({
|
|
172
|
+
value,
|
|
173
|
+
selectedType: newType,
|
|
174
|
+
starterTemplates,
|
|
175
|
+
});
|
|
176
|
+
if (starter !== undefined) {
|
|
177
|
+
onChange(starter);
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
|
|
79
181
|
// When switching to formdata
|
|
80
182
|
if (newType === "formdata") {
|
|
81
183
|
if (!value || value.trim() === "") {
|
|
@@ -242,6 +344,7 @@ export const MultiTypeEditorField: React.FC<MultiTypeEditorFieldProps> = ({
|
|
|
242
344
|
language="javascript"
|
|
243
345
|
minHeight="150px"
|
|
244
346
|
templateProperties={templateProperties}
|
|
347
|
+
typeDefinitions={typeDefinitions}
|
|
245
348
|
/>
|
|
246
349
|
)}
|
|
247
350
|
|
|
@@ -253,6 +356,7 @@ export const MultiTypeEditorField: React.FC<MultiTypeEditorFieldProps> = ({
|
|
|
253
356
|
language="typescript"
|
|
254
357
|
minHeight="150px"
|
|
255
358
|
templateProperties={templateProperties}
|
|
359
|
+
typeDefinitions={typeDefinitions}
|
|
256
360
|
/>
|
|
257
361
|
)}
|
|
258
362
|
|
|
@@ -264,6 +368,7 @@ export const MultiTypeEditorField: React.FC<MultiTypeEditorFieldProps> = ({
|
|
|
264
368
|
language="shell"
|
|
265
369
|
minHeight="150px"
|
|
266
370
|
templateProperties={templateProperties}
|
|
371
|
+
shellEnvVars={shellEnvVars}
|
|
267
372
|
/>
|
|
268
373
|
)}
|
|
269
374
|
</div>
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
|
+
import { pickStarterForEmptyField } from "./starterTemplateSelector";
|
|
3
|
+
import type { EditorStarterTemplates } from "./types";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Regression suite for the starter-template seeding behaviour in
|
|
7
|
+
* `MultiTypeEditorField`.
|
|
8
|
+
*
|
|
9
|
+
* The previous implementation gated seeding behind an `isInitialMount`
|
|
10
|
+
* ref that a sibling effect with `[value]` deps flipped to false BEFORE
|
|
11
|
+
* the seed effect ran — so seeding silently no-op'd on every mount and
|
|
12
|
+
* users always saw a blank canvas. The refactored seed effect delegates
|
|
13
|
+
* to this pure function; testing it here proves the decision is correct
|
|
14
|
+
* regardless of the React effect ordering trap.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
const STARTERS: EditorStarterTemplates = {
|
|
18
|
+
typescript: "// starter for TypeScript",
|
|
19
|
+
shell: "#!/bin/sh\necho starter for shell",
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
describe("pickStarterForEmptyField", () => {
|
|
23
|
+
it("returns the starter for the selected language when the field is empty", () => {
|
|
24
|
+
expect(
|
|
25
|
+
pickStarterForEmptyField({
|
|
26
|
+
value: "",
|
|
27
|
+
selectedType: "typescript",
|
|
28
|
+
starterTemplates: STARTERS,
|
|
29
|
+
}),
|
|
30
|
+
).toBe(STARTERS.typescript);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("returns the starter when the value is whitespace-only", () => {
|
|
34
|
+
// The original bug let trailing whitespace block the seed; explicit
|
|
35
|
+
// coverage so we don't regress.
|
|
36
|
+
expect(
|
|
37
|
+
pickStarterForEmptyField({
|
|
38
|
+
value: " \n\t ",
|
|
39
|
+
selectedType: "typescript",
|
|
40
|
+
starterTemplates: STARTERS,
|
|
41
|
+
}),
|
|
42
|
+
).toBe(STARTERS.typescript);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("returns undefined when the field already has content (don't clobber)", () => {
|
|
46
|
+
expect(
|
|
47
|
+
pickStarterForEmptyField({
|
|
48
|
+
value: "// user's existing code",
|
|
49
|
+
selectedType: "typescript",
|
|
50
|
+
starterTemplates: STARTERS,
|
|
51
|
+
}),
|
|
52
|
+
).toBeUndefined();
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("returns undefined when value is undefined and no starter is defined for the selected language", () => {
|
|
56
|
+
expect(
|
|
57
|
+
pickStarterForEmptyField({
|
|
58
|
+
value: undefined,
|
|
59
|
+
selectedType: "json",
|
|
60
|
+
starterTemplates: STARTERS,
|
|
61
|
+
}),
|
|
62
|
+
).toBeUndefined();
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("returns undefined when no `starterTemplates` are provided at all", () => {
|
|
66
|
+
expect(
|
|
67
|
+
pickStarterForEmptyField({
|
|
68
|
+
value: "",
|
|
69
|
+
selectedType: "typescript",
|
|
70
|
+
}),
|
|
71
|
+
).toBeUndefined();
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("returns the shell starter when shell is selected", () => {
|
|
75
|
+
expect(
|
|
76
|
+
pickStarterForEmptyField({
|
|
77
|
+
value: undefined,
|
|
78
|
+
selectedType: "shell",
|
|
79
|
+
starterTemplates: STARTERS,
|
|
80
|
+
}),
|
|
81
|
+
).toBe(STARTERS.shell);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it("treats an empty-string starter as 'no starter available'", () => {
|
|
85
|
+
// We never want to install a literal empty string on top of an
|
|
86
|
+
// already-empty field; that would just look like the seed silently
|
|
87
|
+
// did nothing.
|
|
88
|
+
expect(
|
|
89
|
+
pickStarterForEmptyField({
|
|
90
|
+
value: "",
|
|
91
|
+
selectedType: "typescript",
|
|
92
|
+
starterTemplates: { typescript: "" },
|
|
93
|
+
}),
|
|
94
|
+
).toBeUndefined();
|
|
95
|
+
});
|
|
96
|
+
});
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { EditorStarterTemplates, EditorType } from "./types";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Pure decision function powering the "seed empty editor with a starter
|
|
5
|
+
* template" behaviour in `MultiTypeEditorField`. Returns the starter
|
|
6
|
+
* content to install, or `undefined` if the field shouldn't be touched.
|
|
7
|
+
*
|
|
8
|
+
* Used both on first mount and when the user switches editor language on
|
|
9
|
+
* a still-empty field, so a single source of truth governs both paths
|
|
10
|
+
* (and a single test suite covers them).
|
|
11
|
+
*
|
|
12
|
+
* Returns `undefined` when:
|
|
13
|
+
* - the field already has non-whitespace content (don't clobber user input)
|
|
14
|
+
* - no `starterTemplates` were provided
|
|
15
|
+
* - no starter exists for the currently-selected language
|
|
16
|
+
* - the matching starter is the empty string
|
|
17
|
+
*/
|
|
18
|
+
export function pickStarterForEmptyField({
|
|
19
|
+
value,
|
|
20
|
+
selectedType,
|
|
21
|
+
starterTemplates,
|
|
22
|
+
}: {
|
|
23
|
+
value: string | undefined;
|
|
24
|
+
selectedType: EditorType;
|
|
25
|
+
starterTemplates?: EditorStarterTemplates;
|
|
26
|
+
}): string | undefined {
|
|
27
|
+
const trimmed = (value ?? "").trim();
|
|
28
|
+
if (trimmed.length > 0) return undefined;
|
|
29
|
+
const starter = starterTemplates?.[selectedType];
|
|
30
|
+
if (!starter || starter.length === 0) return undefined;
|
|
31
|
+
return starter;
|
|
32
|
+
}
|
|
@@ -1,8 +1,11 @@
|
|
|
1
|
-
import type { TemplateProperty } from "../CodeEditor";
|
|
1
|
+
import type { TemplateProperty, ShellEnvVar } from "../CodeEditor";
|
|
2
2
|
import type { EditorType } from "@checkstack/common";
|
|
3
3
|
|
|
4
4
|
// Re-export types used by multi-type editor
|
|
5
5
|
export type { EditorType } from "./utils";
|
|
6
|
+
// Re-export `ShellEnvVar` so DynamicForm consumers don't have to import
|
|
7
|
+
// from two paths. The canonical definition lives in `../CodeEditor`.
|
|
8
|
+
export type { ShellEnvVar } from "../CodeEditor";
|
|
6
9
|
import type {
|
|
7
10
|
JsonSchemaPropertyCore,
|
|
8
11
|
JsonSchemaBase,
|
|
@@ -40,6 +43,13 @@ export type OptionsResolver = (
|
|
|
40
43
|
*/
|
|
41
44
|
export type JsonSchema = JsonSchemaBase<JsonSchemaProperty>;
|
|
42
45
|
|
|
46
|
+
/**
|
|
47
|
+
* Default starter templates per editor language. Used to populate empty
|
|
48
|
+
* multi-type editor fields so users see a working example instead of a
|
|
49
|
+
* blank canvas. Keyed by `EditorType` (e.g. "typescript", "shell").
|
|
50
|
+
*/
|
|
51
|
+
export type EditorStarterTemplates = Partial<Record<EditorType, string>>;
|
|
52
|
+
|
|
43
53
|
export interface DynamicFormProps {
|
|
44
54
|
schema: JsonSchema;
|
|
45
55
|
value: Record<string, unknown>;
|
|
@@ -59,6 +69,25 @@ export interface DynamicFormProps {
|
|
|
59
69
|
* When provided, fields with x-editor-types get {{ autocomplete suggestions.
|
|
60
70
|
*/
|
|
61
71
|
templateProperties?: TemplateProperty[];
|
|
72
|
+
/**
|
|
73
|
+
* Optional TypeScript declarations to inject into Monaco for `typescript`
|
|
74
|
+
* or `javascript` editor-type fields. Typically built from a schema via
|
|
75
|
+
* `generateTypeDefinitions()` so users get autocomplete + type errors
|
|
76
|
+
* against the runtime context they'll see.
|
|
77
|
+
*/
|
|
78
|
+
typeDefinitions?: string;
|
|
79
|
+
/**
|
|
80
|
+
* Optional list of environment-variable names that are available to
|
|
81
|
+
* `shell` editor-type fields. When provided, Monaco autocompletes them
|
|
82
|
+
* after `$` and `${`. Use this to surface platform-injected vars like
|
|
83
|
+
* `EVENT_ID`, `PAYLOAD_*` etc. so users don't have to remember the names.
|
|
84
|
+
*/
|
|
85
|
+
shellEnvVars?: ShellEnvVar[];
|
|
86
|
+
/**
|
|
87
|
+
* Optional initial content per editor language, used to populate empty
|
|
88
|
+
* fields with a working example. Keyed by `EditorType`.
|
|
89
|
+
*/
|
|
90
|
+
starterTemplates?: EditorStarterTemplates;
|
|
62
91
|
}
|
|
63
92
|
|
|
64
93
|
/** Props for the FormField component */
|
|
@@ -71,10 +100,14 @@ export interface FormFieldProps {
|
|
|
71
100
|
formValues: Record<string, unknown>;
|
|
72
101
|
optionsResolvers?: Record<string, OptionsResolver>;
|
|
73
102
|
templateProperties?: TemplateProperty[];
|
|
103
|
+
typeDefinitions?: string;
|
|
104
|
+
shellEnvVars?: ShellEnvVar[];
|
|
105
|
+
starterTemplates?: EditorStarterTemplates;
|
|
74
106
|
/** Callback when value changes. Omit val to clear the field. */
|
|
75
107
|
onChange: (val?: unknown) => void;
|
|
76
108
|
}
|
|
77
109
|
|
|
110
|
+
|
|
78
111
|
/** Props for the DynamicOptionsField component */
|
|
79
112
|
export interface DynamicOptionsFieldProps {
|
|
80
113
|
id: string;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { Inbox } from "lucide-react";
|
|
3
|
+
import { EmptyState } from "./EmptyState";
|
|
4
|
+
|
|
5
|
+
interface ListEmptyStateProps {
|
|
6
|
+
/**
|
|
7
|
+
* The name of the resource type the list would display (e.g. `"checks"`,
|
|
8
|
+
* `"incidents"`). Drives the default `"No {resource} yet"` headline.
|
|
9
|
+
*/
|
|
10
|
+
resource: string;
|
|
11
|
+
/**
|
|
12
|
+
* Optional supplemental description rendered beneath the headline.
|
|
13
|
+
*/
|
|
14
|
+
description?: React.ReactNode;
|
|
15
|
+
/**
|
|
16
|
+
* Optional action area, typically a primary CTA button such as
|
|
17
|
+
* "Create your first check".
|
|
18
|
+
*/
|
|
19
|
+
actions?: React.ReactNode;
|
|
20
|
+
/**
|
|
21
|
+
* Optional icon override. Defaults to the lucide `Inbox` glyph so callers
|
|
22
|
+
* don't have to pick one for every list.
|
|
23
|
+
*/
|
|
24
|
+
icon?: React.ReactNode;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* ListEmptyState - the canonical empty state for a list-shaped resource.
|
|
29
|
+
*
|
|
30
|
+
* Thin wrapper around {@link EmptyState} that supplies a consistent
|
|
31
|
+
* "No {resource} yet" headline and a sensible default icon. Use this on
|
|
32
|
+
* any page that renders a list and may have zero items so the UX stays
|
|
33
|
+
* uniform across plugins.
|
|
34
|
+
*/
|
|
35
|
+
export const ListEmptyState: React.FC<ListEmptyStateProps> = ({
|
|
36
|
+
resource,
|
|
37
|
+
description,
|
|
38
|
+
actions,
|
|
39
|
+
icon,
|
|
40
|
+
}) => {
|
|
41
|
+
const resolvedIcon = icon ?? <Inbox className="h-10 w-10" />;
|
|
42
|
+
|
|
43
|
+
return (
|
|
44
|
+
<EmptyState
|
|
45
|
+
title={`No ${resource} yet`}
|
|
46
|
+
description={description}
|
|
47
|
+
icon={resolvedIcon}
|
|
48
|
+
actions={actions}
|
|
49
|
+
/>
|
|
50
|
+
);
|
|
51
|
+
};
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { AlertCircle } from "lucide-react";
|
|
3
|
+
import { extractErrorMessage } from "@checkstack/common";
|
|
4
|
+
import {
|
|
5
|
+
Alert,
|
|
6
|
+
AlertContent,
|
|
7
|
+
AlertDescription,
|
|
8
|
+
AlertIcon,
|
|
9
|
+
AlertTitle,
|
|
10
|
+
} from "./Alert";
|
|
11
|
+
import { Button } from "./Button";
|
|
12
|
+
|
|
13
|
+
interface QueryErrorStateProps {
|
|
14
|
+
/**
|
|
15
|
+
* The error captured from a failed query (e.g. TanStack Query's `error`).
|
|
16
|
+
* Funnelled through {@link extractErrorMessage} so callers don't have to
|
|
17
|
+
* narrow the type at every call site.
|
|
18
|
+
*/
|
|
19
|
+
error: unknown;
|
|
20
|
+
/**
|
|
21
|
+
* Invoked when the user clicks the "Retry" button. Wire this to the
|
|
22
|
+
* underlying `refetch()` of the failing query.
|
|
23
|
+
*/
|
|
24
|
+
onRetry: () => void;
|
|
25
|
+
/**
|
|
26
|
+
* Optional resource name to personalise the headline, e.g.
|
|
27
|
+
* `resource="checks"` -> "Could not load checks".
|
|
28
|
+
*/
|
|
29
|
+
resource?: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* QueryErrorState - canonical inline error UI for failed list / detail
|
|
34
|
+
* queries. Renders an `error`-variant {@link Alert} with the extracted
|
|
35
|
+
* error message and a Retry button.
|
|
36
|
+
*/
|
|
37
|
+
export const QueryErrorState: React.FC<QueryErrorStateProps> = ({
|
|
38
|
+
error,
|
|
39
|
+
onRetry,
|
|
40
|
+
resource,
|
|
41
|
+
}) => {
|
|
42
|
+
const message = extractErrorMessage(error);
|
|
43
|
+
const title = resource ? `Could not load ${resource}` : "Something went wrong";
|
|
44
|
+
|
|
45
|
+
return (
|
|
46
|
+
<Alert variant="error">
|
|
47
|
+
<AlertIcon>
|
|
48
|
+
<AlertCircle className="h-4 w-4" />
|
|
49
|
+
</AlertIcon>
|
|
50
|
+
<AlertContent>
|
|
51
|
+
<AlertTitle>{title}</AlertTitle>
|
|
52
|
+
<AlertDescription>{message}</AlertDescription>
|
|
53
|
+
</AlertContent>
|
|
54
|
+
<Button
|
|
55
|
+
variant="outline"
|
|
56
|
+
size="sm"
|
|
57
|
+
onClick={onRetry}
|
|
58
|
+
className="shrink-0"
|
|
59
|
+
>
|
|
60
|
+
Retry
|
|
61
|
+
</Button>
|
|
62
|
+
</Alert>
|
|
63
|
+
);
|
|
64
|
+
};
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ResponsiveTable - dual-layout primitive for tabular data that must
|
|
3
|
+
* degrade gracefully on narrow viewports.
|
|
4
|
+
*
|
|
5
|
+
* # API decision
|
|
6
|
+
*
|
|
7
|
+
* The original plan considered a context-driven `priority` prop on a
|
|
8
|
+
* special `ResponsiveTableHead` so cells could declare which columns
|
|
9
|
+
* disappear on mobile. Implementing that without `any` requires either
|
|
10
|
+
* (a) cloning every `TableCell` child to inject a context-derived
|
|
11
|
+
* `data-priority` attribute, or (b) maintaining a parallel index of
|
|
12
|
+
* `TableHead` children to wire their priorities into the cells by
|
|
13
|
+
* position. Both shapes leak the matching responsibility into the
|
|
14
|
+
* primitive and produce gnarly typings around the `Table*` re-exports.
|
|
15
|
+
*
|
|
16
|
+
* Instead this file ships the simpler, fully type-safe fallback:
|
|
17
|
+
*
|
|
18
|
+
* - `<ResponsiveTable>` - a wrapper that renders its children inside
|
|
19
|
+
* the standard {@link Table} layout on `sm` viewports and up, and
|
|
20
|
+
* hides them on smaller screens.
|
|
21
|
+
* - `<MobileCardList>` - a sibling wrapper consumers render alongside
|
|
22
|
+
* the table. It is only visible below `sm`, so callers compose the
|
|
23
|
+
* two side-by-side and decide per-row what the mobile presentation
|
|
24
|
+
* looks like (typically a stacked card with high-priority fields).
|
|
25
|
+
*
|
|
26
|
+
* The two wrappers use Tailwind's `hidden sm:block` / `sm:hidden`
|
|
27
|
+
* utilities, so they swap purely in CSS - no JS media-query gating, no
|
|
28
|
+
* SSR/CSR mismatch risk, and consumers keep full control over which
|
|
29
|
+
* fields surface on mobile.
|
|
30
|
+
*
|
|
31
|
+
* Re-export the standard `Table*` primitives from `@checkstack/ui` for
|
|
32
|
+
* the desktop branch; do NOT use `<table>` markup inside
|
|
33
|
+
* `<MobileCardList>`.
|
|
34
|
+
*/
|
|
35
|
+
import React from "react";
|
|
36
|
+
import { cn } from "../utils";
|
|
37
|
+
|
|
38
|
+
interface ResponsiveTableProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
39
|
+
/**
|
|
40
|
+
* The desktop tabular layout. Compose with the existing `Table`,
|
|
41
|
+
* `TableHeader`, `TableBody`, `TableRow`, `TableHead`, `TableCell`
|
|
42
|
+
* primitives.
|
|
43
|
+
*/
|
|
44
|
+
children: React.ReactNode;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Desktop branch of the dual-layout pattern. Hidden below the `sm`
|
|
49
|
+
* breakpoint; render a {@link MobileCardList} alongside it to cover
|
|
50
|
+
* narrow viewports.
|
|
51
|
+
*/
|
|
52
|
+
export const ResponsiveTable = React.forwardRef<
|
|
53
|
+
HTMLDivElement,
|
|
54
|
+
ResponsiveTableProps
|
|
55
|
+
>(({ children, className, ...props }, ref) => (
|
|
56
|
+
<div
|
|
57
|
+
ref={ref}
|
|
58
|
+
className={cn("hidden sm:block", className)}
|
|
59
|
+
{...props}
|
|
60
|
+
>
|
|
61
|
+
{children}
|
|
62
|
+
</div>
|
|
63
|
+
));
|
|
64
|
+
|
|
65
|
+
ResponsiveTable.displayName = "ResponsiveTable";
|
|
66
|
+
|
|
67
|
+
interface MobileCardListProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
68
|
+
/**
|
|
69
|
+
* The stacked, card-shaped layout for narrow viewports. One item per
|
|
70
|
+
* row; consumers decide which fields are surfaced.
|
|
71
|
+
*/
|
|
72
|
+
children: React.ReactNode;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Mobile branch of the dual-layout pattern. Visible only below the `sm`
|
|
77
|
+
* breakpoint. Pairs with {@link ResponsiveTable}.
|
|
78
|
+
*/
|
|
79
|
+
export const MobileCardList = React.forwardRef<
|
|
80
|
+
HTMLDivElement,
|
|
81
|
+
MobileCardListProps
|
|
82
|
+
>(({ children, className, ...props }, ref) => (
|
|
83
|
+
<div
|
|
84
|
+
ref={ref}
|
|
85
|
+
className={cn("flex flex-col gap-2 sm:hidden", className)}
|
|
86
|
+
{...props}
|
|
87
|
+
>
|
|
88
|
+
{children}
|
|
89
|
+
</div>
|
|
90
|
+
));
|
|
91
|
+
|
|
92
|
+
MobileCardList.displayName = "MobileCardList";
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { cn } from "../utils";
|
|
3
|
+
import { usePerformance } from "./PerformanceProvider";
|
|
4
|
+
|
|
5
|
+
interface SkeletonProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
6
|
+
/**
|
|
7
|
+
* Override the default sizing / shape. The component already renders a
|
|
8
|
+
* muted background; pass dimensions via classes like `h-4 w-32 rounded`.
|
|
9
|
+
*/
|
|
10
|
+
className?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Skeleton - a pulsing placeholder block for loading states.
|
|
15
|
+
*
|
|
16
|
+
* Honours {@link usePerformance}: when `isLowPower` is true the pulse
|
|
17
|
+
* animation is dropped, leaving a static `bg-muted` block so low-power
|
|
18
|
+
* devices aren't forced through an infinite animation loop.
|
|
19
|
+
*/
|
|
20
|
+
export const Skeleton = React.forwardRef<HTMLDivElement, SkeletonProps>(
|
|
21
|
+
({ className, ...props }, ref) => {
|
|
22
|
+
const { isLowPower } = usePerformance();
|
|
23
|
+
|
|
24
|
+
return (
|
|
25
|
+
<div
|
|
26
|
+
ref={ref}
|
|
27
|
+
aria-hidden="true"
|
|
28
|
+
className={cn(
|
|
29
|
+
"rounded-md bg-muted",
|
|
30
|
+
!isLowPower && "animate-pulse",
|
|
31
|
+
className,
|
|
32
|
+
)}
|
|
33
|
+
{...props}
|
|
34
|
+
/>
|
|
35
|
+
);
|
|
36
|
+
},
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
Skeleton.displayName = "Skeleton";
|