@checkstack/ui 1.9.0 → 1.11.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 +417 -0
- package/package.json +15 -7
- package/scripts/generate-stdlib-types.ts +2 -2
- package/src/components/ActionCard.tsx +221 -0
- package/src/components/CodeEditor/CodeEditor.tsx +51 -9
- package/src/components/CodeEditor/TypefoxEditor.tsx +868 -0
- package/src/components/CodeEditor/bracketKeyGroups.test.ts +120 -0
- package/src/components/CodeEditor/bracketKeyGroups.ts +205 -0
- package/src/components/CodeEditor/generateTypeDefinitions.ts +4 -4
- package/src/components/CodeEditor/index.ts +2 -0
- package/src/components/CodeEditor/scriptContext.test.ts +41 -0
- package/src/components/CodeEditor/scriptContext.ts +76 -1
- package/src/components/CodeEditor/templateValidation.ts +51 -0
- package/src/components/CodeEditor/types.ts +109 -0
- package/src/components/CodeEditor/validateJsonTemplate.test.ts +61 -0
- package/src/components/CodeEditor/validateJsonTemplate.ts +26 -0
- package/src/components/CodeEditor/validateXmlTemplate.test.ts +34 -0
- package/src/components/CodeEditor/validateXmlTemplate.ts +35 -0
- package/src/components/CodeEditor/validateYamlTemplate.test.ts +39 -0
- package/src/components/CodeEditor/validateYamlTemplate.ts +28 -0
- package/src/components/DynamicForm/DynamicForm.tsx +2 -0
- package/src/components/DynamicForm/FormField.tsx +29 -9
- package/src/components/DynamicForm/KeyValueEditor.tsx +2 -169
- package/src/components/DynamicForm/MultiTypeEditorField.tsx +16 -7
- package/src/components/DynamicForm/types.ts +11 -0
- 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/components/TemplateInput.tsx +104 -0
- package/src/components/TemplateInputToggle.tsx +111 -0
- package/src/components/TemplateValueInput.test.ts +98 -0
- package/src/components/TemplateValueInput.tsx +470 -0
- package/src/components/VariablePicker.tsx +271 -0
- package/src/hooks/useInitOnceForKey.test.ts +27 -0
- package/src/hooks/useInitOnceForKey.ts +21 -18
- package/src/index.ts +10 -0
- package/src/utils/toastTemplates.test.ts +82 -0
- package/src/utils/toastTemplates.ts +47 -0
- package/stories/ActionCard.stories.tsx +62 -0
- package/stories/Alert.stories.tsx +5 -5
- 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/TemplateInputToggle.stories.tsx +77 -0
- package/stories/TemplateValueInput.stories.tsx +65 -0
- package/stories/VariablePicker.stories.tsx +109 -0
- package/stories/toastTemplates.stories.tsx +60 -0
- package/src/components/CodeEditor/MonacoEditor.tsx +0 -616
- package/src/components/CodeEditor/monacoStdlib.ts +0 -62
- package/src/components/CodeEditor/monacoWorkers.ts +0 -118
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { describe, it, expect } from "bun:test";
|
|
2
|
+
import { extractBracketKeyGroups } from "./bracketKeyGroups";
|
|
3
|
+
|
|
4
|
+
describe("extractBracketKeyGroups", () => {
|
|
5
|
+
it("returns [] when there is no `declare const context`", () => {
|
|
6
|
+
expect(
|
|
7
|
+
extractBracketKeyGroups({ typeDefinitions: "interface Foo { x: string }" }),
|
|
8
|
+
).toEqual([]);
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it("returns [] when no property keys are non-identifiers", () => {
|
|
12
|
+
const typeDefinitions = `declare const context: {
|
|
13
|
+
event: { payload: { repository: string; count: number } };
|
|
14
|
+
};`;
|
|
15
|
+
expect(extractBracketKeyGroups({ typeDefinitions })).toEqual([]);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it("extracts a non-identifier key and its dotted parent path", () => {
|
|
19
|
+
const typeDefinitions = `declare const context: {
|
|
20
|
+
artifacts: {
|
|
21
|
+
"integration-jira.issue": { key: string; summary: string };
|
|
22
|
+
};
|
|
23
|
+
};`;
|
|
24
|
+
expect(extractBracketKeyGroups({ typeDefinitions })).toEqual([
|
|
25
|
+
{ objectExpression: "context.artifacts", keys: ["integration-jira.issue"] },
|
|
26
|
+
]);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("groups multiple non-identifier keys under the same parent", () => {
|
|
30
|
+
const typeDefinitions = `declare const context: {
|
|
31
|
+
artifacts: {
|
|
32
|
+
"integration-jira.issue": { key: string };
|
|
33
|
+
"integration-github.pr": { number: number };
|
|
34
|
+
valid: { ok: boolean };
|
|
35
|
+
};
|
|
36
|
+
};`;
|
|
37
|
+
expect(extractBracketKeyGroups({ typeDefinitions })).toEqual([
|
|
38
|
+
{
|
|
39
|
+
objectExpression: "context.artifacts",
|
|
40
|
+
keys: ["integration-jira.issue", "integration-github.pr"],
|
|
41
|
+
},
|
|
42
|
+
]);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("does NOT treat string-literal union *values* as keys", () => {
|
|
46
|
+
const typeDefinitions = `declare const context: {
|
|
47
|
+
artifacts: {
|
|
48
|
+
"a-b": { status: "open" | "closed"; url: "https://x" };
|
|
49
|
+
};
|
|
50
|
+
};`;
|
|
51
|
+
expect(extractBracketKeyGroups({ typeDefinitions })).toEqual([
|
|
52
|
+
{ objectExpression: "context.artifacts", keys: ["a-b"] },
|
|
53
|
+
]);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("ignores JSDoc and line comments (including braces inside them)", () => {
|
|
57
|
+
const typeDefinitions = `declare const context: {
|
|
58
|
+
/** an object like { not: a real brace } */
|
|
59
|
+
artifacts: {
|
|
60
|
+
// "comment-key": should be ignored
|
|
61
|
+
"real-key": { x: string };
|
|
62
|
+
};
|
|
63
|
+
};`;
|
|
64
|
+
expect(extractBracketKeyGroups({ typeDefinitions })).toEqual([
|
|
65
|
+
{ objectExpression: "context.artifacts", keys: ["real-key"] },
|
|
66
|
+
]);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("handles `readonly` and optional `?` modifiers and deeper paths", () => {
|
|
70
|
+
const typeDefinitions = `declare const context: {
|
|
71
|
+
readonly event: {
|
|
72
|
+
readonly meta?: {
|
|
73
|
+
"x-trace.id": { v: string };
|
|
74
|
+
};
|
|
75
|
+
};
|
|
76
|
+
};`;
|
|
77
|
+
expect(extractBracketKeyGroups({ typeDefinitions })).toEqual([
|
|
78
|
+
{ objectExpression: "context.event.meta", keys: ["x-trace.id"] },
|
|
79
|
+
]);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("skips non-identifier keys nested under a non-identifier parent", () => {
|
|
83
|
+
// `context["a-b"]` is not a dotted objectExpression, so the inner key has
|
|
84
|
+
// no addressable parent and must be skipped.
|
|
85
|
+
const typeDefinitions = `declare const context: {
|
|
86
|
+
"a-b": {
|
|
87
|
+
"c-d": { x: string };
|
|
88
|
+
};
|
|
89
|
+
};`;
|
|
90
|
+
expect(extractBracketKeyGroups({ typeDefinitions })).toEqual([
|
|
91
|
+
{ objectExpression: "context", keys: ["a-b"] },
|
|
92
|
+
]);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("extracts OPTIONAL quoted keys (`\"key\"?:`) - the real generated shape", () => {
|
|
96
|
+
// Mirrors generateAutomationContextTypes output for upstream artifacts:
|
|
97
|
+
// `readonly artifacts: { readonly "integration-jira.issue"?: {...}; }`.
|
|
98
|
+
const typeDefinitions = `declare const context: {
|
|
99
|
+
readonly artifacts: {
|
|
100
|
+
readonly "integration-jira.issue"?: { key: string; url?: string };
|
|
101
|
+
readonly "integration-github.pr"?: { number: number };
|
|
102
|
+
};
|
|
103
|
+
};`;
|
|
104
|
+
expect(extractBracketKeyGroups({ typeDefinitions })).toEqual([
|
|
105
|
+
{
|
|
106
|
+
objectExpression: "context.artifacts",
|
|
107
|
+
keys: ["integration-jira.issue", "integration-github.pr"],
|
|
108
|
+
},
|
|
109
|
+
]);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it("respects a custom root name", () => {
|
|
113
|
+
const typeDefinitions = `declare const scope: {
|
|
114
|
+
vars: { "weird key": { x: string } };
|
|
115
|
+
};`;
|
|
116
|
+
expect(
|
|
117
|
+
extractBracketKeyGroups({ typeDefinitions, rootName: "scope" }),
|
|
118
|
+
).toEqual([{ objectExpression: "scope.vars", keys: ["weird key"] }]);
|
|
119
|
+
});
|
|
120
|
+
});
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
// Stage 2b (monaco-editor -> @typefox/monaco-editor-react migration).
|
|
2
|
+
//
|
|
3
|
+
// The standalone TS worker calls `getCompletionsAtPosition` with no options
|
|
4
|
+
// (tsWorker.js: `getCompletionsAtPosition(fileName, position, void 0)`), so it
|
|
5
|
+
// never surfaces object members whose keys are not valid JS identifiers (e.g.
|
|
6
|
+
// artifact ids like `integration-jira.issue`). The built-in SuggestAdapter also
|
|
7
|
+
// hardcodes `insertText: entry.name` and can't be overridden, so a custom TS
|
|
8
|
+
// worker is not viable. Instead we register our own completion provider that
|
|
9
|
+
// offers those keys as `["key"]` bracket completions.
|
|
10
|
+
//
|
|
11
|
+
// To stay TYPE-DRIVEN (and avoid threading a separate `dottedKeyCompletions`
|
|
12
|
+
// prop), this module derives the groups straight from the injected
|
|
13
|
+
// `context.d.ts`: it finds object members whose property name is a quoted,
|
|
14
|
+
// non-identifier string and reports the dotted access path to their parent
|
|
15
|
+
// object plus the list of such keys.
|
|
16
|
+
|
|
17
|
+
/** A parent object plus the non-identifier keys reachable under it. */
|
|
18
|
+
export interface BracketKeyGroup {
|
|
19
|
+
/** Member-access expression preceding the dot, e.g. `context.artifacts`. */
|
|
20
|
+
objectExpression: string;
|
|
21
|
+
/** Keys to offer; each is inserted as `["<key>"]`. */
|
|
22
|
+
keys: string[];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const IDENTIFIER_RE = /^[A-Za-z_$][\w$]*$/;
|
|
26
|
+
|
|
27
|
+
const isIdentifier = (name: string): boolean => IDENTIFIER_RE.test(name);
|
|
28
|
+
|
|
29
|
+
const isWhitespace = (ch: string): boolean =>
|
|
30
|
+
ch === " " || ch === "\t" || ch === "\n" || ch === "\r";
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Extract bracket-notation completion groups from generated TS type
|
|
34
|
+
* definitions. Walks the `declare const <rootName>: { ... }` object literal,
|
|
35
|
+
* tracking the dotted path as it descends, and records every property whose
|
|
36
|
+
* name is a quoted non-identifier string.
|
|
37
|
+
*
|
|
38
|
+
* The walk is a small purpose-built scanner (not a full TS parser): it is only
|
|
39
|
+
* ever fed our own generated `context.d.ts`, whose shape is a plain nested
|
|
40
|
+
* object type. It correctly ignores string-literal type *values* (e.g.
|
|
41
|
+
* `status: "open" | "closed"`), JSDoc/line comments, and the `readonly` /
|
|
42
|
+
* optional (`?`) modifiers. Keys nested under a non-identifier parent are
|
|
43
|
+
* skipped because no dotted `objectExpression` can address them.
|
|
44
|
+
*/
|
|
45
|
+
export const extractBracketKeyGroups = ({
|
|
46
|
+
typeDefinitions,
|
|
47
|
+
rootName = "context",
|
|
48
|
+
}: {
|
|
49
|
+
typeDefinitions: string;
|
|
50
|
+
rootName?: string;
|
|
51
|
+
}): BracketKeyGroup[] => {
|
|
52
|
+
const src = typeDefinitions;
|
|
53
|
+
// Locate `declare const <rootName>` then the opening `{` of its type.
|
|
54
|
+
const declMatch = new RegExp(
|
|
55
|
+
String.raw`declare\s+const\s+${rootName}\b`,
|
|
56
|
+
).exec(src);
|
|
57
|
+
if (!declMatch) {
|
|
58
|
+
return [];
|
|
59
|
+
}
|
|
60
|
+
let i = src.indexOf("{", declMatch.index + declMatch[0].length);
|
|
61
|
+
if (i === -1) {
|
|
62
|
+
return [];
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Insertion-ordered accumulation so output is stable for tests/snapshots.
|
|
66
|
+
const groups = new Map<string, string[]>();
|
|
67
|
+
const addKey = (objectExpression: string, key: string): void => {
|
|
68
|
+
const existing = groups.get(objectExpression);
|
|
69
|
+
if (existing) {
|
|
70
|
+
if (!existing.includes(key)) {
|
|
71
|
+
existing.push(key);
|
|
72
|
+
}
|
|
73
|
+
} else {
|
|
74
|
+
groups.set(objectExpression, [key]);
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
// path[k] / dottable[k] describe the object open at brace depth k+1.
|
|
79
|
+
const path: string[] = [rootName];
|
|
80
|
+
const dottable: boolean[] = [true];
|
|
81
|
+
let depth = 1; // we consume the root `{` below
|
|
82
|
+
i += 1; // step past the root object's opening brace
|
|
83
|
+
|
|
84
|
+
// The property name most recently parsed, awaiting its value. Used to label
|
|
85
|
+
// the object we descend into on the next `{`.
|
|
86
|
+
let pendingName: { value: string; identifier: boolean } | null = null;
|
|
87
|
+
|
|
88
|
+
const peekNonWs = (from: number): number => {
|
|
89
|
+
let j = from;
|
|
90
|
+
while (j < src.length && isWhitespace(src[j] ?? "")) {
|
|
91
|
+
j += 1;
|
|
92
|
+
}
|
|
93
|
+
return j;
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
while (i < src.length && depth > 0) {
|
|
97
|
+
const ch = src[i] ?? "";
|
|
98
|
+
|
|
99
|
+
// Comments.
|
|
100
|
+
if (ch === "/" && src[i + 1] === "*") {
|
|
101
|
+
const end = src.indexOf("*/", i + 2);
|
|
102
|
+
i = end === -1 ? src.length : end + 2;
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
if (ch === "/" && src[i + 1] === "/") {
|
|
106
|
+
const nl = src.indexOf("\n", i + 2);
|
|
107
|
+
i = nl === -1 ? src.length : nl + 1;
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (isWhitespace(ch)) {
|
|
112
|
+
i += 1;
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// String token: either a property name (if followed by `:`) or a
|
|
117
|
+
// type-position literal (e.g. a union member) which we ignore.
|
|
118
|
+
if (ch === '"' || ch === "'") {
|
|
119
|
+
const quote = ch;
|
|
120
|
+
let j = i + 1;
|
|
121
|
+
let str = "";
|
|
122
|
+
while (j < src.length && src[j] !== quote) {
|
|
123
|
+
if (src[j] === "\\") {
|
|
124
|
+
str += src[j + 1] ?? "";
|
|
125
|
+
j += 2;
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
str += src[j];
|
|
129
|
+
j += 1;
|
|
130
|
+
}
|
|
131
|
+
const afterStr = j + 1; // past closing quote
|
|
132
|
+
// A quoted property name may be optional: `"key"?: ...` (this is exactly
|
|
133
|
+
// how generated artifact keys look, e.g. `"integration-jira.issue"?:`).
|
|
134
|
+
let colon = peekNonWs(afterStr);
|
|
135
|
+
if (src[colon] === "?") {
|
|
136
|
+
colon = peekNonWs(colon + 1);
|
|
137
|
+
}
|
|
138
|
+
if (src[colon] === ":") {
|
|
139
|
+
// Quoted property name. Record under the current object when every
|
|
140
|
+
// ancestor segment is dot-addressable.
|
|
141
|
+
if (dottable.every(Boolean) && !isIdentifier(str)) {
|
|
142
|
+
addKey(path.join("."), str);
|
|
143
|
+
}
|
|
144
|
+
pendingName = { value: str, identifier: isIdentifier(str) };
|
|
145
|
+
i = colon + 1;
|
|
146
|
+
} else {
|
|
147
|
+
// Type-position string literal - not a key.
|
|
148
|
+
i = afterStr;
|
|
149
|
+
}
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Identifier: a property name (if followed by `:`, allowing an optional
|
|
154
|
+
// `?`) or a type keyword / modifier we skip.
|
|
155
|
+
if (/[A-Za-z_$]/.test(ch)) {
|
|
156
|
+
let j = i;
|
|
157
|
+
while (j < src.length && /[\w$]/.test(src[j] ?? "")) {
|
|
158
|
+
j += 1;
|
|
159
|
+
}
|
|
160
|
+
const word = src.slice(i, j);
|
|
161
|
+
let after = peekNonWs(j);
|
|
162
|
+
if (src[after] === "?") {
|
|
163
|
+
after = peekNonWs(after + 1);
|
|
164
|
+
}
|
|
165
|
+
if (src[after] === ":") {
|
|
166
|
+
pendingName = { value: word, identifier: true };
|
|
167
|
+
i = after + 1;
|
|
168
|
+
} else {
|
|
169
|
+
// `readonly`, `string`, `interface`, etc. - not a key for us.
|
|
170
|
+
i = j;
|
|
171
|
+
}
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (ch === "{") {
|
|
176
|
+
depth += 1;
|
|
177
|
+
path.push(pendingName?.value ?? "");
|
|
178
|
+
dottable.push(pendingName?.identifier ?? false);
|
|
179
|
+
pendingName = null;
|
|
180
|
+
i += 1;
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (ch === "}") {
|
|
185
|
+
depth -= 1;
|
|
186
|
+
path.pop();
|
|
187
|
+
dottable.pop();
|
|
188
|
+
pendingName = null;
|
|
189
|
+
i += 1;
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// End of a property's value (e.g. `: string;`) - the pending name had a
|
|
194
|
+
// non-object type, so it never labels a descent.
|
|
195
|
+
if (ch === ";" || ch === ",") {
|
|
196
|
+
pendingName = null;
|
|
197
|
+
}
|
|
198
|
+
i += 1;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return [...groups].map(([objectExpression, keys]) => ({
|
|
202
|
+
objectExpression,
|
|
203
|
+
keys,
|
|
204
|
+
}));
|
|
205
|
+
};
|
|
@@ -89,10 +89,10 @@ declare const context: {
|
|
|
89
89
|
`);
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
-
// We deliberately don't redeclare `console`, `fetch`, the Node stdlib,
|
|
93
|
-
//
|
|
94
|
-
//
|
|
95
|
-
//
|
|
92
|
+
// We deliberately don't redeclare `console`, `fetch`, the Node stdlib, or
|
|
93
|
+
// the Bun globals here: the editor mounts the bundled upstream `@types/node`
|
|
94
|
+
// + `bun-types` declarations into the TypeScript service, so all of that is
|
|
95
|
+
// already in scope.
|
|
96
96
|
return lines.join("\n");
|
|
97
97
|
}
|
|
98
98
|
|
|
@@ -4,6 +4,7 @@ export {
|
|
|
4
4
|
type CodeEditorLanguage,
|
|
5
5
|
type TemplateProperty,
|
|
6
6
|
type ShellEnvVar,
|
|
7
|
+
type EditorMarker,
|
|
7
8
|
} from "./CodeEditor";
|
|
8
9
|
|
|
9
10
|
export {
|
|
@@ -12,6 +13,7 @@ export {
|
|
|
12
13
|
} from "./generateTypeDefinitions";
|
|
13
14
|
|
|
14
15
|
export {
|
|
16
|
+
customShellEnvVars,
|
|
15
17
|
healthcheckScriptContext,
|
|
16
18
|
integrationScriptContext,
|
|
17
19
|
type ScriptEditorContext,
|
|
@@ -52,6 +52,15 @@ describe("healthcheckScriptContext", () => {
|
|
|
52
52
|
expect(ctx.typeDefinitions).toContain("HealthCheckScriptContext");
|
|
53
53
|
});
|
|
54
54
|
|
|
55
|
+
it("exposes `context.check` and `context.system` run-context metadata", () => {
|
|
56
|
+
// The runner injects check/system metadata alongside config, so the
|
|
57
|
+
// editor must type them or `context.system.name` would error.
|
|
58
|
+
const ctx = healthcheckScriptContext({});
|
|
59
|
+
expect(ctx.typeDefinitions).toContain("readonly check: {");
|
|
60
|
+
expect(ctx.typeDefinitions).toContain("readonly system: {");
|
|
61
|
+
expect(ctx.typeDefinitions).toContain("readonly intervalSeconds: number");
|
|
62
|
+
});
|
|
63
|
+
|
|
55
64
|
it("types the `defineHealthCheck` callback parameter from the schema (not `unknown`)", () => {
|
|
56
65
|
// Regression guard: the previous version had `(ctx: unknown) => …`,
|
|
57
66
|
// so `ctx.config.host` produced "'ctx' is of type 'unknown'". The
|
|
@@ -95,10 +104,42 @@ describe("healthcheckScriptContext", () => {
|
|
|
95
104
|
expect(names).toContain("PATH");
|
|
96
105
|
expect(names).toContain("HOME");
|
|
97
106
|
expect(names).toContain("TZ");
|
|
107
|
+
// Run-context vars the shell collector injects are suggested too.
|
|
108
|
+
expect(names).toContain("CHECKSTACK_CHECK_NAME");
|
|
109
|
+
expect(names).toContain("CHECKSTACK_SYSTEM_NAME");
|
|
110
|
+
expect(names).toContain("CHECKSTACK_CHECK_INTERVAL_SECONDS");
|
|
98
111
|
// Integration-only vars must NOT leak into the healthcheck context.
|
|
99
112
|
expect(names).not.toContain("EVENT_ID");
|
|
100
113
|
expect(names).not.toContain("PAYLOAD_TITLE");
|
|
101
114
|
});
|
|
115
|
+
|
|
116
|
+
it("surfaces the user's custom Env (JSON) keys as shell completions", () => {
|
|
117
|
+
const ctx = healthcheckScriptContext({
|
|
118
|
+
customEnv: { API_TOKEN: "secret", "not-an-ident": "x" },
|
|
119
|
+
});
|
|
120
|
+
const names = ctx.shellEnvVars.map((v) => v.name);
|
|
121
|
+
// Valid shell identifier from the user's env is suggested...
|
|
122
|
+
expect(names).toContain("API_TOKEN");
|
|
123
|
+
// ...alongside the whitelist + reserved run-context vars.
|
|
124
|
+
expect(names).toContain("PATH");
|
|
125
|
+
expect(names).toContain("CHECKSTACK_SYSTEM_NAME");
|
|
126
|
+
// Keys that aren't valid `$NAME` identifiers are dropped.
|
|
127
|
+
expect(names).not.toContain("not-an-ident");
|
|
128
|
+
// The user's own var must be ordered ahead of the whitelist + run-context
|
|
129
|
+
// vars so it's not buried at the bottom of the suggest list.
|
|
130
|
+
expect(names.indexOf("API_TOKEN")).toBeLessThan(names.indexOf("PATH"));
|
|
131
|
+
expect(names.indexOf("API_TOKEN")).toBeLessThan(
|
|
132
|
+
names.indexOf("CHECKSTACK_SYSTEM_NAME"),
|
|
133
|
+
);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it("ignores a non-object customEnv without throwing", () => {
|
|
137
|
+
const names = healthcheckScriptContext({
|
|
138
|
+
customEnv: "not an object",
|
|
139
|
+
}).shellEnvVars.map((v) => v.name);
|
|
140
|
+
expect(names).toContain("PATH");
|
|
141
|
+
expect(names).toContain("CHECKSTACK_CHECK_ID");
|
|
142
|
+
});
|
|
102
143
|
});
|
|
103
144
|
|
|
104
145
|
describe("integrationScriptContext", () => {
|
|
@@ -89,6 +89,22 @@ interface HealthCheckScriptResult {
|
|
|
89
89
|
interface HealthCheckScriptContext {
|
|
90
90
|
/** Strongly-typed collector configuration. */
|
|
91
91
|
readonly config: ${configType};
|
|
92
|
+
/** Metadata about the health check this run is for. */
|
|
93
|
+
readonly check: {
|
|
94
|
+
/** The health check configuration id. */
|
|
95
|
+
readonly id: string;
|
|
96
|
+
/** The health check's display name (falls back to the id). */
|
|
97
|
+
readonly name: string;
|
|
98
|
+
/** The configured run interval, in seconds. */
|
|
99
|
+
readonly intervalSeconds: number;
|
|
100
|
+
};
|
|
101
|
+
/** Metadata about the system this check runs for. */
|
|
102
|
+
readonly system: {
|
|
103
|
+
/** The system id. */
|
|
104
|
+
readonly id: string;
|
|
105
|
+
/** The system's display name (falls back to the id). */
|
|
106
|
+
readonly name: string;
|
|
107
|
+
};
|
|
92
108
|
}
|
|
93
109
|
|
|
94
110
|
/**
|
|
@@ -328,6 +344,51 @@ const SAFE_SHELL_VARS: ShellEnvVar[] = [
|
|
|
328
344
|
{ name: "SHELL", description: "User's login shell." },
|
|
329
345
|
];
|
|
330
346
|
|
|
347
|
+
/**
|
|
348
|
+
* Run-context vars the satellite injects into every shell health-check
|
|
349
|
+
* run, describing the check + system it's for. Mirrors the reserved
|
|
350
|
+
* `CHECKSTACK_*` keys set by `healthcheck-script-backend`'s shell
|
|
351
|
+
* collector. User-supplied `env` values override these.
|
|
352
|
+
*/
|
|
353
|
+
const HEALTHCHECK_RUN_CONTEXT_VARS: ShellEnvVar[] = [
|
|
354
|
+
{ name: "CHECKSTACK_CHECK_ID", description: "This health check's configuration id." },
|
|
355
|
+
{
|
|
356
|
+
name: "CHECKSTACK_CHECK_NAME",
|
|
357
|
+
description: "This health check's display name (falls back to the id).",
|
|
358
|
+
},
|
|
359
|
+
{
|
|
360
|
+
name: "CHECKSTACK_CHECK_INTERVAL_SECONDS",
|
|
361
|
+
description: "The configured run interval, in seconds.",
|
|
362
|
+
},
|
|
363
|
+
{ name: "CHECKSTACK_SYSTEM_ID", description: "The id of the system being checked." },
|
|
364
|
+
{
|
|
365
|
+
name: "CHECKSTACK_SYSTEM_NAME",
|
|
366
|
+
description: "The system's display name (falls back to the id).",
|
|
367
|
+
},
|
|
368
|
+
];
|
|
369
|
+
|
|
370
|
+
/** A valid POSIX shell identifier — only these can be referenced as `$NAME`. */
|
|
371
|
+
const SHELL_IDENTIFIER = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* Turn a user's custom `env` object (e.g. a script action's or health
|
|
375
|
+
* check's "Env (JSON)" field) into `$`-completion hints. Defensive about
|
|
376
|
+
* the loosely-typed form value: non-objects yield nothing, and only keys
|
|
377
|
+
* that are valid shell identifiers are surfaced (a `$my-var` completion
|
|
378
|
+
* wouldn't be usable). Exported so editors that build their shell env
|
|
379
|
+
* suggestions outside the `*ScriptContext` helpers (e.g. the automation
|
|
380
|
+
* action editor) can merge custom env keys the same way.
|
|
381
|
+
*/
|
|
382
|
+
export function customShellEnvVars(env: unknown): ShellEnvVar[] {
|
|
383
|
+
if (typeof env !== "object" || env === null || Array.isArray(env)) return [];
|
|
384
|
+
return Object.keys(env)
|
|
385
|
+
.filter((name) => SHELL_IDENTIFIER.test(name))
|
|
386
|
+
.map((name) => ({
|
|
387
|
+
name,
|
|
388
|
+
description: "Custom variable from this check's Env (JSON) field.",
|
|
389
|
+
}));
|
|
390
|
+
}
|
|
391
|
+
|
|
331
392
|
/**
|
|
332
393
|
* Vars injected by the integration platform on every delivery. Per-
|
|
333
394
|
* payload-field `PAYLOAD_*` vars are appended at call time based on the
|
|
@@ -406,6 +467,13 @@ function flattenSchemaToEnvVars(
|
|
|
406
467
|
*/
|
|
407
468
|
export function healthcheckScriptContext(input: {
|
|
408
469
|
collectorConfigSchema?: JsonSchemaProperty;
|
|
470
|
+
/**
|
|
471
|
+
* The collector's current `env` value (the "Env (JSON)" field). Its
|
|
472
|
+
* keys are surfaced as `$`-completions so a user's own declared vars
|
|
473
|
+
* autocomplete alongside the whitelist + reserved run-context vars.
|
|
474
|
+
* Typed `unknown` because it comes from the loosely-typed form value.
|
|
475
|
+
*/
|
|
476
|
+
customEnv?: unknown;
|
|
409
477
|
}): ScriptEditorContext {
|
|
410
478
|
const configType = input.collectorConfigSchema
|
|
411
479
|
? jsonSchemaToTypeScript(input.collectorConfigSchema)
|
|
@@ -418,7 +486,14 @@ export function healthcheckScriptContext(input: {
|
|
|
418
486
|
javascript: HEALTHCHECK_INLINE_TS_STARTER,
|
|
419
487
|
shell: HEALTHCHECK_SHELL_STARTER,
|
|
420
488
|
},
|
|
421
|
-
|
|
489
|
+
// Most-relevant-first: the user's own declared env, then this check's
|
|
490
|
+
// run-context metadata, then the generic OS whitelist (the suggest list
|
|
491
|
+
// is ordered by insertion index, so order here is what the user sees).
|
|
492
|
+
shellEnvVars: [
|
|
493
|
+
...customShellEnvVars(input.customEnv),
|
|
494
|
+
...HEALTHCHECK_RUN_CONTEXT_VARS,
|
|
495
|
+
...SAFE_SHELL_VARS,
|
|
496
|
+
],
|
|
422
497
|
};
|
|
423
498
|
}
|
|
424
499
|
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// Shared bits for template-aware language validation (monaco -> @typefox
|
|
2
|
+
// migration). Markup editors (json / yaml / xml) hold a TEMPLATE that renders
|
|
3
|
+
// to the target language, not the language itself: a value can be templated in
|
|
4
|
+
// any position, including unquoted ones (e.g. a number `timeout: {{x}}`). So we
|
|
5
|
+
// validate "is this valid once the templates are filled in?" - substitute each
|
|
6
|
+
// `{{ ... }}` with a same-length neutral value, then parse with a real parser.
|
|
7
|
+
//
|
|
8
|
+
// Per-language validators live in validate{Json,Yaml,Xml}Template.ts and all
|
|
9
|
+
// return TemplateDiagnostic[]; the editor maps offsets to monaco markers.
|
|
10
|
+
|
|
11
|
+
export interface TemplateDiagnostic {
|
|
12
|
+
/** 0-based character offset into the ORIGINAL text. */
|
|
13
|
+
offset: number;
|
|
14
|
+
/** Length of the offending span (>= 1). */
|
|
15
|
+
length: number;
|
|
16
|
+
/** Human-readable message. */
|
|
17
|
+
message: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// `[^{}]*` (not `[^}]*`) so an unclosed `{{` can't swallow up to a later `}}`.
|
|
21
|
+
const TEMPLATE_PATTERN = /\{\{[^{}]*\}\}/g;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Replace each `{{ ... }}` with `0` padded to the same length with spaces - a
|
|
25
|
+
* neutral value token, valid in value positions (a number followed by
|
|
26
|
+
* whitespace) and harmless inside strings/text. Same length keeps byte offsets
|
|
27
|
+
* identical, so parser offsets map 1:1 onto the original text.
|
|
28
|
+
*/
|
|
29
|
+
export const substituteTemplates = (text: string): string =>
|
|
30
|
+
text.replaceAll(
|
|
31
|
+
TEMPLATE_PATTERN,
|
|
32
|
+
(match) => `0${" ".repeat(match.length - 1)}`,
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
/** Convert a 1-based line/column (some parsers report this) to a 0-based offset. */
|
|
36
|
+
export const lineColumnToOffset = ({
|
|
37
|
+
text,
|
|
38
|
+
line,
|
|
39
|
+
column,
|
|
40
|
+
}: {
|
|
41
|
+
text: string;
|
|
42
|
+
line: number;
|
|
43
|
+
column: number;
|
|
44
|
+
}): number => {
|
|
45
|
+
const lines = text.split("\n");
|
|
46
|
+
let offset = 0;
|
|
47
|
+
for (let index = 0; index < line - 1 && index < lines.length; index += 1) {
|
|
48
|
+
offset += (lines[index]?.length ?? 0) + 1; // +1 for the newline
|
|
49
|
+
}
|
|
50
|
+
return offset + Math.max(column - 1, 0);
|
|
51
|
+
};
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
// Shared, pure type definitions for the CodeEditor public API.
|
|
2
|
+
//
|
|
3
|
+
// NOTE: this file MUST stay free of monaco / @codingame / vite imports so that
|
|
4
|
+
// SSR/test/node paths can reference these types (and the package barrel can
|
|
5
|
+
// re-export them) without pulling in the browser-only editor stack.
|
|
6
|
+
|
|
7
|
+
export type CodeEditorLanguage =
|
|
8
|
+
| "json"
|
|
9
|
+
| "yaml"
|
|
10
|
+
| "xml"
|
|
11
|
+
| "markdown"
|
|
12
|
+
| "javascript"
|
|
13
|
+
| "typescript"
|
|
14
|
+
| "shell";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* A single payload property available for templating. Used by both the
|
|
18
|
+
* Monaco-backed `CodeEditor` and the lightweight single-line
|
|
19
|
+
* `TemplateValueInput` for the simple "insert a `{{ path }}` reference" flow.
|
|
20
|
+
*/
|
|
21
|
+
export interface TemplateProperty {
|
|
22
|
+
/** Full canonical path to the property, e.g., "trigger.payload.title". */
|
|
23
|
+
path: string;
|
|
24
|
+
/**
|
|
25
|
+
* Runtime-parseable `{{ }}` insertion text, e.g.
|
|
26
|
+
* `artifacts["integration-jira.issue"].issueKey`. When present this is
|
|
27
|
+
* what gets inserted; consumers fall back to `path` when it's absent.
|
|
28
|
+
*/
|
|
29
|
+
templateRef?: string;
|
|
30
|
+
/** Type label rendered on the right, e.g. "string", "number". */
|
|
31
|
+
type: string;
|
|
32
|
+
/** Optional description of the property. */
|
|
33
|
+
description?: string;
|
|
34
|
+
/**
|
|
35
|
+
* Known discrete values for this field, when the schema declares an `enum`.
|
|
36
|
+
* Consumed by the template-completion provider to suggest concrete values
|
|
37
|
+
* after a comparator (e.g. `"low"` / `"high"` for a severity field).
|
|
38
|
+
*/
|
|
39
|
+
enumValues?: Array<string | number | boolean>;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* A single environment variable available to shell scripts. Surfaced as a
|
|
44
|
+
* completion item after the user types `$` or `${` in shell mode.
|
|
45
|
+
*/
|
|
46
|
+
export interface ShellEnvVar {
|
|
47
|
+
/** Variable name without the leading `$`, e.g. `EVENT_ID`. */
|
|
48
|
+
name: string;
|
|
49
|
+
/** Optional description shown in the completion popup. */
|
|
50
|
+
description?: string;
|
|
51
|
+
/** Optional example value shown in the completion item detail. */
|
|
52
|
+
example?: string;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* An externally-supplied diagnostic to render as an inline squiggle. Positions
|
|
57
|
+
* are 1-based line/column (the editor's convention). Callers compute these from
|
|
58
|
+
* their own validation (e.g. mapping a definition issue path back to a YAML
|
|
59
|
+
* node range) and pass them via `markers`.
|
|
60
|
+
*/
|
|
61
|
+
export interface EditorMarker {
|
|
62
|
+
startLineNumber: number;
|
|
63
|
+
startColumn: number;
|
|
64
|
+
endLineNumber: number;
|
|
65
|
+
endColumn: number;
|
|
66
|
+
message: string;
|
|
67
|
+
severity?: "error" | "warning" | "info";
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export interface CodeEditorProps {
|
|
71
|
+
/** Unique identifier for the editor. */
|
|
72
|
+
id?: string;
|
|
73
|
+
/** Current value of the editor. */
|
|
74
|
+
value: string;
|
|
75
|
+
/** Callback when the value changes. */
|
|
76
|
+
onChange: (value: string) => void;
|
|
77
|
+
/** Language for syntax highlighting. */
|
|
78
|
+
language?: CodeEditorLanguage;
|
|
79
|
+
/** Minimum height of the editor, as a CSS length (e.g. "240px"). */
|
|
80
|
+
minHeight?: string;
|
|
81
|
+
/** Whether the editor is read-only. */
|
|
82
|
+
readOnly?: boolean;
|
|
83
|
+
/** Placeholder text when empty. */
|
|
84
|
+
placeholder?: string;
|
|
85
|
+
/**
|
|
86
|
+
* TypeScript type definitions to inject for IntelliSense (the `context.d.ts`).
|
|
87
|
+
* Generated from JSON schemas for context-aware autocomplete. For
|
|
88
|
+
* `typescript` / `javascript` editors, non-identifier object keys in these
|
|
89
|
+
* definitions (e.g. artifact ids) are offered as `["key"]` bracket
|
|
90
|
+
* completions automatically.
|
|
91
|
+
*/
|
|
92
|
+
typeDefinitions?: string;
|
|
93
|
+
/**
|
|
94
|
+
* Optional template properties for autocomplete. When provided, typing "{{"
|
|
95
|
+
* triggers autocomplete with the available template variables.
|
|
96
|
+
*/
|
|
97
|
+
templateProperties?: TemplateProperty[];
|
|
98
|
+
/**
|
|
99
|
+
* Optional environment-variable hints for shell mode. When provided and
|
|
100
|
+
* `language === "shell"`, they autocomplete after `$` and `${`.
|
|
101
|
+
*/
|
|
102
|
+
shellEnvVars?: ShellEnvVar[];
|
|
103
|
+
/**
|
|
104
|
+
* Externally-computed diagnostics rendered as inline squiggles (under a
|
|
105
|
+
* dedicated marker owner, so they coexist with the editor's own language
|
|
106
|
+
* diagnostics).
|
|
107
|
+
*/
|
|
108
|
+
markers?: EditorMarker[];
|
|
109
|
+
}
|