@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.
@@ -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
- <Label className="text-sm font-semibold">Configuration</Label>
62
- <p className="text-xs text-muted-foreground">
63
- Configure how this check item behaves.
64
- </p>
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
- <DynamicForm
67
- schema={collectorDef.configSchema}
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
+ }