@skyhook-io/radar-app 0.2.2 → 0.3.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/README.md +7 -1
- package/package.json +33 -25
- package/src/App.tsx +1449 -382
- package/src/RadarApp.tsx +132 -19
- package/src/api/apiResources.ts +1 -1
- package/src/api/client.argoResourceSync.test.ts +69 -0
- package/src/api/client.delta.test.ts +89 -0
- package/src/api/client.deltaSync.test.ts +216 -0
- package/src/api/client.metrics.test.ts +106 -0
- package/src/api/client.rightsizing.test.ts +32 -0
- package/src/api/client.ts +2730 -271
- package/src/api/client.yaml.test.ts +45 -0
- package/src/api/diagnose.ts +289 -0
- package/src/api/quotas.ts +16 -0
- package/src/api/rbac.ts +57 -0
- package/src/api/timelineSource.test.ts +217 -0
- package/src/api/timelineSource.ts +582 -0
- package/src/components/ConnectionErrorView.tsx +186 -70
- package/src/components/ContextSwitcher.tsx +63 -18
- package/src/components/DebugOverlay.tsx +5 -3
- package/src/components/NamespaceSwitcher.tsx +41 -0
- package/src/components/UserMenu.tsx +69 -21
- package/src/components/applications/ApplicationsView.tsx +936 -0
- package/src/components/audit/AuditSettingsDialog.tsx +79 -17
- package/src/components/audit/AuditView.tsx +65 -62
- package/src/components/compare/CompareViewRoute.tsx +124 -0
- package/src/components/compare/useCompareCandidates.ts +27 -0
- package/src/components/compare/useCompareLauncher.tsx +79 -0
- package/src/components/cost/ApplicationCostTab.test.ts +204 -0
- package/src/components/cost/ApplicationCostTab.tsx +571 -0
- package/src/components/cost/CostTrendChart.tsx +106 -75
- package/src/components/cost/CostView.test.ts +12 -0
- package/src/components/cost/CostView.tsx +507 -223
- package/src/components/cost/CostViewTabs.test.tsx +21 -0
- package/src/components/cost/CostViewTabs.tsx +40 -0
- package/src/components/cost/CurrentAllocationUse.test.ts +21 -0
- package/src/components/cost/CurrentAllocationUse.tsx +126 -0
- package/src/components/cost/WorkloadCostTab.test.ts +153 -0
- package/src/components/cost/WorkloadCostTab.tsx +372 -0
- package/src/components/cost/cloud-console.test.ts +39 -0
- package/src/components/cost/cloud-console.ts +81 -0
- package/src/components/cost/errors.ts +8 -0
- package/src/components/cost/format.test.ts +27 -0
- package/src/components/cost/format.ts +46 -0
- package/src/components/cost/kinds.ts +5 -0
- package/src/components/curl/ServiceCurlButton.tsx +445 -0
- package/src/components/diagnose/AISettings.tsx +147 -0
- package/src/components/diagnose/DiagnoseContext.tsx +495 -0
- package/src/components/diagnose/DiagnoseSurface.tsx +394 -0
- package/src/components/diagnose/Home.tsx +163 -0
- package/src/components/diagnose/InvestigationView.tsx +622 -0
- package/src/components/diagnose/LocalDiagnoseAction.tsx +162 -0
- package/src/components/diagnose/launch.ts +65 -0
- package/src/components/diagnose/parts.tsx +1756 -0
- package/src/components/dock/BottomDock.tsx +2 -3
- package/src/components/dock/DockContext.tsx +1 -0
- package/src/components/dock/TerminalTab.tsx +1 -1
- package/src/components/dock/WorkloadLogsTab.tsx +21 -5
- package/src/components/dock/index.ts +1 -1
- package/src/components/execution/BatchExecutionView.test.ts +170 -0
- package/src/components/execution/BatchExecutionView.tsx +1329 -0
- package/src/components/execution/batch-run-actions.test.ts +48 -0
- package/src/components/execution/batch-run-actions.ts +24 -0
- package/src/components/execution/batch-timeline.test.ts +57 -0
- package/src/components/execution/batch-timeline.ts +46 -0
- package/src/components/execution/execution-definition.test.ts +208 -0
- package/src/components/execution/execution-definition.ts +245 -0
- package/src/components/gitops/ArgoResourceDiffLoader.tsx +23 -0
- package/src/components/gitops/GitOpsView.tsx +1042 -0
- package/src/components/gitops/RevisionMetaChip.tsx +63 -0
- package/src/components/helm/ChartBrowser.tsx +87 -31
- package/src/components/helm/HelmCompareRoute.tsx +1341 -0
- package/src/components/helm/HelmReleaseDrawer.test.ts +17 -0
- package/src/components/helm/HelmReleaseDrawer.tsx +1073 -102
- package/src/components/helm/HelmView.tsx +237 -96
- package/src/components/helm/InstallWizard.tsx +94 -38
- package/src/components/helm/ManifestDiffViewer.tsx +8 -27
- package/src/components/helm/OwnedResources.tsx +34 -59
- package/src/components/helm/RevisionHistory.tsx +52 -3
- package/src/components/helm/RoleGatedPanel.tsx +3 -3
- package/src/components/helm/TrackChartSourceDialog.tsx +185 -0
- package/src/components/helm/ValuesDiffPreview.tsx +17 -7
- package/src/components/helm/ValuesViewer.tsx +49 -53
- package/src/components/helm/helm-utils.ts +4 -0
- package/src/components/home/ActivitySummary.tsx +4 -1
- package/src/components/home/ClusterHealthCard.tsx +56 -42
- package/src/components/home/CostCard.tsx +21 -36
- package/src/components/home/GitOpsControllersCard.tsx +110 -0
- package/src/components/home/HelmSummary.tsx +3 -1
- package/src/components/home/HomeView.tsx +339 -105
- package/src/components/home/MCPSetupDialog.tsx +29 -87
- package/src/components/home/TrafficSummary.tsx +2 -2
- package/src/components/home/mcpToolCatalog.ts +333 -0
- package/src/components/issues/IssuesPane.tsx +151 -0
- package/src/components/logs/LogsViewer.tsx +4 -1
- package/src/components/logs/ScheduledWorkloadLogsViewer.tsx +135 -0
- package/src/components/logs/WorkloadLogsViewer.tsx +4 -1
- package/src/components/nav/PrimaryNavRail.tsx +285 -0
- package/src/components/portforward/PortForwardButton.tsx +118 -47
- package/src/components/portforward/PortForwardManager.tsx +253 -131
- package/src/components/resource/HPACharts.tsx +237 -0
- package/src/components/resource/PVCUsageBar.tsx +59 -0
- package/src/components/resource/PrometheusCharts.tsx +160 -584
- package/src/components/resource/PrometheusChartsGrid.tsx +270 -0
- package/src/components/resource/RestartChart.tsx +133 -0
- package/src/components/resource/RightsizingStrip.test.ts +109 -0
- package/src/components/resource/RightsizingStrip.tsx +363 -0
- package/src/components/resource-drawer/ResourceDrawer.tsx +3 -1
- package/src/components/resources/CompositeRenderer.tsx +101 -0
- package/src/components/resources/ImageFilesystemModal.tsx +19 -12
- package/src/components/resources/PodFilesystemModal.tsx +6 -5
- package/src/components/resources/ResourceDetailDrawer.tsx +13 -3
- package/src/components/resources/ResourcesView.tsx +194 -17
- package/src/components/resources/renderers/CronWorkflowRenderer.tsx +1 -0
- package/src/components/resources/renderers/HPARenderer.tsx +20 -1
- package/src/components/resources/renderers/NamespaceRenderer.tsx +31 -0
- package/src/components/resources/renderers/NodeRenderer.tsx +10 -4
- package/src/components/resources/renderers/PVCRenderer.tsx +19 -1
- package/src/components/resources/renderers/PodRenderer.tsx +30 -6
- package/src/components/resources/renderers/RoleBindingRenderer.tsx +45 -1
- package/src/components/resources/renderers/RoleRenderer.tsx +27 -1
- package/src/components/resources/renderers/ServiceAccountRenderer.tsx +28 -1
- package/src/components/resources/renderers/ServiceRenderer.tsx +81 -8
- package/src/components/resources/renderers/WorkloadRenderer.tsx +51 -4
- package/src/components/resources/renderers/index.ts +2 -0
- package/src/components/resources/resource-utils.ts +2 -1
- package/src/components/rightsizing/RightsizingScanView.tsx +938 -0
- package/src/components/rightsizing/copy.test.ts +56 -0
- package/src/components/rightsizing/model.test.ts +227 -0
- package/src/components/rightsizing/model.ts +158 -0
- package/src/components/rightsizing/presentation.test.ts +104 -0
- package/src/components/rightsizing/presentation.ts +94 -0
- package/src/components/settings/MyPermissionsDialog.tsx +241 -0
- package/src/components/settings/SettingsDialog.tsx +1505 -165
- package/src/components/shared/CreateResourceDialog.tsx +9 -2
- package/src/components/shared/LargeClusterNamespacePicker.tsx +3 -3
- package/src/components/timeline/LocalTimelineScrubber.tsx +212 -0
- package/src/components/timeline/RetainedTimelineScrubber.tsx +311 -0
- package/src/components/timeline/TimelineList.tsx +86 -13
- package/src/components/timeline/TimelineSwimlanes.tsx +9 -1299
- package/src/components/timeline/TimelineView.tsx +873 -24
- package/src/components/timeline/TimelineView.urlparams.test.ts +335 -0
- package/src/components/traffic/TrafficFilterSidebar.tsx +10 -45
- package/src/components/traffic/TrafficFlowList.tsx +29 -15
- package/src/components/traffic/TrafficGraph.tsx +42 -24
- package/src/components/traffic/TrafficView.tsx +32 -19
- package/src/components/ui/CommandPalette.tsx +8 -215
- package/src/components/ui/DiagnosticsOverlay.tsx +219 -9
- package/src/components/ui/Markdown.tsx +3 -3
- package/src/components/ui/Omnibar.tsx +602 -0
- package/src/components/ui/RadarOmnibar.tsx +52 -0
- package/src/components/ui/SearchSyntaxHelp.tsx +89 -0
- package/src/components/ui/ShortcutHelpOverlay.tsx +3 -2
- package/src/components/ui/UpdateNotification.tsx +48 -36
- package/src/components/ui/command-items.ts +178 -0
- package/src/components/workload/WorkloadView.tsx +1342 -158
- package/src/context/ConnectionContext.tsx +146 -21
- package/src/context/DiagnoseCustomization.tsx +93 -0
- package/src/context/NavCustomization.tsx +75 -0
- package/src/context/TimelineSource.tsx +50 -0
- package/src/contexts/CapabilitiesContext.tsx +32 -8
- package/src/filter/FilterLocationBridge.tsx +30 -0
- package/src/hooks/useClusterLoadState.ts +73 -0
- package/src/hooks/useDocumentTitle.ts +25 -0
- package/src/hooks/useEventSource.ts +6 -0
- package/src/hooks/useKeyboardShortcuts.tsx +1 -0
- package/src/hooks/useMediaQuery.ts +21 -0
- package/src/hooks/useNavRailPinned.ts +46 -0
- package/src/hooks/useRecentResources.ts +49 -0
- package/src/index.css +162 -1
- package/src/index.ts +73 -1
- package/src/main.tsx +7 -5
- package/src/types/clusterLoadState.ts +33 -0
- package/src/types.ts +2 -0
- package/src/utils/auditBadges.ts +53 -0
- package/src/utils/navigation.ts +64 -1
- package/src/components/ui/NamespaceSelector.tsx +0 -436
|
@@ -0,0 +1,394 @@
|
|
|
1
|
+
// The right-docked shell of the AI surface. Two layouts:
|
|
2
|
+
// - docked: a single-pane right column (app reflows left via the provider's push)
|
|
3
|
+
// - expanded: a master-detail workspace that fills ONLY the content area (does
|
|
4
|
+
// not cover the left nav rail or top bar) — recent list on the left, the
|
|
5
|
+
// selected investigation/report on the right.
|
|
6
|
+
import { useState } from "react";
|
|
7
|
+
import {
|
|
8
|
+
Sparkles,
|
|
9
|
+
X,
|
|
10
|
+
Maximize2,
|
|
11
|
+
Minimize2,
|
|
12
|
+
ChevronLeft,
|
|
13
|
+
Settings2,
|
|
14
|
+
MoreVertical,
|
|
15
|
+
TerminalSquare,
|
|
16
|
+
Copy,
|
|
17
|
+
Check,
|
|
18
|
+
} from "lucide-react";
|
|
19
|
+
import { Tooltip } from "../ui/Tooltip";
|
|
20
|
+
import {
|
|
21
|
+
useDiagnose,
|
|
22
|
+
useDiagnoseLayout,
|
|
23
|
+
agentLabelFor,
|
|
24
|
+
openDiagnoseSettings,
|
|
25
|
+
} from "./DiagnoseContext";
|
|
26
|
+
import { useDiagnoseCustomization } from "../../context/DiagnoseCustomization";
|
|
27
|
+
import { InvestigationView } from "./InvestigationView";
|
|
28
|
+
import { RecentList } from "./Home";
|
|
29
|
+
import { ConsentCard } from "./parts";
|
|
30
|
+
import { buildLaunchCommand, launchAgentLabel, openInTerminal } from "./launch";
|
|
31
|
+
import { type RunSummary } from "../../api/diagnose";
|
|
32
|
+
|
|
33
|
+
function capWord(s: string): string {
|
|
34
|
+
return s ? s[0].toUpperCase() + s.slice(1) : s;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// buildConfigLine renders the active AI config as the header subtitle. Codex shows
|
|
38
|
+
// its isolation mode + effective reasoning effort (Default → medium); a model
|
|
39
|
+
// override is shown for either agent. Reflects a run's recorded settings, or the
|
|
40
|
+
// current defaults on Home.
|
|
41
|
+
function buildConfigLine(cfg: {
|
|
42
|
+
agent?: string;
|
|
43
|
+
isolated?: boolean;
|
|
44
|
+
model?: string;
|
|
45
|
+
effort?: string;
|
|
46
|
+
}): string {
|
|
47
|
+
const parts = [agentLabelFor(cfg.agent ?? "")];
|
|
48
|
+
if (cfg.agent === "codex") {
|
|
49
|
+
parts.push(cfg.isolated === false ? "My setup" : "Isolated");
|
|
50
|
+
parts.push(`${capWord(cfg.effort || "medium")} effort`);
|
|
51
|
+
}
|
|
52
|
+
if (cfg.model) parts.push(capWord(cfg.model));
|
|
53
|
+
return "via " + parts.join(" · ");
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// InvestigationMenu hands an investigation off to the user's own full agent. The
|
|
57
|
+
// PRIMARY action copies a resume command they can paste wherever they actually
|
|
58
|
+
// work (terminal, tmux, IDE) — destination-agnostic. Running it in Radar's built-in
|
|
59
|
+
// terminal is the secondary "run it here" convenience.
|
|
60
|
+
function InvestigationMenu({ run }: { run: RunSummary }) {
|
|
61
|
+
const [open, setOpen] = useState(false);
|
|
62
|
+
const [copied, setCopied] = useState(false);
|
|
63
|
+
const label = launchAgentLabel(run);
|
|
64
|
+
const command = buildLaunchCommand(run, `${window.location.origin}/mcp`);
|
|
65
|
+
// No resumable session yet (or stale run) → nothing to hand off.
|
|
66
|
+
if (!command) return null;
|
|
67
|
+
|
|
68
|
+
const toggle = () => {
|
|
69
|
+
setCopied(false);
|
|
70
|
+
setOpen((v) => !v);
|
|
71
|
+
};
|
|
72
|
+
const copy = () => {
|
|
73
|
+
void navigator.clipboard?.writeText(command);
|
|
74
|
+
setCopied(true);
|
|
75
|
+
setTimeout(() => {
|
|
76
|
+
setCopied(false);
|
|
77
|
+
setOpen(false);
|
|
78
|
+
}, 1100);
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
return (
|
|
82
|
+
<div className="relative flex items-center">
|
|
83
|
+
<Tooltip content="More" position="bottom">
|
|
84
|
+
<button
|
|
85
|
+
onClick={toggle}
|
|
86
|
+
className="rounded-md p-1 text-theme-text-tertiary hover:bg-theme-hover hover:text-theme-text-primary"
|
|
87
|
+
aria-label="More actions"
|
|
88
|
+
aria-haspopup="menu"
|
|
89
|
+
aria-expanded={open}
|
|
90
|
+
>
|
|
91
|
+
<MoreVertical className="h-4 w-4" />
|
|
92
|
+
</button>
|
|
93
|
+
</Tooltip>
|
|
94
|
+
{open && (
|
|
95
|
+
<>
|
|
96
|
+
<div className="fixed inset-0 z-10" onClick={() => setOpen(false)} />
|
|
97
|
+
<div className="absolute right-0 top-full z-20 mt-1 w-64 rounded-lg border border-theme-border bg-theme-surface py-1 shadow-theme-lg">
|
|
98
|
+
<button
|
|
99
|
+
onClick={copy}
|
|
100
|
+
className="flex w-full items-start gap-2 px-3 py-1.5 text-left text-sm text-theme-text-primary hover:bg-theme-hover"
|
|
101
|
+
>
|
|
102
|
+
{copied ? (
|
|
103
|
+
<Check className="mt-0.5 h-4 w-4 shrink-0 text-emerald-500" />
|
|
104
|
+
) : (
|
|
105
|
+
<Copy className="mt-0.5 h-4 w-4 shrink-0 text-theme-text-tertiary" />
|
|
106
|
+
)}
|
|
107
|
+
<span>
|
|
108
|
+
{copied ? "Copied ✓" : `Copy command to continue in ${label}`}
|
|
109
|
+
{!copied && (
|
|
110
|
+
<span className="block text-[11px] text-theme-text-tertiary">
|
|
111
|
+
Paste it wherever you run {label} — resumes this exact
|
|
112
|
+
session.
|
|
113
|
+
</span>
|
|
114
|
+
)}
|
|
115
|
+
</span>
|
|
116
|
+
</button>
|
|
117
|
+
<button
|
|
118
|
+
onClick={() => {
|
|
119
|
+
openInTerminal(command, "Diagnose");
|
|
120
|
+
setOpen(false);
|
|
121
|
+
}}
|
|
122
|
+
className="flex w-full items-center gap-2 px-3 py-1.5 text-left text-sm text-theme-text-primary hover:bg-theme-hover"
|
|
123
|
+
>
|
|
124
|
+
<TerminalSquare className="h-4 w-4 shrink-0 text-theme-text-tertiary" />
|
|
125
|
+
Run in a Radar terminal
|
|
126
|
+
</button>
|
|
127
|
+
</div>
|
|
128
|
+
</>
|
|
129
|
+
)}
|
|
130
|
+
</div>
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// The panel is an ABSOLUTE slot inside the app's body frame (the column under the
|
|
135
|
+
// header, right of the nav rail) — App renders it there and passes topInset (the
|
|
136
|
+
// header height; 0 in chromeless embeds). It shares that frame with the resource/
|
|
137
|
+
// Helm drawers, so it no longer floats viewport-fixed or DOM-measures the chrome.
|
|
138
|
+
export function DiagnoseSurface({ topInset = 0 }: { topInset?: number }) {
|
|
139
|
+
const d = useDiagnose();
|
|
140
|
+
// Injected settings action: undefined = Radar's own Settings dialog;
|
|
141
|
+
// null = hide the gear + links.
|
|
142
|
+
const { consentCopy, onOpenSettings: hostOpenSettings } = useDiagnoseCustomization();
|
|
143
|
+
const openSettings =
|
|
144
|
+
hostOpenSettings === undefined ? openDiagnoseSettings : hostOpenSettings;
|
|
145
|
+
const {
|
|
146
|
+
maximized,
|
|
147
|
+
setMaximized,
|
|
148
|
+
panelWidth: width,
|
|
149
|
+
setPanelWidth: setWidth,
|
|
150
|
+
panelNarrow: narrow,
|
|
151
|
+
panelBounds: { min: minW, max: maxW },
|
|
152
|
+
panelWidthKey: widthKey,
|
|
153
|
+
} = useDiagnoseLayout();
|
|
154
|
+
|
|
155
|
+
const startResize = (e: React.MouseEvent) => {
|
|
156
|
+
e.preventDefault();
|
|
157
|
+
const onMove = (m: MouseEvent) =>
|
|
158
|
+
setWidth(() =>
|
|
159
|
+
Math.min(maxW, Math.max(minW, window.innerWidth - m.clientX)),
|
|
160
|
+
);
|
|
161
|
+
const onUp = () => {
|
|
162
|
+
document.removeEventListener("mousemove", onMove);
|
|
163
|
+
document.removeEventListener("mouseup", onUp);
|
|
164
|
+
setWidth((w) => {
|
|
165
|
+
try {
|
|
166
|
+
localStorage.setItem(widthKey, String(w));
|
|
167
|
+
} catch {
|
|
168
|
+
/* storage disabled — width just won't persist */
|
|
169
|
+
}
|
|
170
|
+
return w;
|
|
171
|
+
});
|
|
172
|
+
};
|
|
173
|
+
document.addEventListener("mousemove", onMove);
|
|
174
|
+
document.addEventListener("mouseup", onUp);
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
const activeRun = d.runs.find((r) => r.id === d.activeRunId) ?? null;
|
|
178
|
+
// A focused run shows the agent it actually ran with; Home reflects the current pick.
|
|
179
|
+
const activeAgentLabel = activeRun?.agent
|
|
180
|
+
? agentLabelFor(activeRun.agent)
|
|
181
|
+
: d.agentLabel;
|
|
182
|
+
// Header subtitle: the config a focused run actually used (it records agent /
|
|
183
|
+
// isolation / model / effort), or the current defaults on Home. Codex shows mode
|
|
184
|
+
// + reasoning effort; model is shown only when overridden. Clicking opens Settings.
|
|
185
|
+
const configLine = buildConfigLine(
|
|
186
|
+
activeRun ?? {
|
|
187
|
+
agent: d.selectedAgent,
|
|
188
|
+
isolated: d.isolated,
|
|
189
|
+
model: d.model,
|
|
190
|
+
effort: d.effort,
|
|
191
|
+
},
|
|
192
|
+
);
|
|
193
|
+
const detailTitle = activeRun
|
|
194
|
+
? `${activeRun.kind} ${activeRun.namespace ? `${activeRun.namespace}/` : ""}${activeRun.name}`
|
|
195
|
+
: "AI investigations";
|
|
196
|
+
|
|
197
|
+
// Absolute within the body frame: maximized fills it; docked is a right slot.
|
|
198
|
+
// topInset clears the header (the frame spans the full column incl. the header).
|
|
199
|
+
const positionStyle: React.CSSProperties = maximized
|
|
200
|
+
? { top: topInset, left: 0, right: 0, bottom: 0 }
|
|
201
|
+
: { top: topInset, right: 0, bottom: 0, width, maxWidth: "100%" };
|
|
202
|
+
|
|
203
|
+
// The detail pane (right side when expanded; the whole body when docked).
|
|
204
|
+
// Keyed by run id so toggling Expand doesn't remount a focused run's view.
|
|
205
|
+
const detail = d.needsConsent ? (
|
|
206
|
+
<div className="flex-1 overflow-y-auto px-4 py-3">
|
|
207
|
+
<div className={maximized ? "mx-auto max-w-3xl" : ""}>
|
|
208
|
+
<ConsentCard
|
|
209
|
+
agentName={d.agentLabel}
|
|
210
|
+
agent={d.selectedAgent}
|
|
211
|
+
isolated={d.isolated}
|
|
212
|
+
copy={consentCopy}
|
|
213
|
+
onOpenSettings={openSettings ?? undefined}
|
|
214
|
+
onApprove={d.approveConsent}
|
|
215
|
+
onCancel={d.cancelConsent}
|
|
216
|
+
/>
|
|
217
|
+
</div>
|
|
218
|
+
</div>
|
|
219
|
+
) : activeRun ? (
|
|
220
|
+
<InvestigationView
|
|
221
|
+
key={activeRun.id}
|
|
222
|
+
run={activeRun}
|
|
223
|
+
agentLabel={activeAgentLabel}
|
|
224
|
+
maximized={maximized}
|
|
225
|
+
/>
|
|
226
|
+
) : d.activeRunId && !d.runsLoaded ? (
|
|
227
|
+
// Deep-linked to a run before the list has ever loaded: show the load
|
|
228
|
+
// state (the 4s poll retries while the panel is open) — never the generic
|
|
229
|
+
// placeholder, and never a premature "no longer available".
|
|
230
|
+
<div className="flex flex-1 items-center justify-center px-6 text-center text-sm text-theme-text-tertiary">
|
|
231
|
+
{d.runsLoadFailed
|
|
232
|
+
? "Couldn't load investigations — retrying…"
|
|
233
|
+
: "Loading investigations…"}
|
|
234
|
+
</div>
|
|
235
|
+
) : d.activeRunId && d.runsLoaded ? (
|
|
236
|
+
// A focused id that isn't in the loaded list — a deep link (?ai-run=…) to a
|
|
237
|
+
// cleared/evicted/unknown run. Say so; the generic "select an
|
|
238
|
+
// investigation" placeholder would read as a broken link.
|
|
239
|
+
<div className="flex flex-1 flex-col items-center justify-center gap-3 px-6 text-center">
|
|
240
|
+
<p className="text-sm text-theme-text-secondary">
|
|
241
|
+
This investigation is no longer available — history keeps the most
|
|
242
|
+
recent investigations, and this one has been cleared.
|
|
243
|
+
</p>
|
|
244
|
+
<button
|
|
245
|
+
onClick={d.goHome}
|
|
246
|
+
className="rounded-lg border border-theme-border px-3 py-1.5 text-sm text-theme-text-secondary hover:bg-theme-hover"
|
|
247
|
+
>
|
|
248
|
+
View investigations
|
|
249
|
+
</button>
|
|
250
|
+
</div>
|
|
251
|
+
) : d.startError ? (
|
|
252
|
+
<div className="flex flex-1 flex-col items-center justify-center gap-3 px-6 text-center">
|
|
253
|
+
<p className="text-sm text-theme-text-secondary">{d.startError}</p>
|
|
254
|
+
<button
|
|
255
|
+
onClick={d.dismissError}
|
|
256
|
+
className="rounded-lg border border-theme-border px-3 py-1.5 text-sm text-theme-text-secondary hover:bg-theme-hover"
|
|
257
|
+
>
|
|
258
|
+
Dismiss
|
|
259
|
+
</button>
|
|
260
|
+
</div>
|
|
261
|
+
) : (
|
|
262
|
+
<div className="flex flex-1 items-center justify-center px-6 text-center text-sm text-theme-text-tertiary">
|
|
263
|
+
Select an investigation, or open a resource and click Diagnose.
|
|
264
|
+
</div>
|
|
265
|
+
);
|
|
266
|
+
|
|
267
|
+
const showBreadcrumb = !maximized && d.view !== "home";
|
|
268
|
+
// The maximized workspace always shows the recent-investigations list (there's
|
|
269
|
+
// room for it in full-wide) — it's the master pane of the master-detail layout.
|
|
270
|
+
const showHistory = maximized;
|
|
271
|
+
|
|
272
|
+
return (
|
|
273
|
+
<div
|
|
274
|
+
role="dialog"
|
|
275
|
+
aria-label="AI investigations"
|
|
276
|
+
className="absolute z-40 flex flex-col border-l border-theme-border bg-theme-surface shadow-drawer"
|
|
277
|
+
style={{
|
|
278
|
+
...positionStyle,
|
|
279
|
+
animation: "slide-in-from-right 0.22s cubic-bezier(0.32,0.72,0,1)",
|
|
280
|
+
}}
|
|
281
|
+
>
|
|
282
|
+
{!maximized && !narrow && (
|
|
283
|
+
<div
|
|
284
|
+
onMouseDown={startResize}
|
|
285
|
+
className="absolute left-0 top-0 z-10 h-full w-1.5 cursor-ew-resize bg-theme-border/40 transition-colors hover:bg-accent/50"
|
|
286
|
+
role="separator"
|
|
287
|
+
aria-orientation="vertical"
|
|
288
|
+
aria-label="Resize panel"
|
|
289
|
+
/>
|
|
290
|
+
)}
|
|
291
|
+
|
|
292
|
+
{/* Header */}
|
|
293
|
+
<div className="flex items-center justify-between border-b border-theme-border px-4 py-2.5">
|
|
294
|
+
<div className="flex min-w-0 items-center gap-2">
|
|
295
|
+
{!showBreadcrumb && (
|
|
296
|
+
<Sparkles className="h-4 w-4 shrink-0 text-accent" />
|
|
297
|
+
)}
|
|
298
|
+
<div className="min-w-0">
|
|
299
|
+
{showBreadcrumb && (
|
|
300
|
+
<button
|
|
301
|
+
onClick={d.goHome}
|
|
302
|
+
className="-ml-1 mb-0.5 flex items-center gap-0.5 rounded px-1 text-[11px] text-theme-text-tertiary hover:text-theme-text-primary"
|
|
303
|
+
>
|
|
304
|
+
<ChevronLeft className="h-3 w-3" />
|
|
305
|
+
Investigations
|
|
306
|
+
</button>
|
|
307
|
+
)}
|
|
308
|
+
<div className="truncate text-sm font-medium text-theme-text-primary">
|
|
309
|
+
{detailTitle}
|
|
310
|
+
</div>
|
|
311
|
+
<div className="flex items-center gap-1 text-xs text-theme-text-tertiary">
|
|
312
|
+
<span className="truncate">{configLine}</span>
|
|
313
|
+
{openSettings && (
|
|
314
|
+
<Tooltip content="AI settings" position="bottom">
|
|
315
|
+
<button
|
|
316
|
+
onClick={openSettings}
|
|
317
|
+
className="shrink-0 rounded p-0.5 text-theme-text-tertiary hover:text-theme-text-primary"
|
|
318
|
+
aria-label="AI settings"
|
|
319
|
+
>
|
|
320
|
+
<Settings2 className="h-3 w-3" />
|
|
321
|
+
</button>
|
|
322
|
+
</Tooltip>
|
|
323
|
+
)}
|
|
324
|
+
</div>
|
|
325
|
+
</div>
|
|
326
|
+
</div>
|
|
327
|
+
<div className="flex shrink-0 items-center gap-0.5">
|
|
328
|
+
{activeRun && <InvestigationMenu run={activeRun} />}
|
|
329
|
+
<Tooltip content={maximized ? "Restore" : "Expand"} position="bottom">
|
|
330
|
+
<button
|
|
331
|
+
onClick={() => setMaximized((v) => !v)}
|
|
332
|
+
className="rounded-md p-1 text-theme-text-tertiary hover:bg-theme-hover hover:text-theme-text-primary"
|
|
333
|
+
aria-label={maximized ? "Restore" : "Expand"}
|
|
334
|
+
>
|
|
335
|
+
{maximized ? (
|
|
336
|
+
<Minimize2 className="h-4 w-4" />
|
|
337
|
+
) : (
|
|
338
|
+
<Maximize2 className="h-4 w-4" />
|
|
339
|
+
)}
|
|
340
|
+
</button>
|
|
341
|
+
</Tooltip>
|
|
342
|
+
<Tooltip content="Close" position="bottom">
|
|
343
|
+
<button
|
|
344
|
+
onClick={d.close}
|
|
345
|
+
className="rounded-md p-1 text-theme-text-tertiary hover:bg-theme-hover hover:text-theme-text-primary"
|
|
346
|
+
aria-label="Close"
|
|
347
|
+
>
|
|
348
|
+
<X className="h-4 w-4" />
|
|
349
|
+
</button>
|
|
350
|
+
</Tooltip>
|
|
351
|
+
</div>
|
|
352
|
+
</div>
|
|
353
|
+
|
|
354
|
+
{/* Body. The detail wrapper keeps a stable position + key across both
|
|
355
|
+
layouts so toggling Expand doesn't remount a live InvestigationView
|
|
356
|
+
(which would discard its transcript and re-run the agent). The aside
|
|
357
|
+
only appears when expanded; keys keep the detail node identity-stable
|
|
358
|
+
as it comes and goes. */}
|
|
359
|
+
<div className="flex min-h-0 flex-1">
|
|
360
|
+
{showHistory && (
|
|
361
|
+
<aside
|
|
362
|
+
key="recent"
|
|
363
|
+
className="w-72 shrink-0 overflow-y-auto border-r border-theme-border px-3 py-3"
|
|
364
|
+
>
|
|
365
|
+
<RecentList
|
|
366
|
+
agentLabel={d.agentLabel}
|
|
367
|
+
runs={d.runs}
|
|
368
|
+
selectedId={d.activeRunId}
|
|
369
|
+
onSelect={d.openRun}
|
|
370
|
+
historyDegraded={d.historyDegraded}
|
|
371
|
+
/>
|
|
372
|
+
</aside>
|
|
373
|
+
)}
|
|
374
|
+
{!maximized && d.view === "home" ? (
|
|
375
|
+
<div
|
|
376
|
+
key="main"
|
|
377
|
+
className="flex-1 overflow-y-auto overflow-x-hidden px-4 py-3"
|
|
378
|
+
>
|
|
379
|
+
<RecentList
|
|
380
|
+
agentLabel={d.agentLabel}
|
|
381
|
+
runs={d.runs}
|
|
382
|
+
onSelect={d.openRun}
|
|
383
|
+
historyDegraded={d.historyDegraded}
|
|
384
|
+
/>
|
|
385
|
+
</div>
|
|
386
|
+
) : (
|
|
387
|
+
<div key="main" className="flex min-h-0 min-w-0 flex-1 flex-col">
|
|
388
|
+
{detail}
|
|
389
|
+
</div>
|
|
390
|
+
)}
|
|
391
|
+
</div>
|
|
392
|
+
</div>
|
|
393
|
+
);
|
|
394
|
+
}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
// The recent-investigations list — now backed by server-side runs (the source of
|
|
2
|
+
// truth), so background/running investigations appear here live. Used both as the
|
|
3
|
+
// docked Home view and the master pane of the maximized workspace.
|
|
4
|
+
import { Loader2, Sparkles } from "lucide-react";
|
|
5
|
+
import { StatusDot, type StatusTone } from "@skyhook-io/k8s-ui";
|
|
6
|
+
import { type RunSummary } from "../../api/diagnose";
|
|
7
|
+
|
|
8
|
+
// Compact "3m ago" / "2h ago" / date label.
|
|
9
|
+
function relativeTime(ts: number, now: number): string {
|
|
10
|
+
const s = Math.max(0, Math.round((now - ts) / 1000));
|
|
11
|
+
if (s < 60) return "just now";
|
|
12
|
+
if (s < 3600) return `${Math.floor(s / 60)}m ago`;
|
|
13
|
+
if (s < 86400) return `${Math.floor(s / 3600)}h ago`;
|
|
14
|
+
if (s < 7 * 86400) return `${Math.floor(s / 86400)}d ago`;
|
|
15
|
+
return new Date(ts).toLocaleDateString();
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Map a run status to the design-system status tone (StatusDot). stopped is
|
|
19
|
+
// user-initiated → neutral/unknown, NOT a failure (distinct from error).
|
|
20
|
+
function runTone(status: RunSummary["status"]): StatusTone {
|
|
21
|
+
switch (status) {
|
|
22
|
+
case "error":
|
|
23
|
+
return "unhealthy";
|
|
24
|
+
case "stale":
|
|
25
|
+
return "degraded";
|
|
26
|
+
case "done":
|
|
27
|
+
return "healthy";
|
|
28
|
+
default: // stopped
|
|
29
|
+
return "unknown";
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function statusDot(status: RunSummary["status"]) {
|
|
34
|
+
if (status === "running")
|
|
35
|
+
return <Loader2 className="h-3 w-3 shrink-0 animate-spin text-accent" />;
|
|
36
|
+
return <StatusDot tone={runTone(status)} className="shrink-0" />;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// A short text status for terminal non-done states, so the run's outcome doesn't
|
|
40
|
+
// rely on decoding a 6px colored dot (and so "I stopped it" reads differently from
|
|
41
|
+
// "it failed"). Done/running are conveyed by the dot + time already.
|
|
42
|
+
function statusWord(
|
|
43
|
+
status: RunSummary["status"],
|
|
44
|
+
): { text: string; cls: string } | null {
|
|
45
|
+
switch (status) {
|
|
46
|
+
case "error":
|
|
47
|
+
return { text: "Failed", cls: "text-red-400" };
|
|
48
|
+
case "stopped":
|
|
49
|
+
return { text: "Stopped", cls: "text-theme-text-tertiary" };
|
|
50
|
+
case "stale":
|
|
51
|
+
// Plain words, not the internal status name: "stale" means the run was
|
|
52
|
+
// about a cluster that's no longer connected.
|
|
53
|
+
return { text: "Different cluster", cls: "text-amber-500" };
|
|
54
|
+
default:
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function RecentList({
|
|
60
|
+
agentLabel,
|
|
61
|
+
runs,
|
|
62
|
+
onSelect,
|
|
63
|
+
selectedId,
|
|
64
|
+
historyDegraded = false,
|
|
65
|
+
}: {
|
|
66
|
+
agentLabel: string;
|
|
67
|
+
runs: RunSummary[];
|
|
68
|
+
onSelect: (id: string) => void;
|
|
69
|
+
selectedId?: string | null;
|
|
70
|
+
historyDegraded?: boolean;
|
|
71
|
+
}) {
|
|
72
|
+
const now = Date.now();
|
|
73
|
+
|
|
74
|
+
// Persistence broke (disk error) — without this the user reasonably assumes
|
|
75
|
+
// their history survives a restart, and it won't.
|
|
76
|
+
const degradedNote = historyDegraded ? (
|
|
77
|
+
<div className="mb-2 rounded-md border border-amber-500/40 bg-amber-500/10 px-2.5 py-1.5 text-[11px] leading-snug text-theme-text-secondary">
|
|
78
|
+
History isn't being saved right now (disk error) — investigations
|
|
79
|
+
won't survive a restart.
|
|
80
|
+
</div>
|
|
81
|
+
) : null;
|
|
82
|
+
|
|
83
|
+
if (runs.length === 0) {
|
|
84
|
+
return (
|
|
85
|
+
<div>
|
|
86
|
+
{degradedNote}
|
|
87
|
+
<div className="flex flex-col items-center px-4 py-12 text-center">
|
|
88
|
+
<Sparkles className="mb-3 h-7 w-7 text-accent" />
|
|
89
|
+
<div className="text-sm font-medium text-theme-text-primary">
|
|
90
|
+
No investigations yet
|
|
91
|
+
</div>
|
|
92
|
+
<p className="mt-1 max-w-xs text-sm text-theme-text-tertiary">
|
|
93
|
+
Open a resource and use its{" "}
|
|
94
|
+
<Sparkles className="inline h-3.5 w-3.5 align-text-bottom text-accent" />{" "}
|
|
95
|
+
action to investigate it with {agentLabel} —{" "}
|
|
96
|
+
<span className="font-medium text-theme-text-secondary">Diagnose</span>{" "}
|
|
97
|
+
a problem, or just ask about it. Investigations run in the background
|
|
98
|
+
and are kept in your history here.
|
|
99
|
+
</p>
|
|
100
|
+
</div>
|
|
101
|
+
</div>
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return (
|
|
106
|
+
<div className="space-y-2">
|
|
107
|
+
{degradedNote}
|
|
108
|
+
<div className="text-[11px] font-medium uppercase tracking-wide text-theme-text-tertiary">
|
|
109
|
+
Investigations
|
|
110
|
+
</div>
|
|
111
|
+
{runs.map((r) => (
|
|
112
|
+
<button
|
|
113
|
+
key={r.id}
|
|
114
|
+
onClick={() => onSelect(r.id)}
|
|
115
|
+
className={`flex w-full flex-col gap-0.5 rounded-md border px-2.5 py-2 text-left ${
|
|
116
|
+
r.id === selectedId
|
|
117
|
+
? "border-accent/50 bg-accent/10"
|
|
118
|
+
: "border-theme-border/60 bg-theme-base/40 hover:bg-theme-hover"
|
|
119
|
+
}`}
|
|
120
|
+
>
|
|
121
|
+
<div className="flex items-center gap-2">
|
|
122
|
+
{statusDot(r.status)}
|
|
123
|
+
<span className="min-w-0 flex-1 truncate text-sm text-theme-text-primary">
|
|
124
|
+
{r.kind} {r.namespace ? `${r.namespace}/` : ""}
|
|
125
|
+
{r.name}
|
|
126
|
+
</span>
|
|
127
|
+
<span className="shrink-0 text-[11px] text-theme-text-tertiary">
|
|
128
|
+
{r.status === "running" ? (
|
|
129
|
+
"running…"
|
|
130
|
+
) : (
|
|
131
|
+
<>
|
|
132
|
+
{(() => {
|
|
133
|
+
const w = statusWord(r.status);
|
|
134
|
+
return w ? (
|
|
135
|
+
<span className={`font-medium ${w.cls}`}>
|
|
136
|
+
{w.text} ·{" "}
|
|
137
|
+
</span>
|
|
138
|
+
) : null;
|
|
139
|
+
})()}
|
|
140
|
+
{relativeTime(new Date(r.updatedAt).getTime(), now)}
|
|
141
|
+
</>
|
|
142
|
+
)}
|
|
143
|
+
</span>
|
|
144
|
+
</div>
|
|
145
|
+
{(r.status === "stale" && r.context) || r.preview ? (
|
|
146
|
+
<div className="truncate pl-3.5 text-xs text-theme-text-tertiary">
|
|
147
|
+
{/* A foreign-cluster run names its cluster — in mixed multi-
|
|
148
|
+
context history, identical-looking rows otherwise give no way
|
|
149
|
+
to tell WHICH cluster an investigation was about. */}
|
|
150
|
+
{r.status === "stale" && r.context ? (
|
|
151
|
+
<span className="text-amber-600/80 dark:text-amber-500/80">
|
|
152
|
+
{r.context}
|
|
153
|
+
</span>
|
|
154
|
+
) : null}
|
|
155
|
+
{r.status === "stale" && r.context && r.preview ? " · " : ""}
|
|
156
|
+
{r.preview}
|
|
157
|
+
</div>
|
|
158
|
+
) : null}
|
|
159
|
+
</button>
|
|
160
|
+
))}
|
|
161
|
+
</div>
|
|
162
|
+
);
|
|
163
|
+
}
|