@checkstack/dependency-frontend 0.3.5 → 0.4.1

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,106 @@
1
1
  # @checkstack/dependency-frontend
2
2
 
3
+ ## 0.4.1
4
+
5
+ ### Patch Changes
6
+
7
+ - Updated dependencies [9016526]
8
+ - @checkstack/common@0.10.0
9
+ - @checkstack/catalog-common@2.2.0
10
+ - @checkstack/healthcheck-common@1.1.0
11
+ - @checkstack/dependency-common@1.1.0
12
+ - @checkstack/gitops-common@0.4.0
13
+ - @checkstack/dashboard-frontend@0.7.1
14
+ - @checkstack/frontend-api@0.5.1
15
+ - @checkstack/gitops-frontend@0.4.1
16
+ - @checkstack/ui@1.8.1
17
+ - @checkstack/signal-frontend@0.1.3
18
+
19
+ ## 0.4.0
20
+
21
+ ### Minor Changes
22
+
23
+ - f6f9a5c: Add a GitOps `System.dependencies` extension and lock the matching UI.
24
+
25
+ Each entry references an upstream system by ref and tunes the impact:
26
+
27
+ ```yaml
28
+ apiVersion: checkstack.io/v1alpha1
29
+ kind: System
30
+ metadata: { name: payments-api }
31
+ spec:
32
+ dependencies:
33
+ - targetRef: { kind: System, name: payments-db }
34
+ impactType: critical
35
+ transitive: false
36
+ label: "primary store"
37
+ ```
38
+
39
+ The reconciler diffs the YAML-declared edges against the persisted ones
40
+ where this system is the source and converges via
41
+ create / update / delete. GitOps is the source of truth, so any edges
42
+ no longer listed are removed. Refs that resolve to the source system
43
+ itself are rejected; refs that fail to resolve abort the diff before
44
+ any mutation.
45
+
46
+ UI gates:
47
+
48
+ - The `DependencyEditor` (system editor drawer) hides Add and disables
49
+ Edit/Delete on upstream rows when the source system is GitOps-managed.
50
+ Downstream rows are gated per-row by the _other_ system's lock.
51
+ - The `DependencyMap` blocks `onConnect` when the source is locked,
52
+ surfaces a "Managed by GitOps" notice in the edge editor panel, and
53
+ disables Save/Delete there.
54
+
55
+ ### Patch Changes
56
+
57
+ - 950d6ec: Fix mobile UserMenu items rendering at zero height, group menu items by
58
+ section, and unstack cramped card headers on small viewports.
59
+
60
+ - **UserMenu mobile bug**: On mobile, the user-menu Sheet rendered every
61
+ menu item as a grid row, which combined with `flex-shrink: 1` on each
62
+ item collapsed the buttons whose internal layout uses `display: flex`
63
+ (the items registered with `useNavigate` rather than `<Link>`) to zero
64
+ content height. Switched the mobile container to a flex column with
65
+ `[&>*]:shrink-0` and added `min-h-0` so the sheet scrolls correctly
66
+ when the list overflows.
67
+
68
+ - **UserMenu grouping**: Slot extensions now accept an optional `group`
69
+ field. The user menu buckets `UserMenuItemsSlot` extensions by `group`
70
+ and renders each group under a labeled header (`Workspace`,
71
+ `Reliability`, `Configuration`, `Documentation`, `Account`). Existing
72
+ core plugins are tagged with the appropriate group; third-party plugins
73
+ can pick any of these or supply their own label. Untagged extensions
74
+ render last with no header. `UserMenuItemsBottomSlot` is unaffected.
75
+
76
+ - **Card header responsiveness**: `CardHeaderRow` (the primitive shared by
77
+ Incident, Maintenance, Auth, Catalog, GitOps and other config cards) now
78
+ stacks vertically on narrow viewports and only switches to a single row
79
+ at the `sm` breakpoint, so titles and adjacent filter controls (e.g.
80
+ status `Select`, "Show resolved" checkbox) no longer cram together on
81
+ mobile. Refactored the Incident and Maintenance config pages to use the
82
+ primitive instead of a hand-rolled `flex items-center justify-between`
83
+ row, and made their `Select` triggers full-width on mobile.
84
+
85
+ - Updated dependencies [42abfff]
86
+ - Updated dependencies [3547670]
87
+ - Updated dependencies [f6f9a5c]
88
+ - Updated dependencies [1ef2e79]
89
+ - Updated dependencies [aa89bc5]
90
+ - Updated dependencies [950d6ec]
91
+ - Updated dependencies [3547670]
92
+ - Updated dependencies [3547670]
93
+ - @checkstack/common@0.9.0
94
+ - @checkstack/ui@1.8.0
95
+ - @checkstack/gitops-common@0.3.0
96
+ - @checkstack/gitops-frontend@0.4.0
97
+ - @checkstack/catalog-common@2.1.0
98
+ - @checkstack/frontend-api@0.5.0
99
+ - @checkstack/dashboard-frontend@0.7.0
100
+ - @checkstack/dependency-common@1.0.2
101
+ - @checkstack/healthcheck-common@1.0.2
102
+ - @checkstack/signal-frontend@0.1.2
103
+
3
104
  ## 0.3.5
4
105
 
5
106
  ### Patch Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@checkstack/dependency-frontend",
3
- "version": "0.3.5",
3
+ "version": "0.4.1",
4
4
  "license": "Elastic-2.0",
5
5
  "type": "module",
6
6
  "main": "src/index.tsx",
@@ -13,14 +13,16 @@
13
13
  "lint:code": "eslint . --max-warnings 0"
14
14
  },
15
15
  "dependencies": {
16
- "@checkstack/catalog-common": "2.0.0",
17
- "@checkstack/common": "0.7.0",
18
- "@checkstack/dashboard-frontend": "0.6.0",
19
- "@checkstack/dependency-common": "1.0.0",
20
- "@checkstack/frontend-api": "0.4.1",
21
- "@checkstack/healthcheck-common": "1.0.0",
22
- "@checkstack/signal-frontend": "0.1.0",
23
- "@checkstack/ui": "1.7.0",
16
+ "@checkstack/catalog-common": "2.1.0",
17
+ "@checkstack/common": "0.9.0",
18
+ "@checkstack/dashboard-frontend": "0.7.0",
19
+ "@checkstack/dependency-common": "1.0.2",
20
+ "@checkstack/frontend-api": "0.5.0",
21
+ "@checkstack/gitops-common": "0.3.0",
22
+ "@checkstack/gitops-frontend": "0.4.0",
23
+ "@checkstack/healthcheck-common": "1.0.2",
24
+ "@checkstack/signal-frontend": "0.1.2",
25
+ "@checkstack/ui": "1.8.0",
24
26
  "@xyflow/react": "^12.10.2",
25
27
  "lucide-react": "^0.344.0",
26
28
  "react": "^18.2.0",
@@ -29,7 +31,7 @@
29
31
  "devDependencies": {
30
32
  "typescript": "^5.0.0",
31
33
  "@types/react": "^18.2.0",
32
- "@checkstack/tsconfig": "0.0.6",
33
- "@checkstack/scripts": "0.1.2"
34
+ "@checkstack/tsconfig": "0.0.7",
35
+ "@checkstack/scripts": "0.3.1"
34
36
  }
35
37
  }
@@ -31,7 +31,12 @@ import {
31
31
  AlertTriangle,
32
32
  RotateCcw,
33
33
  MapIcon,
34
+ GitBranch,
34
35
  } from "lucide-react";
36
+ import {
37
+ useProvenanceLock,
38
+ useProvenanceLocks,
39
+ } from "@checkstack/gitops-frontend";
35
40
 
36
41
  type Props = SlotContext<typeof SystemEditorSlot>;
37
42
 
@@ -58,6 +63,16 @@ export const DependencyEditor: React.FC<Props> = ({ systemId }) => {
58
63
  const depClient = usePluginClient(DependencyApi);
59
64
  const catalogClient = usePluginClient(CatalogApi);
60
65
 
66
+ // GitOps owns the *source* system's `dependencies` extension. Edits to
67
+ // upstream rows (this system → ...) are blocked when this system is
68
+ // managed; downstream rows belong to other source systems and are gated
69
+ // per-row via the bulk hook.
70
+ const { isLocked: sourceLocked } = useProvenanceLock({
71
+ kind: "System",
72
+ entityId: systemId,
73
+ });
74
+ const { getLock: getSystemLock } = useProvenanceLocks();
75
+
61
76
  const [isAdding, setIsAdding] = useState(false);
62
77
  const [selectedTargetId, setSelectedTargetId] = useState("");
63
78
  const [selectedImpactType, setSelectedImpactType] =
@@ -168,12 +183,29 @@ export const DependencyEditor: React.FC<Props> = ({ systemId }) => {
168
183
  return (
169
184
  <div className="space-y-3">
170
185
  <div className="flex items-center justify-between">
171
- <Label>Dependencies</Label>
186
+ <Label className="flex items-center gap-2">
187
+ Dependencies
188
+ {sourceLocked && (
189
+ <span
190
+ className="inline-flex items-center gap-1 text-xs font-normal text-primary"
191
+ title="Managed by GitOps — edit the source YAML"
192
+ >
193
+ <GitBranch className="h-3 w-3" />
194
+ GitOps
195
+ </span>
196
+ )}
197
+ </Label>
172
198
  <Button
173
199
  type="button"
174
200
  variant="outline"
175
201
  size="sm"
176
202
  onClick={() => setIsAdding(!isAdding)}
203
+ disabled={sourceLocked}
204
+ title={
205
+ sourceLocked
206
+ ? "Managed by GitOps — declare dependencies in the System's YAML"
207
+ : undefined
208
+ }
177
209
  >
178
210
  <Plus className="h-3.5 w-3.5 mr-1" />
179
211
  Add
@@ -187,7 +219,7 @@ export const DependencyEditor: React.FC<Props> = ({ systemId }) => {
187
219
  )}
188
220
 
189
221
  {/* Add dependency form */}
190
- {isAdding && (
222
+ {isAdding && !sourceLocked && (
191
223
  <div className="p-3 rounded-lg border border-border bg-muted/30 space-y-3">
192
224
  <div className="space-y-2">
193
225
  <label className="text-sm font-medium">Depends on (upstream)</label>
@@ -257,6 +289,7 @@ export const DependencyEditor: React.FC<Props> = ({ systemId }) => {
257
289
  onDelete={() => handleDelete(dep)}
258
290
  onUpdate={handleUpdate}
259
291
  isUpdating={updateMutation.isPending}
292
+ isLocked={sourceLocked}
260
293
  />
261
294
  ))}
262
295
  </div>
@@ -271,17 +304,26 @@ export const DependencyEditor: React.FC<Props> = ({ systemId }) => {
271
304
  Depended By ({downstreamDeps.length})
272
305
  </h4>
273
306
  <div className="space-y-1">
274
- {downstreamDeps.map((dep) => (
275
- <DependencyRow
276
- key={dep.id}
277
- dependency={dep}
278
- systemName={systemNameMap.get(dep.sourceSystemId) ?? dep.sourceSystemId}
279
- direction="downstream"
280
- onDelete={() => handleDelete(dep)}
281
- onUpdate={handleUpdate}
282
- isUpdating={updateMutation.isPending}
283
- />
284
- ))}
307
+ {downstreamDeps.map((dep) => {
308
+ // The "source" of a downstream edge is *another* system —
309
+ // its lock is what governs editability.
310
+ const otherSourceLocked = getSystemLock({
311
+ kind: "System",
312
+ entityId: dep.sourceSystemId,
313
+ }).isLocked;
314
+ return (
315
+ <DependencyRow
316
+ key={dep.id}
317
+ dependency={dep}
318
+ systemName={systemNameMap.get(dep.sourceSystemId) ?? dep.sourceSystemId}
319
+ direction="downstream"
320
+ onDelete={() => handleDelete(dep)}
321
+ onUpdate={handleUpdate}
322
+ isUpdating={updateMutation.isPending}
323
+ isLocked={otherSourceLocked}
324
+ />
325
+ );
326
+ })}
285
327
  </div>
286
328
  </div>
287
329
  )}
@@ -322,6 +364,7 @@ function DependencyRow({
322
364
  onDelete,
323
365
  onUpdate,
324
366
  isUpdating,
367
+ isLocked = false,
325
368
  }: {
326
369
  dependency: Dependency;
327
370
  systemName: string;
@@ -334,6 +377,8 @@ function DependencyRow({
334
377
  healthCheckRules?: { healthCheckId: string; overrideImpactType: ImpactType }[];
335
378
  }) => void;
336
379
  isUpdating: boolean;
380
+ /** When true, the source system of this edge is GitOps-managed. */
381
+ isLocked?: boolean;
337
382
  }) {
338
383
  const [isEditing, setIsEditing] = useState(false);
339
384
  const [editImpact, setEditImpact] = useState<ImpactType>(
@@ -366,7 +411,7 @@ function DependencyRow({
366
411
  setIsEditing(false);
367
412
  };
368
413
 
369
- if (isEditing) {
414
+ if (isEditing && !isLocked) {
370
415
  return (
371
416
  <div className="p-3 rounded-lg border border-primary/30 bg-muted/30 space-y-3">
372
417
  <div className="flex items-center gap-2">
@@ -415,18 +460,26 @@ function DependencyRow({
415
460
  );
416
461
  }
417
462
 
463
+ const interactive = !isLocked;
418
464
  return (
419
465
  <div
420
- className="flex items-center justify-between p-2 rounded border border-border bg-background hover:bg-muted/30 transition-colors cursor-pointer"
421
- onClick={() => setIsEditing(true)}
422
- role="button"
423
- tabIndex={0}
424
- onKeyDown={(e) => {
425
- if (e.key === "Enter" || e.key === " ") {
426
- e.preventDefault();
427
- setIsEditing(true);
428
- }
429
- }}
466
+ className={`flex items-center justify-between p-2 rounded border border-border bg-background transition-colors ${
467
+ interactive ? "hover:bg-muted/30 cursor-pointer" : ""
468
+ }`}
469
+ onClick={interactive ? () => setIsEditing(true) : undefined}
470
+ role={interactive ? "button" : undefined}
471
+ tabIndex={interactive ? 0 : -1}
472
+ onKeyDown={
473
+ interactive
474
+ ? (e) => {
475
+ if (e.key === "Enter" || e.key === " ") {
476
+ e.preventDefault();
477
+ setIsEditing(true);
478
+ }
479
+ }
480
+ : undefined
481
+ }
482
+ title={isLocked ? "Managed by GitOps" : undefined}
430
483
  >
431
484
  <div className="flex items-center gap-2">
432
485
  {direction === "upstream" ? (
@@ -434,6 +487,9 @@ function DependencyRow({
434
487
  ) : (
435
488
  <ArrowDownRight className="h-4 w-4 text-muted-foreground" />
436
489
  )}
490
+ {isLocked && (
491
+ <GitBranch className="h-3 w-3 text-primary" aria-label="Managed by GitOps" />
492
+ )}
437
493
  <span className="text-sm font-medium">{systemName}</span>
438
494
  {dependency.label && (
439
495
  <span className="text-xs text-muted-foreground">
@@ -460,6 +516,8 @@ function DependencyRow({
460
516
  variant="ghost"
461
517
  size="icon"
462
518
  className="h-7 w-7"
519
+ disabled={isLocked}
520
+ title={isLocked ? "Managed by GitOps" : undefined}
463
521
  onClick={(e) => {
464
522
  e.stopPropagation();
465
523
  onDelete();
@@ -31,6 +31,7 @@ import {
31
31
  import { Maximize2, Save, RefreshCw, Trash2 } from "lucide-react";
32
32
  import type { ImpactType } from "@checkstack/dependency-common";
33
33
  import { DependencyEdgeForm } from "./DependencyEdgeForm";
34
+ import { useProvenanceLocks } from "@checkstack/gitops-frontend";
34
35
 
35
36
  import {
36
37
  SystemNodeComponent,
@@ -86,6 +87,9 @@ function DependencyMapContent() {
86
87
  const catalogClient = usePluginClient(CatalogApi);
87
88
  const healthCheckClient = usePluginClient(HealthCheckApi);
88
89
  const { fitView } = useReactFlow();
90
+ // GitOps owns the *source* system's `dependencies` extension. We use the
91
+ // bulk hook to gate edge mutations per edge based on the source's lock.
92
+ const { getLock: getSystemLock } = useProvenanceLocks();
89
93
  const [nodes, setNodes, onNodesChange] = useNodesState<SystemNode>([]);
90
94
  const [edges, setEdges, onEdgesChange] = useEdgesState<DependencyEdge>([]);
91
95
  const [hasUnsaved, setHasUnsaved] = useState(false);
@@ -217,6 +221,17 @@ function DependencyMapContent() {
217
221
  if (!connection.source || !connection.target) return;
218
222
  if (connection.source === connection.target) return;
219
223
 
224
+ const sourceLocked = getSystemLock({
225
+ kind: "System",
226
+ entityId: connection.source,
227
+ }).isLocked;
228
+ if (sourceLocked) {
229
+ toast.error(
230
+ "Source system is managed by GitOps — declare the dependency in its YAML.",
231
+ );
232
+ return;
233
+ }
234
+
220
235
  createDependency({
221
236
  sourceSystemId: connection.source,
222
237
  targetSystemId: connection.target,
@@ -224,7 +239,7 @@ function DependencyMapContent() {
224
239
  transitive: false,
225
240
  });
226
241
  },
227
- [createDependency],
242
+ [createDependency, getSystemLock, toast],
228
243
  );
229
244
 
230
245
  // Track node positions for saving and for preserving in-memory positions
@@ -568,12 +583,17 @@ function DependencyMapContent() {
568
583
  </Panel>
569
584
 
570
585
  {/* Edge editor panel */}
571
- {selectedEdge && (
586
+ {selectedEdge && (() => {
587
+ const edgeSourceLocked = getSystemLock({
588
+ kind: "System",
589
+ entityId: selectedEdge.sourceSystemId,
590
+ }).isLocked;
591
+ return (
572
592
  <Panel position="top-left">
573
593
  <div className="bg-card/95 backdrop-blur-sm border border-border rounded-lg shadow-lg p-4 w-72 space-y-3">
574
594
  <div className="space-y-1">
575
595
  <p className="text-sm font-semibold text-foreground">
576
- Edit Dependency
596
+ {edgeSourceLocked ? "Dependency (GitOps)" : "Edit Dependency"}
577
597
  </p>
578
598
  <p className="text-xs text-muted-foreground">
579
599
  {systemNameMap.get(selectedEdge.sourceSystemId) ??
@@ -582,6 +602,12 @@ function DependencyMapContent() {
582
602
  {systemNameMap.get(selectedEdge.targetSystemId) ??
583
603
  selectedEdge.targetSystemId}
584
604
  </p>
605
+ {edgeSourceLocked && (
606
+ <p className="text-xs text-warning">
607
+ The source system is managed by GitOps. Edit the
608
+ dependency in its YAML.
609
+ </p>
610
+ )}
585
611
  </div>
586
612
  <DependencyEdgeForm
587
613
  impactType={selectedEdge.impactType}
@@ -610,7 +636,8 @@ function DependencyMapContent() {
610
636
  systemId: selectedEdge.sourceSystemId,
611
637
  })
612
638
  }
613
- disabled={deleteMutation.isPending}
639
+ disabled={deleteMutation.isPending || edgeSourceLocked}
640
+ title={edgeSourceLocked ? "Managed by GitOps" : undefined}
614
641
  >
615
642
  <Trash2 className="h-3.5 w-3.5 mr-1" />
616
643
  Delete
@@ -622,7 +649,7 @@ function DependencyMapContent() {
622
649
  size="sm"
623
650
  onClick={() => setSelectedEdge(undefined)}
624
651
  >
625
- Cancel
652
+ {edgeSourceLocked ? "Close" : "Cancel"}
626
653
  </Button>
627
654
  <Button
628
655
  type="button"
@@ -639,7 +666,8 @@ function DependencyMapContent() {
639
666
  : [],
640
667
  })
641
668
  }
642
- disabled={updateMutation.isPending}
669
+ disabled={updateMutation.isPending || edgeSourceLocked}
670
+ title={edgeSourceLocked ? "Managed by GitOps" : undefined}
643
671
  >
644
672
  {updateMutation.isPending ? "Saving..." : "Save"}
645
673
  </Button>
@@ -647,7 +675,8 @@ function DependencyMapContent() {
647
675
  </div>
648
676
  </div>
649
677
  </Panel>
650
- )}
678
+ );
679
+ })()}
651
680
  </ReactFlow>
652
681
  </div>
653
682
  );
package/src/index.tsx CHANGED
@@ -51,6 +51,7 @@ export default createFrontendPlugin({
51
51
  createSlotExtension(UserMenuItemsSlot, {
52
52
  id: "dependency.user-menu.map",
53
53
  component: DependencyMenuItems,
54
+ metadata: { group: "Workspace" },
54
55
  }),
55
56
  ],
56
57
  });
package/tsconfig.json CHANGED
@@ -19,6 +19,12 @@
19
19
  {
20
20
  "path": "../frontend-api"
21
21
  },
22
+ {
23
+ "path": "../gitops-common"
24
+ },
25
+ {
26
+ "path": "../gitops-frontend"
27
+ },
22
28
  {
23
29
  "path": "../healthcheck-common"
24
30
  },