@checkstack/dependency-frontend 0.2.17 → 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 CHANGED
@@ -1,5 +1,24 @@
1
1
  # @checkstack/dependency-frontend
2
2
 
3
+ ## 0.3.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 35463ef: Improve dependency map directional clarity
8
+
9
+ - Redesigned system nodes with a split footer bar showing directional dependency counts (`← N used by | depends N →`), making each node self-documenting
10
+ - Color-coded connection handles: teal for incoming ("used by") and violet for outgoing ("depends on")
11
+ - Fixed invisible edge arrows by implementing custom SVG marker definitions with impact-type-matched colors (sky for informational, amber for degraded, red for critical)
12
+ - Updated the legend panel to explain handle colors alongside the existing impact type guide
13
+
14
+ ## 0.2.18
15
+
16
+ ### Patch Changes
17
+
18
+ - Updated dependencies [298bf42]
19
+ - @checkstack/catalog-common@1.5.0
20
+ - @checkstack/dashboard-frontend@0.4.5
21
+
3
22
  ## 0.2.17
4
23
 
5
24
  ### Patch Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@checkstack/dependency-frontend",
3
- "version": "0.2.17",
3
+ "version": "0.3.0",
4
4
  "type": "module",
5
5
  "main": "src/index.tsx",
6
6
  "checkstack": {
@@ -12,9 +12,9 @@
12
12
  "lint:code": "eslint . --max-warnings 0"
13
13
  },
14
14
  "dependencies": {
15
- "@checkstack/catalog-common": "1.4.0",
15
+ "@checkstack/catalog-common": "1.4.1",
16
16
  "@checkstack/common": "0.6.5",
17
- "@checkstack/dashboard-frontend": "0.4.2",
17
+ "@checkstack/dashboard-frontend": "0.4.4",
18
18
  "@checkstack/dependency-common": "0.2.1",
19
19
  "@checkstack/frontend-api": "0.3.9",
20
20
  "@checkstack/healthcheck-common": "0.11.0",
@@ -10,7 +10,6 @@ import {
10
10
  ReactFlowProvider,
11
11
  type NodeChange,
12
12
  type Connection,
13
- MarkerType,
14
13
  Panel,
15
14
  } from "@xyflow/react";
16
15
  import "@xyflow/react/dist/style.css";
@@ -247,6 +246,21 @@ function DependencyMapContent() {
247
246
  const savedPositions = posData?.positions ?? [];
248
247
  const warnings = warningsData?.warnings ?? {};
249
248
  const healthStatuses = healthData?.statuses ?? {};
249
+ const deps = depsData?.dependencies ?? [];
250
+
251
+ // Compute per-system dependency counts
252
+ const upstreamCountMap = new Map<string, number>();
253
+ const downstreamCountMap = new Map<string, number>();
254
+ for (const dep of deps) {
255
+ upstreamCountMap.set(
256
+ dep.sourceSystemId,
257
+ (upstreamCountMap.get(dep.sourceSystemId) ?? 0) + 1,
258
+ );
259
+ downstreamCountMap.set(
260
+ dep.targetSystemId,
261
+ (downstreamCountMap.get(dep.targetSystemId) ?? 0) + 1,
262
+ );
263
+ }
250
264
 
251
265
  // Lookup maps for position resolution
252
266
  const savedPositionMap = new Map(
@@ -287,6 +301,8 @@ function DependencyMapContent() {
287
301
  systemId: system.id,
288
302
  status: selfStatus,
289
303
  derivedState: warning?.derivedState,
304
+ upstreamCount: upstreamCountMap.get(system.id) ?? 0,
305
+ downstreamCount: downstreamCountMap.get(system.id) ?? 0,
290
306
  };
291
307
 
292
308
  return {
@@ -298,7 +314,7 @@ function DependencyMapContent() {
298
314
  });
299
315
 
300
316
  setNodes(newNodes);
301
- }, [systemsData, posData, warningsData, healthData, setNodes]);
317
+ }, [systemsData, posData, warningsData, healthData, depsData, setNodes]);
302
318
 
303
319
  // Build edges separately — only depends on dependency data
304
320
  useEffect(() => {
@@ -318,11 +334,6 @@ function DependencyMapContent() {
318
334
  target: dep.targetSystemId,
319
335
  type: "dependency" as const,
320
336
  animated: dep.transitive,
321
- markerEnd: {
322
- type: MarkerType.ArrowClosed,
323
- width: 16,
324
- height: 16,
325
- },
326
337
  data: edgeData,
327
338
  };
328
339
  },
@@ -502,7 +513,28 @@ function DependencyMapContent() {
502
513
  <Panel position="bottom-left">
503
514
  <div className="bg-card/90 backdrop-blur-sm border border-border rounded-lg p-3 shadow-lg max-w-64">
504
515
  <p className="text-xs font-semibold text-muted-foreground mb-2 uppercase tracking-wider">
505
- Impact Legend
516
+ Legend
517
+ </p>
518
+
519
+ {/* Direction legend */}
520
+ <div className="space-y-1.5 mb-3">
521
+ <div className="flex items-center gap-2">
522
+ <div className="w-3 h-3 rounded-full bg-teal-400/60 border-2 border-background shrink-0" />
523
+ <span className="text-xs text-muted-foreground">
524
+ Used by (incoming)
525
+ </span>
526
+ </div>
527
+ <div className="flex items-center gap-2">
528
+ <div className="w-3 h-3 rounded-full bg-violet-400/60 border-2 border-background shrink-0" />
529
+ <span className="text-xs text-muted-foreground">
530
+ Depends on (outgoing)
531
+ </span>
532
+ </div>
533
+ </div>
534
+
535
+ {/* Impact legend */}
536
+ <p className="text-xs font-semibold text-muted-foreground mb-2 uppercase tracking-wider">
537
+ Impact
506
538
  </p>
507
539
  <div className="space-y-1.5">
508
540
  <div className="flex items-center gap-2">
@@ -27,9 +27,17 @@ const impactStrokeWidths: Record<ImpactType, number> = {
27
27
  critical: 2.5,
28
28
  };
29
29
 
30
+ /** Hex colors for SVG marker fills — must match the Tailwind stroke classes. */
31
+ const impactHexColors: Record<ImpactType, string> = {
32
+ informational: "#38bdf8", // sky-400
33
+ degraded: "#fbbf24", // amber-400
34
+ critical: "#f87171", // red-400
35
+ };
36
+
30
37
  /**
31
38
  * Custom React Flow edge displaying dependency impact type via color + thickness.
32
- * Transitive edges use dashed stroke.
39
+ * Transitive edges use dashed stroke. Renders a visible arrowhead marker
40
+ * colored to match the impact type.
33
41
  */
34
42
  export function DependencyEdgeComponent({
35
43
  id,
@@ -55,9 +63,32 @@ export function DependencyEdgeComponent({
55
63
  const isTransitive = data?.transitive ?? false;
56
64
  const colorClass = impactColors[impactType];
57
65
  const strokeWidth = impactStrokeWidths[impactType];
66
+ const hexColor = selected ? "hsl(var(--primary))" : impactHexColors[impactType];
67
+
68
+ // Unique marker ID per edge to avoid color clashes between different impact types
69
+ const markerId = `arrow-${id}`;
58
70
 
59
71
  return (
60
72
  <>
73
+ {/* SVG marker definition for the arrowhead */}
74
+ <defs>
75
+ <marker
76
+ id={markerId}
77
+ markerWidth="12"
78
+ markerHeight="12"
79
+ refX="10"
80
+ refY="6"
81
+ orient="auto"
82
+ markerUnits="userSpaceOnUse"
83
+ >
84
+ <path
85
+ d="M2,2 L10,6 L2,10 L4,6 Z"
86
+ fill={hexColor}
87
+ opacity={selected ? 1 : 0.8}
88
+ />
89
+ </marker>
90
+ </defs>
91
+
61
92
  <BaseEdge
62
93
  id={id}
63
94
  path={edgePath}
@@ -65,6 +96,7 @@ export function DependencyEdgeComponent({
65
96
  style={{
66
97
  strokeWidth: selected ? strokeWidth + 1 : strokeWidth,
67
98
  strokeDasharray: isTransitive ? "6 4" : undefined,
99
+ markerEnd: `url(#${markerId})`,
68
100
  }}
69
101
  />
70
102
 
@@ -6,6 +6,10 @@ export interface SystemNodeData extends Record<string, unknown> {
6
6
  status?: "operational" | "degraded" | "down";
7
7
  derivedState?: "info" | "degraded" | "down";
8
8
  systemId: string;
9
+ /** Number of systems this node depends on (outgoing edges via right handle) */
10
+ upstreamCount: number;
11
+ /** Number of systems that depend on this node (incoming edges via left handle) */
12
+ downstreamCount: number;
9
13
  }
10
14
 
11
15
  export type SystemNode = Node<SystemNodeData, "system">;
@@ -48,6 +52,8 @@ function combineStatus(
48
52
  /**
49
53
  * Custom React Flow node representing a system in the dependency graph.
50
54
  * Color-coded by worst of own status and derived warning state.
55
+ * Features a split footer bar showing directional dependency counts
56
+ * and color-coded handles for clear directionality.
51
57
  */
52
58
  export const SystemNodeComponent = memo(function SystemNodeComponent({
53
59
  data,
@@ -55,87 +61,122 @@ export const SystemNodeComponent = memo(function SystemNodeComponent({
55
61
  }: NodeProps<SystemNode>) {
56
62
  const effectiveStatus = combineStatus(data.status, data.derivedState);
57
63
  const styles = statusStyles[effectiveStatus];
64
+ const hasConnections = data.upstreamCount > 0 || data.downstreamCount > 0;
58
65
 
59
66
  return (
60
67
  <>
61
- {/* Target handle (left) — "I am depended upon" */}
68
+ {/* Target handle (left) — "I am depended upon" — teal */}
62
69
  <Handle
63
70
  type="target"
64
71
  position={Position.Left}
65
- className="!w-2.5 !h-2.5 !bg-muted-foreground/40 !border-2 !border-background"
72
+ className="!w-3 !h-3 !bg-teal-400/60 !border-2 !border-background !z-10"
66
73
  />
67
74
 
68
75
  <div
69
76
  className={`
70
- px-4 py-3 rounded-xl border-2 shadow-lg backdrop-blur-sm
77
+ rounded-xl border-2 shadow-lg backdrop-blur-sm
71
78
  transition-all duration-200
72
79
  ${styles.border} ${styles.bg} ${styles.glow}
73
80
  ${selected ? "ring-2 ring-primary ring-offset-2 ring-offset-background" : ""}
74
81
  hover:scale-[1.02] cursor-grab active:cursor-grabbing
75
- min-w-[140px] max-w-[200px]
82
+ min-w-[140px] max-w-[220px] overflow-hidden
76
83
  `}
77
84
  >
78
- <div className="flex items-center gap-2">
79
- {/* Status dot */}
80
- <div className="relative flex-shrink-0">
81
- <div
82
- className={`w-2.5 h-2.5 rounded-full ${styles.dot}`}
83
- />
84
- {effectiveStatus !== "operational" && (
85
+ {/* Main body */}
86
+ <div className="px-4 py-3">
87
+ <div className="flex items-center gap-2">
88
+ {/* Status dot */}
89
+ <div className="relative flex-shrink-0">
85
90
  <div
86
- className={`absolute inset-0 w-2.5 h-2.5 rounded-full ${styles.dot} animate-ping opacity-75`}
91
+ className={`w-2.5 h-2.5 rounded-full ${styles.dot}`}
87
92
  />
88
- )}
89
- </div>
93
+ {effectiveStatus !== "operational" && (
94
+ <div
95
+ className={`absolute inset-0 w-2.5 h-2.5 rounded-full ${styles.dot} animate-ping opacity-75`}
96
+ />
97
+ )}
98
+ </div>
90
99
 
91
- {/* System name */}
92
- <span className="text-sm font-medium text-foreground truncate">
93
- {data.label}
94
- </span>
95
- </div>
100
+ {/* System name */}
101
+ <span className="text-sm font-medium text-foreground truncate">
102
+ {data.label}
103
+ </span>
104
+ </div>
96
105
 
97
- {/* Status labels */}
98
- <div className="mt-1.5 flex flex-col gap-0.5">
99
- <span
100
- className={`text-[10px] uppercase tracking-wider font-semibold ${
101
- (data.status ?? "operational") === "operational"
102
- ? "text-emerald-500/70"
103
- : (data.status ?? "operational") === "degraded"
104
- ? "text-amber-500/70"
105
- : "text-red-500/70"
106
- }`}
107
- >
108
- {(data.status ?? "operational") === "operational"
109
- ? "Healthy"
110
- : (data.status ?? "operational") === "degraded"
111
- ? "Degraded"
112
- : "Unhealthy"}
113
- </span>
114
- {data.derivedState && (
106
+ {/* Status labels */}
107
+ <div className="mt-1.5 flex flex-col gap-0.5">
115
108
  <span
116
109
  className={`text-[10px] uppercase tracking-wider font-semibold ${
117
- data.derivedState === "info"
118
- ? "text-blue-400/70"
119
- : data.derivedState === "degraded"
110
+ (data.status ?? "operational") === "operational"
111
+ ? "text-emerald-500/70"
112
+ : (data.status ?? "operational") === "degraded"
120
113
  ? "text-amber-500/70"
121
114
  : "text-red-500/70"
122
115
  }`}
123
116
  >
124
- {data.derivedState === "info"
125
- ? "↑ Upstream issue"
126
- : data.derivedState === "degraded"
127
- ? "↑ Upstream degraded"
128
- : "↑ Upstream down"}
117
+ {(data.status ?? "operational") === "operational"
118
+ ? "Healthy"
119
+ : (data.status ?? "operational") === "degraded"
120
+ ? "Degraded"
121
+ : "Unhealthy"}
129
122
  </span>
130
- )}
123
+ {data.derivedState && (
124
+ <span
125
+ className={`text-[10px] uppercase tracking-wider font-semibold ${
126
+ data.derivedState === "info"
127
+ ? "text-blue-400/70"
128
+ : data.derivedState === "degraded"
129
+ ? "text-amber-500/70"
130
+ : "text-red-500/70"
131
+ }`}
132
+ >
133
+ {data.derivedState === "info"
134
+ ? "↑ Upstream issue"
135
+ : data.derivedState === "degraded"
136
+ ? "↑ Upstream degraded"
137
+ : "↑ Upstream down"}
138
+ </span>
139
+ )}
140
+ </div>
131
141
  </div>
142
+
143
+ {/* Directional footer — only shown when the node has connections */}
144
+ {hasConnections && (
145
+ <div className="flex border-t border-border/40">
146
+ {/* Left zone: incoming — systems that depend on this node */}
147
+ <div className="flex-1 flex items-center gap-1 px-2.5 py-1.5 bg-teal-500/5 border-r border-border/30">
148
+ <span className="text-[10px] text-teal-400/80 font-medium">
149
+
150
+ </span>
151
+ <span className="text-[10px] text-teal-400/80 font-medium tabular-nums">
152
+ {data.downstreamCount}
153
+ </span>
154
+ <span className="text-[10px] text-muted-foreground/60 truncate">
155
+ used by
156
+ </span>
157
+ </div>
158
+
159
+ {/* Right zone: outgoing — systems this node depends on */}
160
+ <div className="flex-1 flex items-center justify-end gap-1 px-2.5 py-1.5 bg-violet-500/5">
161
+ <span className="text-[10px] text-muted-foreground/60 truncate">
162
+ depends
163
+ </span>
164
+ <span className="text-[10px] text-violet-400/80 font-medium tabular-nums">
165
+ {data.upstreamCount}
166
+ </span>
167
+ <span className="text-[10px] text-violet-400/80 font-medium">
168
+
169
+ </span>
170
+ </div>
171
+ </div>
172
+ )}
132
173
  </div>
133
174
 
134
- {/* Source handle (right) — "I depend on..." */}
175
+ {/* Source handle (right) — "I depend on..." — violet */}
135
176
  <Handle
136
177
  type="source"
137
178
  position={Position.Right}
138
- className="!w-2.5 !h-2.5 !bg-muted-foreground/40 !border-2 !border-background"
179
+ className="!w-3 !h-3 !bg-violet-400/60 !border-2 !border-background !z-10"
139
180
  />
140
181
  </>
141
182
  );