@skyhook-io/radar-app 1.13.1 → 1.13.2
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/package.json +2 -2
- package/src/App.tsx +19 -1
- package/src/RadarApp.tsx +2 -2
- package/src/api/diagnose.test.ts +268 -0
- package/src/api/diagnose.ts +72 -9
- package/src/components/diagnose/AISettings.tsx +1 -1
- package/src/components/diagnose/AgentSetupNotice.tsx +5 -5
- package/src/components/diagnose/ApplyDialog.test.tsx +72 -0
- package/src/components/diagnose/DiagnoseContext.tsx +12 -17
- package/src/components/diagnose/DiagnoseSurface.test.tsx +211 -16
- package/src/components/diagnose/DiagnoseSurface.tsx +464 -133
- package/src/components/diagnose/Home.test.tsx +293 -0
- package/src/components/diagnose/Home.tsx +289 -119
- package/src/components/diagnose/InvestigationEvidencePane.test.tsx +2170 -0
- package/src/components/diagnose/InvestigationEvidencePane.tsx +2253 -0
- package/src/components/diagnose/InvestigationResourceEvidence.test.tsx +257 -0
- package/src/components/diagnose/InvestigationResourceEvidence.tsx +214 -0
- package/src/components/diagnose/InvestigationView.test.ts +17 -0
- package/src/components/diagnose/InvestigationView.tsx +1900 -393
- package/src/components/diagnose/LocalDiagnoseAction.tsx +42 -25
- package/src/components/diagnose/agentCatalog.ts +1 -1
- package/src/components/diagnose/diagnoseEvidenceTypes.ts +151 -0
- package/src/components/diagnose/investigationEvidence.test.ts +3109 -0
- package/src/components/diagnose/investigationEvidence.ts +3492 -0
- package/src/components/diagnose/investigationEvidencePresentation.test.ts +447 -0
- package/src/components/diagnose/investigationEvidencePresentation.ts +167 -0
- package/src/components/diagnose/investigationExplanation.test.ts +63 -0
- package/src/components/diagnose/investigationExplanation.ts +22 -0
- package/src/components/diagnose/investigationResourceEvidenceModel.ts +322 -0
- package/src/components/diagnose/investigationSourceFocus.test.ts +143 -0
- package/src/components/diagnose/investigationSourceFocus.ts +98 -0
- package/src/components/diagnose/investigationState.test.ts +695 -0
- package/src/components/diagnose/investigationState.ts +451 -0
- package/src/components/diagnose/parts.test.tsx +864 -3
- package/src/components/diagnose/parts.tsx +1337 -541
- package/src/components/diagnose/target.test.ts +39 -0
- package/src/components/diagnose/target.ts +36 -0
- package/src/components/diagnose/useDisclosureReveal.ts +117 -0
- package/src/components/home/MCPSetupDialog.tsx +2 -2
- package/src/components/home/mcpToolCatalog.test.ts +22 -0
- package/src/components/home/mcpToolCatalog.ts +3 -2
- package/src/components/issues/IssuesPane.tsx +5 -1
- package/src/components/settings/SettingsDialog.tsx +11 -13
- package/src/components/workload/WorkloadView.tsx +1 -1
- package/src/context/DiagnoseCustomization.tsx +11 -8
- package/src/index.css +63 -79
- package/src/index.ts +1 -1
|
@@ -1,49 +1,103 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
|
|
4
|
-
import { Loader2, Sparkles } from "lucide-react";
|
|
5
|
-
import { StatusDot, type StatusTone } from "@skyhook-io/k8s-ui";
|
|
6
|
-
import { Badge } from "@skyhook-io/k8s-ui/components/ui/Badge";
|
|
1
|
+
// Server-side runs keep background and running investigations visible in both
|
|
2
|
+
// the docked Home view and the maximized workspace's master pane.
|
|
3
|
+
import { CircleAlert, Loader2, Server, Sparkles, Square } from "lucide-react";
|
|
7
4
|
import { type RunSummary } from "../../api/diagnose";
|
|
5
|
+
import {
|
|
6
|
+
groupQualifiesLaneId,
|
|
7
|
+
pluralToKind,
|
|
8
|
+
} from "@skyhook-io/k8s-ui/utils/navigation";
|
|
9
|
+
import { parseContextName } from "../../utils/context-name";
|
|
10
|
+
import { formatInvestigationTarget } from "./target";
|
|
11
|
+
import { Tooltip } from "../ui/Tooltip";
|
|
8
12
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
-
if (
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
13
|
+
function historyDay(date: Date, now: Date): string {
|
|
14
|
+
if (date.toDateString() === now.toDateString()) return "Today";
|
|
15
|
+
const yesterday = new Date(now);
|
|
16
|
+
yesterday.setDate(yesterday.getDate() - 1);
|
|
17
|
+
if (date.toDateString() === yesterday.toDateString()) return "Yesterday";
|
|
18
|
+
return date.toLocaleDateString(undefined, {
|
|
19
|
+
month: "short",
|
|
20
|
+
day: "numeric",
|
|
21
|
+
...(date.getFullYear() === now.getFullYear() ? {} : { year: "numeric" }),
|
|
22
|
+
});
|
|
17
23
|
}
|
|
18
24
|
|
|
19
|
-
//
|
|
20
|
-
//
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
default: // stopped
|
|
30
|
-
return "unknown";
|
|
31
|
-
}
|
|
32
|
-
}
|
|
25
|
+
// Relative age makes the list easy to scan; a stable local timestamp makes two
|
|
26
|
+
// investigations of the same target distinguishable when they ran close
|
|
27
|
+
// together. Keep today's label compact because the date is redundant there.
|
|
28
|
+
export function absoluteTime(ts: number, now: number): string {
|
|
29
|
+
const date = new Date(ts);
|
|
30
|
+
const current = new Date(now);
|
|
31
|
+
const today =
|
|
32
|
+
date.getFullYear() === current.getFullYear() &&
|
|
33
|
+
date.getMonth() === current.getMonth() &&
|
|
34
|
+
date.getDate() === current.getDate();
|
|
33
35
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
36
|
+
return date.toLocaleString(
|
|
37
|
+
undefined,
|
|
38
|
+
today
|
|
39
|
+
? { hour: "numeric", minute: "2-digit" }
|
|
40
|
+
: {
|
|
41
|
+
month: "short",
|
|
42
|
+
day: "numeric",
|
|
43
|
+
...(date.getFullYear() === current.getFullYear()
|
|
44
|
+
? {}
|
|
45
|
+
: { year: "numeric" as const }),
|
|
46
|
+
hour: "numeric",
|
|
47
|
+
minute: "2-digit",
|
|
48
|
+
},
|
|
49
|
+
);
|
|
38
50
|
}
|
|
39
51
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
52
|
+
const historyStatuses = {
|
|
53
|
+
running: {
|
|
54
|
+
label: "Running",
|
|
55
|
+
short: "Running",
|
|
56
|
+
Icon: Loader2,
|
|
57
|
+
className: "text-accent-text",
|
|
58
|
+
},
|
|
59
|
+
stopping: {
|
|
60
|
+
label: "Stopping",
|
|
61
|
+
short: "Stopping",
|
|
62
|
+
Icon: Loader2,
|
|
63
|
+
className: "text-theme-text-tertiary",
|
|
64
|
+
},
|
|
65
|
+
done: {
|
|
66
|
+
label: "Completed",
|
|
67
|
+
short: "",
|
|
68
|
+
Icon: undefined,
|
|
69
|
+
className: "text-theme-text-tertiary",
|
|
70
|
+
},
|
|
71
|
+
error: {
|
|
72
|
+
label: "Investigation failed",
|
|
73
|
+
short: "Failed",
|
|
74
|
+
Icon: CircleAlert,
|
|
75
|
+
className: "text-theme-text-secondary",
|
|
76
|
+
},
|
|
77
|
+
stopped: {
|
|
78
|
+
label: "Stopped",
|
|
79
|
+
short: "Stopped",
|
|
80
|
+
Icon: Square,
|
|
81
|
+
className: "text-theme-text-secondary",
|
|
82
|
+
},
|
|
83
|
+
stale: {
|
|
84
|
+
label: "Read-only investigation",
|
|
85
|
+
short: "",
|
|
86
|
+
Icon: undefined,
|
|
87
|
+
className: "text-theme-text-tertiary",
|
|
88
|
+
},
|
|
89
|
+
} as const;
|
|
90
|
+
|
|
91
|
+
// A short text status means no run outcome relies on decoding a 6px colored dot.
|
|
92
|
+
export function statusWord(status: RunSummary["status"]): {
|
|
93
|
+
text: string;
|
|
94
|
+
cls: string;
|
|
95
|
+
} {
|
|
46
96
|
switch (status) {
|
|
97
|
+
case "running":
|
|
98
|
+
return { text: "Running", cls: "text-accent" };
|
|
99
|
+
case "done":
|
|
100
|
+
return { text: "Completed", cls: "text-theme-text-secondary" };
|
|
47
101
|
case "error":
|
|
48
102
|
return { text: "Failed", cls: "text-red-400" };
|
|
49
103
|
case "stopped":
|
|
@@ -51,11 +105,7 @@ function statusWord(
|
|
|
51
105
|
case "stopping":
|
|
52
106
|
return { text: "Stopping", cls: "text-theme-text-tertiary" };
|
|
53
107
|
case "stale":
|
|
54
|
-
|
|
55
|
-
// about a cluster that's no longer connected.
|
|
56
|
-
return { text: "Different cluster", cls: "text-amber-500" };
|
|
57
|
-
default:
|
|
58
|
-
return null;
|
|
108
|
+
return { text: "Read-only", cls: "text-theme-text-tertiary" };
|
|
59
109
|
}
|
|
60
110
|
}
|
|
61
111
|
|
|
@@ -65,14 +115,58 @@ export function RecentList({
|
|
|
65
115
|
onSelect,
|
|
66
116
|
selectedId,
|
|
67
117
|
historyDegraded = false,
|
|
118
|
+
currentContext,
|
|
68
119
|
}: {
|
|
69
120
|
agentLabel: string;
|
|
70
121
|
runs: RunSummary[];
|
|
71
122
|
onSelect: (id: string) => void;
|
|
72
123
|
selectedId?: string | null;
|
|
73
124
|
historyDegraded?: boolean;
|
|
125
|
+
currentContext?: string;
|
|
74
126
|
}) {
|
|
75
|
-
const now = Date
|
|
127
|
+
const now = new Date();
|
|
128
|
+
const contexts = new Map(
|
|
129
|
+
runs.map((r) => [r.context, parseContextName(r.context)]),
|
|
130
|
+
);
|
|
131
|
+
const contextsByName = new Map<string, Set<string>>();
|
|
132
|
+
const groupsByKind = new Map<string, Set<string>>();
|
|
133
|
+
for (const [raw, parsed] of contexts) {
|
|
134
|
+
const names = contextsByName.get(parsed.clusterName) ?? new Set<string>();
|
|
135
|
+
names.add(raw);
|
|
136
|
+
contextsByName.set(parsed.clusterName, names);
|
|
137
|
+
}
|
|
138
|
+
for (const r of runs) {
|
|
139
|
+
const kind = pluralToKind(r.kind);
|
|
140
|
+
const groups = groupsByKind.get(kind) ?? new Set<string>();
|
|
141
|
+
// Match Radar's resource-lane display convention: built-in API groups
|
|
142
|
+
// share a readable kind label; custom groups may need disambiguation.
|
|
143
|
+
groups.add(groupQualifiesLaneId(r.group) ? r.group : "");
|
|
144
|
+
groupsByKind.set(kind, groups);
|
|
145
|
+
}
|
|
146
|
+
const organizationRuns = runs.filter(
|
|
147
|
+
(r) => r.trigger === "background" || r.ownedByMe === false,
|
|
148
|
+
);
|
|
149
|
+
const yourRuns = runs.filter((r) => !organizationRuns.includes(r));
|
|
150
|
+
const collections = organizationRuns.length
|
|
151
|
+
? [
|
|
152
|
+
{ label: "Your investigations", runs: yourRuns },
|
|
153
|
+
{ label: "Organization", runs: organizationRuns },
|
|
154
|
+
].filter((collection) => collection.runs.length > 0)
|
|
155
|
+
: [{ label: "", runs }];
|
|
156
|
+
// Status bookkeeping (including cluster switches) updates updatedAt. It must
|
|
157
|
+
// not change the apparent start time or reshuffle the navigation list.
|
|
158
|
+
const groupedCollections = collections.map((collection) => {
|
|
159
|
+
const days = new Map<string, RunSummary[]>();
|
|
160
|
+
for (const r of [...collection.runs].sort(
|
|
161
|
+
(a, b) => Date.parse(b.createdAt) - Date.parse(a.createdAt),
|
|
162
|
+
)) {
|
|
163
|
+
const day = historyDay(new Date(r.createdAt), now);
|
|
164
|
+
const entries = days.get(day) ?? [];
|
|
165
|
+
entries.push(r);
|
|
166
|
+
days.set(day, entries);
|
|
167
|
+
}
|
|
168
|
+
return { label: collection.label, days };
|
|
169
|
+
});
|
|
76
170
|
|
|
77
171
|
// Persistence broke (disk error) — without this the user reasonably assumes
|
|
78
172
|
// their history survives a restart, and it won't.
|
|
@@ -97,7 +191,7 @@ export function RecentList({
|
|
|
97
191
|
<Sparkles className="inline h-3.5 w-3.5 align-text-bottom text-accent" />{" "}
|
|
98
192
|
action to investigate it with {agentLabel} —{" "}
|
|
99
193
|
<span className="font-medium text-theme-text-secondary">
|
|
100
|
-
|
|
194
|
+
Investigate
|
|
101
195
|
</span>{" "}
|
|
102
196
|
a problem, or just ask about it. Investigations run in the
|
|
103
197
|
background and are kept in your history here.
|
|
@@ -107,83 +201,159 @@ export function RecentList({
|
|
|
107
201
|
);
|
|
108
202
|
}
|
|
109
203
|
|
|
110
|
-
const organizationRuns = runs.filter(
|
|
111
|
-
(r) => r.trigger === "background" || r.ownedByMe === false,
|
|
112
|
-
);
|
|
113
|
-
const yourRuns = runs.filter((r) => !organizationRuns.includes(r));
|
|
114
|
-
const groups = organizationRuns.length
|
|
115
|
-
? [
|
|
116
|
-
{ label: "Your investigations", runs: yourRuns },
|
|
117
|
-
{ label: "Organization", runs: organizationRuns },
|
|
118
|
-
].filter((group) => group.runs.length > 0)
|
|
119
|
-
: [{ label: "Investigations", runs }];
|
|
120
|
-
|
|
121
204
|
return (
|
|
122
|
-
<div className="space-y-
|
|
205
|
+
<div className="space-y-4">
|
|
123
206
|
{degradedNote}
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
{
|
|
130
|
-
<
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
{
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
207
|
+
<h2 className="px-2 text-sm font-medium text-theme-text-secondary">
|
|
208
|
+
Investigations
|
|
209
|
+
</h2>
|
|
210
|
+
{groupedCollections.map((collection) => (
|
|
211
|
+
<div key={collection.label} className="space-y-4">
|
|
212
|
+
{collection.label && (
|
|
213
|
+
<h3 className="px-2 text-xs font-semibold text-theme-text-secondary">
|
|
214
|
+
{collection.label}
|
|
215
|
+
</h3>
|
|
216
|
+
)}
|
|
217
|
+
{[...collection.days].map(([day, entries]) => (
|
|
218
|
+
<section key={day} aria-label={day} className="space-y-1">
|
|
219
|
+
<h3 className="px-2 pb-1 text-xs font-medium text-theme-text-tertiary">
|
|
220
|
+
{day}
|
|
221
|
+
</h3>
|
|
222
|
+
{entries.map((r) => {
|
|
223
|
+
const { label, short, Icon, className } =
|
|
224
|
+
historyStatuses[r.status];
|
|
225
|
+
const parsed = contexts.get(r.context)!;
|
|
226
|
+
const collision =
|
|
227
|
+
contextsByName.get(parsed.clusterName)!.size > 1;
|
|
228
|
+
const peers = [
|
|
229
|
+
...contextsByName.get(parsed.clusterName)!,
|
|
230
|
+
].filter((raw) => raw !== r.context);
|
|
231
|
+
const qualifier =
|
|
232
|
+
parsed.account &&
|
|
233
|
+
peers.every(
|
|
234
|
+
(raw) => contexts.get(raw)!.account !== parsed.account,
|
|
235
|
+
)
|
|
236
|
+
? parsed.account
|
|
237
|
+
: parsed.account &&
|
|
238
|
+
parsed.region &&
|
|
239
|
+
peers.every((raw) => {
|
|
240
|
+
const peer = contexts.get(raw)!;
|
|
241
|
+
return (
|
|
242
|
+
peer.account !== parsed.account ||
|
|
243
|
+
peer.region !== parsed.region
|
|
244
|
+
);
|
|
245
|
+
})
|
|
246
|
+
? `${parsed.account} · ${parsed.region}`
|
|
247
|
+
: r.context;
|
|
248
|
+
const readableKind = pluralToKind(r.kind);
|
|
249
|
+
const kind =
|
|
250
|
+
groupsByKind.get(readableKind)!.size > 1
|
|
251
|
+
? `${readableKind} · ${r.group || "core"}`
|
|
252
|
+
: readableKind;
|
|
253
|
+
const initialIssue = r.health?.topReason?.trim();
|
|
254
|
+
const isCurrentCluster = currentContext === r.context;
|
|
255
|
+
const visibility =
|
|
256
|
+
r.trigger === "background"
|
|
257
|
+
? "Automatic"
|
|
258
|
+
: r.visibility === "organization"
|
|
259
|
+
? "Shared"
|
|
260
|
+
: r.visibility === "private"
|
|
261
|
+
? "Private"
|
|
262
|
+
: "";
|
|
263
|
+
const identity = `${formatInvestigationTarget(r)} · ${r.context}${isCurrentCluster ? " · Current cluster" : ""} · ${label}${visibility ? ` · ${visibility}` : ""} · Started ${new Date(r.createdAt).toLocaleString()}${initialIssue ? ` · Started with ${initialIssue}` : ""}`;
|
|
264
|
+
return (
|
|
265
|
+
<button
|
|
266
|
+
key={r.id}
|
|
267
|
+
onClick={() => onSelect(r.id)}
|
|
268
|
+
aria-label={identity}
|
|
269
|
+
aria-current={r.id === selectedId ? "true" : undefined}
|
|
270
|
+
className={`flex w-full min-w-0 flex-col gap-0.5 rounded-md border-l-2 px-2 py-2 text-left focus-visible:outline-2 focus-visible:outline-accent ${
|
|
271
|
+
r.id === selectedId
|
|
272
|
+
? "border-accent bg-accent-muted"
|
|
273
|
+
: "border-transparent hover:bg-theme-hover"
|
|
274
|
+
}`}
|
|
275
|
+
>
|
|
276
|
+
<span className="flex w-full items-start gap-2">
|
|
277
|
+
<Tooltip
|
|
278
|
+
content={r.name}
|
|
279
|
+
position="right"
|
|
280
|
+
delay={600}
|
|
281
|
+
className="pointer-events-none"
|
|
282
|
+
wrapperClassName="min-w-0 flex-1"
|
|
283
|
+
>
|
|
284
|
+
<span className="min-w-0 flex-1 line-clamp-2 break-words text-sm font-medium leading-5 text-theme-text-primary">
|
|
285
|
+
{r.name}
|
|
286
|
+
</span>
|
|
287
|
+
</Tooltip>
|
|
288
|
+
{(Icon || short) && (
|
|
289
|
+
<span
|
|
290
|
+
aria-hidden="true"
|
|
291
|
+
className={`flex shrink-0 items-center gap-1 text-xs leading-5 ${className}`}
|
|
292
|
+
>
|
|
293
|
+
{Icon && (
|
|
294
|
+
<Icon
|
|
295
|
+
className={`mt-0.5 h-3.5 w-3.5 ${r.status === "running" || r.status === "stopping" ? "animate-spin motion-reduce:animate-none" : r.status === "error" ? "text-semantic-error" : ""}`}
|
|
296
|
+
/>
|
|
297
|
+
)}
|
|
298
|
+
{short}
|
|
299
|
+
</span>
|
|
300
|
+
)}
|
|
301
|
+
</span>
|
|
302
|
+
<span className="flex w-full items-baseline gap-2 text-xs leading-4 text-theme-text-secondary">
|
|
303
|
+
<span className="min-w-0 flex-1 truncate">
|
|
304
|
+
{r.namespace ? `${r.namespace} · ` : ""}
|
|
305
|
+
{kind}
|
|
306
|
+
</span>
|
|
307
|
+
<time
|
|
308
|
+
dateTime={r.createdAt}
|
|
309
|
+
className="shrink-0 tabular-nums text-theme-text-tertiary"
|
|
310
|
+
>
|
|
311
|
+
{new Date(r.createdAt).toLocaleTimeString(undefined, {
|
|
312
|
+
hour: "numeric",
|
|
313
|
+
minute: "2-digit",
|
|
314
|
+
})}
|
|
315
|
+
</time>
|
|
180
316
|
</span>
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
317
|
+
<Tooltip
|
|
318
|
+
content={`${r.context}${isCurrentCluster ? " · Current cluster" : ""}`}
|
|
319
|
+
position="right"
|
|
320
|
+
delay={600}
|
|
321
|
+
className="pointer-events-none"
|
|
322
|
+
wrapperClassName="w-full min-w-0"
|
|
323
|
+
>
|
|
324
|
+
<span
|
|
325
|
+
aria-label={
|
|
326
|
+
isCurrentCluster
|
|
327
|
+
? `Current cluster: ${parsed.clusterName}`
|
|
328
|
+
: `Cluster: ${parsed.clusterName}`
|
|
329
|
+
}
|
|
330
|
+
className={`flex w-full items-center gap-1 text-xs leading-4 ${isCurrentCluster ? "text-accent-text" : "text-theme-text-tertiary"}`}
|
|
331
|
+
>
|
|
332
|
+
<Server className="h-3 w-3 shrink-0" aria-hidden />
|
|
333
|
+
<span className="min-w-0 flex-1 truncate">
|
|
334
|
+
{parsed.clusterName}
|
|
335
|
+
</span>
|
|
336
|
+
{visibility && (
|
|
337
|
+
<span className="shrink-0 text-theme-text-tertiary">
|
|
338
|
+
{visibility}
|
|
339
|
+
</span>
|
|
340
|
+
)}
|
|
341
|
+
</span>
|
|
342
|
+
</Tooltip>
|
|
343
|
+
{collision && qualifier !== parsed.clusterName && (
|
|
344
|
+
<span className="w-full break-words text-xs leading-4 text-theme-text-secondary">
|
|
345
|
+
{qualifier}
|
|
346
|
+
</span>
|
|
347
|
+
)}
|
|
348
|
+
{initialIssue && (
|
|
349
|
+
<span className="line-clamp-1 w-full text-xs leading-4 text-theme-text-secondary">
|
|
350
|
+
Started with {initialIssue}
|
|
351
|
+
</span>
|
|
352
|
+
)}
|
|
353
|
+
</button>
|
|
354
|
+
);
|
|
355
|
+
})}
|
|
356
|
+
</section>
|
|
187
357
|
))}
|
|
188
358
|
</div>
|
|
189
359
|
))}
|