@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
|
@@ -0,0 +1,467 @@
|
|
|
1
|
+
import type { JsonSchemaProperty } from "../DynamicForm/types";
|
|
2
|
+
import type { EditorStarterTemplates, ShellEnvVar } from "../DynamicForm/types";
|
|
3
|
+
import { jsonSchemaToTypeScript } from "./generateTypeDefinitions";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Build a complete IntelliSense + starter-template + shell-env-var bundle
|
|
7
|
+
* for the script editor surfaces in checkstack. There are four:
|
|
8
|
+
*
|
|
9
|
+
* - `healthcheckInlineContext` — inline TS/JS health checks.
|
|
10
|
+
* `context.config` is typed from the collector's own config schema.
|
|
11
|
+
* A virtual `@checkstack/healthcheck` module exposes the required
|
|
12
|
+
* return shape so users get a type _error_ when the shape is wrong.
|
|
13
|
+
*
|
|
14
|
+
* - `healthcheckShellContext` — shell health checks. No platform-injected
|
|
15
|
+
* env vars today (the user supplies `env` themselves), so we surface
|
|
16
|
+
* the safe-vars whitelist (`PATH`, `HOME`, ...) and any user-defined
|
|
17
|
+
* env vars passed in the call.
|
|
18
|
+
*
|
|
19
|
+
* - `integrationInlineContext` — TS/JS integration scripts.
|
|
20
|
+
* `context.event.payload` is typed from the event's payload schema.
|
|
21
|
+
* A virtual `@checkstack/integration` module exposes the result shape.
|
|
22
|
+
*
|
|
23
|
+
* - `integrationShellContext` — shell integration scripts. The platform
|
|
24
|
+
* injects `EVENT_ID`, `DELIVERY_ID`, `SUBSCRIPTION_ID`,
|
|
25
|
+
* `SUBSCRIPTION_NAME` plus `PAYLOAD_*` flattened from the payload.
|
|
26
|
+
*
|
|
27
|
+
* The shape that comes back is what `DynamicForm`/`MultiTypeEditorField`/
|
|
28
|
+
* `CodeEditor` consume directly:
|
|
29
|
+
*
|
|
30
|
+
* ```ts
|
|
31
|
+
* const ctx = healthcheckInlineContext({ collectorConfigSchema });
|
|
32
|
+
* <DynamicForm
|
|
33
|
+
* schema={collector.configSchema}
|
|
34
|
+
* typeDefinitions={ctx.typeDefinitions}
|
|
35
|
+
* starterTemplates={ctx.starterTemplates}
|
|
36
|
+
* shellEnvVars={ctx.shellEnvVars}
|
|
37
|
+
* />
|
|
38
|
+
* ```
|
|
39
|
+
*
|
|
40
|
+
* Splitting it into four exported builders (rather than one mega-function)
|
|
41
|
+
* keeps the call sites readable: each builder takes only the schemas the
|
|
42
|
+
* editor will actually use.
|
|
43
|
+
*/
|
|
44
|
+
|
|
45
|
+
export interface ScriptEditorContext {
|
|
46
|
+
/** TypeScript declarations injected into Monaco for ts/js editors. */
|
|
47
|
+
typeDefinitions: string;
|
|
48
|
+
/** Starter templates per editor language. */
|
|
49
|
+
starterTemplates: EditorStarterTemplates;
|
|
50
|
+
/** Shell env vars surfaced via Monaco's `$` completion provider. */
|
|
51
|
+
shellEnvVars: ShellEnvVar[];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
55
|
+
// Virtual ambient modules — these make `import type { ... } from "..."`
|
|
56
|
+
// resolve in the editor and let us declare a typed return contract for
|
|
57
|
+
// inline scripts. They're _virtual_ in the sense that the runtime doesn't
|
|
58
|
+
// ship them (the result type is enforced at the data-coercion layer), but
|
|
59
|
+
// for the user's IDE experience they behave like a real package.
|
|
60
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Build the healthcheck virtual-module declarations including a
|
|
64
|
+
* properly-typed `HealthCheckScriptContext` whose `config` matches the
|
|
65
|
+
* caller's collector schema (or `Record<string, unknown>` as a generic
|
|
66
|
+
* fallback when no schema is supplied).
|
|
67
|
+
*
|
|
68
|
+
* Both the `defineHealthCheck(ctx => ...)` function-argument and the
|
|
69
|
+
* ambient `declare const context` reference the same generated type so
|
|
70
|
+
* users get accurate autocomplete from either entry point.
|
|
71
|
+
*/
|
|
72
|
+
function buildHealthCheckTypes(configType: string): string {
|
|
73
|
+
return `
|
|
74
|
+
/** Result emitted by an inline health-check script. */
|
|
75
|
+
interface HealthCheckScriptResult {
|
|
76
|
+
/** Whether the health check passed. */
|
|
77
|
+
success: boolean;
|
|
78
|
+
/** Optional status message — surfaces in the run detail. */
|
|
79
|
+
message?: string;
|
|
80
|
+
/** Optional numeric value — feeds the value chart + anomaly detection. */
|
|
81
|
+
value?: number;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Runtime context exposed to inline health-check scripts. \`config\` is
|
|
86
|
+
* typed from the collector's own JSON Schema, so the fields you've
|
|
87
|
+
* declared on the configuration are autocompletable inside the script.
|
|
88
|
+
*/
|
|
89
|
+
interface HealthCheckScriptContext {
|
|
90
|
+
/** Strongly-typed collector configuration. */
|
|
91
|
+
readonly config: ${configType};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Helper that asserts the return shape of a health-check script at the
|
|
96
|
+
* type level. Use either form:
|
|
97
|
+
*
|
|
98
|
+
* export default defineHealthCheck({ success: true, value: 42 });
|
|
99
|
+
* export default defineHealthCheck(async (ctx) => ({ success: true }));
|
|
100
|
+
*
|
|
101
|
+
* The result is returned unchanged at runtime — the function only exists
|
|
102
|
+
* for the editor's benefit, narrowing what \`export default\` is allowed
|
|
103
|
+
* to be so mistakes are caught before the script ever runs.
|
|
104
|
+
*
|
|
105
|
+
* Available both as a global (this declaration) and as a named export
|
|
106
|
+
* from \`@checkstack/healthcheck\` (below). The global form means the
|
|
107
|
+
* editor can autocomplete it without the user typing the import first;
|
|
108
|
+
* the module form is for explicit, IDE-style imports.
|
|
109
|
+
*/
|
|
110
|
+
declare function defineHealthCheck<
|
|
111
|
+
T extends
|
|
112
|
+
| HealthCheckScriptResult
|
|
113
|
+
| ((ctx: HealthCheckScriptContext) =>
|
|
114
|
+
| HealthCheckScriptResult
|
|
115
|
+
| Promise<HealthCheckScriptResult>),
|
|
116
|
+
>(value: T): T;
|
|
117
|
+
|
|
118
|
+
declare const context: HealthCheckScriptContext;
|
|
119
|
+
|
|
120
|
+
declare module "@checkstack/healthcheck" {
|
|
121
|
+
export type { HealthCheckScriptResult, HealthCheckScriptContext };
|
|
122
|
+
export { defineHealthCheck };
|
|
123
|
+
}
|
|
124
|
+
`;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Build the integration virtual-module declarations with a typed
|
|
129
|
+
* `IntegrationScriptContext.event.payload` (from the supplied event
|
|
130
|
+
* schema, or `Record<string, unknown>` when none is given).
|
|
131
|
+
*
|
|
132
|
+
* Same dual-export pattern as healthcheck — global helpers + named
|
|
133
|
+
* module exports — and the same shared context type powering both
|
|
134
|
+
* `defineIntegration(ctx => ...)` and the ambient `declare const context`.
|
|
135
|
+
*/
|
|
136
|
+
function buildIntegrationTypes(payloadType: string): string {
|
|
137
|
+
return `
|
|
138
|
+
/** Result emitted by an inline integration script. */
|
|
139
|
+
interface IntegrationScriptResult {
|
|
140
|
+
/** External ID for tracking (e.g. ticket number, message ID). */
|
|
141
|
+
id?: string;
|
|
142
|
+
/** Same as \`id\` — accepted for backwards compatibility. */
|
|
143
|
+
externalId?: string;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Runtime context exposed to inline integration scripts. \`event.payload\`
|
|
148
|
+
* is typed from the event's payload schema so the fields it advertises
|
|
149
|
+
* are autocompletable inside the script.
|
|
150
|
+
*/
|
|
151
|
+
interface IntegrationScriptContext {
|
|
152
|
+
/** The event being delivered. */
|
|
153
|
+
readonly event: {
|
|
154
|
+
/** Fully-qualified event ID, e.g. "incident.created". */
|
|
155
|
+
readonly eventId: string;
|
|
156
|
+
/** Strongly-typed event payload. */
|
|
157
|
+
readonly payload: ${payloadType};
|
|
158
|
+
/** ISO-8601 timestamp of when the event was emitted. */
|
|
159
|
+
readonly timestamp: string;
|
|
160
|
+
/** Unique ID for this delivery attempt. */
|
|
161
|
+
readonly deliveryId: string;
|
|
162
|
+
};
|
|
163
|
+
/** Subscription that triggered this delivery. */
|
|
164
|
+
readonly subscription: {
|
|
165
|
+
readonly id: string;
|
|
166
|
+
readonly name: string;
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Helper that asserts the return shape of an integration script at the
|
|
172
|
+
* type level. See \`defineHealthCheck\` for the analogous health-check
|
|
173
|
+
* helper. Available both as a global and as a named export from
|
|
174
|
+
* \`@checkstack/integration\`.
|
|
175
|
+
*/
|
|
176
|
+
declare function defineIntegration<
|
|
177
|
+
T extends
|
|
178
|
+
| IntegrationScriptResult
|
|
179
|
+
| void
|
|
180
|
+
| ((ctx: IntegrationScriptContext) =>
|
|
181
|
+
| IntegrationScriptResult
|
|
182
|
+
| void
|
|
183
|
+
| Promise<IntegrationScriptResult | void>),
|
|
184
|
+
>(value: T): T;
|
|
185
|
+
|
|
186
|
+
declare const context: IntegrationScriptContext;
|
|
187
|
+
|
|
188
|
+
declare module "@checkstack/integration" {
|
|
189
|
+
export type { IntegrationScriptResult, IntegrationScriptContext };
|
|
190
|
+
export { defineIntegration };
|
|
191
|
+
}
|
|
192
|
+
`;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
196
|
+
// Starter templates
|
|
197
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Default inline TypeScript template — uses `defineHealthCheck` (a runtime
|
|
201
|
+
* global) so the editor type-checks the return shape against
|
|
202
|
+
* `HealthCheckScriptResult`. Includes a `node:os` import to demonstrate
|
|
203
|
+
* that the Node stdlib is available and IntelliSensed.
|
|
204
|
+
*
|
|
205
|
+
* `defineHealthCheck` is available without an import (declared as an
|
|
206
|
+
* ambient global in the editor's type defs and injected onto
|
|
207
|
+
* `globalThis` by the runner). The named-import form
|
|
208
|
+
* `import { defineHealthCheck } from "@checkstack/healthcheck"` also
|
|
209
|
+
* works if users prefer it.
|
|
210
|
+
*/
|
|
211
|
+
const HEALTHCHECK_INLINE_TS_STARTER = `import { loadavg } from "node:os";
|
|
212
|
+
|
|
213
|
+
// Inline health checks run in a fresh Bun subprocess and have the full
|
|
214
|
+
// Node/Bun standard library available. \`context.config\` is typed from
|
|
215
|
+
// this collector's configuration schema — try autocompleting it below.
|
|
216
|
+
|
|
217
|
+
export default defineHealthCheck(() => {
|
|
218
|
+
const load = loadavg()[0];
|
|
219
|
+
return {
|
|
220
|
+
success: load < 0.6,
|
|
221
|
+
message: \`1m load average is \${load.toFixed(2)}\`,
|
|
222
|
+
value: load,
|
|
223
|
+
};
|
|
224
|
+
});
|
|
225
|
+
`;
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Default shell template for health checks. Uses `awk` + `if` to fail
|
|
229
|
+
* when the 1-minute load average exceeds 0.60 — the exact pattern the
|
|
230
|
+
* user originally tried but couldn't get working before the rewrite.
|
|
231
|
+
*
|
|
232
|
+
* Reads the load average via `uptime` rather than `/proc/loadavg` so the
|
|
233
|
+
* starter runs out-of-the-box on macOS satellites too. The two `uptime`
|
|
234
|
+
* formats differ:
|
|
235
|
+
*
|
|
236
|
+
* Linux : `… load average: 0.00, 0.01, 0.05`
|
|
237
|
+
* macOS : `… load averages: 0.45 0.55 0.65` (plural, no commas)
|
|
238
|
+
*
|
|
239
|
+
* `sed 's/.*load average[s]*: //'` strips the prefix on both;
|
|
240
|
+
* `awk '{print $1}'` grabs the 1-minute value;
|
|
241
|
+
* `tr -d ','` removes the trailing comma on Linux.
|
|
242
|
+
*/
|
|
243
|
+
const HEALTHCHECK_SHELL_STARTER = `#!/bin/sh
|
|
244
|
+
# Shell health checks run through \`sh -c\`, so pipes, redirects,
|
|
245
|
+
# command substitution, conditionals, etc. all work. Exit 0 = healthy,
|
|
246
|
+
# anything else = unhealthy. stdout/stderr are captured and shown on
|
|
247
|
+
# the run.
|
|
248
|
+
|
|
249
|
+
# 1-minute load average, portable across Linux + macOS.
|
|
250
|
+
load=$(uptime | sed 's/.*load average[s]*: //' | awk '{ print $1 }' | tr -d ',')
|
|
251
|
+
|
|
252
|
+
if awk -v l="$load" -v t=0.60 'BEGIN { exit (l+0 > t) ? 0 : 1 }'; then
|
|
253
|
+
echo "Load too high: $load"
|
|
254
|
+
exit 1
|
|
255
|
+
fi
|
|
256
|
+
echo "Load OK: $load"
|
|
257
|
+
`;
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Default inline TS template for integration scripts. Uses
|
|
261
|
+
* `defineIntegration` (a runtime global) for the typed return contract
|
|
262
|
+
* and demonstrates the `context.event` shape so the user sees what's
|
|
263
|
+
* available.
|
|
264
|
+
*/
|
|
265
|
+
const INTEGRATION_INLINE_TS_STARTER = `// Integration scripts run when an event fires. \`context.event.payload\`
|
|
266
|
+
// is typed from this event's payload schema — try autocompleting it.
|
|
267
|
+
// \`defineIntegration\` is available as a global (no import needed).
|
|
268
|
+
|
|
269
|
+
export default defineIntegration(async (context) => {
|
|
270
|
+
console.log("Received event:", context.event.eventId);
|
|
271
|
+
|
|
272
|
+
// Example: POST the payload to a webhook.
|
|
273
|
+
// const res = await fetch("https://example.com/webhook", {
|
|
274
|
+
// method: "POST",
|
|
275
|
+
// headers: { "Content-Type": "application/json" },
|
|
276
|
+
// body: JSON.stringify(context.event.payload),
|
|
277
|
+
// });
|
|
278
|
+
|
|
279
|
+
return { id: context.event.deliveryId };
|
|
280
|
+
});
|
|
281
|
+
`;
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Default shell template for integration scripts. Shows the available
|
|
285
|
+
* env vars so users have a concrete example to copy from.
|
|
286
|
+
*/
|
|
287
|
+
const INTEGRATION_SHELL_STARTER = `#!/bin/sh
|
|
288
|
+
# Integration shell scripts run when an event fires. The platform
|
|
289
|
+
# injects event context as environment variables:
|
|
290
|
+
#
|
|
291
|
+
# $EVENT_ID e.g. "incident.created"
|
|
292
|
+
# $EVENT_TIMESTAMP ISO-8601 timestamp
|
|
293
|
+
# $DELIVERY_ID unique per delivery
|
|
294
|
+
# $SUBSCRIPTION_ID
|
|
295
|
+
# $SUBSCRIPTION_NAME
|
|
296
|
+
# $PAYLOAD_* flattened payload fields (PAYLOAD_TITLE, ...)
|
|
297
|
+
#
|
|
298
|
+
# Type \`$\` to see the full list. Exit 0 = delivered, anything else
|
|
299
|
+
# triggers a retry per the subscription's retry policy.
|
|
300
|
+
|
|
301
|
+
echo "Got event $EVENT_ID at $EVENT_TIMESTAMP"
|
|
302
|
+
echo "Payload title: $PAYLOAD_TITLE"
|
|
303
|
+
|
|
304
|
+
# curl -fsS -X POST "https://example.com/webhook" \\
|
|
305
|
+
# -H "Content-Type: application/json" \\
|
|
306
|
+
# -d "{\\"event\\": \\"$EVENT_ID\\", \\"title\\": \\"$PAYLOAD_TITLE\\"}"
|
|
307
|
+
`;
|
|
308
|
+
|
|
309
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
310
|
+
// Shell env vars
|
|
311
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Vars always forwarded to user shell scripts on every flavour (health
|
|
315
|
+
* check + integration). The whitelist is enforced server-side; we list
|
|
316
|
+
* them here so they autocomplete in the editor.
|
|
317
|
+
*/
|
|
318
|
+
const SAFE_SHELL_VARS: ShellEnvVar[] = [
|
|
319
|
+
{ name: "PATH", description: "Executable lookup path.", example: "/usr/local/bin:/usr/bin:/bin" },
|
|
320
|
+
{ name: "HOME", description: "Current user's home directory." },
|
|
321
|
+
{ name: "USER", description: "Current user name." },
|
|
322
|
+
{ name: "LANG", description: "Default locale." },
|
|
323
|
+
{ name: "LC_ALL", description: "Locale override for all categories." },
|
|
324
|
+
{ name: "LC_CTYPE", description: "Locale override for character classification." },
|
|
325
|
+
{ name: "TZ", description: "Time-zone (e.g. \"Europe/Berlin\")." },
|
|
326
|
+
{ name: "TMPDIR", description: "Writable temp directory." },
|
|
327
|
+
{ name: "HOSTNAME", description: "Host name of the satellite." },
|
|
328
|
+
{ name: "SHELL", description: "User's login shell." },
|
|
329
|
+
];
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Vars injected by the integration platform on every delivery. Per-
|
|
333
|
+
* payload-field `PAYLOAD_*` vars are appended at call time based on the
|
|
334
|
+
* concrete event schema.
|
|
335
|
+
*/
|
|
336
|
+
const INTEGRATION_CORE_VARS: ShellEnvVar[] = [
|
|
337
|
+
{
|
|
338
|
+
name: "EVENT_ID",
|
|
339
|
+
description: "Fully-qualified event type, e.g. \"incident.created\".",
|
|
340
|
+
example: "incident.created",
|
|
341
|
+
},
|
|
342
|
+
{
|
|
343
|
+
name: "EVENT_TIMESTAMP",
|
|
344
|
+
description: "ISO-8601 timestamp of when the event was emitted.",
|
|
345
|
+
example: "2026-05-26T08:30:00.000Z",
|
|
346
|
+
},
|
|
347
|
+
{ name: "DELIVERY_ID", description: "Unique ID for this delivery attempt." },
|
|
348
|
+
{ name: "SUBSCRIPTION_ID", description: "ID of the subscription that fired." },
|
|
349
|
+
{ name: "SUBSCRIPTION_NAME", description: "Human-readable subscription name." },
|
|
350
|
+
];
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* Flatten a JSON-Schema payload into `PAYLOAD_*` env-var hints. Mirrors
|
|
354
|
+
* the runtime flattening done by the integration shell provider:
|
|
355
|
+
* uppercase + dots/dashes → underscores. Nested objects are flattened
|
|
356
|
+
* recursively; arrays become a single `PAYLOAD_FOO` (JSON-encoded).
|
|
357
|
+
*/
|
|
358
|
+
function flattenSchemaToEnvVars(
|
|
359
|
+
schema: JsonSchemaProperty,
|
|
360
|
+
prefix = "PAYLOAD",
|
|
361
|
+
): ShellEnvVar[] {
|
|
362
|
+
const out: ShellEnvVar[] = [];
|
|
363
|
+
|
|
364
|
+
if (schema.type !== "object" || !schema.properties) {
|
|
365
|
+
return [];
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
for (const [key, propSchema] of Object.entries(schema.properties)) {
|
|
369
|
+
const envKey = `${prefix}_${key
|
|
370
|
+
.toUpperCase()
|
|
371
|
+
.replaceAll(/[.-]/g, "_")
|
|
372
|
+
.replaceAll(/[^A-Z0-9_]/g, "")}`;
|
|
373
|
+
|
|
374
|
+
if (propSchema.type === "object" && propSchema.properties) {
|
|
375
|
+
out.push(...flattenSchemaToEnvVars(propSchema, envKey));
|
|
376
|
+
} else {
|
|
377
|
+
out.push({
|
|
378
|
+
name: envKey,
|
|
379
|
+
description: propSchema.description,
|
|
380
|
+
example:
|
|
381
|
+
propSchema.type === "string"
|
|
382
|
+
? "<string>"
|
|
383
|
+
: propSchema.type === "number" || propSchema.type === "integer"
|
|
384
|
+
? "<number>"
|
|
385
|
+
: propSchema.type === "boolean"
|
|
386
|
+
? "<true|false>"
|
|
387
|
+
: undefined,
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
return out;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
396
|
+
// Builders
|
|
397
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* Editor context for inline + shell health-check editors.
|
|
401
|
+
*
|
|
402
|
+
* Pass the collector's `configSchema` so `context.config` is typed —
|
|
403
|
+
* both as the ambient global and as the parameter type of
|
|
404
|
+
* `defineHealthCheck(ctx => ...)`. Without a schema, both default to
|
|
405
|
+
* `Record<string, unknown>`.
|
|
406
|
+
*/
|
|
407
|
+
export function healthcheckScriptContext(input: {
|
|
408
|
+
collectorConfigSchema?: JsonSchemaProperty;
|
|
409
|
+
}): ScriptEditorContext {
|
|
410
|
+
const configType = input.collectorConfigSchema
|
|
411
|
+
? jsonSchemaToTypeScript(input.collectorConfigSchema)
|
|
412
|
+
: "Record<string, unknown>";
|
|
413
|
+
|
|
414
|
+
return {
|
|
415
|
+
typeDefinitions: buildHealthCheckTypes(configType),
|
|
416
|
+
starterTemplates: {
|
|
417
|
+
typescript: HEALTHCHECK_INLINE_TS_STARTER,
|
|
418
|
+
javascript: HEALTHCHECK_INLINE_TS_STARTER,
|
|
419
|
+
shell: HEALTHCHECK_SHELL_STARTER,
|
|
420
|
+
},
|
|
421
|
+
shellEnvVars: SAFE_SHELL_VARS,
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
/**
|
|
426
|
+
* Editor context for inline + shell integration script editors.
|
|
427
|
+
*
|
|
428
|
+
* Pass the event's payload schema so `context.event.payload` is typed
|
|
429
|
+
* — both as the ambient global and as the parameter type of
|
|
430
|
+
* `defineIntegration(ctx => ...)`. Without a schema, both default to
|
|
431
|
+
* `Record<string, unknown>`. The schema also drives the `PAYLOAD_*`
|
|
432
|
+
* env-var hints in the shell editor.
|
|
433
|
+
*/
|
|
434
|
+
export function integrationScriptContext(input: {
|
|
435
|
+
eventPayloadSchema?: JsonSchemaProperty;
|
|
436
|
+
}): ScriptEditorContext {
|
|
437
|
+
const payloadType = input.eventPayloadSchema
|
|
438
|
+
? jsonSchemaToTypeScript(input.eventPayloadSchema)
|
|
439
|
+
: "Record<string, unknown>";
|
|
440
|
+
|
|
441
|
+
const payloadVars = input.eventPayloadSchema
|
|
442
|
+
? flattenSchemaToEnvVars(input.eventPayloadSchema)
|
|
443
|
+
: [];
|
|
444
|
+
|
|
445
|
+
return {
|
|
446
|
+
typeDefinitions: buildIntegrationTypes(payloadType),
|
|
447
|
+
starterTemplates: {
|
|
448
|
+
typescript: INTEGRATION_INLINE_TS_STARTER,
|
|
449
|
+
javascript: INTEGRATION_INLINE_TS_STARTER,
|
|
450
|
+
shell: INTEGRATION_SHELL_STARTER,
|
|
451
|
+
},
|
|
452
|
+
shellEnvVars: [...SAFE_SHELL_VARS, ...INTEGRATION_CORE_VARS, ...payloadVars],
|
|
453
|
+
};
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// Exported for unit tests — not part of the public API.
|
|
457
|
+
export const _internals = {
|
|
458
|
+
flattenSchemaToEnvVars,
|
|
459
|
+
SAFE_SHELL_VARS,
|
|
460
|
+
INTEGRATION_CORE_VARS,
|
|
461
|
+
HEALTHCHECK_INLINE_TS_STARTER,
|
|
462
|
+
HEALTHCHECK_SHELL_STARTER,
|
|
463
|
+
INTEGRATION_INLINE_TS_STARTER,
|
|
464
|
+
INTEGRATION_SHELL_STARTER,
|
|
465
|
+
buildHealthCheckTypes,
|
|
466
|
+
buildIntegrationTypes,
|
|
467
|
+
};
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
|
+
import {
|
|
3
|
+
matchShellEnvVarTrigger,
|
|
4
|
+
buildShellEnvVarInsertText,
|
|
5
|
+
} from "./shellEnvVarMatcher";
|
|
6
|
+
|
|
7
|
+
describe("matchShellEnvVarTrigger", () => {
|
|
8
|
+
it("returns null for an empty line", () => {
|
|
9
|
+
expect(matchShellEnvVarTrigger("")).toBeNull();
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it("returns null when the cursor is not at a `$`-prefixed reference", () => {
|
|
13
|
+
expect(matchShellEnvVarTrigger("echo hello")).toBeNull();
|
|
14
|
+
expect(matchShellEnvVarTrigger("echo $FOO ")).toBeNull(); // whitespace breaks the ident
|
|
15
|
+
expect(matchShellEnvVarTrigger("echo ${FOO} bar")).toBeNull();
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it("recognises a bare `$` with no query yet", () => {
|
|
19
|
+
expect(matchShellEnvVarTrigger("echo $")).toEqual({
|
|
20
|
+
form: "bare",
|
|
21
|
+
query: "",
|
|
22
|
+
prefixLength: 1,
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("recognises a bare reference with a partial name", () => {
|
|
27
|
+
expect(matchShellEnvVarTrigger("echo $EV")).toEqual({
|
|
28
|
+
form: "bare",
|
|
29
|
+
query: "EV",
|
|
30
|
+
prefixLength: 3,
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("uppercases the query so case-insensitive matching works downstream", () => {
|
|
35
|
+
expect(matchShellEnvVarTrigger("echo $payload_ti")).toEqual({
|
|
36
|
+
form: "bare",
|
|
37
|
+
query: "PAYLOAD_TI",
|
|
38
|
+
prefixLength: 11,
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("recognises a braced `${` with no query yet", () => {
|
|
43
|
+
expect(matchShellEnvVarTrigger("echo ${")).toEqual({
|
|
44
|
+
form: "braced",
|
|
45
|
+
query: "",
|
|
46
|
+
prefixLength: 2,
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("recognises a braced reference with a partial name", () => {
|
|
51
|
+
expect(matchShellEnvVarTrigger("echo ${PAY")).toEqual({
|
|
52
|
+
form: "braced",
|
|
53
|
+
query: "PAY",
|
|
54
|
+
prefixLength: 5,
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("prefers the braced form when both could match", () => {
|
|
59
|
+
// `echo ${FOO` — the `$` is part of `${`, so we must NOT report as bare.
|
|
60
|
+
const match = matchShellEnvVarTrigger("echo ${FOO");
|
|
61
|
+
expect(match?.form).toBe("braced");
|
|
62
|
+
expect(match?.query).toBe("FOO");
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("accepts underscores and digits in identifiers (shell-name-rule)", () => {
|
|
66
|
+
expect(matchShellEnvVarTrigger("echo $FOO_1")?.query).toBe("FOO_1");
|
|
67
|
+
expect(matchShellEnvVarTrigger("echo $_HIDDEN")?.query).toBe("_HIDDEN");
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("stops at non-identifier characters", () => {
|
|
71
|
+
// The `.` after FOO is not a valid identifier char, so we treat the
|
|
72
|
+
// cursor position as not being inside an env-var reference.
|
|
73
|
+
expect(matchShellEnvVarTrigger("echo $FOO.")).toBeNull();
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
describe("buildShellEnvVarInsertText", () => {
|
|
78
|
+
it("produces a bare `$NAME` for the bare form", () => {
|
|
79
|
+
expect(
|
|
80
|
+
buildShellEnvVarInsertText(
|
|
81
|
+
{ form: "bare", query: "", prefixLength: 1 },
|
|
82
|
+
"EVENT_ID",
|
|
83
|
+
),
|
|
84
|
+
).toBe("$EVENT_ID");
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("produces a `${NAME}` for the braced form", () => {
|
|
88
|
+
expect(
|
|
89
|
+
buildShellEnvVarInsertText(
|
|
90
|
+
{ form: "braced", query: "", prefixLength: 2 },
|
|
91
|
+
"EVENT_ID",
|
|
92
|
+
),
|
|
93
|
+
).toBe("${EVENT_ID}");
|
|
94
|
+
});
|
|
95
|
+
});
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure matching logic powering the Monaco shell-env-var completion
|
|
3
|
+
* provider. Kept in its own file so we can unit-test the matcher without
|
|
4
|
+
* spinning up a real Monaco instance — and so any regex changes are
|
|
5
|
+
* caught by the test suite rather than at smoke-test time.
|
|
6
|
+
*
|
|
7
|
+
* Given the text from the start of the line up to (but not including) the
|
|
8
|
+
* cursor, return either:
|
|
9
|
+
* - `null` if the cursor isn't sitting at a `$`-prefixed env-var
|
|
10
|
+
* reference, or
|
|
11
|
+
* - the shape of the match: which form (`bare` / `braced`), what the
|
|
12
|
+
* user has typed so far (`query`), and how many columns the
|
|
13
|
+
* completion range starts to the left of the cursor (`prefixLength`).
|
|
14
|
+
*/
|
|
15
|
+
export interface ShellEnvVarMatch {
|
|
16
|
+
/** "bare" for `$NAME`, "braced" for `${NAME}` (still being typed). */
|
|
17
|
+
form: "bare" | "braced";
|
|
18
|
+
/** What the user has typed after the `$` or `${`, uppercased for matching. */
|
|
19
|
+
query: string;
|
|
20
|
+
/** Length of the prefix to replace when inserting the completion. */
|
|
21
|
+
prefixLength: number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const BRACED = /\$\{([A-Z0-9_]*)$/i;
|
|
25
|
+
const BARE = /\$([A-Z0-9_]*)$/i;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Inspect the line text up to the cursor and decide whether to offer
|
|
29
|
+
* env-var completions.
|
|
30
|
+
*
|
|
31
|
+
* `${` takes priority over a bare `$` so that, when the user has already
|
|
32
|
+
* opened a brace, we close the completion with `${NAME}` (and don't
|
|
33
|
+
* accidentally suggest a bare `$NAME` which would leave a stray `${`
|
|
34
|
+
* dangling in front).
|
|
35
|
+
*/
|
|
36
|
+
export function matchShellEnvVarTrigger(
|
|
37
|
+
textBeforeCursor: string,
|
|
38
|
+
): ShellEnvVarMatch | null {
|
|
39
|
+
const braced = BRACED.exec(textBeforeCursor);
|
|
40
|
+
if (braced) {
|
|
41
|
+
return {
|
|
42
|
+
form: "braced",
|
|
43
|
+
query: (braced[1] ?? "").toUpperCase(),
|
|
44
|
+
prefixLength: braced[0].length,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const bare = BARE.exec(textBeforeCursor);
|
|
49
|
+
if (bare) {
|
|
50
|
+
return {
|
|
51
|
+
form: "bare",
|
|
52
|
+
query: (bare[1] ?? "").toUpperCase(),
|
|
53
|
+
prefixLength: bare[0].length,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Build the snippet to insert for a matched env-var completion. The form
|
|
62
|
+
* mirrors what the user already typed: `${NAME}` if they opened a brace,
|
|
63
|
+
* `$NAME` otherwise. Either is valid POSIX shell.
|
|
64
|
+
*/
|
|
65
|
+
export function buildShellEnvVarInsertText(
|
|
66
|
+
match: ShellEnvVarMatch,
|
|
67
|
+
varName: string,
|
|
68
|
+
): string {
|
|
69
|
+
return match.form === "braced" ? `\${${varName}}` : `$${varName}`;
|
|
70
|
+
}
|
|
@@ -18,6 +18,9 @@ export const DynamicForm: React.FC<DynamicFormProps> = ({
|
|
|
18
18
|
onValidChange,
|
|
19
19
|
optionsResolvers,
|
|
20
20
|
templateProperties,
|
|
21
|
+
typeDefinitions,
|
|
22
|
+
shellEnvVars,
|
|
23
|
+
starterTemplates,
|
|
21
24
|
}) => {
|
|
22
25
|
// Track previous validity to avoid redundant callbacks
|
|
23
26
|
const prevValidRef = React.useRef<boolean | undefined>(undefined);
|
|
@@ -113,6 +116,9 @@ export const DynamicForm: React.FC<DynamicFormProps> = ({
|
|
|
113
116
|
formValues={value}
|
|
114
117
|
optionsResolvers={optionsResolvers}
|
|
115
118
|
templateProperties={templateProperties}
|
|
119
|
+
typeDefinitions={typeDefinitions}
|
|
120
|
+
shellEnvVars={shellEnvVars}
|
|
121
|
+
starterTemplates={starterTemplates}
|
|
116
122
|
onChange={(val) => onChange({ ...value, [key]: val })}
|
|
117
123
|
/>
|
|
118
124
|
);
|