@checkstack/healthcheck-frontend 0.21.0 → 0.23.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 +309 -0
- package/package.json +21 -18
- package/src/auto-charts/extension.tsx +15 -2
- package/src/components/CatalogBrowseHealthFiller.tsx +60 -0
- package/src/components/CollectorList.tsx +10 -0
- package/src/components/HealthCheckDrawer.tsx +35 -4
- package/src/components/HealthCheckRunsTable.tsx +38 -4
- package/src/components/HealthCheckSystemOverview.tsx +19 -11
- package/src/components/HealthSignalsFiller.tsx +76 -0
- package/src/components/SystemHealthBadge.tsx +7 -2
- package/src/components/assignments/ExecutionPanel.tsx +91 -1
- package/src/components/assignments/NotificationsPanel.tsx +8 -237
- package/src/components/assignments/environment-selector.logic.test.ts +68 -0
- package/src/components/assignments/environment-selector.logic.ts +69 -0
- package/src/components/editor/CollectorScriptTestRenderer.tsx +115 -0
- package/src/components/editor/CollectorSection.tsx +80 -13
- package/src/components/editor/EditorPanel.tsx +26 -0
- package/src/components/editor/collector-preview-context.logic.test.ts +101 -0
- package/src/components/editor/collector-preview-context.logic.ts +88 -0
- package/src/index.tsx +65 -23
- package/src/pages/AssignmentIDEPage.tsx +88 -1
- package/src/pages/HealthCheckIDEPage.tsx +74 -0
- package/tsconfig.json +9 -0
- package/src/components/HealthCheckMenuItems.tsx +0 -30
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { describe, it, expect } from "bun:test";
|
|
2
|
+
import {
|
|
3
|
+
modeFromEnvironmentIds,
|
|
4
|
+
environmentIdsForMode,
|
|
5
|
+
toggleEnvironmentId,
|
|
6
|
+
} from "./environment-selector.logic";
|
|
7
|
+
|
|
8
|
+
describe("modeFromEnvironmentIds", () => {
|
|
9
|
+
it("null => all", () => {
|
|
10
|
+
expect(modeFromEnvironmentIds(null)).toBe("all");
|
|
11
|
+
});
|
|
12
|
+
it("undefined => all", () => {
|
|
13
|
+
expect(modeFromEnvironmentIds(undefined)).toBe("all");
|
|
14
|
+
});
|
|
15
|
+
it("[] => none", () => {
|
|
16
|
+
expect(modeFromEnvironmentIds([])).toBe("none");
|
|
17
|
+
});
|
|
18
|
+
it("non-empty => specific", () => {
|
|
19
|
+
expect(modeFromEnvironmentIds(["prod"])).toBe("specific");
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
describe("environmentIdsForMode", () => {
|
|
24
|
+
it("all => null", () => {
|
|
25
|
+
expect(environmentIdsForMode({ mode: "all", selectedIds: ["x"] })).toBeNull();
|
|
26
|
+
});
|
|
27
|
+
it("none => []", () => {
|
|
28
|
+
expect(environmentIdsForMode({ mode: "none", selectedIds: ["x"] })).toEqual(
|
|
29
|
+
[],
|
|
30
|
+
);
|
|
31
|
+
});
|
|
32
|
+
it("specific => the selected ids verbatim", () => {
|
|
33
|
+
expect(
|
|
34
|
+
environmentIdsForMode({ mode: "specific", selectedIds: ["prod", "qa"] }),
|
|
35
|
+
).toEqual(["prod", "qa"]);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("round-trips through modeFromEnvironmentIds for all three modes", () => {
|
|
39
|
+
expect(
|
|
40
|
+
modeFromEnvironmentIds(
|
|
41
|
+
environmentIdsForMode({ mode: "all", selectedIds: [] }),
|
|
42
|
+
),
|
|
43
|
+
).toBe("all");
|
|
44
|
+
expect(
|
|
45
|
+
modeFromEnvironmentIds(
|
|
46
|
+
environmentIdsForMode({ mode: "none", selectedIds: [] }),
|
|
47
|
+
),
|
|
48
|
+
).toBe("none");
|
|
49
|
+
expect(
|
|
50
|
+
modeFromEnvironmentIds(
|
|
51
|
+
environmentIdsForMode({ mode: "specific", selectedIds: ["e1"] }),
|
|
52
|
+
),
|
|
53
|
+
).toBe("specific");
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
describe("toggleEnvironmentId", () => {
|
|
58
|
+
it("adds an id not present, preserving order", () => {
|
|
59
|
+
expect(
|
|
60
|
+
toggleEnvironmentId({ selectedIds: ["a"], environmentId: "b" }),
|
|
61
|
+
).toEqual(["a", "b"]);
|
|
62
|
+
});
|
|
63
|
+
it("removes an id already present", () => {
|
|
64
|
+
expect(
|
|
65
|
+
toggleEnvironmentId({ selectedIds: ["a", "b"], environmentId: "a" }),
|
|
66
|
+
).toEqual(["b"]);
|
|
67
|
+
});
|
|
68
|
+
});
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure, DOM-free logic for the per-assignment environment selector. Kept
|
|
3
|
+
* separate from the React component so it can be unit-tested in the repo-root
|
|
4
|
+
* `bun test` run (which has no happy-dom).
|
|
5
|
+
*
|
|
6
|
+
* The selector has three modes that map to the `environmentIds` wire value
|
|
7
|
+
* (semantics locked in the environments plan, §2/§7.1):
|
|
8
|
+
* - "all" => `null` : run for ALL environments the system belongs to.
|
|
9
|
+
* - "specific" => `string[]` (non-empty): run for exactly those env ids.
|
|
10
|
+
* - "none" => `[]` : opt out, run ONCE with no environment.
|
|
11
|
+
*/
|
|
12
|
+
export type EnvironmentSelectorMode = "all" | "specific" | "none";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Derive the UI mode from the stored `environmentIds` wire value.
|
|
16
|
+
* `null`/`undefined` = all; `[]` = none; non-empty = specific.
|
|
17
|
+
*/
|
|
18
|
+
export function modeFromEnvironmentIds(
|
|
19
|
+
environmentIds: string[] | null | undefined,
|
|
20
|
+
): EnvironmentSelectorMode {
|
|
21
|
+
if (environmentIds === null || environmentIds === undefined) return "all";
|
|
22
|
+
if (environmentIds.length === 0) return "none";
|
|
23
|
+
return "specific";
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Compute the `environmentIds` wire value to persist for a chosen mode.
|
|
28
|
+
*
|
|
29
|
+
* For "specific", `selectedIds` is used verbatim (empty selection under
|
|
30
|
+
* "specific" is NOT auto-coerced to "none" here — the component keeps the
|
|
31
|
+
* mode explicit and `selectedIds` empty; callers/UI should require at least
|
|
32
|
+
* one selection before this is treated as a meaningful "specific" set).
|
|
33
|
+
*/
|
|
34
|
+
export function environmentIdsForMode({
|
|
35
|
+
mode,
|
|
36
|
+
selectedIds,
|
|
37
|
+
}: {
|
|
38
|
+
mode: EnvironmentSelectorMode;
|
|
39
|
+
selectedIds: string[];
|
|
40
|
+
}): string[] | null {
|
|
41
|
+
switch (mode) {
|
|
42
|
+
case "all": {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
case "none": {
|
|
46
|
+
return [];
|
|
47
|
+
}
|
|
48
|
+
case "specific": {
|
|
49
|
+
return selectedIds;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Toggle one environment id in a "specific" selection, returning the new
|
|
56
|
+
* sorted-stable list (membership order is the caller's concern; this just
|
|
57
|
+
* adds/removes preserving existing order).
|
|
58
|
+
*/
|
|
59
|
+
export function toggleEnvironmentId({
|
|
60
|
+
selectedIds,
|
|
61
|
+
environmentId,
|
|
62
|
+
}: {
|
|
63
|
+
selectedIds: string[];
|
|
64
|
+
environmentId: string;
|
|
65
|
+
}): string[] {
|
|
66
|
+
return selectedIds.includes(environmentId)
|
|
67
|
+
? selectedIds.filter((id) => id !== environmentId)
|
|
68
|
+
: [...selectedIds, environmentId];
|
|
69
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { usePluginClient } from "@checkstack/frontend-api";
|
|
3
|
+
import {
|
|
4
|
+
ScriptTestPanel,
|
|
5
|
+
ContextSampleEditor,
|
|
6
|
+
type ScriptTestRenderer,
|
|
7
|
+
type ScriptTestPanelResult,
|
|
8
|
+
} from "@checkstack/ui";
|
|
9
|
+
import { HealthCheckApi } from "@checkstack/healthcheck-common";
|
|
10
|
+
import { extractErrorMessage } from "@checkstack/common";
|
|
11
|
+
|
|
12
|
+
const TIMEOUT_MS = 30_000;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Placeholder check/system metadata for the auto-seeded sample. A test run
|
|
16
|
+
* has no real check assignment, so we surface stable example values for
|
|
17
|
+
* `context.check` / `context.system` (and the `$CHECKSTACK_*` shell vars).
|
|
18
|
+
*/
|
|
19
|
+
const SAMPLE_RUN_CONTEXT = {
|
|
20
|
+
check: { id: "test-check", name: "Test Check", intervalSeconds: 60 },
|
|
21
|
+
system: { id: "test-system", name: "Test System" },
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Build the auto-seeded sample context JSON for a collector script. There
|
|
26
|
+
* is no healthcheck replay (executions don't persist the script/config),
|
|
27
|
+
* so the seed is the collector's live config plus placeholder check/system.
|
|
28
|
+
*/
|
|
29
|
+
function seedSample(config: Record<string, unknown>): string {
|
|
30
|
+
return JSON.stringify(
|
|
31
|
+
{ config, ...SAMPLE_RUN_CONTEXT },
|
|
32
|
+
null,
|
|
33
|
+
2,
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
interface CollectorScriptTestPanelProps {
|
|
38
|
+
kind: "typescript" | "shell";
|
|
39
|
+
script: string;
|
|
40
|
+
/** Live collector config, used to auto-seed `context.config`. */
|
|
41
|
+
config: Record<string, unknown>;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const CollectorScriptTestPanel: React.FC<CollectorScriptTestPanelProps> = ({
|
|
45
|
+
kind,
|
|
46
|
+
script,
|
|
47
|
+
config,
|
|
48
|
+
}) => {
|
|
49
|
+
const client = usePluginClient(HealthCheckApi);
|
|
50
|
+
const testMutation = client.testCollectorScript.useMutation();
|
|
51
|
+
const [sampleContext, setSampleContext] = React.useState(() =>
|
|
52
|
+
seedSample(config),
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
const handleRun = React.useCallback(async (): Promise<ScriptTestPanelResult> => {
|
|
56
|
+
// Collector config has no `x-secret-env` field, so there are no secret
|
|
57
|
+
// overrides to forward; the panel passes none.
|
|
58
|
+
let parsed: {
|
|
59
|
+
config?: Record<string, unknown>;
|
|
60
|
+
check?: { id: string; name: string; intervalSeconds: number };
|
|
61
|
+
system?: { id: string; name: string };
|
|
62
|
+
} = {};
|
|
63
|
+
if (sampleContext.trim().length > 0) {
|
|
64
|
+
try {
|
|
65
|
+
parsed = JSON.parse(sampleContext) as typeof parsed;
|
|
66
|
+
} catch (error) {
|
|
67
|
+
return {
|
|
68
|
+
stdout: "",
|
|
69
|
+
stderr: "",
|
|
70
|
+
durationMs: 0,
|
|
71
|
+
timedOut: false,
|
|
72
|
+
error: `Sample context is not valid JSON: ${extractErrorMessage(error)}`,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return testMutation.mutateAsync({
|
|
77
|
+
kind,
|
|
78
|
+
script,
|
|
79
|
+
config: parsed.config,
|
|
80
|
+
runContext: {
|
|
81
|
+
...(parsed.check ? { check: parsed.check } : {}),
|
|
82
|
+
...(parsed.system ? { system: parsed.system } : {}),
|
|
83
|
+
},
|
|
84
|
+
timeoutMs: TIMEOUT_MS,
|
|
85
|
+
});
|
|
86
|
+
}, [testMutation, kind, script, sampleContext]);
|
|
87
|
+
|
|
88
|
+
return (
|
|
89
|
+
<ScriptTestPanel
|
|
90
|
+
onRun={handleRun}
|
|
91
|
+
disabled={script.trim().length === 0}
|
|
92
|
+
contextEditor={
|
|
93
|
+
<ContextSampleEditor value={sampleContext} onChange={setSampleContext} />
|
|
94
|
+
}
|
|
95
|
+
/>
|
|
96
|
+
);
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Build the {@link ScriptTestRenderer} for the health-check collector
|
|
101
|
+
* editor. Captures the live collector `config` so the auto-seeded sample
|
|
102
|
+
* matches what the operator is editing.
|
|
103
|
+
*/
|
|
104
|
+
export function createCollectorScriptTestRenderer(
|
|
105
|
+
config: Record<string, unknown>,
|
|
106
|
+
): ScriptTestRenderer {
|
|
107
|
+
return ({ fieldId, kind, script }) => (
|
|
108
|
+
<CollectorScriptTestPanel
|
|
109
|
+
key={`${fieldId}-test`}
|
|
110
|
+
kind={kind}
|
|
111
|
+
script={script}
|
|
112
|
+
config={config}
|
|
113
|
+
/>
|
|
114
|
+
);
|
|
115
|
+
}
|
|
@@ -10,7 +10,18 @@ import {
|
|
|
10
10
|
healthcheckScriptContext,
|
|
11
11
|
} from "@checkstack/ui";
|
|
12
12
|
import { Trash2 } from "lucide-react";
|
|
13
|
+
import {
|
|
14
|
+
useScriptPackageTypeAcquisition,
|
|
15
|
+
useSdkTypeInjection,
|
|
16
|
+
} from "@checkstack/script-packages-frontend";
|
|
17
|
+
import { useSecretNames } from "@checkstack/secrets-frontend";
|
|
18
|
+
import {
|
|
19
|
+
EnvironmentPreviewPicker,
|
|
20
|
+
type Environment,
|
|
21
|
+
} from "@checkstack/catalog-frontend";
|
|
13
22
|
import { AssertionBuilder, type Assertion } from "../AssertionBuilder";
|
|
23
|
+
import { createCollectorScriptTestRenderer } from "./CollectorScriptTestRenderer";
|
|
24
|
+
import { schemaHasTemplatableFields } from "./collector-preview-context.logic";
|
|
14
25
|
|
|
15
26
|
interface CollectorSectionProps {
|
|
16
27
|
entry: CollectorConfigEntry;
|
|
@@ -19,6 +30,21 @@ interface CollectorSectionProps {
|
|
|
19
30
|
onAssertionsChange: (assertions: CollectorConfigEntry["assertions"]) => void;
|
|
20
31
|
onValidChange: (isValid: boolean) => void;
|
|
21
32
|
onRemove: () => void;
|
|
33
|
+
/**
|
|
34
|
+
* Environments offered in the "Preview as" picker (the system's when one is
|
|
35
|
+
* in context, else all). Empty disables the picker.
|
|
36
|
+
*/
|
|
37
|
+
previewEnvironments: ReadonlyArray<Environment>;
|
|
38
|
+
/** Currently selected preview environment id (shared across collectors). */
|
|
39
|
+
previewEnvironmentId: string | null;
|
|
40
|
+
/** Called when the author picks (or clears) a preview environment. */
|
|
41
|
+
onPreviewEnvironmentChange: (environmentId: string | null) => void;
|
|
42
|
+
/**
|
|
43
|
+
* Sample context for previewing `x-templatable` fields, built from the
|
|
44
|
+
* selected environment's custom fields plus curated check/system metadata.
|
|
45
|
+
* `undefined` when no environment is selected (preview line stays hidden).
|
|
46
|
+
*/
|
|
47
|
+
templatePreviewContext?: Record<string, unknown>;
|
|
22
48
|
}
|
|
23
49
|
|
|
24
50
|
export const CollectorSection: React.FC<CollectorSectionProps> = ({
|
|
@@ -28,7 +54,25 @@ export const CollectorSection: React.FC<CollectorSectionProps> = ({
|
|
|
28
54
|
onAssertionsChange,
|
|
29
55
|
onValidChange,
|
|
30
56
|
onRemove,
|
|
57
|
+
previewEnvironments,
|
|
58
|
+
previewEnvironmentId,
|
|
59
|
+
onPreviewEnvironmentChange,
|
|
60
|
+
templatePreviewContext,
|
|
31
61
|
}) => {
|
|
62
|
+
const scriptTestRenderer = React.useMemo(
|
|
63
|
+
() => createCollectorScriptTestRenderer(entry.config),
|
|
64
|
+
[entry.config],
|
|
65
|
+
);
|
|
66
|
+
// Lazy ATA: collector scripts get package IntelliSense (incl. `@types/*`)
|
|
67
|
+
// on demand for whatever npm packages they import. `importablePackages`
|
|
68
|
+
// drives import-specifier name completion before any module is registered.
|
|
69
|
+
const { acquireTypes, acquireResetKey, importablePackages } =
|
|
70
|
+
useScriptPackageTypeAcquisition();
|
|
71
|
+
// SDK editor types so `@checkstack/sdk/healthcheck` imports resolve.
|
|
72
|
+
const { sdkTypes, sdkTypesResetKey } = useSdkTypeInjection();
|
|
73
|
+
// Secret names (never values) for the secret -> env mapping editor.
|
|
74
|
+
const { secretNames } = useSecretNames();
|
|
75
|
+
|
|
32
76
|
return (
|
|
33
77
|
<div className="space-y-6">
|
|
34
78
|
{/* Header */}
|
|
@@ -57,23 +101,46 @@ export const CollectorSection: React.FC<CollectorSectionProps> = ({
|
|
|
57
101
|
{/* Configuration */}
|
|
58
102
|
{collectorDef?.configSchema && (
|
|
59
103
|
<div className="space-y-3">
|
|
60
|
-
<div>
|
|
61
|
-
<
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
104
|
+
<div className="flex items-start justify-between gap-4">
|
|
105
|
+
<div>
|
|
106
|
+
<Label className="text-sm font-semibold">Configuration</Label>
|
|
107
|
+
<p className="text-xs text-muted-foreground">
|
|
108
|
+
Configure how this check item behaves.
|
|
109
|
+
</p>
|
|
110
|
+
</div>
|
|
111
|
+
{/* Only offer the preview picker when a templatable field exists. */}
|
|
112
|
+
{schemaHasTemplatableFields(collectorDef.configSchema) && (
|
|
113
|
+
<EnvironmentPreviewPicker
|
|
114
|
+
environments={previewEnvironments}
|
|
115
|
+
selectedId={previewEnvironmentId}
|
|
116
|
+
onSelect={onPreviewEnvironmentChange}
|
|
117
|
+
/>
|
|
118
|
+
)}
|
|
65
119
|
</div>
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
value={entry.config}
|
|
69
|
-
onChange={onConfigChange}
|
|
70
|
-
onValidChange={onValidChange}
|
|
71
|
-
{...healthcheckScriptContext({
|
|
120
|
+
{(() => {
|
|
121
|
+
const ctx = healthcheckScriptContext({
|
|
72
122
|
collectorConfigSchema: collectorDef.configSchema,
|
|
73
123
|
// Surface the user's own `env` keys as `$`-completions.
|
|
74
124
|
customEnv: entry.config.env,
|
|
75
|
-
})
|
|
76
|
-
|
|
125
|
+
});
|
|
126
|
+
return (
|
|
127
|
+
<DynamicForm
|
|
128
|
+
schema={collectorDef.configSchema}
|
|
129
|
+
value={entry.config}
|
|
130
|
+
onChange={onConfigChange}
|
|
131
|
+
onValidChange={onValidChange}
|
|
132
|
+
templatePreviewContext={templatePreviewContext}
|
|
133
|
+
{...ctx}
|
|
134
|
+
scriptTestRenderer={scriptTestRenderer}
|
|
135
|
+
secretNames={secretNames}
|
|
136
|
+
acquireTypes={acquireTypes}
|
|
137
|
+
acquireResetKey={acquireResetKey}
|
|
138
|
+
sdkTypes={sdkTypes}
|
|
139
|
+
sdkTypesResetKey={sdkTypesResetKey}
|
|
140
|
+
importablePackages={importablePackages}
|
|
141
|
+
/>
|
|
142
|
+
);
|
|
143
|
+
})()}
|
|
77
144
|
</div>
|
|
78
145
|
)}
|
|
79
146
|
|
|
@@ -10,6 +10,7 @@ import { CollectorSection } from "./CollectorSection";
|
|
|
10
10
|
import { CollectorPicker } from "./CollectorPicker";
|
|
11
11
|
import { SystemsSection } from "./SystemsSection";
|
|
12
12
|
import { TeamAccessEditor } from "@checkstack/auth-frontend";
|
|
13
|
+
import type { Environment } from "@checkstack/catalog-frontend";
|
|
13
14
|
|
|
14
15
|
// =============================================================================
|
|
15
16
|
// TYPES
|
|
@@ -49,6 +50,21 @@ interface EditorPanelProps {
|
|
|
49
50
|
systemsLoading?: boolean;
|
|
50
51
|
selectedSystemIds?: string[];
|
|
51
52
|
onSystemsChange?: (systemIds: string[]) => void;
|
|
53
|
+
/**
|
|
54
|
+
* Environments offered in the collector "Preview as" picker (the system's
|
|
55
|
+
* environments when a single system is in context, else all). Empty hides
|
|
56
|
+
* the picker.
|
|
57
|
+
*/
|
|
58
|
+
previewEnvironments?: ReadonlyArray<Environment>;
|
|
59
|
+
/** Selected preview environment id (shared across collectors). */
|
|
60
|
+
previewEnvironmentId?: string | null;
|
|
61
|
+
/** Called when the author picks (or clears) a preview environment. */
|
|
62
|
+
onPreviewEnvironmentChange?: (environmentId: string | null) => void;
|
|
63
|
+
/**
|
|
64
|
+
* Sample context for previewing the selected collector's `x-templatable`
|
|
65
|
+
* fields, built from the chosen environment. `undefined` when none chosen.
|
|
66
|
+
*/
|
|
67
|
+
templatePreviewContext?: Record<string, unknown>;
|
|
52
68
|
}
|
|
53
69
|
|
|
54
70
|
// =============================================================================
|
|
@@ -78,6 +94,10 @@ export const EditorPanel: React.FC<EditorPanelProps> = ({
|
|
|
78
94
|
systemsLoading = false,
|
|
79
95
|
selectedSystemIds = [],
|
|
80
96
|
onSystemsChange,
|
|
97
|
+
previewEnvironments = [],
|
|
98
|
+
previewEnvironmentId = null,
|
|
99
|
+
onPreviewEnvironmentChange,
|
|
100
|
+
templatePreviewContext,
|
|
81
101
|
}) => {
|
|
82
102
|
// --- General Section ---
|
|
83
103
|
if (selectedNode === "general") {
|
|
@@ -180,6 +200,12 @@ export const EditorPanel: React.FC<EditorPanelProps> = ({
|
|
|
180
200
|
}
|
|
181
201
|
onValidChange={(isValid) => onCollectorValidChange(entryId, isValid)}
|
|
182
202
|
onRemove={() => onCollectorRemove(entryId)}
|
|
203
|
+
previewEnvironments={previewEnvironments}
|
|
204
|
+
previewEnvironmentId={previewEnvironmentId}
|
|
205
|
+
onPreviewEnvironmentChange={(id) =>
|
|
206
|
+
onPreviewEnvironmentChange?.(id)
|
|
207
|
+
}
|
|
208
|
+
templatePreviewContext={templatePreviewContext}
|
|
183
209
|
/>
|
|
184
210
|
</div>
|
|
185
211
|
);
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { describe, it, expect } from "bun:test";
|
|
2
|
+
import {
|
|
3
|
+
schemaHasTemplatableFields,
|
|
4
|
+
buildTemplatePreviewContext,
|
|
5
|
+
} from "./collector-preview-context.logic";
|
|
6
|
+
import type { JsonSchema } from "@checkstack/ui";
|
|
7
|
+
|
|
8
|
+
describe("schemaHasTemplatableFields", () => {
|
|
9
|
+
it("returns false for empty/missing schema", () => {
|
|
10
|
+
expect(schemaHasTemplatableFields(undefined)).toBe(false);
|
|
11
|
+
expect(schemaHasTemplatableFields(null)).toBe(false);
|
|
12
|
+
expect(schemaHasTemplatableFields({ type: "object" })).toBe(false);
|
|
13
|
+
expect(
|
|
14
|
+
schemaHasTemplatableFields({ type: "object", properties: {} }),
|
|
15
|
+
).toBe(false);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it("detects a top-level templatable field", () => {
|
|
19
|
+
const schema: JsonSchema = {
|
|
20
|
+
type: "object",
|
|
21
|
+
properties: {
|
|
22
|
+
url: { type: "string", "x-templatable": true },
|
|
23
|
+
method: { type: "string" },
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
expect(schemaHasTemplatableFields(schema)).toBe(true);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("returns false when no field is templatable", () => {
|
|
30
|
+
const schema: JsonSchema = {
|
|
31
|
+
type: "object",
|
|
32
|
+
properties: {
|
|
33
|
+
url: { type: "string" },
|
|
34
|
+
timeout: { type: "number" },
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
expect(schemaHasTemplatableFields(schema)).toBe(false);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("detects a nested templatable field in an object property", () => {
|
|
41
|
+
const schema: JsonSchema = {
|
|
42
|
+
type: "object",
|
|
43
|
+
properties: {
|
|
44
|
+
auth: {
|
|
45
|
+
type: "object",
|
|
46
|
+
properties: {
|
|
47
|
+
token: { type: "string", "x-templatable": true },
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
expect(schemaHasTemplatableFields(schema)).toBe(true);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("detects a templatable field inside array items", () => {
|
|
56
|
+
const schema: JsonSchema = {
|
|
57
|
+
type: "object",
|
|
58
|
+
properties: {
|
|
59
|
+
headers: {
|
|
60
|
+
type: "array",
|
|
61
|
+
items: {
|
|
62
|
+
type: "object",
|
|
63
|
+
properties: {
|
|
64
|
+
value: { type: "string", "x-templatable": true },
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
expect(schemaHasTemplatableFields(schema)).toBe(true);
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
describe("buildTemplatePreviewContext", () => {
|
|
75
|
+
it("places environment fields under `environment`", () => {
|
|
76
|
+
const ctx = buildTemplatePreviewContext({
|
|
77
|
+
environmentFields: { baseUrl: "https://staging.example.com" },
|
|
78
|
+
});
|
|
79
|
+
expect(ctx.environment).toEqual({
|
|
80
|
+
baseUrl: "https://staging.example.com",
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it("defaults check/system to empty objects when absent", () => {
|
|
85
|
+
const ctx = buildTemplatePreviewContext({ environmentFields: {} });
|
|
86
|
+
expect(ctx).toEqual({ environment: {}, check: {}, system: {} });
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("includes curated check/system metadata when provided", () => {
|
|
90
|
+
const ctx = buildTemplatePreviewContext({
|
|
91
|
+
environmentFields: { baseUrl: "https://x" },
|
|
92
|
+
check: { id: "c1", name: "API health", intervalSeconds: 60 },
|
|
93
|
+
system: { id: "s1", name: "Payments" },
|
|
94
|
+
});
|
|
95
|
+
expect(ctx).toEqual({
|
|
96
|
+
environment: { baseUrl: "https://x" },
|
|
97
|
+
check: { id: "c1", name: "API health", intervalSeconds: 60 },
|
|
98
|
+
system: { id: "s1", name: "Payments" },
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
});
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DOM-free helpers backing the collector config "preview-as-environment" UX.
|
|
3
|
+
*
|
|
4
|
+
* Two concerns, both kept pure so they run in the repo-root `bun test` (no
|
|
5
|
+
* happy-dom):
|
|
6
|
+
*
|
|
7
|
+
* 1. Detect whether a collector's config schema has any `x-templatable` field
|
|
8
|
+
* (recursively), so the editor only shows the environment picker when a
|
|
9
|
+
* preview would actually be meaningful.
|
|
10
|
+
* 2. Build the `templatePreviewContext` fed to `DynamicForm` from a chosen
|
|
11
|
+
* environment's custom fields plus curated check/system metadata. The shape
|
|
12
|
+
* `{ environment, check, system }` mirrors the backend's run-time render
|
|
13
|
+
* context exactly (see `queue-executor.ts`), so the preview never diverges
|
|
14
|
+
* from what runs.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import type { JsonSchema, JsonSchemaProperty } from "@checkstack/ui";
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* True when `schema` (or any nested object/array property) declares at least
|
|
21
|
+
* one `x-templatable` field. Used to gate the preview picker: no templatable
|
|
22
|
+
* field means no `{{ … }}` to resolve, so the picker stays hidden.
|
|
23
|
+
*/
|
|
24
|
+
export function schemaHasTemplatableFields(
|
|
25
|
+
schema: JsonSchema | undefined | null,
|
|
26
|
+
): boolean {
|
|
27
|
+
if (!schema?.properties) return false;
|
|
28
|
+
return Object.values(schema.properties).some((property) =>
|
|
29
|
+
propertyHasTemplatable(property),
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function propertyHasTemplatable(property: JsonSchemaProperty): boolean {
|
|
34
|
+
if (property["x-templatable"]) return true;
|
|
35
|
+
if (
|
|
36
|
+
property.properties &&
|
|
37
|
+
Object.values(property.properties).some((nested) =>
|
|
38
|
+
propertyHasTemplatable(nested),
|
|
39
|
+
)
|
|
40
|
+
) {
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
43
|
+
if (property.items && propertyHasTemplatable(property.items)) {
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Curated check/system metadata exposed to the preview, matching the run-time
|
|
51
|
+
* `CollectorRunContext` subset that templating sees. Optional because the
|
|
52
|
+
* editor may not have a concrete system in context (the author writes a config
|
|
53
|
+
* that several systems may later use).
|
|
54
|
+
*/
|
|
55
|
+
export interface PreviewCheckMeta {
|
|
56
|
+
id: string;
|
|
57
|
+
name: string;
|
|
58
|
+
intervalSeconds: number;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface PreviewSystemMeta {
|
|
62
|
+
id: string;
|
|
63
|
+
name: string;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Build the sample `templatePreviewContext` for `DynamicForm`. `environment`
|
|
68
|
+
* carries the chosen environment's verbatim custom fields (its catalog
|
|
69
|
+
* `metadata`); `check`/`system` carry curated metadata when available. The
|
|
70
|
+
* resulting shape `{ environment, check, system }` matches the backend
|
|
71
|
+
* `templateContext` so `{{ environment.baseUrl }}` previews exactly as it
|
|
72
|
+
* renders at run time.
|
|
73
|
+
*/
|
|
74
|
+
export function buildTemplatePreviewContext({
|
|
75
|
+
environmentFields,
|
|
76
|
+
check,
|
|
77
|
+
system,
|
|
78
|
+
}: {
|
|
79
|
+
environmentFields: Record<string, unknown>;
|
|
80
|
+
check?: PreviewCheckMeta;
|
|
81
|
+
system?: PreviewSystemMeta;
|
|
82
|
+
}): Record<string, unknown> {
|
|
83
|
+
return {
|
|
84
|
+
environment: environmentFields,
|
|
85
|
+
check: check ?? {},
|
|
86
|
+
system: system ?? {},
|
|
87
|
+
};
|
|
88
|
+
}
|