@checkstack/automation-frontend 0.2.0 → 0.3.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 +352 -0
- package/package.json +13 -9
- package/src/components/AutomationGroupCombobox.tsx +133 -0
- package/src/editor/ActionEditor.tsx +180 -90
- package/src/editor/ActionListEditor.tsx +27 -1
- package/src/editor/AddActionDialog.tsx +15 -45
- package/src/editor/AddConditionDialog.tsx +86 -0
- package/src/editor/AddTriggerDialog.tsx +97 -0
- package/src/editor/AutomationDefinitionEditor.tsx +41 -2
- package/src/editor/ConditionEditor.tsx +359 -70
- package/src/editor/ConditionsEditor.tsx +113 -44
- package/src/editor/ItemSheet.tsx +51 -0
- package/src/editor/RunReplayPicker.tsx +97 -0
- package/src/editor/ScriptServicesBooter.tsx +53 -0
- package/src/editor/ScriptTestRenderer.tsx +150 -0
- package/src/editor/SystemEntityPicker.test.ts +37 -0
- package/src/editor/SystemEntityPicker.tsx +109 -0
- package/src/editor/TriggersEditor.tsx +345 -137
- package/src/editor/action-helpers.test.ts +107 -0
- package/src/editor/action-helpers.ts +72 -0
- package/src/editor/action-leaf-cards.tsx +98 -1
- package/src/editor/condition-kind.test.ts +126 -0
- package/src/editor/condition-kind.ts +130 -0
- package/src/editor/item-summary.test.ts +171 -0
- package/src/editor/item-summary.ts +210 -0
- package/src/editor/picker-dialog.tsx +156 -0
- package/src/editor/registry-context.tsx +9 -2
- package/src/editor/script-actions.test.ts +184 -0
- package/src/editor/script-actions.ts +146 -0
- package/src/editor/system-entity-picker.logic.ts +23 -0
- package/src/editor/template-completion.test.ts +22 -3
- package/src/editor/template-completion.ts +16 -8
- package/src/editor/template-helpers.ts +4 -0
- package/src/editor/trigger-helpers.test.ts +28 -0
- package/src/editor/trigger-helpers.ts +17 -0
- package/src/editor/useScriptDiagnostics.ts +108 -0
- package/src/index.tsx +2 -0
- package/src/pages/AutomationEditPage.tsx +95 -47
- package/src/pages/AutomationListPage.tsx +172 -123
- package/src/pages/automation-grouping.test.ts +86 -0
- package/src/pages/automation-grouping.ts +65 -0
- package/src/script-context.test.ts +142 -1
- package/src/script-context.ts +115 -0
- package/tsconfig.json +12 -0
|
@@ -12,11 +12,29 @@ import {
|
|
|
12
12
|
import { TriggersEditor } from "./TriggersEditor";
|
|
13
13
|
import { ConditionsEditor } from "./ConditionsEditor";
|
|
14
14
|
import { ActionListEditor } from "./ActionListEditor";
|
|
15
|
+
import { ValidationProvider, type DefinitionIssue } from "./editor-validation";
|
|
16
|
+
import { useScriptDiagnostics } from "./useScriptDiagnostics";
|
|
17
|
+
import { collectScriptActions } from "./script-actions";
|
|
18
|
+
import { ScriptServicesBooter } from "./ScriptServicesBooter";
|
|
15
19
|
|
|
16
20
|
export interface AutomationDefinitionEditorProps {
|
|
17
21
|
value: AutomationDefinition;
|
|
18
22
|
onChange: (next: AutomationDefinition) => void;
|
|
19
23
|
disabled?: boolean;
|
|
24
|
+
/**
|
|
25
|
+
* Id of the automation being edited, if it already exists (omitted for
|
|
26
|
+
* a brand-new, unsaved automation). Enables the script-test "Load from
|
|
27
|
+
* run" picker, which lists this automation's prior runs.
|
|
28
|
+
*/
|
|
29
|
+
automationId?: string;
|
|
30
|
+
/**
|
|
31
|
+
* Structural validation issues (from the backend `validateDefinition`),
|
|
32
|
+
* merged here with the live, frontend-computed inline-script TYPE issues so
|
|
33
|
+
* both surface through the same per-card badges. Script type-checking lives
|
|
34
|
+
* inside this component because it needs the registry (trigger/action
|
|
35
|
+
* catalog) the `AutomationRegistryProvider` supplies.
|
|
36
|
+
*/
|
|
37
|
+
structuralIssues?: DefinitionIssue[];
|
|
20
38
|
}
|
|
21
39
|
|
|
22
40
|
/**
|
|
@@ -31,7 +49,7 @@ export interface AutomationDefinitionEditorProps {
|
|
|
31
49
|
export const AutomationDefinitionEditor: React.FC<
|
|
32
50
|
AutomationDefinitionEditorProps
|
|
33
51
|
> = (props) => (
|
|
34
|
-
<AutomationRegistryProvider>
|
|
52
|
+
<AutomationRegistryProvider automationId={props.automationId}>
|
|
35
53
|
<AutomationDefinitionProvider definition={props.value}>
|
|
36
54
|
<EditorBody {...props} />
|
|
37
55
|
</AutomationDefinitionProvider>
|
|
@@ -42,8 +60,26 @@ const EditorBody: React.FC<AutomationDefinitionEditorProps> = ({
|
|
|
42
60
|
value,
|
|
43
61
|
onChange,
|
|
44
62
|
disabled,
|
|
63
|
+
structuralIssues = [],
|
|
45
64
|
}) => {
|
|
46
|
-
const { loading } = useAutomationRegistry();
|
|
65
|
+
const { loading, actions } = useAutomationRegistry();
|
|
66
|
+
|
|
67
|
+
// Live type-check of every inline script action (collapsed cards included),
|
|
68
|
+
// merged with the backend's structural issues so both drive the same
|
|
69
|
+
// per-card badges via `ValidationProvider`.
|
|
70
|
+
const scriptIssues = useScriptDiagnostics({ definition: value });
|
|
71
|
+
const issues = React.useMemo(
|
|
72
|
+
() => [...structuralIssues, ...scriptIssues],
|
|
73
|
+
[structuralIssues, scriptIssues],
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
// When the automation already contains inline scripts, boot the editor
|
|
77
|
+
// services up-front (a hidden editor) so those scripts validate the moment
|
|
78
|
+
// the automation opens - not only after a card is expanded.
|
|
79
|
+
const hasScriptActions = React.useMemo(
|
|
80
|
+
() => collectScriptActions({ definition: value, actions }).length > 0,
|
|
81
|
+
[value, actions],
|
|
82
|
+
);
|
|
47
83
|
|
|
48
84
|
// Scope at the root path = `trigger.*` only. The conditions editor
|
|
49
85
|
// uses `variableNodes` for the explicit "fx" tree and
|
|
@@ -55,6 +91,8 @@ const EditorBody: React.FC<AutomationDefinitionEditorProps> = ({
|
|
|
55
91
|
});
|
|
56
92
|
|
|
57
93
|
return (
|
|
94
|
+
<ValidationProvider issues={issues}>
|
|
95
|
+
{hasScriptActions && <ScriptServicesBooter />}
|
|
58
96
|
<div className="space-y-4">
|
|
59
97
|
{loading && (
|
|
60
98
|
<Card className="border-dashed">
|
|
@@ -95,5 +133,6 @@ const EditorBody: React.FC<AutomationDefinitionEditorProps> = ({
|
|
|
95
133
|
</CardContent>
|
|
96
134
|
</Card>
|
|
97
135
|
</div>
|
|
136
|
+
</ValidationProvider>
|
|
98
137
|
);
|
|
99
138
|
};
|
|
@@ -4,43 +4,38 @@ import {
|
|
|
4
4
|
Button,
|
|
5
5
|
Card,
|
|
6
6
|
CardContent,
|
|
7
|
+
DurationInput,
|
|
8
|
+
Input,
|
|
9
|
+
Label,
|
|
7
10
|
Select,
|
|
8
11
|
SelectContent,
|
|
12
|
+
SelectGroup,
|
|
9
13
|
SelectItem,
|
|
14
|
+
SelectLabel,
|
|
15
|
+
SelectSeparator,
|
|
10
16
|
SelectTrigger,
|
|
11
17
|
SelectValue,
|
|
12
18
|
TemplateValueInput,
|
|
19
|
+
TimeOfDayInput,
|
|
20
|
+
Toggle,
|
|
13
21
|
VariablePicker,
|
|
22
|
+
type DurationValue,
|
|
14
23
|
type TemplateCompletionProvider,
|
|
15
24
|
type VariableNode,
|
|
16
25
|
} from "@checkstack/ui";
|
|
17
|
-
import type {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
case "expr": {
|
|
31
|
-
return "";
|
|
32
|
-
}
|
|
33
|
-
case "and": {
|
|
34
|
-
return { and: [""] };
|
|
35
|
-
}
|
|
36
|
-
case "or": {
|
|
37
|
-
return { or: [""] };
|
|
38
|
-
}
|
|
39
|
-
case "not": {
|
|
40
|
-
return { not: "" };
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
}
|
|
26
|
+
import type {
|
|
27
|
+
ConditionInput,
|
|
28
|
+
Duration,
|
|
29
|
+
NumericStateCondition,
|
|
30
|
+
StateCondition,
|
|
31
|
+
TimeCondition,
|
|
32
|
+
} from "@checkstack/automation-common";
|
|
33
|
+
import {
|
|
34
|
+
defaultForKind,
|
|
35
|
+
kindOf,
|
|
36
|
+
type ConditionKind,
|
|
37
|
+
} from "./condition-kind";
|
|
38
|
+
import { SystemEntityPicker } from "./SystemEntityPicker";
|
|
44
39
|
|
|
45
40
|
export interface ConditionEditorProps {
|
|
46
41
|
value: ConditionInput;
|
|
@@ -48,30 +43,39 @@ export interface ConditionEditorProps {
|
|
|
48
43
|
variableNodes: VariableNode[];
|
|
49
44
|
/**
|
|
50
45
|
* Staged completion provider (expression mode) for the inline
|
|
51
|
-
* expression input. Conditions are bare expressions
|
|
52
|
-
* wrapper
|
|
46
|
+
* expression input. Conditions are bare expressions - no `{{ }}`
|
|
47
|
+
* wrapper - so this is the `expressionCompletion` from
|
|
53
48
|
* `useVariableScope`. The hierarchical `variableNodes` powers the
|
|
54
49
|
* explicit "fx" picker.
|
|
55
50
|
*/
|
|
56
51
|
completionProvider: TemplateCompletionProvider;
|
|
57
|
-
/** Render without the wrapping card
|
|
52
|
+
/** Render without the wrapping card - used when inlining inside an action card. */
|
|
58
53
|
bare?: boolean;
|
|
54
|
+
/**
|
|
55
|
+
* Suppress the inline kind `Select`. Used only for the top-level condition in
|
|
56
|
+
* the Conditions-section sheet, where the kind is chosen up front via
|
|
57
|
+
* `AddConditionDialog` (swap kind = delete + re-add, matching actions).
|
|
58
|
+
* Nested combinator children and the action `condition`-guard body do NOT
|
|
59
|
+
* set this, so their inline selector stays. The `expr` "fx" picker still
|
|
60
|
+
* renders when the kind is `expr`, even with the selector hidden.
|
|
61
|
+
*/
|
|
62
|
+
hideKindSelector?: boolean;
|
|
59
63
|
depth?: number;
|
|
60
64
|
}
|
|
61
65
|
|
|
62
66
|
/**
|
|
63
|
-
* Recursive editor over the `ConditionInput`
|
|
64
|
-
*
|
|
67
|
+
* Recursive editor over the `ConditionInput` union. Each level renders a
|
|
68
|
+
* kind selector plus the matching body:
|
|
65
69
|
*
|
|
66
|
-
* -
|
|
67
|
-
* -
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
* not → single child condition.
|
|
70
|
+
* - expression - TemplateValueInput + the VariablePicker "fx" trigger.
|
|
71
|
+
* - and / or - list of child conditions (recursive).
|
|
72
|
+
* - not - single child condition.
|
|
73
|
+
* - numeric_state - value (path/template) + operator + threshold.
|
|
74
|
+
* - time - after / before (TimeOfDayInput) + weekday + timezone.
|
|
75
|
+
* - state - entity + status + optional dwell (DurationInput).
|
|
73
76
|
*
|
|
74
|
-
*
|
|
77
|
+
* The raw expression stays the escape hatch for anything the structured
|
|
78
|
+
* variants don't cover. No depth cap - operator can nest arbitrarily.
|
|
75
79
|
*/
|
|
76
80
|
export const ConditionEditor: React.FC<ConditionEditorProps> = ({
|
|
77
81
|
value,
|
|
@@ -79,42 +83,69 @@ export const ConditionEditor: React.FC<ConditionEditorProps> = ({
|
|
|
79
83
|
variableNodes,
|
|
80
84
|
completionProvider,
|
|
81
85
|
bare,
|
|
86
|
+
hideKindSelector,
|
|
82
87
|
depth = 0,
|
|
83
88
|
}) => {
|
|
84
89
|
const kind = kindOf(value);
|
|
85
90
|
|
|
86
91
|
const body = (
|
|
87
92
|
<div className="space-y-2">
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
93
|
+
{/* The top row holds the kind selector and the `expr` "fx" picker. Render
|
|
94
|
+
it when EITHER is present; omit it entirely (e.g. hidden selector on a
|
|
95
|
+
non-expr top-level condition) so no empty row remains. */}
|
|
96
|
+
{(!hideKindSelector || kind === "expr") && (
|
|
97
|
+
<div className="flex items-center gap-2">
|
|
98
|
+
{!hideKindSelector && (
|
|
99
|
+
<Select
|
|
100
|
+
value={kind}
|
|
101
|
+
onValueChange={(next) =>
|
|
102
|
+
onChange(defaultForKind(next as ConditionKind))
|
|
103
|
+
}
|
|
104
|
+
>
|
|
105
|
+
<SelectTrigger className="h-7 w-40 text-xs">
|
|
106
|
+
<SelectValue />
|
|
107
|
+
</SelectTrigger>
|
|
108
|
+
<SelectContent>
|
|
109
|
+
{/* Structured kinds are the common case and lead the list. */}
|
|
110
|
+
<SelectGroup>
|
|
111
|
+
<SelectLabel>Structured</SelectLabel>
|
|
112
|
+
<SelectItem value="numeric_state">numeric state</SelectItem>
|
|
113
|
+
<SelectItem value="time">time of day</SelectItem>
|
|
114
|
+
<SelectItem value="state">system state</SelectItem>
|
|
115
|
+
</SelectGroup>
|
|
116
|
+
<SelectSeparator />
|
|
117
|
+
<SelectGroup>
|
|
118
|
+
<SelectLabel>Logical</SelectLabel>
|
|
119
|
+
<SelectItem value="and">and</SelectItem>
|
|
120
|
+
<SelectItem value="or">or</SelectItem>
|
|
121
|
+
<SelectItem value="not">not</SelectItem>
|
|
122
|
+
</SelectGroup>
|
|
123
|
+
<SelectSeparator />
|
|
124
|
+
{/* Raw expression stays the escape hatch for anything the
|
|
125
|
+
structured variants don't cover — de-emphasised at the
|
|
126
|
+
bottom, but still reachable. */}
|
|
127
|
+
<SelectGroup>
|
|
128
|
+
<SelectLabel>Advanced</SelectLabel>
|
|
129
|
+
<SelectItem value="expr">expression</SelectItem>
|
|
130
|
+
</SelectGroup>
|
|
131
|
+
</SelectContent>
|
|
132
|
+
</Select>
|
|
133
|
+
)}
|
|
134
|
+
{kind === "expr" && (
|
|
135
|
+
<VariablePicker
|
|
136
|
+
scope={variableNodes}
|
|
137
|
+
onSelect={(path) => {
|
|
138
|
+
// Conditions are bare expressions - insert the raw path,
|
|
139
|
+
// not a `{{ … }}`-wrapped reference.
|
|
140
|
+
const before = typeof value === "string" ? value : "";
|
|
141
|
+
const sep =
|
|
142
|
+
before.length > 0 && !before.endsWith(" ") ? " " : "";
|
|
143
|
+
onChange(`${before}${sep}${path}`);
|
|
144
|
+
}}
|
|
145
|
+
/>
|
|
146
|
+
)}
|
|
147
|
+
</div>
|
|
148
|
+
)}
|
|
118
149
|
|
|
119
150
|
{kind === "expr" && (
|
|
120
151
|
<TemplateValueInput
|
|
@@ -125,6 +156,23 @@ export const ConditionEditor: React.FC<ConditionEditorProps> = ({
|
|
|
125
156
|
/>
|
|
126
157
|
)}
|
|
127
158
|
|
|
159
|
+
{kind === "numeric_state" && (
|
|
160
|
+
<NumericStateBody
|
|
161
|
+
value={value as NumericStateCondition}
|
|
162
|
+
onChange={onChange}
|
|
163
|
+
variableNodes={variableNodes}
|
|
164
|
+
completionProvider={completionProvider}
|
|
165
|
+
/>
|
|
166
|
+
)}
|
|
167
|
+
|
|
168
|
+
{kind === "time" && (
|
|
169
|
+
<TimeBody value={value as TimeCondition} onChange={onChange} />
|
|
170
|
+
)}
|
|
171
|
+
|
|
172
|
+
{kind === "state" && (
|
|
173
|
+
<StateBody value={value as StateCondition} onChange={onChange} />
|
|
174
|
+
)}
|
|
175
|
+
|
|
128
176
|
{(kind === "and" || kind === "or") && (
|
|
129
177
|
<CombinatorList
|
|
130
178
|
kind={kind}
|
|
@@ -167,6 +215,247 @@ export const ConditionEditor: React.FC<ConditionEditorProps> = ({
|
|
|
167
215
|
);
|
|
168
216
|
};
|
|
169
217
|
|
|
218
|
+
// ─── numeric_state ─────────────────────────────────────────────────────
|
|
219
|
+
|
|
220
|
+
type NumericOp = "above" | "below" | "between";
|
|
221
|
+
|
|
222
|
+
function numericOpOf(c: NumericStateCondition["numeric_state"]): NumericOp {
|
|
223
|
+
if (c.above !== undefined && c.below !== undefined) return "between";
|
|
224
|
+
if (c.below !== undefined) return "below";
|
|
225
|
+
return "above";
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const NumericStateBody: React.FC<{
|
|
229
|
+
value: NumericStateCondition;
|
|
230
|
+
onChange: (next: ConditionInput) => void;
|
|
231
|
+
variableNodes: VariableNode[];
|
|
232
|
+
completionProvider: TemplateCompletionProvider;
|
|
233
|
+
}> = ({ value, onChange, variableNodes, completionProvider }) => {
|
|
234
|
+
const ns = value.numeric_state;
|
|
235
|
+
const op = numericOpOf(ns);
|
|
236
|
+
const patch = (next: Partial<NumericStateCondition["numeric_state"]>) =>
|
|
237
|
+
onChange({ numeric_state: { ...ns, ...next } });
|
|
238
|
+
|
|
239
|
+
return (
|
|
240
|
+
<div className="space-y-2 border-l border-border pl-3">
|
|
241
|
+
<div className="space-y-1">
|
|
242
|
+
<Label className="text-xs">Value (path or template)</Label>
|
|
243
|
+
<div className="flex items-center gap-2">
|
|
244
|
+
<div className="flex-1">
|
|
245
|
+
<TemplateValueInput
|
|
246
|
+
value={typeof ns.value === "string" ? ns.value : String(ns.value)}
|
|
247
|
+
onChange={(next) => patch({ value: next })}
|
|
248
|
+
placeholder="health.system.p95_latency_ms"
|
|
249
|
+
completionProvider={completionProvider}
|
|
250
|
+
/>
|
|
251
|
+
</div>
|
|
252
|
+
<VariablePicker
|
|
253
|
+
scope={variableNodes}
|
|
254
|
+
onSelect={(path) => patch({ value: path })}
|
|
255
|
+
/>
|
|
256
|
+
</div>
|
|
257
|
+
</div>
|
|
258
|
+
<div className="flex items-center gap-2">
|
|
259
|
+
<Select
|
|
260
|
+
value={op}
|
|
261
|
+
onValueChange={(next) => {
|
|
262
|
+
// Reset to a clean shape for the chosen operator.
|
|
263
|
+
if (next === "above") {
|
|
264
|
+
patch({ above: ns.above ?? 0, below: undefined });
|
|
265
|
+
} else if (next === "below") {
|
|
266
|
+
patch({ above: undefined, below: ns.below ?? 0 });
|
|
267
|
+
} else {
|
|
268
|
+
patch({ above: ns.above ?? 0, below: ns.below ?? 0 });
|
|
269
|
+
}
|
|
270
|
+
}}
|
|
271
|
+
>
|
|
272
|
+
<SelectTrigger className="h-8 w-32 text-xs">
|
|
273
|
+
<SelectValue />
|
|
274
|
+
</SelectTrigger>
|
|
275
|
+
<SelectContent>
|
|
276
|
+
<SelectItem value="above">above (>)</SelectItem>
|
|
277
|
+
<SelectItem value="below">below (<)</SelectItem>
|
|
278
|
+
<SelectItem value="between">between</SelectItem>
|
|
279
|
+
</SelectContent>
|
|
280
|
+
</Select>
|
|
281
|
+
{op !== "below" && (
|
|
282
|
+
<Input
|
|
283
|
+
type="number"
|
|
284
|
+
className="w-28"
|
|
285
|
+
placeholder="above"
|
|
286
|
+
value={ns.above ?? ""}
|
|
287
|
+
onChange={(event) =>
|
|
288
|
+
patch({
|
|
289
|
+
above:
|
|
290
|
+
event.target.value === ""
|
|
291
|
+
? undefined
|
|
292
|
+
: Number(event.target.value),
|
|
293
|
+
})
|
|
294
|
+
}
|
|
295
|
+
/>
|
|
296
|
+
)}
|
|
297
|
+
{op !== "above" && (
|
|
298
|
+
<Input
|
|
299
|
+
type="number"
|
|
300
|
+
className="w-28"
|
|
301
|
+
placeholder="below"
|
|
302
|
+
value={ns.below ?? ""}
|
|
303
|
+
onChange={(event) =>
|
|
304
|
+
patch({
|
|
305
|
+
below:
|
|
306
|
+
event.target.value === ""
|
|
307
|
+
? undefined
|
|
308
|
+
: Number(event.target.value),
|
|
309
|
+
})
|
|
310
|
+
}
|
|
311
|
+
/>
|
|
312
|
+
)}
|
|
313
|
+
</div>
|
|
314
|
+
</div>
|
|
315
|
+
);
|
|
316
|
+
};
|
|
317
|
+
|
|
318
|
+
// ─── time ──────────────────────────────────────────────────────────────
|
|
319
|
+
|
|
320
|
+
const WEEKDAYS: Array<{ value: number; label: string }> = [
|
|
321
|
+
{ value: 0, label: "Sun" },
|
|
322
|
+
{ value: 1, label: "Mon" },
|
|
323
|
+
{ value: 2, label: "Tue" },
|
|
324
|
+
{ value: 3, label: "Wed" },
|
|
325
|
+
{ value: 4, label: "Thu" },
|
|
326
|
+
{ value: 5, label: "Fri" },
|
|
327
|
+
{ value: 6, label: "Sat" },
|
|
328
|
+
];
|
|
329
|
+
|
|
330
|
+
const TimeBody: React.FC<{
|
|
331
|
+
value: TimeCondition;
|
|
332
|
+
onChange: (next: ConditionInput) => void;
|
|
333
|
+
}> = ({ value, onChange }) => {
|
|
334
|
+
const t = value.time;
|
|
335
|
+
const patch = (next: Partial<TimeCondition["time"]>) =>
|
|
336
|
+
onChange({ time: { ...t, ...next } });
|
|
337
|
+
const weekdays = t.weekday ?? [];
|
|
338
|
+
|
|
339
|
+
const toggleDay = (day: number) => {
|
|
340
|
+
const next = weekdays.includes(day)
|
|
341
|
+
? weekdays.filter((d) => d !== day)
|
|
342
|
+
: [...weekdays, day].toSorted((a, b) => a - b);
|
|
343
|
+
patch({ weekday: next.length > 0 ? next : undefined });
|
|
344
|
+
};
|
|
345
|
+
|
|
346
|
+
return (
|
|
347
|
+
<div className="space-y-3 border-l border-border pl-3">
|
|
348
|
+
<div className="flex flex-wrap items-end gap-3">
|
|
349
|
+
<div className="space-y-1">
|
|
350
|
+
<Label className="text-xs">After</Label>
|
|
351
|
+
<TimeOfDayInput
|
|
352
|
+
value={t.after}
|
|
353
|
+
onChange={(next) => patch({ after: next })}
|
|
354
|
+
/>
|
|
355
|
+
</div>
|
|
356
|
+
<div className="space-y-1">
|
|
357
|
+
<Label className="text-xs">Before</Label>
|
|
358
|
+
<TimeOfDayInput
|
|
359
|
+
value={t.before}
|
|
360
|
+
onChange={(next) => patch({ before: next })}
|
|
361
|
+
/>
|
|
362
|
+
</div>
|
|
363
|
+
</div>
|
|
364
|
+
<div className="space-y-1">
|
|
365
|
+
<Label className="text-xs">Weekdays (any when none selected)</Label>
|
|
366
|
+
<div className="flex flex-wrap gap-1">
|
|
367
|
+
{WEEKDAYS.map((d) => {
|
|
368
|
+
const active = weekdays.includes(d.value);
|
|
369
|
+
return (
|
|
370
|
+
<Button
|
|
371
|
+
key={d.value}
|
|
372
|
+
type="button"
|
|
373
|
+
variant={active ? "primary" : "outline"}
|
|
374
|
+
size="sm"
|
|
375
|
+
className="h-7 w-11 text-xs"
|
|
376
|
+
onClick={() => toggleDay(d.value)}
|
|
377
|
+
aria-pressed={active}
|
|
378
|
+
>
|
|
379
|
+
{d.label}
|
|
380
|
+
</Button>
|
|
381
|
+
);
|
|
382
|
+
})}
|
|
383
|
+
</div>
|
|
384
|
+
</div>
|
|
385
|
+
<div className="space-y-1">
|
|
386
|
+
<Label className="text-xs">Timezone (IANA, defaults to UTC)</Label>
|
|
387
|
+
<Input
|
|
388
|
+
className="max-w-xs font-mono text-xs"
|
|
389
|
+
value={t.timezone ?? ""}
|
|
390
|
+
placeholder="Europe/Berlin"
|
|
391
|
+
onChange={(event) =>
|
|
392
|
+
patch({ timezone: event.target.value || undefined })
|
|
393
|
+
}
|
|
394
|
+
/>
|
|
395
|
+
</div>
|
|
396
|
+
</div>
|
|
397
|
+
);
|
|
398
|
+
};
|
|
399
|
+
|
|
400
|
+
// ─── state ─────────────────────────────────────────────────────────────
|
|
401
|
+
|
|
402
|
+
const StateBody: React.FC<{
|
|
403
|
+
value: StateCondition;
|
|
404
|
+
onChange: (next: ConditionInput) => void;
|
|
405
|
+
}> = ({ value, onChange }) => {
|
|
406
|
+
const s = value.state;
|
|
407
|
+
const patch = (next: Partial<StateCondition["state"]>) =>
|
|
408
|
+
onChange({ state: { ...s, ...next } });
|
|
409
|
+
const hasDwell = s.for !== undefined;
|
|
410
|
+
|
|
411
|
+
return (
|
|
412
|
+
<div className="space-y-2 border-l border-border pl-3">
|
|
413
|
+
<div className="grid gap-2 sm:grid-cols-2">
|
|
414
|
+
<div className="space-y-1">
|
|
415
|
+
<Label className="text-xs">System (entity)</Label>
|
|
416
|
+
<SystemEntityPicker
|
|
417
|
+
value={s.entity}
|
|
418
|
+
onChange={(next) => patch({ entity: next })}
|
|
419
|
+
/>
|
|
420
|
+
</div>
|
|
421
|
+
<div className="space-y-1">
|
|
422
|
+
<Label className="text-xs">Status</Label>
|
|
423
|
+
<Select
|
|
424
|
+
value={s.status}
|
|
425
|
+
onValueChange={(next) =>
|
|
426
|
+
patch({ status: next as StateCondition["state"]["status"] })
|
|
427
|
+
}
|
|
428
|
+
>
|
|
429
|
+
<SelectTrigger className="h-8 text-xs">
|
|
430
|
+
<SelectValue />
|
|
431
|
+
</SelectTrigger>
|
|
432
|
+
<SelectContent>
|
|
433
|
+
<SelectItem value="healthy">healthy</SelectItem>
|
|
434
|
+
<SelectItem value="degraded">degraded</SelectItem>
|
|
435
|
+
<SelectItem value="unhealthy">unhealthy</SelectItem>
|
|
436
|
+
</SelectContent>
|
|
437
|
+
</Select>
|
|
438
|
+
</div>
|
|
439
|
+
</div>
|
|
440
|
+
<div className="flex items-center gap-2">
|
|
441
|
+
<Toggle
|
|
442
|
+
checked={hasDwell}
|
|
443
|
+
onCheckedChange={(checked) =>
|
|
444
|
+
patch({ for: checked ? { minutes: 30 } : undefined })
|
|
445
|
+
}
|
|
446
|
+
/>
|
|
447
|
+
<Label className="text-xs">Require held for a minimum duration</Label>
|
|
448
|
+
</div>
|
|
449
|
+
{hasDwell && (
|
|
450
|
+
<DurationInput
|
|
451
|
+
value={s.for as DurationValue | undefined}
|
|
452
|
+
onChange={(next) => patch({ for: (next as Duration) ?? undefined })}
|
|
453
|
+
/>
|
|
454
|
+
)}
|
|
455
|
+
</div>
|
|
456
|
+
);
|
|
457
|
+
};
|
|
458
|
+
|
|
170
459
|
const CombinatorList: React.FC<{
|
|
171
460
|
kind: "and" | "or";
|
|
172
461
|
children: ConditionInput[];
|