@checkstack/integration-frontend 0.5.1 → 0.6.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,89 @@
1
1
  # @checkstack/integration-frontend
2
2
 
3
+ ## 0.6.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 9dcc848: Surface integration connection validation errors inline, and fix blank secrets clearing stored credentials on edit.
8
+
9
+ `@checkstack/ui` `DynamicForm` gains opt-in, backward-compatible props plus pure DOM-free helpers:
10
+
11
+ - `showInlineErrors` (default `false`): renders a concise per-field error under each touched required field; the `onValidChange` validity boolean derives from the same per-field error map.
12
+ - `fieldErrors`: an externally-supplied `{ [fieldPath]: message }` map (dot-joined for nested fields) for surfacing SERVER validation inline; nested paths flag their parent.
13
+ - `keepExistingSecretFields`: in EDIT mode, lists `x-secret` keys already stored server-side - a blank input means "keep existing" and is treated as VALID (CREATE mode omits it). New exported helpers: `deriveClientFieldErrors`, `deriveServerFieldErrors`, `parseServerValidationData`, `omitKeepExistingSecrets`, `listSecretFieldKeys`. `DynamicForm` also no longer shows a required (`*`) marker on the child fields of an OPTIONAL nested object while that object is empty (e.g. the OpenAI-compatible `spendCap`); required nested objects are unchanged.
14
+
15
+ `@checkstack/integration-backend`: connection-config validation failures attach the structured zod issues to `ORPCError.data` under a `CONFIG_VALIDATION` discriminator; the human-readable message is unchanged.
16
+
17
+ `@checkstack/integration-frontend` `ProviderConnectionsPage`: validation failures appear inline on the offending fields (the toast remains a fallback); Create/Save stays disabled while invalid; on edit a blank `x-secret` field is treated as "keep existing" (no required error, omitted from the update so the stored secret is not cleared).
18
+
19
+ BREAKING CHANGES: none. The new `DynamicForm` props are optional and default to previous behavior.
20
+
21
+ This is a beta minor.
22
+
23
+ - 9dcc848: Cut initial-load JS: lazy plugin contributions, a hardened lazy-by-default contribution contract, on-demand Monaco, and a lighter icon/chart load.
24
+
25
+ - Lazy plugin route pages: each plugin's route `element` references a `React.lazy`-wrapped page rendered inside a shared `<Suspense>` boundary. Plugins still register synchronously, so nav, slots, commands, API factories, and `foreignSignals` are available on first paint. This moves ~37 route-page chunks (~600 KB) out of the entry; the entry chunk drops from ~2.4 MB to ~190 KB. Auth flow pages stay eager. The `@checkstack/scripts` scaffold template generates lazy route pages too.
26
+ - Hardened contribution contract (BREAKING, frontend plugin contract): plugins declare contributions lazily and let the framework own code-splitting, Suspense, and per-plugin error isolation. Routes use `load: () => import("./Page").then((m) => ({ default: m.Page }))` instead of `element: <Page />` (`element` is still accepted for the rare page that must paint without a chunk fetch; provide exactly one). Slot extensions accept either an eager `component` or a lazy `load`; new `getLazyContribution` + `ExtensionComponent` exports from `@checkstack/frontend-api` render either kind. This also fixes runtime-installed plugins: `ExtensionSlot` subscribes to the plugin registry, and the API registry rebuilds when the plugin set changes (`getPlugins()` returns an immutable snapshot via `useSyncExternalStore`). A per-plugin error boundary contains a bad contribution.
27
+ - On-demand Monaco: the `@checkstack/ui` barrel no longer pulls the `@codingame/*` / `monaco-languageclient` stack into the initial load. `CodeEditor` lazy-loads its Monaco-backed editor behind `React.lazy` + Suspense, `validateTypeScriptSources` imports the editor API via in-body `await import(...)`, and the "vscode services ready" signal moved to a Monaco-free module. The ~10 MB editor body loads only when a `CodeEditor` mounts. A `react-vendor` `manualChunks` split was added for stable vendor caching.
28
+ - lucide-react 1.x + lighter icons/charts (BREAKING for icon consumers): lucide-react unified from three drifting ranges to `^1.17.0`. lucide v1 removed brand icons, so the GitHub/GitLab marks are vendored in `@checkstack/ui` (`GithubIcon`, `GitlabIcon`, `brandIcons`); a new `IconName` type (`LucideIconName | BrandIconName`) in `@checkstack/common` is canonical, accepted by `AuthStrategy.icon` and the card components, so data-driven brand names keep working. `DynamicIcon` no longer eagerly imports lucide's ~1600-icon map (~1 MB) - it lives in a `React.lazy` `iconRegistry` chunk fetched on first data-driven render, while statically named-imported icons tree-shake normally. The recharts-backed health-check charts (~300 KB) and the `HealthCheckSystemOverview` drawer leave the initial load.
29
+
30
+ BREAKING CHANGES:
31
+
32
+ - Frontend plugin contract: routes/slot contributions are lazy-by-default (`load` instead of `element`/eager elements) as described above.
33
+ - Any external consumer importing a brand icon from `lucide-react` (e.g. `import { Github } from "lucide-react"`) must switch to the vendored `@checkstack/ui` brand icons or a custom SVG.
34
+
35
+ This is a beta minor.
36
+
37
+ - 9dcc848: Align workspace dependency versions and migrate React Router to v7.
38
+
39
+ BREAKING CHANGES (React Router v7): All frontend packages now depend on `react-router-dom@^7.16.0`. Previously the workspace declared four divergent ranges (`^6.20.0`, `^6.22.0`, `^7.1.1`, `^7.14.2`), which resolved both `react-router@6` and `react-router@7` into a single bundle. Everything is now unified on v7. The public imports the app uses (`BrowserRouter`, `Routes`, `Route`, `Link`, `NavLink`, `MemoryRouter`, `useNavigate`, `useParams`, `useSearchParams`, `useLocation`) are unchanged between v6 and v7, so no source rewrites were required - but any out-of-tree plugin still on react-router v6 should upgrade to v7 (see the React Router v6 -> v7 upgrade guide) to share the host's single router instance via the import map.
40
+
41
+ Other unified ranges (no API change): `react` -> `^18.3.1`, the `@orpc/*` family (`contract`, `server`, `client`, `tanstack-query`, `openapi`, `zod`) -> `^1.14.4`, and `better-auth` -> `^1.6.13`.
42
+
43
+ Removed the pre-rename `@orpc/react-query` leftover from `@checkstack/frontend-api`; its `createRouterUtils` / `RouterUtils` / `ProcedureUtils` now come from `@orpc/tanstack-query` (the package already in use).
44
+
45
+ Stale in-range runtime deps pulled up to current published versions: `hono` `^4.12.23`, `@tanstack/react-query` (+devtools) `^5.100.14`, `date-fns` `^4.4.0`, `jose` `^6.2.3`, `tar` `^7.5.16`, `semver` `^7.8.1`, `@xyflow/react` `^12.11.0`.
46
+
47
+ ### Patch Changes
48
+
49
+ - 9dcc848: Move primary navigation into a left sidebar, and serve the user guide in-app.
50
+
51
+ Feature navigation (a ~20-item user-menu dropdown) now lives in a persistent left sidebar (a slide-over drawer on mobile), grouped by section with the active route highlighted; the user menu keeps only account actions. A route opts into the sidebar with new `nav` metadata (`{ group, icon, label?, order?, accessRule? }`) on its registration, co-located with path + access + title. The sidebar filters entries with the same access check as page guards. `@checkstack/common` gains `isAccessRuleSatisfied` and a centralized set of in-app doc slugs (`APP_DOC_SLUGS` + `docsPath`, with a test asserting each resolves to a real docs page); `@checkstack/auth-frontend` exports `useAccessRules`.
52
+
53
+ The backend now serves the Astro Starlight docs build same-origin at `/checkstack/*` (the same artifact deployed to GitHub Pages), so the user guide is available inside the app including for self-hosted / air-gapped installs (served verbatim, no rebuild, no link rewriting; from `CHECKSTACK_DOCS_DIST`, before the SPA catch-all, degrading gracefully when absent; the Docker image builds and ships `docs/dist`; Vite proxies `/checkstack` in dev). The "Docs" link is a shell-owned external sidebar entry under the Documentation group (book icon), opening `/checkstack/user-guide/` in a new tab; the group renders even when no plugin route contributes to it.
54
+
55
+ BREAKING (plugin authors): `UserMenuItemsSlot` is no longer the way to add navigation - registering a top user-menu item no longer surfaces it anywhere. Add `nav` to the page's route instead. `UserMenuItemsBottomSlot` (account items) is unchanged. All bundled plugins have been migrated.
56
+
57
+ This is a beta minor.
58
+
59
+ - 9dcc848: Guard component animations behind isLowPower, and add a shared inline Spinner.
60
+
61
+ - `@checkstack/ui` shared components (`Tabs`, `ConfirmationModal`, `Accordion`, `CodeEditor` popout-button backdrop blur) now drop their `animate-*` / `backdrop-blur` classes when the device reports the low-power tier, matching `LoadingSpinner` / `Skeleton`. No public API change; normal-power rendering is unchanged.
62
+ - A new shared inline `Spinner` (`@checkstack/ui`) renders a lucide `Loader2` whose `animate-spin` is gated internally behind `usePerformance().isLowPower`, so call sites inherit the low-power guard. Props: `size` (`sm`/`md`/`lg`), `className`, rest spread to the icon; decorative by default (`aria-hidden`), `role="status"` when given `aria-label`. The hand-rolled `Loader2` button/table spinners in `HealthCheckDrawer`, `HealthCheckRunsTable`, `IncidentEditor`, `IncidentUpdateForm`, `ProviderConnectionsPage`, `MaintenanceEditor`, `MaintenanceUpdateForm`, `UserChannelCard`, and `DynamicOptionsField` are migrated onto it.
63
+ - Remaining unguarded `animate-*` / `animate-in` / blur classes across the auth, gitops, healthcheck, incident, integration, maintenance, and notification frontends are gated behind `usePerformance().isLowPower`, so effects degrade gracefully on low-power devices per the performance rule.
64
+
65
+ Normal-power behavior is unchanged; low-power rendering drops the animations.
66
+
67
+ This is a beta minor.
68
+
69
+ - Updated dependencies [9dcc848]
70
+ - Updated dependencies [9dcc848]
71
+ - Updated dependencies [9dcc848]
72
+ - Updated dependencies [9dcc848]
73
+ - Updated dependencies [9dcc848]
74
+ - Updated dependencies [9dcc848]
75
+ - Updated dependencies [9dcc848]
76
+ - Updated dependencies [9dcc848]
77
+ - Updated dependencies [9dcc848]
78
+ - Updated dependencies [9dcc848]
79
+ - Updated dependencies [9dcc848]
80
+ - @checkstack/ui@1.13.0
81
+ - @checkstack/common@0.13.0
82
+ - @checkstack/frontend-api@0.7.0
83
+ - @checkstack/tips-frontend@0.3.0
84
+ - @checkstack/integration-common@0.7.0
85
+ - @checkstack/signal-frontend@0.2.0
86
+
3
87
  ## 0.5.1
4
88
 
5
89
  ### Patch Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@checkstack/integration-frontend",
3
- "version": "0.5.1",
3
+ "version": "0.6.0",
4
4
  "license": "Elastic-2.0",
5
5
  "type": "module",
6
6
  "main": "src/index.tsx",
@@ -17,11 +17,11 @@
17
17
  "@checkstack/frontend-api": "0.6.0",
18
18
  "@checkstack/integration-common": "0.6.0",
19
19
  "@checkstack/signal-frontend": "0.1.5",
20
- "@checkstack/tips-frontend": "0.2.6",
21
- "@checkstack/ui": "1.11.0",
22
- "lucide-react": "^0.344.0",
23
- "react": "^18.2.0",
24
- "react-router-dom": "^6.22.0"
20
+ "@checkstack/tips-frontend": "0.2.7",
21
+ "@checkstack/ui": "1.12.0",
22
+ "lucide-react": "^1.17.0",
23
+ "react": "^18.3.1",
24
+ "react-router-dom": "^7.16.0"
25
25
  },
26
26
  "devDependencies": {
27
27
  "typescript": "^5.0.0",
package/src/index.tsx CHANGED
@@ -1,16 +1,10 @@
1
- import {
2
- createFrontendPlugin,
3
- createSlotExtension,
4
- UserMenuItemsSlot,
5
- } from "@checkstack/frontend-api";
1
+ import { createFrontendPlugin } from "@checkstack/frontend-api";
6
2
  import {
7
3
  integrationRoutes,
8
4
  pluginMetadata,
9
5
  integrationAccess,
10
6
  } from "@checkstack/integration-common";
11
- import { IntegrationsLandingPage } from "./pages/IntegrationsLandingPage";
12
- import { ProviderConnectionsPage } from "./pages/ProviderConnectionsPage";
13
- import { IntegrationMenuItem } from "./components/IntegrationMenuItem";
7
+ import { Webhook } from "lucide-react";
14
8
 
15
9
  /**
16
10
  * Integration frontend — now scoped to connection management. The
@@ -24,22 +18,26 @@ export const integrationPlugin = createFrontendPlugin({
24
18
  routes: [
25
19
  {
26
20
  route: integrationRoutes.routes.list,
27
- element: <IntegrationsLandingPage />,
21
+ load: () =>
22
+ import("./pages/IntegrationsLandingPage").then((m) => ({
23
+ default: m.IntegrationsLandingPage,
24
+ })),
28
25
  accessRule: integrationAccess.manage,
26
+ nav: {
27
+ group: "Configuration",
28
+ icon: Webhook,
29
+ label: "Integrations",
30
+ },
29
31
  },
30
32
  {
31
33
  route: integrationRoutes.routes.connections,
32
- element: <ProviderConnectionsPage />,
34
+ load: () =>
35
+ import("./pages/ProviderConnectionsPage").then((m) => ({
36
+ default: m.ProviderConnectionsPage,
37
+ })),
33
38
  accessRule: integrationAccess.manage,
34
39
  },
35
40
  ],
36
- extensions: [
37
- createSlotExtension(UserMenuItemsSlot, {
38
- id: "integration.user-menu.link",
39
- component: IntegrationMenuItem,
40
- metadata: { group: "Configuration" },
41
- }),
42
- ],
43
41
  });
44
42
 
45
43
  export default integrationPlugin;
@@ -13,7 +13,6 @@ import {
13
13
  TestTube2,
14
14
  CheckCircle2,
15
15
  XCircle,
16
- Loader2,
17
16
  } from "lucide-react";
18
17
  import {
19
18
  PageLayout,
@@ -43,6 +42,12 @@ import {
43
42
  ConfirmationModal,
44
43
  BackLink,
45
44
  toastError,
45
+ Spinner,
46
+ deriveServerFieldErrors,
47
+ parseServerValidationData,
48
+ omitKeepExistingSecrets,
49
+ listSecretFieldKeys,
50
+ type FieldErrorMap,
46
51
  type LucideIconName,
47
52
  } from "@checkstack/ui";
48
53
  import { usePluginClient } from "@checkstack/frontend-api";
@@ -81,6 +86,8 @@ export const ProviderConnectionsPage = () => {
81
86
 
82
87
  // Form validation state
83
88
  const [configValid, setConfigValid] = useState(false);
89
+ // Server validation errors mapped to form fields (cleared on each submit).
90
+ const [serverFieldErrors, setServerFieldErrors] = useState<FieldErrorMap>({});
84
91
 
85
92
  // Queries using hooks
86
93
  const { data: providers = [], isLoading: providersLoading } =
@@ -98,6 +105,40 @@ export const ProviderConnectionsPage = () => {
98
105
  const loading = providersLoading || connectionsLoading;
99
106
  const provider = providers.find((p) => p.qualifiedId === providerId);
100
107
 
108
+ // Try to surface a mutation error INLINE on the offending form fields. The
109
+ // backend attaches structured zod issues (field path + message) to the
110
+ // error `data` for connection-config validation failures; parse that
111
+ // payload (typed, via the shared schema) and map each issue to its field.
112
+ // Anything not field-mappable (generic / unknown errors, or issues whose
113
+ // root is not a rendered field) falls back to the existing toast so nothing
114
+ // is swallowed.
115
+ const handleMutationError = (action: string, error: unknown): void => {
116
+ const schema = provider?.connectionSchema;
117
+ const data =
118
+ error && typeof error === "object" && "data" in error
119
+ ? error.data
120
+ : undefined;
121
+ const parsed = schema ? parseServerValidationData(data) : undefined;
122
+
123
+ if (schema && parsed) {
124
+ const { mapped, unmapped } = deriveServerFieldErrors({
125
+ issues: parsed.issues,
126
+ schema,
127
+ });
128
+ if (Object.keys(mapped).length > 0) {
129
+ setServerFieldErrors(mapped);
130
+ }
131
+ // Surface any non-mappable issues (and a fully-unmappable payload) via
132
+ // the toast so they are never silently dropped.
133
+ if (unmapped.length > 0 || Object.keys(mapped).length === 0) {
134
+ toastError(toast, action, error);
135
+ }
136
+ return;
137
+ }
138
+
139
+ toastError(toast, action, error);
140
+ };
141
+
101
142
  // Mutations
102
143
  const createMutation = client.createConnection.useMutation({
103
144
  onSuccess: () => {
@@ -109,7 +150,7 @@ export const ProviderConnectionsPage = () => {
109
150
  setSaving(false);
110
151
  },
111
152
  onError: (error) => {
112
- toastError(toast, "Failed to create connection", error);
153
+ handleMutationError("Failed to create connection", error);
113
154
  setSaving(false);
114
155
  },
115
156
  });
@@ -123,7 +164,7 @@ export const ProviderConnectionsPage = () => {
123
164
  setSaving(false);
124
165
  },
125
166
  onError: (error) => {
126
- toastError(toast, "Failed to update connection", error);
167
+ handleMutationError("Failed to update connection", error);
127
168
  setSaving(false);
128
169
  },
129
170
  });
@@ -163,8 +204,18 @@ export const ProviderConnectionsPage = () => {
163
204
  },
164
205
  });
165
206
 
207
+ // In EDIT mode every `x-secret` field defined by the provider schema may
208
+ // already hold a stored value (secrets are redacted out of the loaded
209
+ // preview), so a blank input means "keep existing" and is valid. In CREATE
210
+ // mode no secret is stored yet, so this is empty and blank secrets stay
211
+ // required.
212
+ const keepExistingSecretFields = provider?.connectionSchema
213
+ ? listSecretFieldKeys(provider.connectionSchema)
214
+ : [];
215
+
166
216
  const handleCreate = () => {
167
217
  if (!providerId || !formName.trim()) return;
218
+ setServerFieldErrors({});
168
219
  setSaving(true);
169
220
  createMutation.mutate({
170
221
  providerId,
@@ -178,17 +229,26 @@ export const ProviderConnectionsPage = () => {
178
229
  setFormName("");
179
230
  setFormConfig({});
180
231
  setConfigValid(false);
232
+ setServerFieldErrors({});
181
233
  setCreateDialogOpen(true);
182
234
  };
183
235
 
184
236
  const handleUpdate = () => {
185
- if (!selectedConnection) return;
237
+ if (!selectedConnection || !provider?.connectionSchema) return;
238
+ setServerFieldErrors({});
186
239
  setSaving(true);
240
+ // Drop blank keep-existing secrets so an empty input does not clear the
241
+ // stored secret on update.
242
+ const config = omitKeepExistingSecrets({
243
+ schema: provider.connectionSchema,
244
+ value: formConfig,
245
+ keepExistingSecretFields,
246
+ });
187
247
  updateMutation.mutate({
188
248
  connectionId: selectedConnection.id,
189
249
  updates: {
190
250
  name: formName.trim() || selectedConnection.name,
191
- config: formConfig,
251
+ config,
192
252
  },
193
253
  });
194
254
  };
@@ -208,6 +268,7 @@ export const ProviderConnectionsPage = () => {
208
268
  setFormName(connection.name);
209
269
  setFormConfig(connection.configPreview);
210
270
  setConfigValid(true); // Existing connections should have valid config
271
+ setServerFieldErrors({});
211
272
  setEditDialogOpen(true);
212
273
  };
213
274
 
@@ -347,7 +408,7 @@ export const ProviderConnectionsPage = () => {
347
408
  disabled={isTesting}
348
409
  >
349
410
  {isTesting ? (
350
- <Loader2 className="h-4 w-4 animate-spin" />
411
+ <Spinner size="sm" />
351
412
  ) : (
352
413
  <TestTube2 className="h-4 w-4" />
353
414
  )}
@@ -402,6 +463,8 @@ export const ProviderConnectionsPage = () => {
402
463
  value={formConfig}
403
464
  onChange={setFormConfig}
404
465
  onValidChange={setConfigValid}
466
+ showInlineErrors
467
+ fieldErrors={serverFieldErrors}
405
468
  />
406
469
  )}
407
470
  </div>
@@ -416,7 +479,7 @@ export const ProviderConnectionsPage = () => {
416
479
  onClick={handleCreate}
417
480
  disabled={!formName.trim() || !configValid || saving}
418
481
  >
419
- {saving && <Loader2 className="h-4 w-4 mr-2 animate-spin" />}
482
+ {saving && <Spinner size="sm" className="mr-2" />}
420
483
  Create
421
484
  </Button>
422
485
  </DialogFooter>
@@ -445,6 +508,9 @@ export const ProviderConnectionsPage = () => {
445
508
  value={formConfig}
446
509
  onChange={setFormConfig}
447
510
  onValidChange={setConfigValid}
511
+ showInlineErrors
512
+ fieldErrors={serverFieldErrors}
513
+ keepExistingSecretFields={keepExistingSecretFields}
448
514
  />
449
515
  )}
450
516
  </div>
@@ -453,7 +519,7 @@ export const ProviderConnectionsPage = () => {
453
519
  Cancel
454
520
  </Button>
455
521
  <Button onClick={handleUpdate} disabled={!configValid || saving}>
456
- {saving && <Loader2 className="h-4 w-4 mr-2 animate-spin" />}
522
+ {saving && <Spinner size="sm" className="mr-2" />}
457
523
  Save Changes
458
524
  </Button>
459
525
  </DialogFooter>
@@ -1,37 +0,0 @@
1
- import React from "react";
2
- import { Link } from "react-router-dom";
3
- import { Webhook } from "lucide-react";
4
- import { DropdownMenuItem } from "@checkstack/ui";
5
- import type { UserMenuItemsContext } from "@checkstack/frontend-api";
6
- import { resolveRoute } from "@checkstack/common";
7
- import {
8
- integrationRoutes,
9
- integrationAccess,
10
- pluginMetadata,
11
- } from "@checkstack/integration-common";
12
-
13
- /**
14
- * "Integrations" entry in the user menu. Links to the integrations landing
15
- * page (provider list); each provider links on to its connection-management
16
- * page. Gated on `integration.manage` — mirrors the access check the routes
17
- * themselves enforce, but hiding the link is the cleaner UX for users who
18
- * can't manage connections.
19
- */
20
- export const IntegrationMenuItem = ({
21
- accessRules: userPerms,
22
- }: UserMenuItemsContext) => {
23
- const qualifiedId = `${pluginMetadata.pluginId}.${integrationAccess.manage.id}`;
24
- const allowed = userPerms.includes("*") || userPerms.includes(qualifiedId);
25
-
26
- if (!allowed) {
27
- return <React.Fragment />;
28
- }
29
-
30
- return (
31
- <Link to={resolveRoute(integrationRoutes.routes.list)}>
32
- <DropdownMenuItem icon={<Webhook className="h-4 w-4" />}>
33
- Integrations
34
- </DropdownMenuItem>
35
- </Link>
36
- );
37
- };