@checkstack/automation-frontend 0.7.1 → 0.8.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 CHANGED
@@ -1,5 +1,165 @@
1
1
  # @checkstack/automation-frontend
2
2
 
3
+ ## 0.8.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 8cad340: feat: live run polling, optimistic automation toggle, and relative public-status freshness
8
+
9
+ Implements three loading/feedback UX findings from the read-only review.
10
+
11
+ - **Automation run detail goes live.** `RunDetailPage` now polls
12
+ `getRun` every 2s while the run is `running`/`waiting` and stops the
13
+ moment it reaches a terminal status, so a watched execution updates
14
+ its status badge and step timeline without a manual reload. A subtle
15
+ "Live" indicator shows in the header while polling.
16
+ - **Optimistic automation enable/disable.** The per-row toggle on
17
+ `AutomationListPage` now applies the documented optimistic pattern:
18
+ `onMutate` cancels in-flight refetches, snapshots, and flips the row
19
+ in the cache so the switch flips on click; `onError` rolls back from
20
+ the snapshot and surfaces an error toast; `onSettled` invalidates to
21
+ reconcile with server truth. The success toast is suppressed (the
22
+ switch flip is the feedback), per `optimistic-updates.md`.
23
+ - **Relative, visibly-live public-status freshness.** The public status
24
+ page renders "Updated x ago" as relative time (was a static absolute
25
+ timestamp) and ticks periodically so the wording stays honest. A small
26
+ refresh dot pulses on each successful 60s refetch (gated behind
27
+ `usePerformance().isLowPower`, falling back to a static dot on
28
+ low-power devices). The "auto-updates every minute" copy is unchanged.
29
+
30
+ BREAKING CHANGE: the automation enable/disable toggle no longer raises a
31
+ "<name> enabled/disabled" success toast; the optimistic switch flip is now
32
+ the sole success feedback (error toast retained on failure).
33
+
34
+ - 8cad340: Make data-dense tables mobile-friendly and align status colors with semantic tokens.
35
+
36
+ - Migrated the remaining data-dense tables to the `ResponsiveTable` + `MobileCardList` dual-layout: catalog (Systems/Groups/Environments), incident config, maintenance config + system history, announcement management, notification delivery attempts, plugin manager (installed plugins + events), satellite list, automation list, healthcheck runs, OAuth applications, and the queue runtime panel. On viewports below `sm` these now render stacked cards surfacing the high-priority fields instead of an overflowing table. Genuinely narrow or runtime-diagnostic panels (cache runtime, healthcheck history, anomaly mute list) were intentionally left as plain tables.
37
+ - Swapped hardcoded semantic status colors for design tokens (`text-warning`, `text-success`, `text-destructive`, `text-muted-foreground`) in GitOps provenance status, healthcheck editor warnings, dependency canvas node status, automation run-step status, queue runtime tone map, and script-packages settings. Chart-series literals, syntax/terminal palettes, and intentional brand accents (tips lightbulb, SLO streak flame ramp) were left untouched.
38
+ - Extracted pure display/validation logic into sibling `.logic.ts` modules (SLO display + editor, maintenance editor + config summary, dependency display, incident sort + validation, gitops kind-registry YAML) so it can be unit-tested in isolation. These extractions are behavior-preserving.
39
+
40
+ - 8cad340: Explain why Save is disabled and guard against losing unsaved edits in the
41
+ automation and health-check editors.
42
+
43
+ - A greyed-out Save is no longer a dead end: both editors now render a
44
+ "N issue(s) blocking" affordance next to the Save button. Opening it
45
+ lists every blocker, and clicking one jumps to the offending field/section
46
+ (the automation Name / Run-as fields or the visual definition editor; the
47
+ health-check tree node that owns the issue). The existing validation logic is
48
+ unchanged - the blockers are just surfaced and made actionable.
49
+ - The first field of a fresh automation (Name) now auto-focuses so keyboard-first
50
+ users can type immediately.
51
+ - Both editors now use the shared `useUnsavedChanges` hook for unsaved-changes
52
+ protection: a native prompt on tab close / refresh plus an in-app
53
+ "Discard unsaved changes?" confirmation when navigating away mid-edit. The
54
+ health-check editor's previous hand-rolled `beforeunload` listener is migrated
55
+ to the shared hook; the automation editor gains dirty tracking and the same
56
+ guard.
57
+
58
+ ### Patch Changes
59
+
60
+ - 8cad340: Design-system rework: a premium, consistent UI language across the platform.
61
+
62
+ Foundation (`@checkstack/ui` + the shared Tailwind preset):
63
+
64
+ - A token system wired into the shared preset so it generates app-wide: a
65
+ surface elevation ramp (`surface` / `surface-2` / `surface-inset`), the
66
+ aurora gradient stops, a colorblind-safe `status` triad, and `grid-line`.
67
+ - A density model (`comfortable` / `compact`) via `--d-*` vars + `DensityProvider`
68
+ / `useDensity`, with a user-menu density toggle, plus the polished
69
+ skeleton / empty / error state set.
70
+ - Honest, token-driven chart primitives (`TimeSeriesChart`, `Sparkline`,
71
+ `RadialGauge` / aurora hero, `RequestWaterfall`, `UptimeRibbon`).
72
+ - A signature aurora moment per page: `PageHeader` paints its icon strokes with
73
+ the aurora gradient and adds a hairline; `Card` gains soft layered depth.
74
+
75
+ Shell + surfaces:
76
+
77
+ - The app shell adopts the elevation ramp (header `surface-2`, sidebar
78
+ `surface`, content on the ambient base).
79
+ - The system-health dashboard, health-check latency / single-run views, and the
80
+ SLO dashboard are reskinned onto the primitives (aurora confidence gauge,
81
+ honest p50/p95 latency, request waterfall, number-led status cards).
82
+
83
+ App-wide adoption + premium rework:
84
+
85
+ - Every plugin frontend adopts the tokens, status triad, density, and elevation.
86
+ - The highest-impact surfaces in each plugin are then redesigned to a premium
87
+ bar: real depth, number-led hierarchy, multi-encoded status (pill + dot +
88
+ accent stripe), and refined list/table density. Several plugins extract pure
89
+ tone/label/format logic into unit-tested modules.
90
+
91
+ Alerts:
92
+
93
+ - Every alert/callout is unified onto a single premium `Alert` (depth surface +
94
+ status-accent stripe + toned icon chip, variant-driven).
95
+
96
+ BREAKING CHANGE: the duplicate `InfoBanner` component (and its sub-components)
97
+ is removed; use `Alert` instead - it is a drop-in replacement with the same
98
+ variants and composable parts.
99
+
100
+ - 8cad340: fix: make data tables responsive on narrow viewports
101
+
102
+ The users, teams, and roles management tables (auth-frontend), the automation
103
+ run-history table (automation-frontend), and the integration provider
104
+ connections table (integration-frontend) previously overflowed horizontally on
105
+ phone-width (~375px) viewports. Each now uses the `ResponsiveTable` +
106
+ `MobileCardList` dual-layout primitive from `@checkstack/ui`: the existing table
107
+ renders unchanged on `sm` and up, with a stacked per-row card surfacing the key
108
+ fields and action buttons below `sm`. Shared per-row rendering (role checkboxes,
109
+ team/role/connection action buttons, connection status) was lifted into small
110
+ local components so both layouts stay in sync.
111
+
112
+ - 8cad340: Adopt the canonical `toastError` helper from `@checkstack/ui` for error toasts.
113
+
114
+ Error toasts that previously called `toast.error(extractErrorMessage(error, "Failed to X"))`
115
+ (or interpolated `Failed to X: ${extractErrorMessage(error)}` strings) now use
116
+ `toastError(toast, "Failed to X", error)`. This centralizes the
117
+ "Failed to <action>: <message>" voice and applies the shared 100-character
118
+ truncation. Error toasts that did not previously prefix the action now gain the
119
+ canonical prefix; success toasts and terse validation one-liners are unchanged.
120
+
121
+ - Updated dependencies [8cad340]
122
+ - Updated dependencies [8cad340]
123
+ - Updated dependencies [8cad340]
124
+ - Updated dependencies [8cad340]
125
+ - Updated dependencies [8cad340]
126
+ - Updated dependencies [8cad340]
127
+ - Updated dependencies [8cad340]
128
+ - Updated dependencies [8cad340]
129
+ - Updated dependencies [8cad340]
130
+ - Updated dependencies [8cad340]
131
+ - Updated dependencies [8cad340]
132
+ - Updated dependencies [8cad340]
133
+ - Updated dependencies [8cad340]
134
+ - Updated dependencies [8cad340]
135
+ - Updated dependencies [8cad340]
136
+ - Updated dependencies [8cad340]
137
+ - Updated dependencies [8cad340]
138
+ - Updated dependencies [8cad340]
139
+ - Updated dependencies [8cad340]
140
+ - Updated dependencies [8cad340]
141
+ - Updated dependencies [8cad340]
142
+ - Updated dependencies [8cad340]
143
+ - Updated dependencies [8cad340]
144
+ - Updated dependencies [8cad340]
145
+ - Updated dependencies [8cad340]
146
+ - Updated dependencies [8cad340]
147
+ - Updated dependencies [8cad340]
148
+ - @checkstack/auth-frontend@0.9.0
149
+ - @checkstack/ai-common@0.6.0
150
+ - @checkstack/ui@1.17.0
151
+ - @checkstack/gitops-frontend@0.6.0
152
+ - @checkstack/script-packages-frontend@0.4.0
153
+ - @checkstack/secrets-frontend@0.3.0
154
+ - @checkstack/common@0.17.0
155
+ - @checkstack/auth-common@0.11.0
156
+ - @checkstack/frontend-api@0.11.1
157
+ - @checkstack/catalog-common@2.4.2
158
+ - @checkstack/automation-common@0.7.1
159
+ - @checkstack/integration-common@0.9.2
160
+ - @checkstack/template-engine@0.4.6
161
+ - @checkstack/signal-frontend@0.2.6
162
+
3
163
  ## 0.7.1
4
164
 
5
165
  ### Patch Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@checkstack/automation-frontend",
3
- "version": "0.7.1",
3
+ "version": "0.8.0",
4
4
  "license": "Elastic-2.0",
5
5
  "type": "module",
6
6
  "main": "src/index.tsx",
@@ -13,20 +13,20 @@
13
13
  "lint:code": "eslint . --max-warnings 0"
14
14
  },
15
15
  "dependencies": {
16
- "@checkstack/auth-common": "0.10.0",
17
- "@checkstack/auth-frontend": "0.8.1",
18
- "@checkstack/automation-common": "0.7.0",
19
- "@checkstack/ai-common": "0.5.0",
20
- "@checkstack/catalog-common": "2.4.1",
21
- "@checkstack/common": "0.16.0",
22
- "@checkstack/frontend-api": "0.11.0",
23
- "@checkstack/gitops-frontend": "0.5.9",
24
- "@checkstack/integration-common": "0.9.1",
25
- "@checkstack/script-packages-frontend": "0.3.13",
26
- "@checkstack/secrets-frontend": "0.2.8",
27
- "@checkstack/signal-frontend": "0.2.5",
28
- "@checkstack/template-engine": "0.4.5",
29
- "@checkstack/ui": "1.16.2",
16
+ "@checkstack/auth-common": "0.11.0",
17
+ "@checkstack/auth-frontend": "0.9.0",
18
+ "@checkstack/automation-common": "0.7.1",
19
+ "@checkstack/ai-common": "0.6.0",
20
+ "@checkstack/catalog-common": "2.4.2",
21
+ "@checkstack/common": "0.17.0",
22
+ "@checkstack/frontend-api": "0.11.1",
23
+ "@checkstack/gitops-frontend": "0.6.0",
24
+ "@checkstack/integration-common": "0.9.2",
25
+ "@checkstack/script-packages-frontend": "0.4.0",
26
+ "@checkstack/secrets-frontend": "0.3.0",
27
+ "@checkstack/signal-frontend": "0.2.6",
28
+ "@checkstack/template-engine": "0.4.6",
29
+ "@checkstack/ui": "1.17.0",
30
30
  "@dnd-kit/core": "^6.3.1",
31
31
  "@dnd-kit/sortable": "^8.0.0",
32
32
  "@dnd-kit/utilities": "^3.2.2",
@@ -40,6 +40,6 @@
40
40
  "typescript": "^5.0.0",
41
41
  "@types/react": "^19.0.0",
42
42
  "@checkstack/tsconfig": "0.0.7",
43
- "@checkstack/scripts": "0.6.2"
43
+ "@checkstack/scripts": "0.6.3"
44
44
  }
45
45
  }
@@ -11,10 +11,10 @@ import {
11
11
  SelectItem,
12
12
  SelectTrigger,
13
13
  SelectValue,
14
- InfoBanner,
15
- InfoBannerIcon,
16
- InfoBannerContent,
17
- InfoBannerDescription,
14
+ Alert,
15
+ AlertIcon,
16
+ AlertContent,
17
+ AlertDescription,
18
18
  EmptyState,
19
19
  Button,
20
20
  } from "@checkstack/ui";
@@ -143,12 +143,12 @@ export const RunAsServiceAccountPicker: React.FC<
143
143
  }
144
144
  />
145
145
  ) : (
146
- <InfoBanner variant="info">
147
- <InfoBannerIcon>
146
+ <Alert variant="info">
147
+ <AlertIcon>
148
148
  <ShieldCheck className="h-4 w-4" />
149
- </InfoBannerIcon>
150
- <InfoBannerContent>
151
- <InfoBannerDescription>
149
+ </AlertIcon>
150
+ <AlertContent>
151
+ <AlertDescription>
152
152
  A service account is an application the automation runs as. It
153
153
  executes with that account's permissions (assigned via roles), so
154
154
  picking one is required.{" "}
@@ -161,9 +161,9 @@ export const RunAsServiceAccountPicker: React.FC<
161
161
  Learn more
162
162
  <ExternalLink className="h-3 w-3" />
163
163
  </a>
164
- </InfoBannerDescription>
165
- </InfoBannerContent>
166
- </InfoBanner>
164
+ </AlertDescription>
165
+ </AlertContent>
166
+ </Alert>
167
167
  )}
168
168
  </div>
169
169
  );
@@ -0,0 +1,75 @@
1
+ import React from "react";
2
+ import { AlertCircle } from "lucide-react";
3
+ import {
4
+ Button,
5
+ Popover,
6
+ PopoverContent,
7
+ PopoverTrigger,
8
+ } from "@checkstack/ui";
9
+ import type { BlockingIssue } from "../editor/blocking-issues";
10
+
11
+ export interface SaveBlockersSummaryProps {
12
+ /** Ordered list of everything blocking the Save button. */
13
+ blockers: BlockingIssue[];
14
+ /**
15
+ * Navigate to / focus the surface that owns a blocker. Wired by the page to
16
+ * focus the offending field or reveal the definition editor.
17
+ */
18
+ onResolve: (blocker: BlockingIssue) => void;
19
+ }
20
+
21
+ /**
22
+ * Compact "N issue(s) blocking save" affordance shown next to the Save button
23
+ * when the automation is invalid. Each row links to the offending field /
24
+ * section so a greyed-out Save is never a dead end.
25
+ *
26
+ * Renders nothing when there are no blockers, so the page can drop it next to
27
+ * Save unconditionally.
28
+ */
29
+ export const SaveBlockersSummary: React.FC<SaveBlockersSummaryProps> = ({
30
+ blockers,
31
+ onResolve,
32
+ }) => {
33
+ const [open, setOpen] = React.useState(false);
34
+ if (blockers.length === 0) return null;
35
+
36
+ const count = blockers.length;
37
+
38
+ return (
39
+ <Popover open={open} onOpenChange={setOpen}>
40
+ <PopoverTrigger asChild>
41
+ <Button
42
+ type="button"
43
+ variant="outline"
44
+ size="sm"
45
+ className="border-destructive/50 text-destructive hover:text-destructive"
46
+ >
47
+ <AlertCircle className="mr-1 h-4 w-4" />
48
+ {count} {count === 1 ? "issue" : "issues"} blocking
49
+ </Button>
50
+ </PopoverTrigger>
51
+ <PopoverContent align="end" className="w-80 p-0">
52
+ <div className="border-b px-3 py-2 text-xs font-medium text-muted-foreground">
53
+ Fix these to save
54
+ </div>
55
+ <ul className="max-h-64 overflow-y-auto py-1">
56
+ {blockers.map((blocker, index) => (
57
+ <li key={`${blocker.message}-${index}`}>
58
+ <button
59
+ type="button"
60
+ onClick={() => {
61
+ setOpen(false);
62
+ onResolve(blocker);
63
+ }}
64
+ className="flex w-full items-start gap-2 px-3 py-1.5 text-left text-sm transition-colors hover:bg-muted/50"
65
+ >
66
+ <AlertCircle className="mt-0.5 h-3.5 w-3.5 shrink-0 text-destructive" />
67
+ <span className="break-words">{blocker.message}</span>
68
+ </button>
69
+ </li>
70
+ ))}
71
+ </ul>
72
+ </PopoverContent>
73
+ </Popover>
74
+ );
75
+ };
@@ -83,7 +83,7 @@ export const RunReplayPicker: React.FC<RunReplayPickerProps> = ({
83
83
  <SelectContent>
84
84
  {runs.map((run) => (
85
85
  <SelectItem key={run.id} value={run.id}>
86
- {new Date(run.startedAt).toLocaleString()} — {run.status}
86
+ {new Date(run.startedAt).toLocaleString()} - {run.status}
87
87
  </SelectItem>
88
88
  ))}
89
89
  </SelectContent>
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Tests for the save-blockers summary that explains why the automation Save
3
+ * button is disabled. The list must mirror the page's `canSave` predicate
4
+ * (name + run-as + definition validation) so the count and the disabled state
5
+ * never disagree.
6
+ */
7
+ import { describe, expect, it } from "bun:test";
8
+ import { summarizeBlockingIssues } from "./blocking-issues";
9
+
10
+ describe("summarizeBlockingIssues", () => {
11
+ it("returns no blockers when everything is valid", () => {
12
+ expect(
13
+ summarizeBlockingIssues({ definitionIssues: [] }),
14
+ ).toEqual([]);
15
+ });
16
+
17
+ it("includes the name error targeting the name field", () => {
18
+ const blockers = summarizeBlockingIssues({
19
+ nameError: "Name is required",
20
+ definitionIssues: [],
21
+ });
22
+ expect(blockers).toEqual([
23
+ { message: "Name is required", target: { kind: "field", field: "name" } },
24
+ ]);
25
+ });
26
+
27
+ it("includes the run-as error targeting the run-as field", () => {
28
+ const blockers = summarizeBlockingIssues({
29
+ runAsError: "A service account is required",
30
+ definitionIssues: [],
31
+ });
32
+ expect(blockers).toEqual([
33
+ {
34
+ message: "A service account is required",
35
+ target: { kind: "field", field: "runAs" },
36
+ },
37
+ ]);
38
+ });
39
+
40
+ it("formats a definition issue as `path: message`", () => {
41
+ const blockers = summarizeBlockingIssues({
42
+ definitionIssues: [
43
+ { path: ["triggers", 0, "kind"], message: "Required" },
44
+ ],
45
+ });
46
+ expect(blockers).toEqual([
47
+ { message: "triggers.0.kind: Required", target: { kind: "definition" } },
48
+ ]);
49
+ });
50
+
51
+ it("renders a top-level (pathless) definition issue as just its message", () => {
52
+ const blockers = summarizeBlockingIssues({
53
+ definitionIssues: [
54
+ { path: [], message: "An automation needs at least one trigger." },
55
+ ],
56
+ });
57
+ expect(blockers).toEqual([
58
+ {
59
+ message: "An automation needs at least one trigger.",
60
+ target: { kind: "definition" },
61
+ },
62
+ ]);
63
+ });
64
+
65
+ it("orders blockers name → run-as → definition issues", () => {
66
+ const blockers = summarizeBlockingIssues({
67
+ nameError: "Name is required",
68
+ runAsError: "A service account is required",
69
+ definitionIssues: [{ path: ["actions"], message: "bad" }],
70
+ });
71
+ expect(blockers.map((b) => b.target)).toEqual([
72
+ { kind: "field", field: "name" },
73
+ { kind: "field", field: "runAs" },
74
+ { kind: "definition" },
75
+ ]);
76
+ });
77
+ });
@@ -0,0 +1,68 @@
1
+ import type { DefinitionIssue } from "./editor-validation";
2
+
3
+ /**
4
+ * Where a blocking issue can be "fixed" from. Drives the click affordance in
5
+ * the save-blockers summary: metadata-field targets focus their input, while
6
+ * definition targets reveal the validated definition (visual editor).
7
+ */
8
+ export type BlockingIssueTarget =
9
+ | { kind: "field"; field: "name" | "runAs" }
10
+ | { kind: "definition" };
11
+
12
+ export interface BlockingIssue {
13
+ /** Human-readable, one-line description of what is incomplete. */
14
+ message: string;
15
+ /** Where the user should go to resolve it. */
16
+ target: BlockingIssueTarget;
17
+ }
18
+
19
+ /**
20
+ * Summarize everything currently blocking a save into a flat, ordered list of
21
+ * actionable items. Mirrors the `canSave` predicate in `AutomationEditPage`
22
+ * (name + run-as + definition validation), so the count shown next to the Save
23
+ * button always matches whether Save is enabled.
24
+ *
25
+ * Pure and DOM-free so it can be unit-tested in isolation; the page wires the
26
+ * resulting `target`s to focus/tab navigation.
27
+ */
28
+ export function summarizeBlockingIssues({
29
+ nameError,
30
+ runAsError,
31
+ definitionIssues,
32
+ }: {
33
+ nameError?: string;
34
+ runAsError?: string;
35
+ definitionIssues: DefinitionIssue[];
36
+ }): BlockingIssue[] {
37
+ const blockers: BlockingIssue[] = [];
38
+
39
+ if (nameError) {
40
+ blockers.push({ message: nameError, target: { kind: "field", field: "name" } });
41
+ }
42
+ if (runAsError) {
43
+ blockers.push({
44
+ message: runAsError,
45
+ target: { kind: "field", field: "runAs" },
46
+ });
47
+ }
48
+
49
+ for (const issue of definitionIssues) {
50
+ blockers.push({
51
+ message: formatDefinitionIssue(issue),
52
+ target: { kind: "definition" },
53
+ });
54
+ }
55
+
56
+ return blockers;
57
+ }
58
+
59
+ /**
60
+ * Render a definition validation issue as a single `path: message` line,
61
+ * matching how the visual editor surfaces unattributed issues. A pathless
62
+ * (top-level) issue shows just its message.
63
+ */
64
+ function formatDefinitionIssue(issue: DefinitionIssue): string {
65
+ return issue.path.length > 0
66
+ ? `${issue.path.join(".")}: ${issue.message}`
67
+ : issue.message;
68
+ }
@@ -79,7 +79,7 @@ export const CONDITION_KIND_META: Record<ConditionKind, ConditionKindMeta> = {
79
79
  expr: {
80
80
  kind: "expr",
81
81
  label: "expression",
82
- description: "Raw boolean expression — the escape hatch.",
82
+ description: "Raw boolean expression - the escape hatch.",
83
83
  icon: "Braces",
84
84
  group: "Advanced",
85
85
  },