@skyhook-io/radar-app 1.14.2 → 1.14.3
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/api/client.ts +30 -0
- package/src/components/CloudConnectFlow.tsx +21 -7
- package/src/components/CloudFunnelButton.tsx +85 -8
- package/src/components/cloudConnectHandoff.test.ts +10 -1
- package/src/components/cloudConnectHandoff.ts +15 -1
- package/src/components/diagnose/AIMarkdown.tsx +87 -0
- package/src/components/diagnose/ActivityTurn.tsx +1244 -0
- package/src/components/diagnose/AgentControls.tsx +603 -0
- package/src/components/diagnose/AnalysisStory.test.tsx +31 -25
- package/src/components/diagnose/AnalysisStory.tsx +13 -18
- package/src/components/diagnose/ApplyDialog.tsx +289 -0
- package/src/components/diagnose/AssessmentCard.tsx +1560 -0
- package/src/components/diagnose/DiagnoseSurface.test.tsx +9 -9
- package/src/components/diagnose/DiagnoseSurface.tsx +5 -1
- package/src/components/diagnose/EvidenceCard.tsx +611 -0
- package/src/components/diagnose/Home.test.tsx +1 -3
- package/src/components/diagnose/Home.tsx +2 -1
- package/src/components/diagnose/InvestigationEvidencePane.story.test.tsx +6 -8
- package/src/components/diagnose/InvestigationEvidencePane.test.tsx +5 -3
- package/src/components/diagnose/InvestigationEvidencePane.tsx +31 -3445
- package/src/components/diagnose/InvestigationEvidenceSections.tsx +523 -0
- package/src/components/diagnose/InvestigationView.tsx +11 -28
- package/src/components/diagnose/StoryExcerpt.tsx +118 -0
- package/src/components/diagnose/assessmentCopy.ts +169 -0
- package/src/components/diagnose/investigationCase.test.tsx +631 -2
- package/src/components/diagnose/investigationCase.ts +132 -24
- package/src/components/diagnose/investigationEvidence/adapters/coverage.test.tsx +660 -0
- package/src/components/diagnose/investigationEvidence/adapters/{helm.test.ts → helm.test.tsx} +63 -0
- package/src/components/diagnose/investigationEvidence/adapters/helm.ts +70 -1
- package/src/components/diagnose/investigationEvidence/adapters/listings.ts +134 -0
- package/src/components/diagnose/investigationEvidence/adapters/{permissions.test.ts → permissions.test.tsx} +50 -0
- package/src/components/diagnose/investigationEvidence/adapters/permissions.ts +18 -0
- package/src/components/diagnose/investigationEvidence/adapters/posture.ts +272 -0
- package/src/components/diagnose/investigationEvidence/adapters/prometheus.ts +49 -0
- package/src/components/diagnose/investigationEvidence/adapters/rankings.ts +147 -0
- package/src/components/diagnose/investigationEvidence/adapters/resource.ts +20 -10
- package/src/components/diagnose/investigationEvidence/bodies/index.tsx +127 -0
- package/src/components/diagnose/investigationEvidence/bodies/inventory.tsx +158 -0
- package/src/components/diagnose/investigationEvidence/bodies/metrics.tsx +373 -0
- package/src/components/diagnose/investigationEvidence/bodies/network.tsx +274 -0
- package/src/components/diagnose/investigationEvidence/bodies/platform.tsx +475 -0
- package/src/components/diagnose/investigationEvidence/bodies/posture.tsx +60 -0
- package/src/components/diagnose/investigationEvidence/bodies/ranking.tsx +76 -0
- package/src/components/diagnose/investigationEvidence/bodies/streams.tsx +212 -0
- package/src/components/diagnose/investigationEvidence/bodies/workload.tsx +335 -0
- package/src/components/diagnose/investigationEvidence/builder.ts +15 -0
- package/src/components/diagnose/investigationEvidence/cardParts.tsx +312 -0
- package/src/components/diagnose/investigationEvidence/excerpt.test.ts +105 -0
- package/src/components/diagnose/investigationEvidence/excerpt.ts +92 -0
- package/src/components/diagnose/investigationEvidence/identity.test.ts +5 -7
- package/src/components/diagnose/investigationEvidence/index.ts +1 -1
- package/src/components/diagnose/investigationEvidence/navigation.tsx +26 -0
- package/src/components/diagnose/investigationEvidence/observations.ts +11 -0
- package/src/components/diagnose/investigationEvidence/projection.test.ts +1 -1
- package/src/components/diagnose/investigationEvidence/types.ts +97 -2
- package/src/components/diagnose/investigationEvidenceKinds.ts +22 -0
- package/src/components/diagnose/investigationEvidencePartition.ts +296 -0
- package/src/components/diagnose/parts.test.tsx +3 -1
- package/src/components/diagnose/parts.tsx +26 -3904
- package/src/components/diagnose/target.ts +9 -3
- package/src/components/diagnose/toolCallLabel.test.ts +47 -1
- package/src/components/diagnose/toolCallLabel.ts +41 -2
- package/src/components/helm/HelmReleaseDrawer.tsx +1 -1
- package/src/components/home/MCPSetupDialog.tsx +1 -1
- package/src/components/resource/PrometheusChartsGrid.tsx +1 -1
- package/src/components/resource/WorkloadMetricsHelpDialog.tsx +1 -1
- package/src/components/resource/WorkloadMetricsSection.tsx +1 -1
- package/src/components/resources/PodFilePreview.tsx +1 -1
- package/src/components/ui/CommandPalette.tsx +12 -8
- package/src/components/ui/DiagnosticsOverlay.tsx +7 -5
- package/src/components/ui/Omnibar.tsx +33 -10
- package/src/components/ui/ShortcutHelpOverlay.tsx +8 -5
- package/src/components/ui/Disclosure.tsx +0 -47
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import { VISIBLE_LOG_EVIDENCE_LINES } from "../excerpt";
|
|
2
|
+
import { useContext } from "react";
|
|
3
|
+
import {
|
|
4
|
+
Badge,
|
|
5
|
+
DiffViewer,
|
|
6
|
+
TerminalBlock,
|
|
7
|
+
formatRelativeAgeTime,
|
|
8
|
+
ResourceLink,
|
|
9
|
+
stripAnsi,
|
|
10
|
+
} from "@skyhook-io/k8s-ui";
|
|
11
|
+
import { apiVersionToGroup } from "../../../../utils/navigation";
|
|
12
|
+
import { Tooltip } from "../../../ui/Tooltip";
|
|
13
|
+
import { EvidenceNavigationContext } from "../navigation";
|
|
14
|
+
import type { EvidenceDataOf } from "../cardParts";
|
|
15
|
+
|
|
16
|
+
export { VISIBLE_LOG_EVIDENCE_LINES } from "../excerpt";
|
|
17
|
+
|
|
18
|
+
export function LogsBody({
|
|
19
|
+
data,
|
|
20
|
+
condensed = false,
|
|
21
|
+
}: {
|
|
22
|
+
data: EvidenceDataOf<"logs">;
|
|
23
|
+
condensed?: boolean;
|
|
24
|
+
}) {
|
|
25
|
+
const lines = data.logs?.lines ?? [];
|
|
26
|
+
const visibleLines = lines
|
|
27
|
+
.slice(-VISIBLE_LOG_EVIDENCE_LINES)
|
|
28
|
+
.map((line) => stripAnsi(line));
|
|
29
|
+
const omittedLines = lines.length - visibleLines.length;
|
|
30
|
+
return (
|
|
31
|
+
<div className="space-y-2">
|
|
32
|
+
<div className="flex flex-wrap items-center gap-1.5">
|
|
33
|
+
{condensed ? null : (
|
|
34
|
+
<>
|
|
35
|
+
<Badge tone="structural" size="sm">
|
|
36
|
+
{data.pod} / {data.container}
|
|
37
|
+
</Badge>
|
|
38
|
+
<Badge severity="neutral" size="sm">
|
|
39
|
+
{data.previous ? "previous instance" : "current instance"}
|
|
40
|
+
</Badge>
|
|
41
|
+
</>
|
|
42
|
+
)}
|
|
43
|
+
{data.logs?.fallback ? (
|
|
44
|
+
<Badge severity="neutral" size="sm">
|
|
45
|
+
Log tail · filter matched nothing
|
|
46
|
+
</Badge>
|
|
47
|
+
) : null}
|
|
48
|
+
{data.logs ? (
|
|
49
|
+
<span className="text-xs text-theme-text-tertiary">
|
|
50
|
+
{data.logs.matchedLines} of {data.logs.totalLines} read lines
|
|
51
|
+
matched the filter
|
|
52
|
+
</span>
|
|
53
|
+
) : null}
|
|
54
|
+
</div>
|
|
55
|
+
{condensed ? (
|
|
56
|
+
omittedLines > 0 ? (
|
|
57
|
+
<p className="text-xs text-theme-text-tertiary">
|
|
58
|
+
Showing the last {visibleLines.length} of {lines.length} lines.
|
|
59
|
+
</p>
|
|
60
|
+
) : null
|
|
61
|
+
) : visibleLines.length > 0 ? (
|
|
62
|
+
<TerminalBlock
|
|
63
|
+
label={
|
|
64
|
+
omittedLines > 0
|
|
65
|
+
? `Selected log excerpt · last ${visibleLines.length} of ${lines.length} lines`
|
|
66
|
+
: "Selected log excerpt"
|
|
67
|
+
}
|
|
68
|
+
>
|
|
69
|
+
{visibleLines.join("\n")}
|
|
70
|
+
</TerminalBlock>
|
|
71
|
+
) : (
|
|
72
|
+
<p className="text-xs italic text-theme-text-tertiary">
|
|
73
|
+
No lines were captured from this stream.
|
|
74
|
+
</p>
|
|
75
|
+
)}
|
|
76
|
+
{data.error ? (
|
|
77
|
+
<p className="text-xs leading-relaxed text-red-400">{data.error}</p>
|
|
78
|
+
) : null}
|
|
79
|
+
{data.warnings.map((warning) => (
|
|
80
|
+
<p key={warning} className="text-xs text-warning-text">
|
|
81
|
+
{warning}
|
|
82
|
+
</p>
|
|
83
|
+
))}
|
|
84
|
+
</div>
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function EventsBody({ data }: { data: EvidenceDataOf<"events"> }) {
|
|
89
|
+
return (
|
|
90
|
+
<div>
|
|
91
|
+
<p className="mb-2 text-xs text-theme-text-tertiary">{data.scope}</p>
|
|
92
|
+
<ol className="max-h-[28rem] space-y-0 overflow-y-auto pr-1">
|
|
93
|
+
{data.events.map((event, index) => (
|
|
94
|
+
<li
|
|
95
|
+
key={`${event.reason}-${event.lastTimestamp}-${index}`}
|
|
96
|
+
className="relative flex gap-3 pb-3 last:pb-0"
|
|
97
|
+
>
|
|
98
|
+
{index < data.events.length - 1 ? (
|
|
99
|
+
<span className="absolute bottom-0 left-[5px] top-3 w-px bg-theme-border" />
|
|
100
|
+
) : null}
|
|
101
|
+
<span className="relative mt-1.5 h-2.5 w-2.5 shrink-0 rounded-full border-2 border-amber-500 bg-theme-surface" />
|
|
102
|
+
<div className="min-w-0 flex-1">
|
|
103
|
+
<div className="flex flex-wrap items-baseline justify-between gap-x-3 gap-y-0.5">
|
|
104
|
+
<span className="text-xs font-semibold text-theme-text-primary">
|
|
105
|
+
{event.reason}
|
|
106
|
+
{event.count > 1 ? (
|
|
107
|
+
<span className="ml-1.5 font-mono text-[11px] font-normal text-theme-text-tertiary">
|
|
108
|
+
×{event.count}
|
|
109
|
+
</span>
|
|
110
|
+
) : null}
|
|
111
|
+
</span>
|
|
112
|
+
<Tooltip
|
|
113
|
+
content={new Date(event.lastTimestamp).toLocaleString()}
|
|
114
|
+
delay={150}
|
|
115
|
+
position="left"
|
|
116
|
+
>
|
|
117
|
+
<time
|
|
118
|
+
dateTime={event.lastTimestamp}
|
|
119
|
+
className="text-xs text-theme-text-tertiary"
|
|
120
|
+
>
|
|
121
|
+
{formatRelativeAgeTime(event.lastTimestamp)}
|
|
122
|
+
</time>
|
|
123
|
+
</Tooltip>
|
|
124
|
+
</div>
|
|
125
|
+
<p className="mt-0.5 text-xs leading-relaxed text-theme-text-secondary">
|
|
126
|
+
{event.message}
|
|
127
|
+
</p>
|
|
128
|
+
</div>
|
|
129
|
+
</li>
|
|
130
|
+
))}
|
|
131
|
+
</ol>
|
|
132
|
+
</div>
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function ChangesBody({ data }: { data: EvidenceDataOf<"changes"> }) {
|
|
137
|
+
const { onOpenResource } = useContext(EvidenceNavigationContext);
|
|
138
|
+
return (
|
|
139
|
+
<div className="space-y-2.5">
|
|
140
|
+
{data.changeContext?.evidence ? (
|
|
141
|
+
<p className="text-xs leading-relaxed text-theme-text-secondary [overflow-wrap:anywhere]">
|
|
142
|
+
{data.changeContext.evidence}
|
|
143
|
+
</p>
|
|
144
|
+
) : null}
|
|
145
|
+
{data.changes.map((change, index) => (
|
|
146
|
+
<div
|
|
147
|
+
key={`${change.kind}-${change.namespace ?? ""}-${change.name}-${change.timestamp}-${index}`}
|
|
148
|
+
className="rounded-md border border-theme-border/70 bg-theme-base/30 p-2.5"
|
|
149
|
+
>
|
|
150
|
+
<div className="flex flex-wrap items-center gap-1.5">
|
|
151
|
+
<Badge tone="structural" size="sm">
|
|
152
|
+
{change.kind}
|
|
153
|
+
</Badge>
|
|
154
|
+
<span className="font-mono text-xs">
|
|
155
|
+
<ResourceLink
|
|
156
|
+
name={change.name}
|
|
157
|
+
kind={change.kind}
|
|
158
|
+
namespace={change.namespace ?? ""}
|
|
159
|
+
group={
|
|
160
|
+
change.apiVersion
|
|
161
|
+
? apiVersionToGroup(change.apiVersion)
|
|
162
|
+
: undefined
|
|
163
|
+
}
|
|
164
|
+
label={`${change.namespace ? `${change.namespace}/` : ""}${change.name}`}
|
|
165
|
+
onNavigate={
|
|
166
|
+
change.apiVersion && onOpenResource
|
|
167
|
+
? (ref) => onOpenResource(ref)
|
|
168
|
+
: undefined
|
|
169
|
+
}
|
|
170
|
+
/>
|
|
171
|
+
</span>
|
|
172
|
+
<Badge tone="note" size="sm">
|
|
173
|
+
{change.changeType.replaceAll("_", " ")}
|
|
174
|
+
</Badge>
|
|
175
|
+
<Tooltip
|
|
176
|
+
content={new Date(change.timestamp).toLocaleString()}
|
|
177
|
+
delay={150}
|
|
178
|
+
position="left"
|
|
179
|
+
wrapperClassName="ml-auto"
|
|
180
|
+
>
|
|
181
|
+
<time
|
|
182
|
+
dateTime={change.timestamp}
|
|
183
|
+
className="text-xs text-theme-text-tertiary"
|
|
184
|
+
>
|
|
185
|
+
{formatRelativeAgeTime(change.timestamp)}
|
|
186
|
+
</time>
|
|
187
|
+
</Tooltip>
|
|
188
|
+
</div>
|
|
189
|
+
{change.summary ? (
|
|
190
|
+
<p className="mt-1.5 text-xs text-theme-text-secondary">
|
|
191
|
+
{change.summary}
|
|
192
|
+
</p>
|
|
193
|
+
) : null}
|
|
194
|
+
{change.fields?.length ? (
|
|
195
|
+
<div className="mt-2">
|
|
196
|
+
<DiffViewer
|
|
197
|
+
diff={{
|
|
198
|
+
summary: `${change.fields.length} changed ${change.fields.length === 1 ? "field" : "fields"}`,
|
|
199
|
+
fields: change.fields.map((field) => ({
|
|
200
|
+
path: field.path,
|
|
201
|
+
oldValue: field.oldValue ?? null,
|
|
202
|
+
newValue: field.newValue ?? null,
|
|
203
|
+
})),
|
|
204
|
+
}}
|
|
205
|
+
/>
|
|
206
|
+
</div>
|
|
207
|
+
) : null}
|
|
208
|
+
</div>
|
|
209
|
+
))}
|
|
210
|
+
</div>
|
|
211
|
+
);
|
|
212
|
+
}
|
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
import { useContext } from "react";
|
|
2
|
+
import { clsx } from "clsx";
|
|
3
|
+
import { Info } from "lucide-react";
|
|
4
|
+
import {
|
|
5
|
+
Badge,
|
|
6
|
+
StatusDot,
|
|
7
|
+
TerminalBlock,
|
|
8
|
+
displayKind,
|
|
9
|
+
HPADiagnosisSummary,
|
|
10
|
+
ResourceLink,
|
|
11
|
+
} from "@skyhook-io/k8s-ui";
|
|
12
|
+
import type {
|
|
13
|
+
DiagnosisResourceContext,
|
|
14
|
+
DiagnosisResourceRef,
|
|
15
|
+
DiagnosisScalerRef,
|
|
16
|
+
} from "../../diagnoseEvidenceTypes";
|
|
17
|
+
import { InvestigationResourceEvidence } from "../../InvestigationResourceEvidence";
|
|
18
|
+
import { EvidenceNavigationContext } from "../navigation";
|
|
19
|
+
import {
|
|
20
|
+
type EvidenceDataOf,
|
|
21
|
+
ResourceFact,
|
|
22
|
+
conditionStatusTone,
|
|
23
|
+
gitOpsValueSeverity,
|
|
24
|
+
severityBadge,
|
|
25
|
+
} from "../cardParts";
|
|
26
|
+
|
|
27
|
+
export function IssueBody({
|
|
28
|
+
data,
|
|
29
|
+
cardSummary,
|
|
30
|
+
}: {
|
|
31
|
+
data: EvidenceDataOf<"issue">;
|
|
32
|
+
cardSummary?: string;
|
|
33
|
+
}) {
|
|
34
|
+
const issue = data.issue;
|
|
35
|
+
const showCause =
|
|
36
|
+
Boolean(issue.cause) && issue.cause?.trim() !== cardSummary?.trim();
|
|
37
|
+
const showMessage =
|
|
38
|
+
Boolean(issue.message) &&
|
|
39
|
+
issue.message?.trim() !== cardSummary?.trim() &&
|
|
40
|
+
issue.message?.trim() !== issue.cause?.trim();
|
|
41
|
+
return (
|
|
42
|
+
<div className="space-y-2">
|
|
43
|
+
<div className="flex flex-wrap gap-1.5">
|
|
44
|
+
<Badge
|
|
45
|
+
severity={issue.severity === "critical" ? "error" : "warning"}
|
|
46
|
+
size="sm"
|
|
47
|
+
>
|
|
48
|
+
{issue.severity}
|
|
49
|
+
</Badge>
|
|
50
|
+
<Badge tone="structural" size="sm">
|
|
51
|
+
{issue.kind}
|
|
52
|
+
</Badge>
|
|
53
|
+
<Badge tone="structural" size="sm">
|
|
54
|
+
{issue.namespace ? `${issue.namespace}/` : ""}
|
|
55
|
+
{issue.name}
|
|
56
|
+
</Badge>
|
|
57
|
+
{data.pods && data.pods.length > 0 ? (
|
|
58
|
+
<>
|
|
59
|
+
<Badge tone="structural" size="sm">
|
|
60
|
+
{data.pods.length === 1 ? "1 Pod" : `${data.pods.length} Pods`}
|
|
61
|
+
</Badge>
|
|
62
|
+
{data.pods.map((pod) => (
|
|
63
|
+
<Badge key={pod} tone="structural" size="sm">
|
|
64
|
+
{pod}
|
|
65
|
+
</Badge>
|
|
66
|
+
))}
|
|
67
|
+
</>
|
|
68
|
+
) : null}
|
|
69
|
+
</div>
|
|
70
|
+
{showCause ? (
|
|
71
|
+
<p className="text-sm font-medium leading-relaxed text-theme-text-primary">
|
|
72
|
+
{issue.cause}
|
|
73
|
+
</p>
|
|
74
|
+
) : null}
|
|
75
|
+
{showMessage ? (
|
|
76
|
+
<p className="text-xs leading-relaxed text-theme-text-secondary">
|
|
77
|
+
{issue.message}
|
|
78
|
+
</p>
|
|
79
|
+
) : null}
|
|
80
|
+
</div>
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function StartupBody({ data }: { data: EvidenceDataOf<"startup"> }) {
|
|
85
|
+
const blocker = data.blocker;
|
|
86
|
+
const pods = data.pods && data.pods.length > 1 ? data.pods : [blocker.name];
|
|
87
|
+
return (
|
|
88
|
+
<div className="space-y-2">
|
|
89
|
+
<div className="flex flex-wrap gap-1.5">
|
|
90
|
+
<Badge tone="structural" size="sm">
|
|
91
|
+
{pods.length > 1 ? `${pods.length} ${blocker.kind}s` : blocker.kind}
|
|
92
|
+
</Badge>
|
|
93
|
+
{pods.map((pod) => (
|
|
94
|
+
<Badge key={pod} tone="structural" size="sm">
|
|
95
|
+
{pod}
|
|
96
|
+
</Badge>
|
|
97
|
+
))}
|
|
98
|
+
<Badge severity={severityBadge(blocker.severity)} size="sm">
|
|
99
|
+
{blocker.severity}
|
|
100
|
+
</Badge>
|
|
101
|
+
</div>
|
|
102
|
+
<p className="text-sm leading-relaxed text-theme-text-primary">
|
|
103
|
+
{blocker.message}
|
|
104
|
+
</p>
|
|
105
|
+
</div>
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function CrashBody({ data }: { data: EvidenceDataOf<"crash"> }) {
|
|
110
|
+
const crash = data.crash;
|
|
111
|
+
return (
|
|
112
|
+
<div className="space-y-2.5">
|
|
113
|
+
<div className="flex flex-wrap gap-1.5">
|
|
114
|
+
<Badge severity="error" size="sm">
|
|
115
|
+
{crash.reason || crash.state}
|
|
116
|
+
</Badge>
|
|
117
|
+
<Badge tone="structural" size="sm">
|
|
118
|
+
exit {crash.exitCode}
|
|
119
|
+
</Badge>
|
|
120
|
+
<Badge tone="structural" size="sm">
|
|
121
|
+
{crash.container}
|
|
122
|
+
</Badge>
|
|
123
|
+
</div>
|
|
124
|
+
<p className="text-xs text-theme-text-tertiary">
|
|
125
|
+
{crash.pods.join(", ")} · {crash.logSource.replaceAll("_", " ")}
|
|
126
|
+
</p>
|
|
127
|
+
<TerminalBlock label="Selected crash line">{crash.logLine}</TerminalBlock>
|
|
128
|
+
</div>
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function ResourceBody({ data }: { data: EvidenceDataOf<"resource"> }) {
|
|
133
|
+
const replicas = data.resourceContext?.workloadSummary?.replicas;
|
|
134
|
+
// SealedSecret's dedicated body renders the resource's conditions beside
|
|
135
|
+
// its controller state, so repeating the derived summary here adds noise.
|
|
136
|
+
const conditions =
|
|
137
|
+
data.resource.kind.toLowerCase() === "sealedsecret"
|
|
138
|
+
? []
|
|
139
|
+
: (data.resourceContext?.statusSummary?.conditions ?? []);
|
|
140
|
+
const desired = replicas?.desired;
|
|
141
|
+
const ready = replicas ? (replicas.ready ?? 0) : undefined;
|
|
142
|
+
const shortfall =
|
|
143
|
+
desired !== undefined && ready !== undefined && ready < desired;
|
|
144
|
+
const scalers = diagnosedScalers(data.resourceContext);
|
|
145
|
+
return (
|
|
146
|
+
<div className="space-y-3">
|
|
147
|
+
<InvestigationResourceEvidence resource={data.resource} />
|
|
148
|
+
{data.gitOpsDiagnosis ? (
|
|
149
|
+
<GitOpsStatusBody status={data.gitOpsDiagnosis} />
|
|
150
|
+
) : null}
|
|
151
|
+
{desired !== undefined && ready !== undefined ? (
|
|
152
|
+
<dl className="flex flex-wrap gap-x-6 gap-y-2 text-xs">
|
|
153
|
+
<div>
|
|
154
|
+
<dt className="text-theme-text-tertiary">Ready replicas</dt>
|
|
155
|
+
<dd
|
|
156
|
+
className={clsx(
|
|
157
|
+
"font-mono font-semibold tabular-nums",
|
|
158
|
+
shortfall ? "text-warning-text" : "text-theme-text-primary",
|
|
159
|
+
)}
|
|
160
|
+
>
|
|
161
|
+
{ready}/{desired}
|
|
162
|
+
</dd>
|
|
163
|
+
</div>
|
|
164
|
+
<ResourceFact label="Available" value={replicas?.available} />
|
|
165
|
+
<ResourceFact label="Updated" value={replicas?.updated} />
|
|
166
|
+
<ResourceFact label="Unavailable" value={replicas?.unavailable} />
|
|
167
|
+
<ResourceFact
|
|
168
|
+
label="Phase"
|
|
169
|
+
value={data.resourceContext?.statusSummary?.phase}
|
|
170
|
+
/>
|
|
171
|
+
</dl>
|
|
172
|
+
) : null}
|
|
173
|
+
{conditions.length > 0 ? (
|
|
174
|
+
<div>
|
|
175
|
+
<div className="mb-1.5 text-xs font-semibold uppercase tracking-wide text-theme-text-tertiary">
|
|
176
|
+
Conditions
|
|
177
|
+
</div>
|
|
178
|
+
<div className="max-h-52 space-y-1.5 overflow-y-auto pr-1">
|
|
179
|
+
{conditions.map((condition) => (
|
|
180
|
+
<div
|
|
181
|
+
key={`${condition.type}-${condition.status}-${condition.reason ?? ""}`}
|
|
182
|
+
className="flex min-w-0 items-start gap-2 text-xs"
|
|
183
|
+
>
|
|
184
|
+
<StatusDot
|
|
185
|
+
tone={conditionStatusTone(condition)}
|
|
186
|
+
className="mt-1 shrink-0"
|
|
187
|
+
/>
|
|
188
|
+
<p className="min-w-0 text-theme-text-secondary">
|
|
189
|
+
<span className="font-medium text-theme-text-primary">
|
|
190
|
+
{condition.type}={condition.status}
|
|
191
|
+
</span>
|
|
192
|
+
{condition.reason ? ` · ${condition.reason}` : ""}
|
|
193
|
+
{condition.message ? ` — ${condition.message}` : ""}
|
|
194
|
+
</p>
|
|
195
|
+
</div>
|
|
196
|
+
))}
|
|
197
|
+
</div>
|
|
198
|
+
</div>
|
|
199
|
+
) : null}
|
|
200
|
+
{scalers.length > 0 ? (
|
|
201
|
+
<ScaledBySection
|
|
202
|
+
scalers={scalers}
|
|
203
|
+
allScalers={data.resourceContext?.scaledBy ?? []}
|
|
204
|
+
/>
|
|
205
|
+
) : null}
|
|
206
|
+
{data.warnings.length > 0 ? (
|
|
207
|
+
<ul className="space-y-1 text-xs text-theme-text-secondary">
|
|
208
|
+
{data.warnings.map((warning) => (
|
|
209
|
+
<li key={warning} className="flex items-start gap-1.5">
|
|
210
|
+
<Info className="mt-0.5 h-3.5 w-3.5 shrink-0 text-theme-text-tertiary" />
|
|
211
|
+
<span>{warning}</span>
|
|
212
|
+
</li>
|
|
213
|
+
))}
|
|
214
|
+
</ul>
|
|
215
|
+
) : null}
|
|
216
|
+
</div>
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
type DiagnosedScaler = DiagnosisScalerRef &
|
|
221
|
+
Required<Pick<DiagnosisScalerRef, "hpaSummary">>;
|
|
222
|
+
|
|
223
|
+
export function diagnosedScalers(
|
|
224
|
+
context: DiagnosisResourceContext | undefined,
|
|
225
|
+
): DiagnosedScaler[] {
|
|
226
|
+
return (context?.scaledBy ?? []).filter(
|
|
227
|
+
(scaler): scaler is DiagnosedScaler => scaler.hpaSummary !== undefined,
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function ScaledBySection({
|
|
232
|
+
scalers,
|
|
233
|
+
allScalers,
|
|
234
|
+
}: {
|
|
235
|
+
scalers: DiagnosedScaler[];
|
|
236
|
+
allScalers: DiagnosisScalerRef[];
|
|
237
|
+
}) {
|
|
238
|
+
const { onOpenResource } = useContext(EvidenceNavigationContext);
|
|
239
|
+
// The ScaledObject is only a link when Radar listed it as a scaler too; a
|
|
240
|
+
// name read off the HPA's owner reference alone is not a resource we hold.
|
|
241
|
+
const listedScaler = (ref: DiagnosisResourceRef) =>
|
|
242
|
+
allScalers.some(
|
|
243
|
+
(scaler) =>
|
|
244
|
+
scaler.kind === ref.kind &&
|
|
245
|
+
scaler.name === ref.name &&
|
|
246
|
+
(scaler.namespace ?? "") === (ref.namespace ?? ""),
|
|
247
|
+
);
|
|
248
|
+
return (
|
|
249
|
+
<div>
|
|
250
|
+
<div className="mb-1.5 text-xs font-semibold uppercase tracking-wide text-theme-text-tertiary">
|
|
251
|
+
Scaled by
|
|
252
|
+
</div>
|
|
253
|
+
<div className="space-y-2">
|
|
254
|
+
{scalers.map((scaler) => (
|
|
255
|
+
<HPADiagnosisSummary
|
|
256
|
+
key={`${scaler.namespace ?? ""}/${scaler.name}`}
|
|
257
|
+
diagnosis={scaler.hpaSummary}
|
|
258
|
+
variant="inline"
|
|
259
|
+
header={
|
|
260
|
+
<>
|
|
261
|
+
<span className="font-medium text-theme-text-primary">
|
|
262
|
+
{displayKind(scaler.kind)} {scaler.name}
|
|
263
|
+
</span>
|
|
264
|
+
{scaler.managedBy ? (
|
|
265
|
+
<span className="text-theme-text-tertiary">
|
|
266
|
+
managed by KEDA {displayKind(scaler.managedBy.kind)}{" "}
|
|
267
|
+
<ResourceLink
|
|
268
|
+
name={scaler.managedBy.name}
|
|
269
|
+
kind={scaler.managedBy.kind}
|
|
270
|
+
namespace={scaler.managedBy.namespace ?? ""}
|
|
271
|
+
group={scaler.managedBy.group}
|
|
272
|
+
onNavigate={
|
|
273
|
+
onOpenResource && listedScaler(scaler.managedBy)
|
|
274
|
+
? (ref) => onOpenResource(ref)
|
|
275
|
+
: undefined
|
|
276
|
+
}
|
|
277
|
+
/>
|
|
278
|
+
</span>
|
|
279
|
+
) : null}
|
|
280
|
+
</>
|
|
281
|
+
}
|
|
282
|
+
/>
|
|
283
|
+
))}
|
|
284
|
+
</div>
|
|
285
|
+
</div>
|
|
286
|
+
);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function GitOpsStatusBody({
|
|
290
|
+
status,
|
|
291
|
+
}: {
|
|
292
|
+
status: NonNullable<EvidenceDataOf<"resource">["gitOpsDiagnosis"]>;
|
|
293
|
+
}) {
|
|
294
|
+
const fields = [
|
|
295
|
+
["Sync", status.sync],
|
|
296
|
+
["Health", status.health],
|
|
297
|
+
["Operation", status.operationPhase],
|
|
298
|
+
["Ready", status.ready],
|
|
299
|
+
] as const;
|
|
300
|
+
return (
|
|
301
|
+
<div className="rounded-md border border-theme-border bg-theme-base/40 p-2.5">
|
|
302
|
+
<div className="mb-2 flex flex-wrap items-center gap-1.5">
|
|
303
|
+
<span className="text-xs font-semibold uppercase tracking-wide text-theme-text-tertiary">
|
|
304
|
+
GitOps controller status
|
|
305
|
+
</span>
|
|
306
|
+
<Badge tone="note" size="sm">
|
|
307
|
+
{status.tool === "argocd" ? "Argo CD" : "Flux"}
|
|
308
|
+
</Badge>
|
|
309
|
+
{status.suspended ? (
|
|
310
|
+
<Badge severity="info" size="sm">
|
|
311
|
+
Suspended
|
|
312
|
+
</Badge>
|
|
313
|
+
) : null}
|
|
314
|
+
</div>
|
|
315
|
+
<div className="flex flex-wrap gap-1.5">
|
|
316
|
+
{fields.map(([label, value]) =>
|
|
317
|
+
value ? (
|
|
318
|
+
<Badge
|
|
319
|
+
key={label}
|
|
320
|
+
severity={gitOpsValueSeverity(label, value)}
|
|
321
|
+
size="sm"
|
|
322
|
+
>
|
|
323
|
+
{label}: {value}
|
|
324
|
+
</Badge>
|
|
325
|
+
) : null,
|
|
326
|
+
)}
|
|
327
|
+
{status.appliedRevision ? (
|
|
328
|
+
<Badge tone="structural" size="sm">
|
|
329
|
+
{status.appliedRevision}
|
|
330
|
+
</Badge>
|
|
331
|
+
) : null}
|
|
332
|
+
</div>
|
|
333
|
+
</div>
|
|
334
|
+
);
|
|
335
|
+
}
|
|
@@ -6,11 +6,19 @@ import { adaptChanges } from "./adapters/changes";
|
|
|
6
6
|
import { adaptDiagnose } from "./adapters/diagnose";
|
|
7
7
|
import { adaptEvents } from "./adapters/events";
|
|
8
8
|
import { adaptHelmRelease } from "./adapters/helm";
|
|
9
|
+
import {
|
|
10
|
+
adaptListHelmReleases,
|
|
11
|
+
adaptListPackages,
|
|
12
|
+
adaptSearch,
|
|
13
|
+
} from "./adapters/listings";
|
|
14
|
+
import { adaptTopResources } from "./adapters/rankings";
|
|
15
|
+
import { adaptClusterAudit, adaptUpgradeReadiness } from "./adapters/posture";
|
|
9
16
|
import { adaptIssues } from "./adapters/issues";
|
|
10
17
|
import { adaptPodLogs, adaptWorkloadLogs } from "./adapters/logs";
|
|
11
18
|
import { adaptSubjectPermissions } from "./adapters/permissions";
|
|
12
19
|
import {
|
|
13
20
|
adaptPrometheusRules,
|
|
21
|
+
adaptDiscoverMetrics,
|
|
14
22
|
adaptQueryPrometheus,
|
|
15
23
|
} from "./adapters/prometheus";
|
|
16
24
|
import {
|
|
@@ -66,6 +74,13 @@ const ADAPTERS: Record<
|
|
|
66
74
|
get_resource: adaptGetResource,
|
|
67
75
|
list_resources: adaptListResources,
|
|
68
76
|
list_namespaces: adaptListNamespaces,
|
|
77
|
+
list_helm_releases: adaptListHelmReleases,
|
|
78
|
+
list_packages: adaptListPackages,
|
|
79
|
+
search: adaptSearch,
|
|
80
|
+
top_resources: adaptTopResources,
|
|
81
|
+
get_cluster_audit: adaptClusterAudit,
|
|
82
|
+
get_cluster_upgrade_readiness: adaptUpgradeReadiness,
|
|
83
|
+
discover_metrics: adaptDiscoverMetrics,
|
|
69
84
|
get_events: adaptEvents,
|
|
70
85
|
get_pod_logs: adaptPodLogs,
|
|
71
86
|
get_changes: adaptChanges,
|