@checkstack/dependency-frontend 0.2.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 +42 -0
- package/package.json +34 -0
- package/src/components/DependencyAlert.tsx +144 -0
- package/src/components/DependencyBadge.tsx +78 -0
- package/src/components/DependencyEdgeForm.tsx +84 -0
- package/src/components/DependencyEditor.tsx +561 -0
- package/src/components/DependencyMapPage.tsx +631 -0
- package/src/components/DependencyMenuItems.tsx +20 -0
- package/src/components/HealthCheckRulesEditor.tsx +194 -0
- package/src/components/canvas/DependencyEdge.tsx +89 -0
- package/src/components/canvas/SystemNode.tsx +142 -0
- package/src/index.tsx +51 -0
- package/tsconfig.json +6 -0
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import React, { useState } from "react";
|
|
2
|
+
import { usePluginClient } from "@checkstack/frontend-api";
|
|
3
|
+
import { HealthCheckApi } from "@checkstack/healthcheck-common";
|
|
4
|
+
import type { ImpactType } from "@checkstack/dependency-common";
|
|
5
|
+
import { Button, Badge } from "@checkstack/ui";
|
|
6
|
+
import { Plus, Trash2, ChevronDown, ChevronUp, Activity } from "lucide-react";
|
|
7
|
+
|
|
8
|
+
interface HealthCheckRule {
|
|
9
|
+
healthCheckId: string;
|
|
10
|
+
overrideImpactType: ImpactType;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
interface Props {
|
|
14
|
+
/** The upstream system ID to query health checks for */
|
|
15
|
+
targetSystemId: string;
|
|
16
|
+
/** Current rules */
|
|
17
|
+
rules: HealthCheckRule[];
|
|
18
|
+
/** Callback when rules change */
|
|
19
|
+
onChange: (rules: HealthCheckRule[]) => void;
|
|
20
|
+
/** Compact mode for tight layouts like the map edge editor */
|
|
21
|
+
compact?: boolean;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Collapsible editor for health check rules on a dependency edge.
|
|
26
|
+
* When rules are configured, the dependency only triggers for failures
|
|
27
|
+
* of the specified health checks (with optional per-check impact override).
|
|
28
|
+
*/
|
|
29
|
+
export const HealthCheckRulesEditor: React.FC<Props> = ({
|
|
30
|
+
targetSystemId,
|
|
31
|
+
rules,
|
|
32
|
+
onChange,
|
|
33
|
+
compact = false,
|
|
34
|
+
}) => {
|
|
35
|
+
const [expanded, setExpanded] = useState(rules.length > 0);
|
|
36
|
+
|
|
37
|
+
// Fetch health checks for the target (upstream) system
|
|
38
|
+
const healthCheckClient = usePluginClient(HealthCheckApi);
|
|
39
|
+
const { data: associations } =
|
|
40
|
+
healthCheckClient.getSystemAssociations.useQuery(
|
|
41
|
+
{ systemId: targetSystemId },
|
|
42
|
+
{ enabled: !!targetSystemId },
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
const availableChecks = associations ?? [];
|
|
46
|
+
|
|
47
|
+
// Filter out already-added checks
|
|
48
|
+
const unusedChecks = availableChecks.filter(
|
|
49
|
+
(check) => !rules.some((r) => r.healthCheckId === check.configurationId),
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
const handleAddRule = (configurationId: string) => {
|
|
53
|
+
onChange([
|
|
54
|
+
...rules,
|
|
55
|
+
{ healthCheckId: configurationId, overrideImpactType: "critical" },
|
|
56
|
+
]);
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const handleRemoveRule = (healthCheckId: string) => {
|
|
60
|
+
onChange(rules.filter((r) => r.healthCheckId !== healthCheckId));
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const handleImpactChange = (
|
|
64
|
+
healthCheckId: string,
|
|
65
|
+
impactType: ImpactType,
|
|
66
|
+
) => {
|
|
67
|
+
onChange(
|
|
68
|
+
rules.map((r) =>
|
|
69
|
+
r.healthCheckId === healthCheckId
|
|
70
|
+
? { ...r, overrideImpactType: impactType }
|
|
71
|
+
: r,
|
|
72
|
+
),
|
|
73
|
+
);
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const getCheckName = (healthCheckId: string): string =>
|
|
77
|
+
availableChecks.find((c) => c.configurationId === healthCheckId)
|
|
78
|
+
?.configurationName ?? healthCheckId;
|
|
79
|
+
|
|
80
|
+
if (availableChecks.length === 0) {
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return (
|
|
85
|
+
<div
|
|
86
|
+
className={`rounded-lg border border-border ${compact ? "p-2" : "p-3"} bg-muted/20`}
|
|
87
|
+
>
|
|
88
|
+
<button
|
|
89
|
+
type="button"
|
|
90
|
+
className="flex items-center justify-between w-full text-left"
|
|
91
|
+
onClick={() => setExpanded(!expanded)}
|
|
92
|
+
>
|
|
93
|
+
<div className="flex items-center gap-2">
|
|
94
|
+
<Activity className="h-3.5 w-3.5 text-muted-foreground" />
|
|
95
|
+
<span className={`font-medium ${compact ? "text-xs" : "text-sm"}`}>
|
|
96
|
+
Health check rules
|
|
97
|
+
</span>
|
|
98
|
+
{rules.length > 0 && (
|
|
99
|
+
<Badge variant="outline" className="text-xs h-5">
|
|
100
|
+
{rules.length}
|
|
101
|
+
</Badge>
|
|
102
|
+
)}
|
|
103
|
+
</div>
|
|
104
|
+
{expanded ? (
|
|
105
|
+
<ChevronUp className="h-3.5 w-3.5 text-muted-foreground" />
|
|
106
|
+
) : (
|
|
107
|
+
<ChevronDown className="h-3.5 w-3.5 text-muted-foreground" />
|
|
108
|
+
)}
|
|
109
|
+
</button>
|
|
110
|
+
|
|
111
|
+
{expanded && (
|
|
112
|
+
<div className="mt-2 space-y-2">
|
|
113
|
+
<p className="text-xs text-muted-foreground">
|
|
114
|
+
When rules are configured, this dependency only triggers warnings
|
|
115
|
+
when the specified health checks fail. Each rule can override the
|
|
116
|
+
impact type.
|
|
117
|
+
</p>
|
|
118
|
+
|
|
119
|
+
{/* Existing rules */}
|
|
120
|
+
{rules.map((rule) => (
|
|
121
|
+
<div
|
|
122
|
+
key={rule.healthCheckId}
|
|
123
|
+
className="flex items-center gap-2 rounded border border-border bg-background p-2"
|
|
124
|
+
>
|
|
125
|
+
<span className="text-xs flex-1 truncate font-medium">
|
|
126
|
+
{getCheckName(rule.healthCheckId)}
|
|
127
|
+
</span>
|
|
128
|
+
<select
|
|
129
|
+
className="text-xs rounded border border-input bg-background px-2 py-1"
|
|
130
|
+
value={rule.overrideImpactType}
|
|
131
|
+
onChange={(e) =>
|
|
132
|
+
handleImpactChange(
|
|
133
|
+
rule.healthCheckId,
|
|
134
|
+
e.target.value as ImpactType,
|
|
135
|
+
)
|
|
136
|
+
}
|
|
137
|
+
>
|
|
138
|
+
<option value="informational">Info</option>
|
|
139
|
+
<option value="degraded">Degraded</option>
|
|
140
|
+
<option value="critical">Critical</option>
|
|
141
|
+
</select>
|
|
142
|
+
<Button
|
|
143
|
+
type="button"
|
|
144
|
+
variant="ghost"
|
|
145
|
+
size="icon"
|
|
146
|
+
className="h-6 w-6"
|
|
147
|
+
onClick={() => handleRemoveRule(rule.healthCheckId)}
|
|
148
|
+
>
|
|
149
|
+
<Trash2 className="h-3 w-3 text-destructive" />
|
|
150
|
+
</Button>
|
|
151
|
+
</div>
|
|
152
|
+
))}
|
|
153
|
+
|
|
154
|
+
{/* Add new rule */}
|
|
155
|
+
{unusedChecks.length > 0 && (
|
|
156
|
+
<div className="flex items-center gap-2">
|
|
157
|
+
<select
|
|
158
|
+
id="add-hc-rule"
|
|
159
|
+
className="flex-1 text-xs rounded border border-input bg-background px-2 py-1.5"
|
|
160
|
+
defaultValue=""
|
|
161
|
+
onChange={(e) => {
|
|
162
|
+
if (e.target.value) {
|
|
163
|
+
handleAddRule(e.target.value);
|
|
164
|
+
e.target.value = "";
|
|
165
|
+
}
|
|
166
|
+
}}
|
|
167
|
+
>
|
|
168
|
+
<option value="" disabled>
|
|
169
|
+
Add health check...
|
|
170
|
+
</option>
|
|
171
|
+
{unusedChecks.map((check) => (
|
|
172
|
+
<option
|
|
173
|
+
key={check.configurationId}
|
|
174
|
+
value={check.configurationId}
|
|
175
|
+
>
|
|
176
|
+
{check.configurationName}
|
|
177
|
+
</option>
|
|
178
|
+
))}
|
|
179
|
+
</select>
|
|
180
|
+
<Plus className="h-3.5 w-3.5 text-muted-foreground" />
|
|
181
|
+
</div>
|
|
182
|
+
)}
|
|
183
|
+
|
|
184
|
+
{rules.length === 0 && unusedChecks.length > 0 && (
|
|
185
|
+
<p className="text-xs text-muted-foreground/60 italic">
|
|
186
|
+
No rules configured — all health check failures trigger this
|
|
187
|
+
dependency.
|
|
188
|
+
</p>
|
|
189
|
+
)}
|
|
190
|
+
</div>
|
|
191
|
+
)}
|
|
192
|
+
</div>
|
|
193
|
+
);
|
|
194
|
+
};
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
|
|
2
|
+
import {
|
|
3
|
+
BaseEdge,
|
|
4
|
+
getBezierPath,
|
|
5
|
+
type EdgeProps,
|
|
6
|
+
type Edge,
|
|
7
|
+
} from "@xyflow/react";
|
|
8
|
+
import type { ImpactType } from "@checkstack/dependency-common";
|
|
9
|
+
|
|
10
|
+
export interface DependencyEdgeData extends Record<string, unknown> {
|
|
11
|
+
impactType: ImpactType;
|
|
12
|
+
transitive: boolean;
|
|
13
|
+
label?: string | null;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export type DependencyEdge = Edge<DependencyEdgeData, "dependency">;
|
|
17
|
+
|
|
18
|
+
const impactColors: Record<ImpactType, string> = {
|
|
19
|
+
informational: "stroke-sky-400/50",
|
|
20
|
+
degraded: "stroke-amber-400/60",
|
|
21
|
+
critical: "stroke-red-400/70",
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const impactStrokeWidths: Record<ImpactType, number> = {
|
|
25
|
+
informational: 1.5,
|
|
26
|
+
degraded: 2,
|
|
27
|
+
critical: 2.5,
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Custom React Flow edge displaying dependency impact type via color + thickness.
|
|
32
|
+
* Transitive edges use dashed stroke.
|
|
33
|
+
*/
|
|
34
|
+
export function DependencyEdgeComponent({
|
|
35
|
+
id,
|
|
36
|
+
sourceX,
|
|
37
|
+
sourceY,
|
|
38
|
+
targetX,
|
|
39
|
+
targetY,
|
|
40
|
+
sourcePosition,
|
|
41
|
+
targetPosition,
|
|
42
|
+
data,
|
|
43
|
+
selected,
|
|
44
|
+
}: EdgeProps<DependencyEdge>) {
|
|
45
|
+
const [edgePath, labelX, labelY] = getBezierPath({
|
|
46
|
+
sourceX,
|
|
47
|
+
sourceY,
|
|
48
|
+
sourcePosition,
|
|
49
|
+
targetX,
|
|
50
|
+
targetY,
|
|
51
|
+
targetPosition,
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
const impactType = data?.impactType ?? "informational";
|
|
55
|
+
const isTransitive = data?.transitive ?? false;
|
|
56
|
+
const colorClass = impactColors[impactType];
|
|
57
|
+
const strokeWidth = impactStrokeWidths[impactType];
|
|
58
|
+
|
|
59
|
+
return (
|
|
60
|
+
<>
|
|
61
|
+
<BaseEdge
|
|
62
|
+
id={id}
|
|
63
|
+
path={edgePath}
|
|
64
|
+
className={`${colorClass} ${selected ? "!stroke-primary" : ""}`}
|
|
65
|
+
style={{
|
|
66
|
+
strokeWidth: selected ? strokeWidth + 1 : strokeWidth,
|
|
67
|
+
strokeDasharray: isTransitive ? "6 4" : undefined,
|
|
68
|
+
}}
|
|
69
|
+
/>
|
|
70
|
+
|
|
71
|
+
{/* Edge label */}
|
|
72
|
+
{data?.label && (
|
|
73
|
+
<foreignObject
|
|
74
|
+
x={labelX - 40}
|
|
75
|
+
y={labelY - 10}
|
|
76
|
+
width={80}
|
|
77
|
+
height={20}
|
|
78
|
+
className="pointer-events-none overflow-visible"
|
|
79
|
+
>
|
|
80
|
+
<div className="flex justify-center">
|
|
81
|
+
<span className="text-[10px] bg-background/90 border border-border rounded px-1.5 py-0.5 text-muted-foreground whitespace-nowrap">
|
|
82
|
+
{data.label}
|
|
83
|
+
</span>
|
|
84
|
+
</div>
|
|
85
|
+
</foreignObject>
|
|
86
|
+
)}
|
|
87
|
+
</>
|
|
88
|
+
);
|
|
89
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { memo } from "react";
|
|
2
|
+
import { Handle, Position, type NodeProps, type Node } from "@xyflow/react";
|
|
3
|
+
|
|
4
|
+
export interface SystemNodeData extends Record<string, unknown> {
|
|
5
|
+
label: string;
|
|
6
|
+
status?: "operational" | "degraded" | "down";
|
|
7
|
+
derivedState?: "info" | "degraded" | "down";
|
|
8
|
+
systemId: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export type SystemNode = Node<SystemNodeData, "system">;
|
|
12
|
+
|
|
13
|
+
const statusStyles: Record<string, { border: string; bg: string; glow: string; dot: string }> = {
|
|
14
|
+
operational: {
|
|
15
|
+
border: "border-emerald-500/40",
|
|
16
|
+
bg: "bg-emerald-500/5",
|
|
17
|
+
glow: "shadow-emerald-500/10",
|
|
18
|
+
dot: "bg-emerald-500",
|
|
19
|
+
},
|
|
20
|
+
degraded: {
|
|
21
|
+
border: "border-amber-500/40",
|
|
22
|
+
bg: "bg-amber-500/5",
|
|
23
|
+
glow: "shadow-amber-500/10",
|
|
24
|
+
dot: "bg-amber-500",
|
|
25
|
+
},
|
|
26
|
+
down: {
|
|
27
|
+
border: "border-red-500/40",
|
|
28
|
+
bg: "bg-red-500/5",
|
|
29
|
+
glow: "shadow-red-500/10",
|
|
30
|
+
dot: "bg-red-500",
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
function combineStatus(
|
|
35
|
+
status?: string,
|
|
36
|
+
derivedState?: string,
|
|
37
|
+
): "operational" | "degraded" | "down" {
|
|
38
|
+
const order = { operational: 0, info: 0, degraded: 1, down: 2 };
|
|
39
|
+
const ownLevel = order[(status ?? "operational") as keyof typeof order] ?? 0;
|
|
40
|
+
const derivedLevel =
|
|
41
|
+
order[(derivedState ?? "operational") as keyof typeof order] ?? 0;
|
|
42
|
+
const level = Math.max(ownLevel, derivedLevel);
|
|
43
|
+
if (level >= 2) return "down";
|
|
44
|
+
if (level >= 1) return "degraded";
|
|
45
|
+
return "operational";
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Custom React Flow node representing a system in the dependency graph.
|
|
50
|
+
* Color-coded by worst of own status and derived warning state.
|
|
51
|
+
*/
|
|
52
|
+
export const SystemNodeComponent = memo(function SystemNodeComponent({
|
|
53
|
+
data,
|
|
54
|
+
selected,
|
|
55
|
+
}: NodeProps<SystemNode>) {
|
|
56
|
+
const effectiveStatus = combineStatus(data.status, data.derivedState);
|
|
57
|
+
const styles = statusStyles[effectiveStatus];
|
|
58
|
+
|
|
59
|
+
return (
|
|
60
|
+
<>
|
|
61
|
+
{/* Target handle (left) — "I am depended upon" */}
|
|
62
|
+
<Handle
|
|
63
|
+
type="target"
|
|
64
|
+
position={Position.Left}
|
|
65
|
+
className="!w-2.5 !h-2.5 !bg-muted-foreground/40 !border-2 !border-background"
|
|
66
|
+
/>
|
|
67
|
+
|
|
68
|
+
<div
|
|
69
|
+
className={`
|
|
70
|
+
px-4 py-3 rounded-xl border-2 shadow-lg backdrop-blur-sm
|
|
71
|
+
transition-all duration-200
|
|
72
|
+
${styles.border} ${styles.bg} ${styles.glow}
|
|
73
|
+
${selected ? "ring-2 ring-primary ring-offset-2 ring-offset-background" : ""}
|
|
74
|
+
hover:scale-[1.02] cursor-grab active:cursor-grabbing
|
|
75
|
+
min-w-[140px] max-w-[200px]
|
|
76
|
+
`}
|
|
77
|
+
>
|
|
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
|
+
<div
|
|
86
|
+
className={`absolute inset-0 w-2.5 h-2.5 rounded-full ${styles.dot} animate-ping opacity-75`}
|
|
87
|
+
/>
|
|
88
|
+
)}
|
|
89
|
+
</div>
|
|
90
|
+
|
|
91
|
+
{/* System name */}
|
|
92
|
+
<span className="text-sm font-medium text-foreground truncate">
|
|
93
|
+
{data.label}
|
|
94
|
+
</span>
|
|
95
|
+
</div>
|
|
96
|
+
|
|
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 && (
|
|
115
|
+
<span
|
|
116
|
+
className={`text-[10px] uppercase tracking-wider font-semibold ${
|
|
117
|
+
data.derivedState === "info"
|
|
118
|
+
? "text-blue-400/70"
|
|
119
|
+
: data.derivedState === "degraded"
|
|
120
|
+
? "text-amber-500/70"
|
|
121
|
+
: "text-red-500/70"
|
|
122
|
+
}`}
|
|
123
|
+
>
|
|
124
|
+
{data.derivedState === "info"
|
|
125
|
+
? "↑ Upstream issue"
|
|
126
|
+
: data.derivedState === "degraded"
|
|
127
|
+
? "↑ Upstream degraded"
|
|
128
|
+
: "↑ Upstream down"}
|
|
129
|
+
</span>
|
|
130
|
+
)}
|
|
131
|
+
</div>
|
|
132
|
+
</div>
|
|
133
|
+
|
|
134
|
+
{/* Source handle (right) — "I depend on..." */}
|
|
135
|
+
<Handle
|
|
136
|
+
type="source"
|
|
137
|
+
position={Position.Right}
|
|
138
|
+
className="!w-2.5 !h-2.5 !bg-muted-foreground/40 !border-2 !border-background"
|
|
139
|
+
/>
|
|
140
|
+
</>
|
|
141
|
+
);
|
|
142
|
+
});
|
package/src/index.tsx
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createFrontendPlugin,
|
|
3
|
+
createSlotExtension,
|
|
4
|
+
UserMenuItemsSlot,
|
|
5
|
+
} from "@checkstack/frontend-api";
|
|
6
|
+
import {
|
|
7
|
+
pluginMetadata,
|
|
8
|
+
dependencyRoutes,
|
|
9
|
+
dependencyAccess,
|
|
10
|
+
} from "@checkstack/dependency-common";
|
|
11
|
+
import {
|
|
12
|
+
SystemDetailsTopSlot,
|
|
13
|
+
SystemStateBadgesSlot,
|
|
14
|
+
SystemEditorSlot,
|
|
15
|
+
} from "@checkstack/catalog-common";
|
|
16
|
+
import { DependencyBadge } from "./components/DependencyBadge";
|
|
17
|
+
import { DependencyAlert } from "./components/DependencyAlert";
|
|
18
|
+
import { DependencyEditor } from "./components/DependencyEditor";
|
|
19
|
+
import { DependencyMapPage } from "./components/DependencyMapPage";
|
|
20
|
+
import { DependencyMenuItems } from "./components/DependencyMenuItems";
|
|
21
|
+
|
|
22
|
+
export default createFrontendPlugin({
|
|
23
|
+
metadata: pluginMetadata,
|
|
24
|
+
routes: [
|
|
25
|
+
{
|
|
26
|
+
route: dependencyRoutes.routes.map,
|
|
27
|
+
element: <DependencyMapPage />,
|
|
28
|
+
title: "Dependency Map",
|
|
29
|
+
accessRule: dependencyAccess.dependency.read,
|
|
30
|
+
},
|
|
31
|
+
],
|
|
32
|
+
apis: [],
|
|
33
|
+
extensions: [
|
|
34
|
+
createSlotExtension(SystemStateBadgesSlot, {
|
|
35
|
+
id: "dependency.system-state-badge",
|
|
36
|
+
component: DependencyBadge,
|
|
37
|
+
}),
|
|
38
|
+
createSlotExtension(SystemDetailsTopSlot, {
|
|
39
|
+
id: "dependency.system-details-top.alert",
|
|
40
|
+
component: DependencyAlert,
|
|
41
|
+
}),
|
|
42
|
+
createSlotExtension(SystemEditorSlot, {
|
|
43
|
+
id: "dependency.system-editor",
|
|
44
|
+
component: DependencyEditor,
|
|
45
|
+
}),
|
|
46
|
+
createSlotExtension(UserMenuItemsSlot, {
|
|
47
|
+
id: "dependency.user-menu.map",
|
|
48
|
+
component: DependencyMenuItems,
|
|
49
|
+
}),
|
|
50
|
+
],
|
|
51
|
+
});
|