@skyhook-io/k8s-ui 1.8.11 → 1.8.13
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 +4 -4
- package/src/components/applications/ApplicationDetail.test.tsx +136 -8
- package/src/components/applications/ApplicationDetail.tsx +1215 -487
- package/src/components/gitops/GitOpsDetailLayout.tsx +13 -3
- package/src/components/gitops/GitOpsTableView.tsx +10 -3
- package/src/components/gitops/SyncOptionsDialog.test.ts +48 -0
- package/src/components/gitops/SyncOptionsDialog.tsx +189 -45
- package/src/components/gitops/insights/ArgoResourceDiff.tsx +172 -0
- package/src/components/gitops/insights/GitOpsInsightViews.tsx +201 -21
- package/src/components/gitops/insights/index.ts +1 -0
- package/src/components/gitops/insights/insights-helpers.test.ts +16 -0
- package/src/components/gitops/insights/insights-helpers.ts +5 -0
- package/src/components/gitops/tree/GitOpsTreeGraph.tsx +5 -0
- package/src/components/issues/IssuesView.tsx +55 -29
- package/src/components/issues/ResourceIssuesSection.test.tsx +33 -1
- package/src/components/issues/ResourceIssuesSection.tsx +21 -1
- package/src/components/issues/severity.ts +1 -0
- package/src/components/resources/ResourcesView.tsx +109 -19
- package/src/components/resources/column-filter-serialization.test.ts +79 -1
- package/src/components/resources/renderers/AWSMachineRenderer.tsx +3 -2
- package/src/components/resources/renderers/AWSManagedClusterRenderer.tsx +3 -2
- package/src/components/resources/renderers/AWSManagedControlPlaneRenderer.tsx +3 -2
- package/src/components/resources/renderers/AWSManagedMachinePoolRenderer.tsx +3 -2
- package/src/components/resources/renderers/AzureMachineRenderer.tsx +3 -2
- package/src/components/resources/renderers/AzureManagedControlPlaneRenderer.tsx +3 -2
- package/src/components/resources/renderers/AzureManagedMachinePoolRenderer.tsx +3 -2
- package/src/components/resources/renderers/CAPIClusterRenderer.tsx +3 -2
- package/src/components/resources/renderers/CAPIKubeadmControlPlaneRenderer.tsx +3 -2
- package/src/components/resources/renderers/CAPIMachineDeploymentRenderer.tsx +3 -2
- package/src/components/resources/renderers/CAPIMachineHealthCheckRenderer.tsx +3 -2
- package/src/components/resources/renderers/CAPIMachinePoolRenderer.tsx +3 -2
- package/src/components/resources/renderers/CAPIMachineRenderer.tsx +3 -2
- package/src/components/resources/renderers/CAPIMachineSetRenderer.tsx +3 -2
- package/src/components/resources/renderers/CertificateRenderer.tsx +3 -2
- package/src/components/resources/renderers/CertificateRequestRenderer.tsx +3 -2
- package/src/components/resources/renderers/CompositeRenderer.tsx +3 -2
- package/src/components/resources/renderers/ExternalSecretRenderer.tsx +3 -2
- package/src/components/resources/renderers/GCPMachineRenderer.tsx +3 -2
- package/src/components/resources/renderers/GCPManagedControlPlaneRenderer.tsx +3 -2
- package/src/components/resources/renderers/GCPManagedMachinePoolRenderer.tsx +3 -2
- package/src/components/resources/renderers/GRPCRouteRenderer.tsx +7 -3
- package/src/components/resources/renderers/GatewayClassRenderer.tsx +3 -2
- package/src/components/resources/renderers/HTTPRouteRenderer.tsx +7 -3
- package/src/components/resources/renderers/KarpenterEC2NodeClassRenderer.tsx +3 -2
- package/src/components/resources/renderers/KarpenterNodeClaimRenderer.tsx +3 -2
- package/src/components/resources/renderers/KarpenterNodePoolRenderer.tsx +3 -2
- package/src/components/resources/renderers/KedaScaledJobRenderer.tsx +3 -2
- package/src/components/resources/renderers/KedaScaledObjectRenderer.tsx +3 -2
- package/src/components/resources/renderers/ManagedResourceRenderer.tsx +3 -2
- package/src/components/resources/renderers/PVCRenderer.tsx +3 -2
- package/src/components/resources/renderers/SealedSecretRenderer.tsx +3 -2
- package/src/components/resources/renderers/SecretStoreRenderer.tsx +3 -2
- package/src/components/resources/renderers/SimpleRouteRenderer.tsx +7 -3
- package/src/components/resources/resource-utils-argo.test.ts +18 -1
- package/src/components/resources/resource-utils-argo.ts +8 -0
- package/src/components/resources/resource-utils.ts +94 -25
- package/src/components/shared/ResourceActionsBar.tsx +54 -22
- package/src/components/shared/UnifiedDiff.tsx +51 -0
- package/src/components/shared/index.ts +1 -0
- package/src/components/timeline/TimelineList.tsx +7 -3
- package/src/components/timeline/TimelineToolbarState.test.tsx +24 -0
- package/src/components/topology/TopologyBreadcrumb.tsx +30 -0
- package/src/components/topology/TopologyControls.tsx +3 -1
- package/src/components/topology/TopologyGraph.tsx +76 -90
- package/src/components/topology/TopologyOverlayBar.tsx +30 -0
- package/src/components/topology/TopologySearch.tsx +23 -15
- package/src/components/topology/index.ts +2 -0
- package/src/components/ui/Badge.tsx +1 -1
- package/src/components/ui/SelectMenu.tsx +81 -0
- package/src/components/ui/Tooltip.test.tsx +21 -0
- package/src/components/ui/Tooltip.tsx +9 -12
- package/src/components/ui/drawer-components.tsx +9 -6
- package/src/components/ui/index.ts +2 -0
- package/src/components/workload/WorkloadView.tsx +24 -3
- package/src/theme/tailwind-theme.css +1 -0
- package/src/theme/variables.css +1 -0
- package/src/types/gitops-insights.ts +69 -1
- package/src/types/gitops.ts +3 -3
- package/src/utils/api-resources.test.ts +36 -1
- package/src/utils/api-resources.ts +16 -0
- package/src/utils/application-history.test.ts +538 -0
- package/src/utils/application-history.ts +257 -0
- package/src/utils/applications.test.ts +34 -0
- package/src/utils/applications.ts +25 -0
- package/src/utils/index.ts +1 -0
- package/src/utils/resource-hierarchy.test.ts +35 -1
- package/src/utils/resource-hierarchy.ts +20 -0
|
@@ -1,19 +1,52 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
import {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
1
|
+
import {
|
|
2
|
+
useMemo,
|
|
3
|
+
useState,
|
|
4
|
+
useCallback,
|
|
5
|
+
useEffect,
|
|
6
|
+
useRef,
|
|
7
|
+
type ReactNode,
|
|
8
|
+
} from "react";
|
|
9
|
+
import {
|
|
10
|
+
AlertTriangle,
|
|
11
|
+
Activity,
|
|
12
|
+
ArrowLeft,
|
|
13
|
+
Boxes,
|
|
14
|
+
ChevronDown,
|
|
15
|
+
Clock3,
|
|
16
|
+
DollarSign,
|
|
17
|
+
ExternalLink,
|
|
18
|
+
GitCommit,
|
|
19
|
+
Layers,
|
|
20
|
+
Radio,
|
|
21
|
+
RefreshCw,
|
|
22
|
+
Search,
|
|
23
|
+
} from "lucide-react";
|
|
24
|
+
import { clsx } from "clsx";
|
|
25
|
+
import type { ResourceRef, Topology, TopologyNode } from "../../types";
|
|
26
|
+
import argoCdLogo from "../../assets/gitops/argocd.png";
|
|
27
|
+
import fluxLogo from "../../assets/gitops/flux.svg";
|
|
28
|
+
import { StatusDot, mapHealthToTone } from "../ui/status-tone";
|
|
29
|
+
import { Tooltip } from "../ui/Tooltip";
|
|
30
|
+
import { EmptyState } from "../ui/EmptyState";
|
|
31
|
+
import { ResourceRefBadge } from "../ui/drawer-components";
|
|
32
|
+
import { TopologyGraph } from "../topology/TopologyGraph";
|
|
33
|
+
import { pluralize } from "../../utils/pluralize";
|
|
34
|
+
import { kindToPlural, refToSelectedResource } from "../../utils/navigation";
|
|
35
|
+
import {
|
|
36
|
+
batchRunParentNodes,
|
|
37
|
+
tagWorkloadOwnership,
|
|
38
|
+
seedNodeIds,
|
|
39
|
+
ownershipOf,
|
|
40
|
+
workloadKey,
|
|
41
|
+
type NeighborhoodSeed,
|
|
42
|
+
} from "../../utils/topology-neighborhood";
|
|
43
|
+
import {
|
|
44
|
+
workloadHue,
|
|
45
|
+
NEUTRAL_OWNER,
|
|
46
|
+
type WorkloadFocus,
|
|
47
|
+
} from "../../utils/workload-colors";
|
|
48
|
+
import { getTopologyIcon } from "../../utils/resource-icons";
|
|
49
|
+
import { getKindLabel } from "../../utils/api-resources";
|
|
17
50
|
import {
|
|
18
51
|
type AppRow,
|
|
19
52
|
type AppHistory,
|
|
@@ -43,13 +76,31 @@ import {
|
|
|
43
76
|
compareVersions,
|
|
44
77
|
appSourceLabel,
|
|
45
78
|
overlayProvenance,
|
|
46
|
-
} from
|
|
47
|
-
import { PaneLoader } from
|
|
48
|
-
import { midTruncate } from
|
|
49
|
-
import { VersionTooltip, AppIdentityTooltip } from
|
|
50
|
-
import {
|
|
51
|
-
|
|
52
|
-
|
|
79
|
+
} from "../../utils/applications";
|
|
80
|
+
import { PaneLoader } from "../ui/PaneLoader";
|
|
81
|
+
import { midTruncate } from "../../utils/format";
|
|
82
|
+
import { VersionTooltip, AppIdentityTooltip } from "./AppTooltips";
|
|
83
|
+
import {
|
|
84
|
+
ProvenanceBadge,
|
|
85
|
+
ClassBadge,
|
|
86
|
+
CategoryChip,
|
|
87
|
+
VersionInfo,
|
|
88
|
+
BatchSignalChip,
|
|
89
|
+
} from "./AppChips";
|
|
90
|
+
import { ReadyBar } from "./ReadyBar";
|
|
91
|
+
import {
|
|
92
|
+
collapseStableReplicaSets,
|
|
93
|
+
layerDeploymentInventory,
|
|
94
|
+
topologyGroup,
|
|
95
|
+
type DeploymentInventory,
|
|
96
|
+
type DeploymentTopologyLayer,
|
|
97
|
+
} from "../../utils/application-topology";
|
|
98
|
+
import {
|
|
99
|
+
buildApplicationHistoryItems,
|
|
100
|
+
type ApplicationHistoryCategory,
|
|
101
|
+
type ApplicationHistoryItem,
|
|
102
|
+
type ApplicationHistoryRange,
|
|
103
|
+
} from "../../utils/application-history";
|
|
53
104
|
|
|
54
105
|
// ApplicationDetail owns the application chrome and scope switcher. The selected
|
|
55
106
|
// scope decides the one tab row shown in the detail pane: app scope gets
|
|
@@ -77,10 +128,17 @@ export interface AppIdentityInstance {
|
|
|
77
128
|
type SelectionProps =
|
|
78
129
|
| {
|
|
79
130
|
selectedWorkloadKey: string | null;
|
|
80
|
-
onSelectWorkload: (
|
|
131
|
+
onSelectWorkload: (
|
|
132
|
+
key: string | null,
|
|
133
|
+
options?: AppWorkloadSelectionOptions,
|
|
134
|
+
) => void;
|
|
81
135
|
}
|
|
82
136
|
| { selectedWorkloadKey?: undefined; onSelectWorkload?: undefined };
|
|
83
137
|
|
|
138
|
+
export interface AppWorkloadSelectionOptions {
|
|
139
|
+
tab?: string;
|
|
140
|
+
}
|
|
141
|
+
|
|
84
142
|
type CanonicalApplicationView = "overview" | "topology" | "history";
|
|
85
143
|
|
|
86
144
|
export type ApplicationView = CanonicalApplicationView;
|
|
@@ -114,6 +172,17 @@ export type ApplicationDetailProps = {
|
|
|
114
172
|
onBack: () => void;
|
|
115
173
|
/** Render the host's WorkloadView for the chosen workload. */
|
|
116
174
|
renderWorkload: (workload: SelectedAppWorkload) => ReactNode;
|
|
175
|
+
/** Optional host-rendered app-scope Cost view. When absent, no Cost tab is shown. */
|
|
176
|
+
renderCostView?: (props: {
|
|
177
|
+
app: AppRow;
|
|
178
|
+
workloads: AppWorkload[];
|
|
179
|
+
onSelectWorkload: (
|
|
180
|
+
workload: AppWorkload,
|
|
181
|
+
options?: AppWorkloadSelectionOptions,
|
|
182
|
+
) => void;
|
|
183
|
+
}) => ReactNode;
|
|
184
|
+
costViewSelected?: boolean;
|
|
185
|
+
onSelectCostView?: () => void;
|
|
117
186
|
/** Render host-scoped operational issues for the app overview. */
|
|
118
187
|
renderOverviewIssues?: () => ReactNode;
|
|
119
188
|
/** Whether the host-scoped issue surface contains current issues. */
|
|
@@ -123,9 +192,9 @@ export type ApplicationDetailProps = {
|
|
|
123
192
|
topology?: Topology;
|
|
124
193
|
/** True while the host's topology fetch is in flight. Without it, a
|
|
125
194
|
* multi-workload app can briefly show an empty topology while topology loads. */
|
|
126
|
-
topologyLoading?: boolean
|
|
195
|
+
topologyLoading?: boolean;
|
|
127
196
|
/** Exact resources reported by the app's Argo CD, Flux, or Helm deployment source. */
|
|
128
|
-
deploymentInventory?: DeploymentInventory
|
|
197
|
+
deploymentInventory?: DeploymentInventory;
|
|
129
198
|
/** Open a related (non-workload) resource clicked in the app graph. */
|
|
130
199
|
onNavigateToResource?: (resource: {
|
|
131
200
|
kind: string;
|
|
@@ -158,6 +227,19 @@ export type ApplicationDetailProps = {
|
|
|
158
227
|
activeInstanceKey?: string;
|
|
159
228
|
history?: AppHistory;
|
|
160
229
|
historyLoading?: boolean;
|
|
230
|
+
historyItems?: ApplicationHistoryItem[];
|
|
231
|
+
historyRuntimeLoading?: boolean;
|
|
232
|
+
historyRuntimeError?: boolean;
|
|
233
|
+
historyMode?: "local" | "retained";
|
|
234
|
+
historyRange?: ApplicationHistoryRange;
|
|
235
|
+
historyRangeOptions?: Array<{
|
|
236
|
+
value: ApplicationHistoryRange;
|
|
237
|
+
label: string;
|
|
238
|
+
}>;
|
|
239
|
+
historyCoverageRecordCount?: number;
|
|
240
|
+
historyRuntimeLimited?: boolean;
|
|
241
|
+
onHistoryRangeChange?: (range: ApplicationHistoryRange) => void;
|
|
242
|
+
onOpenTimeline?: (timestamp?: string) => void;
|
|
161
243
|
onOpenSource?: (source: AppSourceRef) => void;
|
|
162
244
|
} & SelectionProps &
|
|
163
245
|
ViewProps;
|
|
@@ -220,7 +302,42 @@ function compareDefinedVersions(
|
|
|
220
302
|
return compareVersions(a, b) ?? 0;
|
|
221
303
|
}
|
|
222
304
|
|
|
223
|
-
export function ApplicationDetail({
|
|
305
|
+
export function ApplicationDetail({
|
|
306
|
+
app,
|
|
307
|
+
onBack,
|
|
308
|
+
renderWorkload,
|
|
309
|
+
renderCostView,
|
|
310
|
+
costViewSelected,
|
|
311
|
+
onSelectCostView,
|
|
312
|
+
renderOverviewIssues,
|
|
313
|
+
hasOverviewIssues,
|
|
314
|
+
topology,
|
|
315
|
+
topologyLoading,
|
|
316
|
+
deploymentInventory,
|
|
317
|
+
onNavigateToResource,
|
|
318
|
+
onSelectWorkloadRun,
|
|
319
|
+
identityInstances,
|
|
320
|
+
onSwitchInstance,
|
|
321
|
+
discoveredEnvs,
|
|
322
|
+
activeInstanceKey,
|
|
323
|
+
history,
|
|
324
|
+
historyLoading,
|
|
325
|
+
historyItems,
|
|
326
|
+
historyRuntimeLoading,
|
|
327
|
+
historyRuntimeError,
|
|
328
|
+
historyMode,
|
|
329
|
+
historyRange,
|
|
330
|
+
historyRangeOptions,
|
|
331
|
+
historyCoverageRecordCount,
|
|
332
|
+
historyRuntimeLimited,
|
|
333
|
+
onHistoryRangeChange,
|
|
334
|
+
onOpenTimeline,
|
|
335
|
+
onOpenSource,
|
|
336
|
+
selectedWorkloadKey,
|
|
337
|
+
onSelectWorkload,
|
|
338
|
+
selectedView,
|
|
339
|
+
onSelectView,
|
|
340
|
+
}: ApplicationDetailProps) {
|
|
224
341
|
// Stable order regardless of API ordering: rail rows and the per-workload
|
|
225
342
|
// color assignment both follow this array, so an order flap between
|
|
226
343
|
// refetches must not reshuffle rows or reassign a workload's hue.
|
|
@@ -262,14 +379,27 @@ export function ApplicationDetail({ app, onBack, renderWorkload, renderOverviewI
|
|
|
262
379
|
const [internalView, setInternalView] = useState<CanonicalApplicationView>(
|
|
263
380
|
DEFAULT_APPLICATION_VIEW,
|
|
264
381
|
);
|
|
382
|
+
const [internalCostSelected, setInternalCostSelected] = useState(false);
|
|
265
383
|
const activeView = canonicalApplicationView(selectedView ?? internalView);
|
|
384
|
+
const costSelected = Boolean(
|
|
385
|
+
renderCostView && (costViewSelected ?? internalCostSelected),
|
|
386
|
+
);
|
|
266
387
|
const setView = useCallback(
|
|
267
|
-
(view: CanonicalApplicationView) =>
|
|
268
|
-
|
|
388
|
+
(view: CanonicalApplicationView) => {
|
|
389
|
+
setInternalCostSelected(false);
|
|
390
|
+
if (onSelectView) onSelectView(view);
|
|
391
|
+
else setInternalView(view);
|
|
392
|
+
},
|
|
269
393
|
[onSelectView],
|
|
270
394
|
);
|
|
395
|
+
const selectCostView = useCallback(() => {
|
|
396
|
+
if (!renderCostView) return;
|
|
397
|
+
if (onSelectCostView) onSelectCostView();
|
|
398
|
+
else setInternalCostSelected(true);
|
|
399
|
+
}, [onSelectCostView, renderCostView]);
|
|
271
400
|
useEffect(() => {
|
|
272
401
|
if (selectedView === undefined) setInternalView(DEFAULT_APPLICATION_VIEW);
|
|
402
|
+
setInternalCostSelected(false);
|
|
273
403
|
}, [app.key, selectedView]);
|
|
274
404
|
|
|
275
405
|
const [internalSelected, setInternalSelected] = useState<string | null>(null);
|
|
@@ -280,8 +410,10 @@ export function ApplicationDetail({ app, onBack, renderWorkload, renderOverviewI
|
|
|
280
410
|
? selectedWorkloadKey
|
|
281
411
|
: (internalSelected ?? implicitSingleWorkloadKey);
|
|
282
412
|
const setSelected = useCallback(
|
|
283
|
-
(key: string | null) =>
|
|
284
|
-
onSelectWorkload
|
|
413
|
+
(key: string | null, options?: AppWorkloadSelectionOptions) =>
|
|
414
|
+
onSelectWorkload
|
|
415
|
+
? onSelectWorkload(key, options)
|
|
416
|
+
: setInternalSelected(key),
|
|
285
417
|
[onSelectWorkload],
|
|
286
418
|
);
|
|
287
419
|
const selectedWorkload = rawSelected
|
|
@@ -304,17 +436,19 @@ export function ApplicationDetail({ app, onBack, renderWorkload, renderOverviewI
|
|
|
304
436
|
// Hover-focus: the workload (or NEUTRAL_OWNER) whose nodes should stay lit
|
|
305
437
|
// while the rest of the graph dims. Driven by the rail and, reciprocally, by
|
|
306
438
|
// hovering a node.
|
|
307
|
-
const [focusedOwnerId, setFocusedOwnerId] = useState<WorkloadFocus>(null)
|
|
308
|
-
const [expandedReplicaSetOwners, setExpandedReplicaSetOwners] = useState<
|
|
309
|
-
|
|
439
|
+
const [focusedOwnerId, setFocusedOwnerId] = useState<WorkloadFocus>(null);
|
|
440
|
+
const [expandedReplicaSetOwners, setExpandedReplicaSetOwners] = useState<
|
|
441
|
+
Set<string>
|
|
442
|
+
>(new Set());
|
|
443
|
+
useEffect(() => setExpandedReplicaSetOwners(new Set()), [app.key]);
|
|
310
444
|
const toggleReplicaSets = useCallback((ownerID: string) => {
|
|
311
445
|
setExpandedReplicaSetOwners((current) => {
|
|
312
|
-
const next = new Set(current)
|
|
313
|
-
if (next.has(ownerID)) next.delete(ownerID)
|
|
314
|
-
else next.add(ownerID)
|
|
315
|
-
return next
|
|
316
|
-
})
|
|
317
|
-
}, [])
|
|
446
|
+
const next = new Set(current);
|
|
447
|
+
if (next.has(ownerID)) next.delete(ownerID);
|
|
448
|
+
else next.add(ownerID);
|
|
449
|
+
return next;
|
|
450
|
+
});
|
|
451
|
+
}, []);
|
|
318
452
|
|
|
319
453
|
const appSeeds = useMemo(
|
|
320
454
|
() =>
|
|
@@ -328,20 +462,31 @@ export function ApplicationDetail({ app, onBack, renderWorkload, renderOverviewI
|
|
|
328
462
|
);
|
|
329
463
|
// Neighborhood subgraph + per-workload color/ownership tagging in one pass.
|
|
330
464
|
const composedTopology = useMemo(
|
|
331
|
-
() =>
|
|
465
|
+
() =>
|
|
466
|
+
topology
|
|
467
|
+
? collapseStableReplicaSets(topology, expandedReplicaSetOwners)
|
|
468
|
+
: null,
|
|
332
469
|
[topology, expandedReplicaSetOwners],
|
|
333
|
-
)
|
|
470
|
+
);
|
|
334
471
|
const ownership = useMemo(
|
|
335
|
-
() =>
|
|
472
|
+
() =>
|
|
473
|
+
composedTopology
|
|
474
|
+
? tagWorkloadOwnership(composedTopology, appSeeds)
|
|
475
|
+
: null,
|
|
336
476
|
[composedTopology, appSeeds],
|
|
337
|
-
)
|
|
338
|
-
const appGraph = ownership?.topology ?? null
|
|
477
|
+
);
|
|
478
|
+
const appGraph = ownership?.topology ?? null;
|
|
339
479
|
const deploymentLayer = useMemo(
|
|
340
|
-
() =>
|
|
341
|
-
|
|
342
|
-
|
|
480
|
+
() =>
|
|
481
|
+
appGraph && deploymentInventory && app.sourceRef
|
|
482
|
+
? layerDeploymentInventory(
|
|
483
|
+
appGraph,
|
|
484
|
+
deploymentInventory,
|
|
485
|
+
sourceInventoryLabel(app.sourceRef),
|
|
486
|
+
)
|
|
487
|
+
: null,
|
|
343
488
|
[appGraph, deploymentInventory, app.sourceRef],
|
|
344
|
-
)
|
|
489
|
+
);
|
|
345
490
|
const appGraphFocusId = useMemo(
|
|
346
491
|
() => (topology ? seedNodeIds(topology, appSeeds)[0] : undefined),
|
|
347
492
|
[topology, appSeeds],
|
|
@@ -370,11 +515,14 @@ export function ApplicationDetail({ app, onBack, renderWorkload, renderOverviewI
|
|
|
370
515
|
const handleAppNodeClick = useCallback(
|
|
371
516
|
(node: TopologyNode) => {
|
|
372
517
|
if (node.data?.sourceInventoryGroup && app.sourceRef) {
|
|
373
|
-
onOpenSource?.(app.sourceRef)
|
|
374
|
-
return
|
|
518
|
+
onOpenSource?.(app.sourceRef);
|
|
519
|
+
return;
|
|
375
520
|
}
|
|
376
|
-
const ns = (node.data?.namespace as string) ||
|
|
377
|
-
const match = workloads.find(
|
|
521
|
+
const ns = (node.data?.namespace as string) || "";
|
|
522
|
+
const match = workloads.find(
|
|
523
|
+
(w) =>
|
|
524
|
+
w.kind === node.kind && w.name === node.name && w.namespace === ns,
|
|
525
|
+
);
|
|
378
526
|
if (match) {
|
|
379
527
|
setSelected(workloadKey(match));
|
|
380
528
|
return;
|
|
@@ -399,14 +547,22 @@ export function ApplicationDetail({ app, onBack, renderWorkload, renderOverviewI
|
|
|
399
547
|
namespace: ns,
|
|
400
548
|
name: node.name,
|
|
401
549
|
group: topologyGroup(node),
|
|
402
|
-
})
|
|
550
|
+
});
|
|
403
551
|
},
|
|
404
|
-
[
|
|
405
|
-
|
|
552
|
+
[
|
|
553
|
+
app.sourceRef,
|
|
554
|
+
appGraph,
|
|
555
|
+
workloads,
|
|
556
|
+
onNavigateToResource,
|
|
557
|
+
onOpenSource,
|
|
558
|
+
onSelectWorkloadRun,
|
|
559
|
+
setSelected,
|
|
560
|
+
],
|
|
561
|
+
);
|
|
406
562
|
|
|
407
|
-
const appTopology = deploymentLayer?.topology ?? appGraph ?? null
|
|
408
|
-
const appTopologyAvailable = !!appTopology && appTopology.nodes.length > 0
|
|
409
|
-
const colorByWorkload = ownership?.colorByWorkload ?? null
|
|
563
|
+
const appTopology = deploymentLayer?.topology ?? appGraph ?? null;
|
|
564
|
+
const appTopologyAvailable = !!appTopology && appTopology.nodes.length > 0;
|
|
565
|
+
const colorByWorkload = ownership?.colorByWorkload ?? null;
|
|
410
566
|
|
|
411
567
|
return (
|
|
412
568
|
<div className="flex min-h-0 w-full flex-1 flex-col bg-theme-base">
|
|
@@ -561,7 +717,10 @@ export function ApplicationDetail({ app, onBack, renderWorkload, renderOverviewI
|
|
|
561
717
|
<ApplicationWorkspace
|
|
562
718
|
app={app}
|
|
563
719
|
activeView={activeView}
|
|
720
|
+
costSelected={costSelected}
|
|
564
721
|
onViewChange={setView}
|
|
722
|
+
onCostViewChange={selectCostView}
|
|
723
|
+
renderCostView={renderCostView}
|
|
565
724
|
workloads={workloads}
|
|
566
725
|
ready={ready}
|
|
567
726
|
desired={desired}
|
|
@@ -577,10 +736,22 @@ export function ApplicationDetail({ app, onBack, renderWorkload, renderOverviewI
|
|
|
577
736
|
onNodeClick={handleAppNodeClick}
|
|
578
737
|
onNodeHover={handleNodeHover}
|
|
579
738
|
onFocusWorkload={setFocusedOwnerId}
|
|
580
|
-
onSelectWorkload={(workload) =>
|
|
739
|
+
onSelectWorkload={(workload, options) =>
|
|
740
|
+
setSelected(workloadKey(workload), options)
|
|
741
|
+
}
|
|
581
742
|
onNavigateToResource={onNavigateToResource}
|
|
582
743
|
history={history}
|
|
583
744
|
historyLoading={historyLoading}
|
|
745
|
+
historyItems={historyItems}
|
|
746
|
+
historyRuntimeLoading={historyRuntimeLoading}
|
|
747
|
+
historyRuntimeError={historyRuntimeError}
|
|
748
|
+
historyMode={historyMode}
|
|
749
|
+
historyRange={historyRange}
|
|
750
|
+
historyRangeOptions={historyRangeOptions}
|
|
751
|
+
historyCoverageRecordCount={historyCoverageRecordCount}
|
|
752
|
+
historyRuntimeLimited={historyRuntimeLimited}
|
|
753
|
+
onHistoryRangeChange={onHistoryRangeChange}
|
|
754
|
+
onOpenTimeline={onOpenTimeline}
|
|
584
755
|
onOpenSource={onOpenSource}
|
|
585
756
|
onToggleReplicaSets={toggleReplicaSets}
|
|
586
757
|
renderOverviewIssues={renderOverviewIssues}
|
|
@@ -595,7 +766,10 @@ export function ApplicationDetail({ app, onBack, renderWorkload, renderOverviewI
|
|
|
595
766
|
function ApplicationWorkspace({
|
|
596
767
|
app,
|
|
597
768
|
activeView,
|
|
769
|
+
costSelected,
|
|
598
770
|
onViewChange,
|
|
771
|
+
onCostViewChange,
|
|
772
|
+
renderCostView,
|
|
599
773
|
workloads,
|
|
600
774
|
ready,
|
|
601
775
|
desired,
|
|
@@ -615,49 +789,87 @@ function ApplicationWorkspace({
|
|
|
615
789
|
onNavigateToResource,
|
|
616
790
|
history,
|
|
617
791
|
historyLoading,
|
|
792
|
+
historyItems,
|
|
793
|
+
historyRuntimeLoading,
|
|
794
|
+
historyRuntimeError,
|
|
795
|
+
historyMode,
|
|
796
|
+
historyRange,
|
|
797
|
+
historyRangeOptions,
|
|
798
|
+
historyCoverageRecordCount,
|
|
799
|
+
historyRuntimeLimited,
|
|
800
|
+
onHistoryRangeChange,
|
|
801
|
+
onOpenTimeline,
|
|
618
802
|
onOpenSource,
|
|
619
803
|
onToggleReplicaSets,
|
|
620
804
|
renderOverviewIssues,
|
|
621
805
|
hasOverviewIssues,
|
|
622
806
|
}: {
|
|
623
|
-
app: AppRow
|
|
624
|
-
activeView: CanonicalApplicationView
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
807
|
+
app: AppRow;
|
|
808
|
+
activeView: CanonicalApplicationView;
|
|
809
|
+
costSelected: boolean;
|
|
810
|
+
onViewChange: (view: CanonicalApplicationView) => void;
|
|
811
|
+
onCostViewChange: () => void;
|
|
812
|
+
renderCostView?: ApplicationDetailProps["renderCostView"];
|
|
813
|
+
workloads: AppWorkload[];
|
|
814
|
+
ready: number;
|
|
815
|
+
desired: number;
|
|
816
|
+
versions: string[];
|
|
817
|
+
topology: Topology | null;
|
|
818
|
+
topologyLoading?: boolean;
|
|
819
|
+
deploymentLayer: DeploymentTopologyLayer | null;
|
|
820
|
+
deploymentSource?: AppSourceRef;
|
|
821
|
+
topologyAvailable: boolean;
|
|
822
|
+
focusNodeId?: string;
|
|
823
|
+
focusedOwnerId: WorkloadFocus;
|
|
824
|
+
colorByWorkload: Map<string, number> | null;
|
|
825
|
+
onNodeClick: (node: TopologyNode) => void;
|
|
826
|
+
onNodeHover: (node: TopologyNode | null) => void;
|
|
827
|
+
onFocusWorkload: (owner: WorkloadFocus) => void;
|
|
828
|
+
onSelectWorkload: (
|
|
829
|
+
workload: AppWorkload,
|
|
830
|
+
options?: AppWorkloadSelectionOptions,
|
|
831
|
+
) => void;
|
|
832
|
+
onNavigateToResource?: (resource: {
|
|
833
|
+
kind: string;
|
|
834
|
+
namespace: string;
|
|
835
|
+
name: string;
|
|
836
|
+
group?: string;
|
|
837
|
+
}) => void;
|
|
838
|
+
history?: AppHistory;
|
|
839
|
+
historyLoading?: boolean;
|
|
840
|
+
historyItems?: ApplicationHistoryItem[];
|
|
841
|
+
historyRuntimeLoading?: boolean;
|
|
842
|
+
historyRuntimeError?: boolean;
|
|
843
|
+
historyMode?: "local" | "retained";
|
|
844
|
+
historyRange?: ApplicationHistoryRange;
|
|
845
|
+
historyRangeOptions?: Array<{
|
|
846
|
+
value: ApplicationHistoryRange;
|
|
847
|
+
label: string;
|
|
848
|
+
}>;
|
|
849
|
+
historyCoverageRecordCount?: number;
|
|
850
|
+
historyRuntimeLimited?: boolean;
|
|
851
|
+
onHistoryRangeChange?: (range: ApplicationHistoryRange) => void;
|
|
852
|
+
onOpenTimeline?: (timestamp?: string) => void;
|
|
853
|
+
onOpenSource?: (source: AppSourceRef) => void;
|
|
854
|
+
onToggleReplicaSets: (ownerID: string) => void;
|
|
855
|
+
renderOverviewIssues?: () => ReactNode;
|
|
856
|
+
hasOverviewIssues?: boolean;
|
|
649
857
|
}) {
|
|
650
|
-
const historyCount =
|
|
651
|
-
(history?.anchors?.length ?? 0) +
|
|
652
|
-
(history?.incidents?.length ?? app.events?.length ?? 0);
|
|
653
858
|
return (
|
|
654
859
|
<div className="flex h-full min-h-0 flex-col">
|
|
655
860
|
<ApplicationViewTabs
|
|
656
861
|
activeView={activeView}
|
|
657
|
-
|
|
862
|
+
costSelected={costSelected}
|
|
863
|
+
costAvailable={Boolean(renderCostView)}
|
|
658
864
|
onChange={onViewChange}
|
|
865
|
+
onCostChange={onCostViewChange}
|
|
659
866
|
/>
|
|
660
|
-
{
|
|
867
|
+
{costSelected && renderCostView && (
|
|
868
|
+
<div className="min-h-0 flex-1 overflow-y-auto px-4 py-4 sm:px-6">
|
|
869
|
+
{renderCostView({ app, workloads, onSelectWorkload })}
|
|
870
|
+
</div>
|
|
871
|
+
)}
|
|
872
|
+
{!costSelected && activeView === "overview" && (
|
|
661
873
|
<ApplicationOverview
|
|
662
874
|
app={app}
|
|
663
875
|
workloads={workloads}
|
|
@@ -673,7 +885,7 @@ function ApplicationWorkspace({
|
|
|
673
885
|
hasOverviewIssues={hasOverviewIssues}
|
|
674
886
|
/>
|
|
675
887
|
)}
|
|
676
|
-
{activeView === "topology" && (
|
|
888
|
+
{!costSelected && activeView === "topology" && (
|
|
677
889
|
<ApplicationTopology
|
|
678
890
|
topology={topology}
|
|
679
891
|
loading={topologyLoading}
|
|
@@ -692,13 +904,23 @@ function ApplicationWorkspace({
|
|
|
692
904
|
onToggleReplicaSets={onToggleReplicaSets}
|
|
693
905
|
/>
|
|
694
906
|
)}
|
|
695
|
-
{activeView === "history" && (
|
|
907
|
+
{!costSelected && activeView === "history" && (
|
|
696
908
|
<ApplicationHistoryView
|
|
697
909
|
history={history}
|
|
698
|
-
|
|
699
|
-
|
|
910
|
+
sourceLoading={historyLoading}
|
|
911
|
+
items={historyItems}
|
|
912
|
+
runtimeLoading={historyRuntimeLoading}
|
|
913
|
+
runtimeError={historyRuntimeError}
|
|
914
|
+
mode={historyMode}
|
|
915
|
+
range={historyRange}
|
|
916
|
+
rangeOptions={historyRangeOptions}
|
|
917
|
+
coverageRecordCount={historyCoverageRecordCount}
|
|
918
|
+
runtimeLimited={historyRuntimeLimited}
|
|
700
919
|
workloads={workloads}
|
|
701
920
|
onSelectWorkload={onSelectWorkload}
|
|
921
|
+
onNavigateToResource={onNavigateToResource}
|
|
922
|
+
onRangeChange={onHistoryRangeChange}
|
|
923
|
+
onOpenTimeline={onOpenTimeline}
|
|
702
924
|
onOpenSource={onOpenSource}
|
|
703
925
|
/>
|
|
704
926
|
)}
|
|
@@ -708,12 +930,16 @@ function ApplicationWorkspace({
|
|
|
708
930
|
|
|
709
931
|
function ApplicationViewTabs({
|
|
710
932
|
activeView,
|
|
711
|
-
|
|
933
|
+
costSelected,
|
|
934
|
+
costAvailable,
|
|
712
935
|
onChange,
|
|
936
|
+
onCostChange,
|
|
713
937
|
}: {
|
|
714
938
|
activeView: CanonicalApplicationView;
|
|
715
|
-
|
|
939
|
+
costSelected: boolean;
|
|
940
|
+
costAvailable: boolean;
|
|
716
941
|
onChange: (view: CanonicalApplicationView) => void;
|
|
942
|
+
onCostChange: () => void;
|
|
717
943
|
}) {
|
|
718
944
|
return (
|
|
719
945
|
<div
|
|
@@ -723,9 +949,7 @@ function ApplicationViewTabs({
|
|
|
723
949
|
>
|
|
724
950
|
<div className="flex min-w-0 gap-1 overflow-x-auto">
|
|
725
951
|
{APPLICATION_VIEWS.map((view) => {
|
|
726
|
-
const active = view.id === activeView;
|
|
727
|
-
const badge =
|
|
728
|
-
view.id === "history" && historyCount > 0 ? historyCount : null;
|
|
952
|
+
const active = !costSelected && view.id === activeView;
|
|
729
953
|
return (
|
|
730
954
|
<button
|
|
731
955
|
key={view.id}
|
|
@@ -741,14 +965,26 @@ function ApplicationViewTabs({
|
|
|
741
965
|
)}
|
|
742
966
|
>
|
|
743
967
|
{view.label}
|
|
744
|
-
{badge !== null && (
|
|
745
|
-
<span className="rounded bg-theme-hover px-1.5 py-0.5 text-[10px] font-semibold text-theme-text-tertiary">
|
|
746
|
-
{badge}
|
|
747
|
-
</span>
|
|
748
|
-
)}
|
|
749
968
|
</button>
|
|
750
969
|
);
|
|
751
970
|
})}
|
|
971
|
+
{costAvailable && (
|
|
972
|
+
<button
|
|
973
|
+
type="button"
|
|
974
|
+
role="tab"
|
|
975
|
+
aria-selected={costSelected}
|
|
976
|
+
onClick={onCostChange}
|
|
977
|
+
className={clsx(
|
|
978
|
+
"flex items-center gap-1.5 whitespace-nowrap border-b-2 px-3 py-2 text-sm font-medium transition-colors",
|
|
979
|
+
costSelected
|
|
980
|
+
? "border-skyhook-500 text-theme-text-primary"
|
|
981
|
+
: "border-transparent text-theme-text-secondary hover:border-theme-border-light hover:text-theme-text-primary",
|
|
982
|
+
)}
|
|
983
|
+
>
|
|
984
|
+
<DollarSign className="h-4 w-4" />
|
|
985
|
+
Cost
|
|
986
|
+
</button>
|
|
987
|
+
)}
|
|
752
988
|
</div>
|
|
753
989
|
</div>
|
|
754
990
|
);
|
|
@@ -778,60 +1014,88 @@ function ApplicationOverview({
|
|
|
778
1014
|
renderOverviewIssues,
|
|
779
1015
|
hasOverviewIssues,
|
|
780
1016
|
}: {
|
|
781
|
-
app: AppRow
|
|
782
|
-
workloads: AppWorkload[]
|
|
783
|
-
ready: number
|
|
784
|
-
desired: number
|
|
785
|
-
versions: string[]
|
|
786
|
-
onSelectWorkload: (workload: AppWorkload) => void
|
|
787
|
-
onNavigateToResource?: (resource: {
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
1017
|
+
app: AppRow;
|
|
1018
|
+
workloads: AppWorkload[];
|
|
1019
|
+
ready: number;
|
|
1020
|
+
desired: number;
|
|
1021
|
+
versions: string[];
|
|
1022
|
+
onSelectWorkload: (workload: AppWorkload) => void;
|
|
1023
|
+
onNavigateToResource?: (resource: {
|
|
1024
|
+
kind: string;
|
|
1025
|
+
namespace: string;
|
|
1026
|
+
name: string;
|
|
1027
|
+
group?: string;
|
|
1028
|
+
}) => void;
|
|
1029
|
+
history?: AppHistory;
|
|
1030
|
+
onSelectHistory: () => void;
|
|
1031
|
+
onOpenSource?: (source: AppSourceRef) => void;
|
|
1032
|
+
renderOverviewIssues?: () => ReactNode;
|
|
1033
|
+
hasOverviewIssues?: boolean;
|
|
793
1034
|
}) {
|
|
794
|
-
const rel = app.relationships
|
|
795
|
-
const hasEntrypoints = Boolean(
|
|
796
|
-
|
|
797
|
-
relationshipRefs(rel,
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
1035
|
+
const rel = app.relationships;
|
|
1036
|
+
const hasEntrypoints = Boolean(
|
|
1037
|
+
rel &&
|
|
1038
|
+
(relationshipRefs(rel, "service").length > 0 ||
|
|
1039
|
+
relationshipRefs(rel, "ingress").length > 0 ||
|
|
1040
|
+
relationshipRefs(rel, "route").length > 0),
|
|
1041
|
+
);
|
|
1042
|
+
const hasDependencies = Boolean(
|
|
1043
|
+
rel &&
|
|
1044
|
+
(relationshipRefs(rel, "config").length > 0 ||
|
|
1045
|
+
relationshipRefs(rel, "scaler").length > 0 ||
|
|
1046
|
+
relationshipRefs(rel, "storage").length > 0 ||
|
|
1047
|
+
relationshipRefs(rel, "pdb").length > 0 ||
|
|
1048
|
+
relationshipRefs(rel, "networkPolicy").length > 0),
|
|
1049
|
+
);
|
|
803
1050
|
const composition = classCompositionOf(app)
|
|
804
1051
|
.map(({ cls, count }) => `${count} ${cls}`)
|
|
805
|
-
.join(
|
|
806
|
-
const issues = useMemo(
|
|
1052
|
+
.join(" / ");
|
|
1053
|
+
const issues = useMemo(
|
|
1054
|
+
() => buildAppIssues(workloads, app.events ?? []),
|
|
1055
|
+
[workloads, app.events],
|
|
1056
|
+
);
|
|
807
1057
|
const currentIssuesPresent = renderOverviewIssues
|
|
808
|
-
? hasOverviewIssues ?? issues.length > 0
|
|
809
|
-
: issues.length > 0
|
|
810
|
-
const batchActivity = useMemo(() => batchActivityForApp(app), [app])
|
|
811
|
-
const batchStats = batchOverviewStats(batchActivity)
|
|
812
|
-
const pureBatch = workloadClassOf(app.workload_class) ===
|
|
813
|
-
const workloadComposition = workloadKindComposition(workloads)
|
|
814
|
-
const latestChange =
|
|
815
|
-
|
|
816
|
-
const
|
|
817
|
-
|
|
1058
|
+
? (hasOverviewIssues ?? issues.length > 0)
|
|
1059
|
+
: issues.length > 0;
|
|
1060
|
+
const batchActivity = useMemo(() => batchActivityForApp(app), [app]);
|
|
1061
|
+
const batchStats = batchOverviewStats(batchActivity);
|
|
1062
|
+
const pureBatch = workloadClassOf(app.workload_class) === "job";
|
|
1063
|
+
const workloadComposition = workloadKindComposition(workloads);
|
|
1064
|
+
const latestChange =
|
|
1065
|
+
history?.summary?.state === "change" ? history.summary : undefined;
|
|
1066
|
+
const runtimeHealth = healthOf(
|
|
1067
|
+
app.runtimeHealth ??
|
|
1068
|
+
worstHealth(workloads.map((workload) => workload.health)),
|
|
1069
|
+
);
|
|
1070
|
+
const hasDeliveryStatus = Boolean(
|
|
1071
|
+
app.sourceStatus?.sync || app.sourceStatus?.health,
|
|
1072
|
+
);
|
|
1073
|
+
const extraFactCount =
|
|
1074
|
+
Number(Boolean(latestChange)) + Number(hasDeliveryStatus);
|
|
818
1075
|
const factGrid = pureBatch
|
|
819
1076
|
? extraFactCount === 2
|
|
820
|
-
?
|
|
1077
|
+
? "2xl:grid-cols-6"
|
|
821
1078
|
: extraFactCount === 1
|
|
822
|
-
?
|
|
823
|
-
:
|
|
1079
|
+
? "2xl:grid-cols-5"
|
|
1080
|
+
: "2xl:grid-cols-4"
|
|
824
1081
|
: extraFactCount === 2
|
|
825
|
-
?
|
|
1082
|
+
? "2xl:grid-cols-5"
|
|
826
1083
|
: extraFactCount === 1
|
|
827
|
-
?
|
|
828
|
-
:
|
|
1084
|
+
? "2xl:grid-cols-4"
|
|
1085
|
+
: "2xl:grid-cols-3";
|
|
829
1086
|
|
|
830
1087
|
return (
|
|
831
1088
|
<div className="min-h-0 flex-1 overflow-auto">
|
|
832
1089
|
<div className="grid w-full max-w-[2400px] gap-4 p-4 sm:p-6 xl:grid-cols-[minmax(0,1fr)_minmax(320px,380px)]">
|
|
833
1090
|
<div className="min-w-0 space-y-4">
|
|
834
|
-
{renderOverviewIssues ?
|
|
1091
|
+
{renderOverviewIssues ? (
|
|
1092
|
+
renderOverviewIssues()
|
|
1093
|
+
) : (
|
|
1094
|
+
<ApplicationNow
|
|
1095
|
+
issues={issues}
|
|
1096
|
+
onSelectWorkload={onSelectWorkload}
|
|
1097
|
+
/>
|
|
1098
|
+
)}
|
|
835
1099
|
<ApplicationLatestHistory
|
|
836
1100
|
history={history}
|
|
837
1101
|
sourceRef={app.sourceRef}
|
|
@@ -839,29 +1103,67 @@ function ApplicationOverview({
|
|
|
839
1103
|
onSelectHistory={onSelectHistory}
|
|
840
1104
|
onOpenSource={onOpenSource}
|
|
841
1105
|
/>
|
|
842
|
-
<div className={clsx(
|
|
1106
|
+
<div className={clsx("grid gap-3 md:grid-cols-2", factGrid)}>
|
|
843
1107
|
<ApplicationFact
|
|
844
1108
|
label="Runtime"
|
|
845
|
-
value={
|
|
846
|
-
|
|
1109
|
+
value={
|
|
1110
|
+
pureBatch
|
|
1111
|
+
? batchRuntimeForApp(app).label
|
|
1112
|
+
: HEALTH_META[runtimeHealth].label
|
|
1113
|
+
}
|
|
1114
|
+
detail={
|
|
1115
|
+
pureBatch
|
|
1116
|
+
? batchStats.activeDetail
|
|
1117
|
+
: desired > 0
|
|
1118
|
+
? `${ready}/${desired} ready`
|
|
1119
|
+
: "No desired replicas"
|
|
1120
|
+
}
|
|
847
1121
|
/>
|
|
848
1122
|
{hasDeliveryStatus && (
|
|
849
1123
|
<ApplicationFact
|
|
850
1124
|
label="Delivery"
|
|
851
1125
|
value={<SourceDeliveryStatus status={app.sourceStatus!} />}
|
|
852
|
-
detail={
|
|
1126
|
+
detail={
|
|
1127
|
+
app.sourceRef ? sourceObjectLabel(app.sourceRef) : undefined
|
|
1128
|
+
}
|
|
853
1129
|
/>
|
|
854
1130
|
)}
|
|
855
1131
|
{pureBatch ? (
|
|
856
1132
|
<>
|
|
857
|
-
<ApplicationFact
|
|
858
|
-
|
|
859
|
-
|
|
1133
|
+
<ApplicationFact
|
|
1134
|
+
label="Batch resources"
|
|
1135
|
+
value={String(workloads.length)}
|
|
1136
|
+
detail={workloadComposition || "No workloads"}
|
|
1137
|
+
/>
|
|
1138
|
+
<ApplicationFact
|
|
1139
|
+
label="Active runs"
|
|
1140
|
+
value={batchStats.activeValue}
|
|
1141
|
+
detail={batchStats.activeDetail}
|
|
1142
|
+
/>
|
|
1143
|
+
<ApplicationFact
|
|
1144
|
+
label="Retained runs"
|
|
1145
|
+
value={batchStats.retainedValue}
|
|
1146
|
+
detail="Kubernetes-retained history"
|
|
1147
|
+
/>
|
|
860
1148
|
</>
|
|
861
1149
|
) : (
|
|
862
1150
|
<>
|
|
863
|
-
<ApplicationFact
|
|
864
|
-
|
|
1151
|
+
<ApplicationFact
|
|
1152
|
+
label="Workloads"
|
|
1153
|
+
value={String(workloads.length)}
|
|
1154
|
+
detail={composition || "No workloads"}
|
|
1155
|
+
/>
|
|
1156
|
+
<ApplicationFact
|
|
1157
|
+
label="Version"
|
|
1158
|
+
value={
|
|
1159
|
+
app.appVersion ||
|
|
1160
|
+
(versions.length === 1
|
|
1161
|
+
? versions[0]
|
|
1162
|
+
: versions.length > 1
|
|
1163
|
+
? `${versions.length} versions`
|
|
1164
|
+
: "Unknown")
|
|
1165
|
+
}
|
|
1166
|
+
/>
|
|
865
1167
|
</>
|
|
866
1168
|
)}
|
|
867
1169
|
{latestChange && (
|
|
@@ -887,9 +1189,19 @@ function ApplicationOverview({
|
|
|
887
1189
|
</ApplicationPanel>
|
|
888
1190
|
{!pureBatch && <ApplicationBatchOverview activity={batchActivity} />}
|
|
889
1191
|
</div>
|
|
890
|
-
<aside
|
|
1192
|
+
<aside
|
|
1193
|
+
className={clsx(
|
|
1194
|
+
"min-w-0 gap-4 xl:block xl:space-y-4",
|
|
1195
|
+
hasDependencies ? "grid md:grid-cols-2" : "max-w-xl xl:max-w-none",
|
|
1196
|
+
)}
|
|
1197
|
+
>
|
|
891
1198
|
<ApplicationSourceProvenance app={app} onOpenSource={onOpenSource} />
|
|
892
|
-
{hasDependencies &&
|
|
1199
|
+
{hasDependencies && (
|
|
1200
|
+
<ApplicationDependencies
|
|
1201
|
+
relationships={rel}
|
|
1202
|
+
onNavigateToResource={onNavigateToResource}
|
|
1203
|
+
/>
|
|
1204
|
+
)}
|
|
893
1205
|
</aside>
|
|
894
1206
|
</div>
|
|
895
1207
|
</div>
|
|
@@ -915,21 +1227,27 @@ function ApplicationSourceProvenance({
|
|
|
915
1227
|
app,
|
|
916
1228
|
onOpenSource,
|
|
917
1229
|
}: {
|
|
918
|
-
app: AppRow
|
|
919
|
-
onOpenSource?: (source: AppSourceRef) => void
|
|
1230
|
+
app: AppRow;
|
|
1231
|
+
onOpenSource?: (source: AppSourceRef) => void;
|
|
920
1232
|
}) {
|
|
921
1233
|
if (app.sourceRef) {
|
|
922
|
-
const sourceName = `${app.sourceRef.namespace}/${app.sourceRef.name}
|
|
1234
|
+
const sourceName = `${app.sourceRef.namespace}/${app.sourceRef.name}`;
|
|
923
1235
|
return (
|
|
924
1236
|
<ApplicationPanel title="Deployment source">
|
|
925
1237
|
<div className="space-y-3">
|
|
926
1238
|
<div>
|
|
927
|
-
<div className="text-xs font-semibold uppercase text-theme-text-tertiary">
|
|
1239
|
+
<div className="text-xs font-semibold uppercase text-theme-text-tertiary">
|
|
1240
|
+
{sourceObjectLabel(app.sourceRef)}
|
|
1241
|
+
</div>
|
|
928
1242
|
<Tooltip content={sourceName} delay={150}>
|
|
929
|
-
<div className="mt-1 truncate font-mono text-sm font-medium text-theme-text-primary">
|
|
1243
|
+
<div className="mt-1 truncate font-mono text-sm font-medium text-theme-text-primary">
|
|
1244
|
+
{sourceName}
|
|
1245
|
+
</div>
|
|
930
1246
|
</Tooltip>
|
|
931
1247
|
</div>
|
|
932
|
-
{app.sourceStatus &&
|
|
1248
|
+
{app.sourceStatus && (
|
|
1249
|
+
<SourceDeliveryStatus status={app.sourceStatus} />
|
|
1250
|
+
)}
|
|
933
1251
|
{onOpenSource && (
|
|
934
1252
|
<button
|
|
935
1253
|
type="button"
|
|
@@ -942,7 +1260,7 @@ function ApplicationSourceProvenance({
|
|
|
942
1260
|
)}
|
|
943
1261
|
</div>
|
|
944
1262
|
</ApplicationPanel>
|
|
945
|
-
)
|
|
1263
|
+
);
|
|
946
1264
|
}
|
|
947
1265
|
|
|
948
1266
|
if (app.sourceConflict) {
|
|
@@ -950,37 +1268,58 @@ function ApplicationSourceProvenance({
|
|
|
950
1268
|
<ApplicationPanel title="Deployment source">
|
|
951
1269
|
<div className="space-y-2">
|
|
952
1270
|
<div className="flex items-center gap-2">
|
|
953
|
-
<AlertTriangle
|
|
954
|
-
|
|
1271
|
+
<AlertTriangle
|
|
1272
|
+
className="h-4 w-4 shrink-0 text-amber-500"
|
|
1273
|
+
aria-hidden
|
|
1274
|
+
/>
|
|
1275
|
+
<span className="text-sm font-semibold text-theme-text-primary">
|
|
1276
|
+
Multiple deployment sources detected
|
|
1277
|
+
</span>
|
|
955
1278
|
</div>
|
|
956
|
-
<p className="text-sm text-theme-text-secondary">
|
|
1279
|
+
<p className="text-sm text-theme-text-secondary">
|
|
1280
|
+
Workloads in this application do not share one deployment manager.
|
|
1281
|
+
</p>
|
|
957
1282
|
</div>
|
|
958
1283
|
</ApplicationPanel>
|
|
959
|
-
)
|
|
1284
|
+
);
|
|
960
1285
|
}
|
|
961
1286
|
|
|
962
|
-
const inferred = app.identity?.confidence ===
|
|
963
|
-
const identityLabel = app.identity?.source
|
|
964
|
-
|
|
1287
|
+
const inferred = app.identity?.confidence === "medium";
|
|
1288
|
+
const identityLabel = app.identity?.source
|
|
1289
|
+
? appSourceLabel(app.identity.source)
|
|
1290
|
+
: undefined;
|
|
1291
|
+
const tierLabel =
|
|
1292
|
+
!app.identity && app.tier ? overlayProvenance(app.tier) : undefined;
|
|
965
1293
|
return (
|
|
966
1294
|
<ApplicationPanel title="Application identity">
|
|
967
1295
|
<div className="space-y-2">
|
|
968
1296
|
<div className="flex items-center gap-2">
|
|
969
|
-
{inferred &&
|
|
1297
|
+
{inferred && (
|
|
1298
|
+
<AlertTriangle
|
|
1299
|
+
className="h-4 w-4 shrink-0 text-amber-500"
|
|
1300
|
+
aria-hidden
|
|
1301
|
+
/>
|
|
1302
|
+
)}
|
|
970
1303
|
<span className="text-sm font-semibold text-theme-text-primary">
|
|
971
1304
|
{identityLabel
|
|
972
1305
|
? inferred
|
|
973
|
-
?
|
|
1306
|
+
? "Inferred application boundary"
|
|
974
1307
|
: `Identified by ${identityLabel}`
|
|
975
1308
|
: tierLabel
|
|
976
1309
|
? `Grouped by ${tierLabel} metadata`
|
|
977
|
-
:
|
|
1310
|
+
: "Grouped from Kubernetes ownership"}
|
|
978
1311
|
</span>
|
|
979
1312
|
</div>
|
|
980
|
-
{inferred && identityLabel &&
|
|
1313
|
+
{inferred && identityLabel && (
|
|
1314
|
+
<p className="text-sm text-theme-text-secondary">
|
|
1315
|
+
Matched using {identityLabel}.
|
|
1316
|
+
</p>
|
|
1317
|
+
)}
|
|
981
1318
|
{app.identity?.evidence && (
|
|
982
1319
|
<Tooltip content={app.identity.evidence} delay={150}>
|
|
983
|
-
<div className="truncate font-mono text-xs text-theme-text-tertiary">
|
|
1320
|
+
<div className="truncate font-mono text-xs text-theme-text-tertiary">
|
|
1321
|
+
{app.identity.evidence}
|
|
1322
|
+
</div>
|
|
984
1323
|
</Tooltip>
|
|
985
1324
|
)}
|
|
986
1325
|
</div>
|
|
@@ -992,17 +1331,22 @@ function ApplicationEntrypoints({
|
|
|
992
1331
|
relationships,
|
|
993
1332
|
onNavigateToResource,
|
|
994
1333
|
}: {
|
|
995
|
-
relationships: AppRow[
|
|
996
|
-
onNavigateToResource?: (resource: {
|
|
1334
|
+
relationships: AppRow["relationships"];
|
|
1335
|
+
onNavigateToResource?: (resource: {
|
|
1336
|
+
kind: string;
|
|
1337
|
+
namespace: string;
|
|
1338
|
+
name: string;
|
|
1339
|
+
group?: string;
|
|
1340
|
+
}) => void;
|
|
997
1341
|
}) {
|
|
998
|
-
const serviceRefs = relationshipRefs(relationships,
|
|
999
|
-
const ingressRefs = relationshipRefs(relationships,
|
|
1000
|
-
const routeRefs = relationshipRefs(relationships,
|
|
1001
|
-
const serviceCount = serviceRefs.length
|
|
1002
|
-
const ingressCount = ingressRefs.length
|
|
1003
|
-
const routeCount = routeRefs.length
|
|
1004
|
-
const hasExternal = ingressCount + routeCount > 0
|
|
1005
|
-
if (serviceCount + ingressCount + routeCount === 0) return null
|
|
1342
|
+
const serviceRefs = relationshipRefs(relationships, "service");
|
|
1343
|
+
const ingressRefs = relationshipRefs(relationships, "ingress");
|
|
1344
|
+
const routeRefs = relationshipRefs(relationships, "route");
|
|
1345
|
+
const serviceCount = serviceRefs.length;
|
|
1346
|
+
const ingressCount = ingressRefs.length;
|
|
1347
|
+
const routeCount = routeRefs.length;
|
|
1348
|
+
const hasExternal = ingressCount + routeCount > 0;
|
|
1349
|
+
if (serviceCount + ingressCount + routeCount === 0) return null;
|
|
1006
1350
|
|
|
1007
1351
|
return (
|
|
1008
1352
|
<ApplicationPanel title="Entrypoints">
|
|
@@ -1054,7 +1398,11 @@ function ApplicationEntrypoints({
|
|
|
1054
1398
|
refs={ingressRefs}
|
|
1055
1399
|
onNavigateToResource={onNavigateToResource}
|
|
1056
1400
|
/>
|
|
1057
|
-
<ApplicationRelatedNameGroup
|
|
1401
|
+
<ApplicationRelatedNameGroup
|
|
1402
|
+
label="Routes"
|
|
1403
|
+
refs={routeRefs}
|
|
1404
|
+
onNavigateToResource={onNavigateToResource}
|
|
1405
|
+
/>
|
|
1058
1406
|
</div>
|
|
1059
1407
|
</div>
|
|
1060
1408
|
</ApplicationPanel>
|
|
@@ -1065,24 +1413,67 @@ function ApplicationDependencies({
|
|
|
1065
1413
|
relationships,
|
|
1066
1414
|
onNavigateToResource,
|
|
1067
1415
|
}: {
|
|
1068
|
-
relationships: AppRow[
|
|
1069
|
-
onNavigateToResource?: (resource: {
|
|
1416
|
+
relationships: AppRow["relationships"];
|
|
1417
|
+
onNavigateToResource?: (resource: {
|
|
1418
|
+
kind: string;
|
|
1419
|
+
namespace: string;
|
|
1420
|
+
name: string;
|
|
1421
|
+
group?: string;
|
|
1422
|
+
}) => void;
|
|
1070
1423
|
}) {
|
|
1071
|
-
const configs = relationshipRefs(relationships,
|
|
1072
|
-
const scalers = relationshipRefs(relationships,
|
|
1073
|
-
const storage = relationshipRefs(relationships,
|
|
1074
|
-
const pdbs = relationshipRefs(relationships,
|
|
1075
|
-
const networkPolicies = relationshipRefs(relationships,
|
|
1076
|
-
if (
|
|
1424
|
+
const configs = relationshipRefs(relationships, "config");
|
|
1425
|
+
const scalers = relationshipRefs(relationships, "scaler");
|
|
1426
|
+
const storage = relationshipRefs(relationships, "storage");
|
|
1427
|
+
const pdbs = relationshipRefs(relationships, "pdb");
|
|
1428
|
+
const networkPolicies = relationshipRefs(relationships, "networkPolicy");
|
|
1429
|
+
if (
|
|
1430
|
+
configs.length +
|
|
1431
|
+
scalers.length +
|
|
1432
|
+
storage.length +
|
|
1433
|
+
pdbs.length +
|
|
1434
|
+
networkPolicies.length ===
|
|
1435
|
+
0
|
|
1436
|
+
)
|
|
1437
|
+
return null;
|
|
1077
1438
|
|
|
1078
1439
|
return (
|
|
1079
1440
|
<ApplicationPanel title="Dependencies">
|
|
1080
1441
|
<div className="space-y-3">
|
|
1081
|
-
<ApplicationDependencyRow
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1442
|
+
<ApplicationDependencyRow
|
|
1443
|
+
label="Configuration"
|
|
1444
|
+
refs={configs}
|
|
1445
|
+
totalCount={relationships?.configs}
|
|
1446
|
+
detail="ConfigMaps and Secrets referenced by app workloads."
|
|
1447
|
+
onNavigateToResource={onNavigateToResource}
|
|
1448
|
+
/>
|
|
1449
|
+
<ApplicationDependencyRow
|
|
1450
|
+
label="Autoscaling"
|
|
1451
|
+
refs={scalers}
|
|
1452
|
+
totalCount={relationships?.scalers}
|
|
1453
|
+
detail="Autoscalers controlling app workloads."
|
|
1454
|
+
onNavigateToResource={onNavigateToResource}
|
|
1455
|
+
/>
|
|
1456
|
+
<ApplicationDependencyRow
|
|
1457
|
+
label="Storage"
|
|
1458
|
+
refs={storage}
|
|
1459
|
+
totalCount={relationships?.storage}
|
|
1460
|
+
detail="PersistentVolumeClaims mounted by app workloads."
|
|
1461
|
+
onNavigateToResource={onNavigateToResource}
|
|
1462
|
+
/>
|
|
1463
|
+
<ApplicationDependencyRow
|
|
1464
|
+
label="Availability policy"
|
|
1465
|
+
refs={pdbs}
|
|
1466
|
+
totalCount={relationships?.pdbs}
|
|
1467
|
+
detail="PodDisruptionBudgets protecting app workloads."
|
|
1468
|
+
onNavigateToResource={onNavigateToResource}
|
|
1469
|
+
/>
|
|
1470
|
+
<ApplicationDependencyRow
|
|
1471
|
+
label="Network policy"
|
|
1472
|
+
refs={networkPolicies}
|
|
1473
|
+
totalCount={relationships?.networkPolicies}
|
|
1474
|
+
detail="Network policies selecting app workloads."
|
|
1475
|
+
onNavigateToResource={onNavigateToResource}
|
|
1476
|
+
/>
|
|
1086
1477
|
</div>
|
|
1087
1478
|
</ApplicationPanel>
|
|
1088
1479
|
);
|
|
@@ -1095,14 +1486,19 @@ function ApplicationDependencyRow({
|
|
|
1095
1486
|
detail,
|
|
1096
1487
|
onNavigateToResource,
|
|
1097
1488
|
}: {
|
|
1098
|
-
label: string
|
|
1099
|
-
refs: ResourceRef[]
|
|
1100
|
-
totalCount?: number
|
|
1101
|
-
detail: string
|
|
1102
|
-
onNavigateToResource?: (resource: {
|
|
1489
|
+
label: string;
|
|
1490
|
+
refs: ResourceRef[];
|
|
1491
|
+
totalCount?: number;
|
|
1492
|
+
detail: string;
|
|
1493
|
+
onNavigateToResource?: (resource: {
|
|
1494
|
+
kind: string;
|
|
1495
|
+
namespace: string;
|
|
1496
|
+
name: string;
|
|
1497
|
+
group?: string;
|
|
1498
|
+
}) => void;
|
|
1103
1499
|
}) {
|
|
1104
|
-
if (refs.length === 0) return null
|
|
1105
|
-
const count = Math.max(totalCount ?? refs.length, refs.length)
|
|
1500
|
+
if (refs.length === 0) return null;
|
|
1501
|
+
const count = Math.max(totalCount ?? refs.length, refs.length);
|
|
1106
1502
|
return (
|
|
1107
1503
|
<div className="min-w-0 border-b border-theme-border pb-3 last:border-b-0">
|
|
1108
1504
|
<div className="flex items-baseline justify-between gap-3">
|
|
@@ -1114,7 +1510,13 @@ function ApplicationDependencyRow({
|
|
|
1114
1510
|
</span>
|
|
1115
1511
|
</div>
|
|
1116
1512
|
<div className="mt-1 text-xs text-theme-text-tertiary">{detail}</div>
|
|
1117
|
-
<ApplicationRelatedNameGroup
|
|
1513
|
+
<ApplicationRelatedNameGroup
|
|
1514
|
+
label={label}
|
|
1515
|
+
refs={refs}
|
|
1516
|
+
totalCount={count}
|
|
1517
|
+
onNavigateToResource={onNavigateToResource}
|
|
1518
|
+
hideLabel
|
|
1519
|
+
/>
|
|
1118
1520
|
</div>
|
|
1119
1521
|
);
|
|
1120
1522
|
}
|
|
@@ -1126,55 +1528,83 @@ function ApplicationRelatedNameGroup({
|
|
|
1126
1528
|
onNavigateToResource,
|
|
1127
1529
|
hideLabel,
|
|
1128
1530
|
}: {
|
|
1129
|
-
label: string
|
|
1130
|
-
refs?: ResourceRef[]
|
|
1131
|
-
totalCount?: number
|
|
1132
|
-
onNavigateToResource?: (resource: {
|
|
1133
|
-
|
|
1531
|
+
label: string;
|
|
1532
|
+
refs?: ResourceRef[];
|
|
1533
|
+
totalCount?: number;
|
|
1534
|
+
onNavigateToResource?: (resource: {
|
|
1535
|
+
kind: string;
|
|
1536
|
+
namespace: string;
|
|
1537
|
+
name: string;
|
|
1538
|
+
group?: string;
|
|
1539
|
+
}) => void;
|
|
1540
|
+
hideLabel?: boolean;
|
|
1134
1541
|
}) {
|
|
1135
|
-
if (!refs || refs.length === 0) return null
|
|
1542
|
+
if (!refs || refs.length === 0) return null;
|
|
1136
1543
|
|
|
1137
|
-
const visible = refs.slice(0, 12)
|
|
1138
|
-
const count = Math.max(totalCount ?? refs.length, refs.length)
|
|
1139
|
-
const overflow = Math.max(0, count - visible.length)
|
|
1544
|
+
const visible = refs.slice(0, 12);
|
|
1545
|
+
const count = Math.max(totalCount ?? refs.length, refs.length);
|
|
1546
|
+
const overflow = Math.max(0, count - visible.length);
|
|
1140
1547
|
return (
|
|
1141
|
-
<div className={hideLabel ?
|
|
1142
|
-
{!hideLabel &&
|
|
1548
|
+
<div className={hideLabel ? "mt-2" : undefined}>
|
|
1549
|
+
{!hideLabel && (
|
|
1550
|
+
<div className="mb-1 text-xs font-medium text-theme-text-tertiary">
|
|
1551
|
+
{label}
|
|
1552
|
+
{count > 1 ? ` (${count})` : ""}
|
|
1553
|
+
</div>
|
|
1554
|
+
)}
|
|
1143
1555
|
<div className="flex flex-wrap gap-1.5">
|
|
1144
1556
|
{visible.map((ref) => (
|
|
1145
1557
|
<ResourceRefBadge
|
|
1146
|
-
key={`${ref.group ||
|
|
1558
|
+
key={`${ref.group || ""}/${ref.kind}/${ref.namespace}/${ref.name}`}
|
|
1147
1559
|
resourceRef={ref}
|
|
1148
|
-
onClick={
|
|
1560
|
+
onClick={
|
|
1561
|
+
onNavigateToResource
|
|
1562
|
+
? (clicked) =>
|
|
1563
|
+
onNavigateToResource(refToSelectedResource(clicked))
|
|
1564
|
+
: undefined
|
|
1565
|
+
}
|
|
1149
1566
|
/>
|
|
1150
1567
|
))}
|
|
1151
|
-
{overflow > 0 &&
|
|
1568
|
+
{overflow > 0 && (
|
|
1569
|
+
<span className={`${CHIP} ${CHIP_TONE.muted}`}>+{overflow} more</span>
|
|
1570
|
+
)}
|
|
1152
1571
|
</div>
|
|
1153
1572
|
</div>
|
|
1154
1573
|
);
|
|
1155
1574
|
}
|
|
1156
1575
|
|
|
1157
|
-
type RelationshipGroup =
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1576
|
+
type RelationshipGroup =
|
|
1577
|
+
| "service"
|
|
1578
|
+
| "ingress"
|
|
1579
|
+
| "route"
|
|
1580
|
+
| "config"
|
|
1581
|
+
| "scaler"
|
|
1582
|
+
| "storage"
|
|
1583
|
+
| "pdb"
|
|
1584
|
+
| "networkPolicy";
|
|
1585
|
+
|
|
1586
|
+
function relationshipRefs(
|
|
1587
|
+
relationships: AppRow["relationships"] | undefined,
|
|
1588
|
+
group: RelationshipGroup,
|
|
1589
|
+
): ResourceRef[] {
|
|
1590
|
+
if (!relationships) return [];
|
|
1161
1591
|
switch (group) {
|
|
1162
|
-
case
|
|
1163
|
-
return relationships.serviceRefs ?? []
|
|
1164
|
-
case
|
|
1165
|
-
return relationships.ingressRefs ?? []
|
|
1166
|
-
case
|
|
1167
|
-
return relationships.routeRefs ?? []
|
|
1168
|
-
case
|
|
1169
|
-
return relationships.configRefs ?? []
|
|
1170
|
-
case
|
|
1171
|
-
return relationships.scalerRefs ?? []
|
|
1172
|
-
case
|
|
1173
|
-
return relationships.storageRefs ?? []
|
|
1174
|
-
case
|
|
1175
|
-
return relationships.pdbRefs ?? []
|
|
1176
|
-
case
|
|
1177
|
-
return relationships.networkPolicyRefs ?? []
|
|
1592
|
+
case "service":
|
|
1593
|
+
return relationships.serviceRefs ?? [];
|
|
1594
|
+
case "ingress":
|
|
1595
|
+
return relationships.ingressRefs ?? [];
|
|
1596
|
+
case "route":
|
|
1597
|
+
return relationships.routeRefs ?? [];
|
|
1598
|
+
case "config":
|
|
1599
|
+
return relationships.configRefs ?? [];
|
|
1600
|
+
case "scaler":
|
|
1601
|
+
return relationships.scalerRefs ?? [];
|
|
1602
|
+
case "storage":
|
|
1603
|
+
return relationships.storageRefs ?? [];
|
|
1604
|
+
case "pdb":
|
|
1605
|
+
return relationships.pdbRefs ?? [];
|
|
1606
|
+
case "networkPolicy":
|
|
1607
|
+
return relationships.networkPolicyRefs ?? [];
|
|
1178
1608
|
}
|
|
1179
1609
|
}
|
|
1180
1610
|
|
|
@@ -1182,10 +1612,10 @@ function ApplicationNow({
|
|
|
1182
1612
|
issues,
|
|
1183
1613
|
onSelectWorkload,
|
|
1184
1614
|
}: {
|
|
1185
|
-
issues: AppIssue[]
|
|
1186
|
-
onSelectWorkload: (workload: AppWorkload) => void
|
|
1615
|
+
issues: AppIssue[];
|
|
1616
|
+
onSelectWorkload: (workload: AppWorkload) => void;
|
|
1187
1617
|
}) {
|
|
1188
|
-
if (issues.length === 0) return null
|
|
1618
|
+
if (issues.length === 0) return null;
|
|
1189
1619
|
|
|
1190
1620
|
const top = issues[0];
|
|
1191
1621
|
return (
|
|
@@ -1398,27 +1828,31 @@ function ApplicationLatestHistory({
|
|
|
1398
1828
|
onSelectHistory,
|
|
1399
1829
|
onOpenSource,
|
|
1400
1830
|
}: {
|
|
1401
|
-
history?: AppHistory
|
|
1402
|
-
sourceRef?: AppSourceRef
|
|
1403
|
-
showIncidentPreview?: boolean
|
|
1404
|
-
onSelectHistory: () => void
|
|
1405
|
-
onOpenSource?: (source: AppSourceRef) => void
|
|
1831
|
+
history?: AppHistory;
|
|
1832
|
+
sourceRef?: AppSourceRef;
|
|
1833
|
+
showIncidentPreview?: boolean;
|
|
1834
|
+
onSelectHistory: () => void;
|
|
1835
|
+
onOpenSource?: (source: AppSourceRef) => void;
|
|
1406
1836
|
}) {
|
|
1407
|
-
const summary = history?.summary
|
|
1408
|
-
const showIncident = summary?.state ===
|
|
1409
|
-
if (!summary || !showIncident) return null
|
|
1410
|
-
const resolvedSource = sourceRef ?? history?.sourceRef
|
|
1411
|
-
const tone = CHIP_TONE.amber
|
|
1837
|
+
const summary = history?.summary;
|
|
1838
|
+
const showIncident = summary?.state === "incident" && showIncidentPreview;
|
|
1839
|
+
if (!summary || !showIncident) return null;
|
|
1840
|
+
const resolvedSource = sourceRef ?? history?.sourceRef;
|
|
1841
|
+
const tone = CHIP_TONE.amber;
|
|
1412
1842
|
|
|
1413
1843
|
return (
|
|
1414
1844
|
<section className="rounded-lg border border-theme-border bg-theme-surface px-4 py-3 shadow-theme-sm">
|
|
1415
1845
|
<div className="flex flex-wrap items-start gap-3">
|
|
1416
|
-
<span
|
|
1846
|
+
<span
|
|
1847
|
+
className={`flex h-8 w-8 shrink-0 items-center justify-center rounded-md ring-1 ring-inset ${tone}`}
|
|
1848
|
+
>
|
|
1417
1849
|
<AlertTriangle className="h-4 w-4" aria-hidden />
|
|
1418
1850
|
</span>
|
|
1419
1851
|
<div className="min-w-0 flex-1">
|
|
1420
1852
|
<div className="flex flex-wrap items-center gap-2">
|
|
1421
|
-
<h2 className="text-sm font-semibold text-theme-text-primary">
|
|
1853
|
+
<h2 className="text-sm font-semibold text-theme-text-primary">
|
|
1854
|
+
{summary.title}
|
|
1855
|
+
</h2>
|
|
1422
1856
|
<span className={`${CHIP} ${tone}`}>Latest incident</span>
|
|
1423
1857
|
</div>
|
|
1424
1858
|
{summary.detail && (
|
|
@@ -1434,7 +1868,11 @@ function ApplicationLatestHistory({
|
|
|
1434
1868
|
</div>
|
|
1435
1869
|
<div className="flex shrink-0 items-center gap-2">
|
|
1436
1870
|
{resolvedSource && !sourceRef && onOpenSource && (
|
|
1437
|
-
<button
|
|
1871
|
+
<button
|
|
1872
|
+
type="button"
|
|
1873
|
+
onClick={() => onOpenSource(resolvedSource)}
|
|
1874
|
+
className="rounded-md px-2.5 py-1.5 text-sm font-medium text-accent-text hover:bg-theme-hover"
|
|
1875
|
+
>
|
|
1438
1876
|
{sourceLinkLabel(resolvedSource)}
|
|
1439
1877
|
</button>
|
|
1440
1878
|
)}
|
|
@@ -1457,29 +1895,53 @@ function ApplicationLatestChangeFact({
|
|
|
1457
1895
|
onSelectHistory,
|
|
1458
1896
|
onOpenSource,
|
|
1459
1897
|
}: {
|
|
1460
|
-
summary: NonNullable<AppHistory[
|
|
1461
|
-
sourceRef?: AppSourceRef
|
|
1462
|
-
onSelectHistory: () => void
|
|
1463
|
-
onOpenSource?: (source: AppSourceRef) => void
|
|
1898
|
+
summary: NonNullable<AppHistory["summary"]>;
|
|
1899
|
+
sourceRef?: AppSourceRef;
|
|
1900
|
+
onSelectHistory: () => void;
|
|
1901
|
+
onOpenSource?: (source: AppSourceRef) => void;
|
|
1464
1902
|
}) {
|
|
1465
1903
|
return (
|
|
1466
1904
|
<div className="min-w-0 rounded-lg border border-theme-border bg-theme-surface px-4 py-3 shadow-theme-sm md:col-span-2 2xl:col-span-1">
|
|
1467
1905
|
<div className="flex items-center justify-between gap-2">
|
|
1468
|
-
<div className="text-[10px] font-semibold uppercase tracking-wide text-theme-text-tertiary">
|
|
1469
|
-
|
|
1906
|
+
<div className="text-[10px] font-semibold uppercase tracking-wide text-theme-text-tertiary">
|
|
1907
|
+
Latest change
|
|
1908
|
+
</div>
|
|
1909
|
+
<button
|
|
1910
|
+
type="button"
|
|
1911
|
+
onClick={onSelectHistory}
|
|
1912
|
+
className="shrink-0 text-xs font-medium text-accent-text hover:underline"
|
|
1913
|
+
>
|
|
1914
|
+
History
|
|
1915
|
+
</button>
|
|
1470
1916
|
</div>
|
|
1471
|
-
<div className="mt-1 truncate text-sm font-semibold text-theme-text-primary">
|
|
1472
|
-
|
|
1917
|
+
<div className="mt-1 truncate text-sm font-semibold text-theme-text-primary">
|
|
1918
|
+
{summary.title}
|
|
1919
|
+
</div>
|
|
1920
|
+
{summary.detail && (
|
|
1921
|
+
<div className="mt-0.5 truncate font-mono text-xs text-theme-text-tertiary">
|
|
1922
|
+
{summary.detail}
|
|
1923
|
+
</div>
|
|
1924
|
+
)}
|
|
1473
1925
|
{sourceRef && onOpenSource && (
|
|
1474
|
-
<button
|
|
1926
|
+
<button
|
|
1927
|
+
type="button"
|
|
1928
|
+
onClick={() => onOpenSource(sourceRef)}
|
|
1929
|
+
className="mt-1 text-xs font-medium text-accent-text hover:underline"
|
|
1930
|
+
>
|
|
1475
1931
|
{sourceLinkLabel(sourceRef)}
|
|
1476
1932
|
</button>
|
|
1477
1933
|
)}
|
|
1478
1934
|
</div>
|
|
1479
|
-
)
|
|
1935
|
+
);
|
|
1480
1936
|
}
|
|
1481
1937
|
|
|
1482
|
-
function WorkloadsMatrix({
|
|
1938
|
+
function WorkloadsMatrix({
|
|
1939
|
+
workloads,
|
|
1940
|
+
onSelectWorkload,
|
|
1941
|
+
}: {
|
|
1942
|
+
workloads: AppWorkload[];
|
|
1943
|
+
onSelectWorkload: (workload: AppWorkload) => void;
|
|
1944
|
+
}) {
|
|
1483
1945
|
if (workloads.length === 0) {
|
|
1484
1946
|
return <EmptyState variant="inline" headline="No inspectable workloads." />;
|
|
1485
1947
|
}
|
|
@@ -1499,8 +1961,11 @@ function WorkloadsMatrix({ workloads, onSelectWorkload }: { workloads: AppWorklo
|
|
|
1499
1961
|
</thead>
|
|
1500
1962
|
<tbody>
|
|
1501
1963
|
{workloads.map((w) => {
|
|
1502
|
-
const status = workloadRuntimeStatus(w)
|
|
1503
|
-
const reason =
|
|
1964
|
+
const status = workloadRuntimeStatus(w);
|
|
1965
|
+
const reason =
|
|
1966
|
+
w.reason === "Completed" && w.kind !== "Job"
|
|
1967
|
+
? undefined
|
|
1968
|
+
: w.reason;
|
|
1504
1969
|
return (
|
|
1505
1970
|
<tr
|
|
1506
1971
|
key={workloadKey(w)}
|
|
@@ -1514,10 +1979,18 @@ function WorkloadsMatrix({ workloads, onSelectWorkload }: { workloads: AppWorklo
|
|
|
1514
1979
|
>
|
|
1515
1980
|
{w.name}
|
|
1516
1981
|
</button>
|
|
1517
|
-
{reason &&
|
|
1982
|
+
{reason && (
|
|
1983
|
+
<div className="truncate text-xs text-theme-text-tertiary">
|
|
1984
|
+
{reason}
|
|
1985
|
+
</div>
|
|
1986
|
+
)}
|
|
1987
|
+
</td>
|
|
1988
|
+
<td className="px-2 py-2 text-theme-text-secondary">
|
|
1989
|
+
{w.kind}
|
|
1990
|
+
</td>
|
|
1991
|
+
<td className="px-2 py-2 text-theme-text-secondary">
|
|
1992
|
+
{workloadClassOf(w.workload_class)}
|
|
1518
1993
|
</td>
|
|
1519
|
-
<td className="px-2 py-2 text-theme-text-secondary">{w.kind}</td>
|
|
1520
|
-
<td className="px-2 py-2 text-theme-text-secondary">{workloadClassOf(w.workload_class)}</td>
|
|
1521
1994
|
<td className="px-2 py-2">
|
|
1522
1995
|
<span className="inline-flex items-center gap-1.5 text-theme-text-secondary">
|
|
1523
1996
|
<StatusDot tone={mapHealthToTone(status.health)} />
|
|
@@ -1588,28 +2061,29 @@ function ApplicationTopology({
|
|
|
1588
2061
|
onOpenSource,
|
|
1589
2062
|
onToggleReplicaSets,
|
|
1590
2063
|
}: {
|
|
1591
|
-
topology: Topology | null
|
|
1592
|
-
loading?: boolean
|
|
1593
|
-
available: boolean
|
|
1594
|
-
workloads: AppWorkload[]
|
|
1595
|
-
colorByWorkload: Map<string, number> | null
|
|
1596
|
-
focusNodeId?: string
|
|
1597
|
-
focusedOwnerId: WorkloadFocus
|
|
1598
|
-
onNodeClick: (node: TopologyNode) => void
|
|
1599
|
-
onNodeHover: (node: TopologyNode | null) => void
|
|
1600
|
-
onFocusWorkload: (owner: WorkloadFocus) => void
|
|
1601
|
-
onSelectWorkload: (workload: AppWorkload) => void
|
|
1602
|
-
deploymentLayer: DeploymentTopologyLayer | null
|
|
1603
|
-
deploymentSource?: AppSourceRef
|
|
1604
|
-
onOpenSource?: (source: AppSourceRef) => void
|
|
1605
|
-
onToggleReplicaSets: (ownerID: string) => void
|
|
2064
|
+
topology: Topology | null;
|
|
2065
|
+
loading?: boolean;
|
|
2066
|
+
available: boolean;
|
|
2067
|
+
workloads: AppWorkload[];
|
|
2068
|
+
colorByWorkload: Map<string, number> | null;
|
|
2069
|
+
focusNodeId?: string;
|
|
2070
|
+
focusedOwnerId: WorkloadFocus;
|
|
2071
|
+
onNodeClick: (node: TopologyNode) => void;
|
|
2072
|
+
onNodeHover: (node: TopologyNode | null) => void;
|
|
2073
|
+
onFocusWorkload: (owner: WorkloadFocus) => void;
|
|
2074
|
+
onSelectWorkload: (workload: AppWorkload) => void;
|
|
2075
|
+
deploymentLayer: DeploymentTopologyLayer | null;
|
|
2076
|
+
deploymentSource?: AppSourceRef;
|
|
2077
|
+
onOpenSource?: (source: AppSourceRef) => void;
|
|
2078
|
+
onToggleReplicaSets: (ownerID: string) => void;
|
|
1606
2079
|
}) {
|
|
1607
2080
|
const hasSharedOrUnscopedNodes = useMemo(
|
|
1608
|
-
() =>
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
2081
|
+
() =>
|
|
2082
|
+
topology?.nodes.some((node) => {
|
|
2083
|
+
if (node.data?.deploymentMembership === "source-only") return false;
|
|
2084
|
+
const stamp = ownershipOf(node.data);
|
|
2085
|
+
return !stamp.ownerWorkloadId && stamp.focusWorkloadIds.length !== 1;
|
|
2086
|
+
}) ?? false,
|
|
1613
2087
|
[topology],
|
|
1614
2088
|
);
|
|
1615
2089
|
|
|
@@ -1628,7 +2102,12 @@ function ApplicationTopology({
|
|
|
1628
2102
|
focusedOwnerId={focusedOwnerId}
|
|
1629
2103
|
onNodeHover={onNodeHover}
|
|
1630
2104
|
onToggleReplicaSets={onToggleReplicaSets}
|
|
1631
|
-
fitViewPadding={{
|
|
2105
|
+
fitViewPadding={{
|
|
2106
|
+
top: 0.08,
|
|
2107
|
+
right: 0.05,
|
|
2108
|
+
bottom: 0.08,
|
|
2109
|
+
left: "360px",
|
|
2110
|
+
}}
|
|
1632
2111
|
/>
|
|
1633
2112
|
<TopologyWorkloadLegend
|
|
1634
2113
|
workloads={workloads}
|
|
@@ -1658,202 +2137,402 @@ function ApplicationTopology({
|
|
|
1658
2137
|
|
|
1659
2138
|
function ApplicationHistoryView({
|
|
1660
2139
|
history,
|
|
1661
|
-
|
|
1662
|
-
|
|
2140
|
+
sourceLoading,
|
|
2141
|
+
items: suppliedItems,
|
|
2142
|
+
runtimeLoading,
|
|
2143
|
+
runtimeError,
|
|
2144
|
+
mode,
|
|
2145
|
+
range,
|
|
2146
|
+
rangeOptions,
|
|
2147
|
+
coverageRecordCount,
|
|
2148
|
+
runtimeLimited,
|
|
1663
2149
|
workloads,
|
|
1664
2150
|
onSelectWorkload,
|
|
2151
|
+
onNavigateToResource,
|
|
2152
|
+
onRangeChange,
|
|
2153
|
+
onOpenTimeline,
|
|
1665
2154
|
onOpenSource,
|
|
1666
2155
|
}: {
|
|
1667
2156
|
history?: AppHistory;
|
|
1668
|
-
|
|
1669
|
-
|
|
2157
|
+
sourceLoading?: boolean;
|
|
2158
|
+
items?: ApplicationHistoryItem[];
|
|
2159
|
+
runtimeLoading?: boolean;
|
|
2160
|
+
runtimeError?: boolean;
|
|
2161
|
+
mode?: "local" | "retained";
|
|
2162
|
+
range?: ApplicationHistoryRange;
|
|
2163
|
+
rangeOptions?: Array<{ value: ApplicationHistoryRange; label: string }>;
|
|
2164
|
+
coverageRecordCount?: number;
|
|
2165
|
+
runtimeLimited?: boolean;
|
|
1670
2166
|
workloads: AppWorkload[];
|
|
1671
2167
|
onSelectWorkload: (workload: AppWorkload) => void;
|
|
2168
|
+
onNavigateToResource?: (resource: ResourceRef) => void;
|
|
2169
|
+
onRangeChange?: (range: ApplicationHistoryRange) => void;
|
|
2170
|
+
onOpenTimeline?: (timestamp?: string) => void;
|
|
1672
2171
|
onOpenSource?: (source: AppSourceRef) => void;
|
|
1673
2172
|
}) {
|
|
1674
|
-
const
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
2173
|
+
const [category, setCategory] = useState<ApplicationHistoryCategory | "all">(
|
|
2174
|
+
"all",
|
|
2175
|
+
);
|
|
2176
|
+
const items = useMemo(
|
|
2177
|
+
() => suppliedItems ?? buildApplicationHistoryItems(history, []),
|
|
2178
|
+
[history, suppliedItems],
|
|
2179
|
+
);
|
|
2180
|
+
const availableCategories = useMemo(
|
|
2181
|
+
() => Array.from(new Set(items.map((item) => item.category))),
|
|
2182
|
+
[items],
|
|
2183
|
+
);
|
|
2184
|
+
useEffect(() => {
|
|
2185
|
+
if (category !== "all" && !availableCategories.includes(category))
|
|
2186
|
+
setCategory("all");
|
|
2187
|
+
}, [availableCategories, category]);
|
|
2188
|
+
const visibleItems = useMemo(
|
|
2189
|
+
() =>
|
|
2190
|
+
category === "all"
|
|
2191
|
+
? items
|
|
2192
|
+
: items.filter((item) => item.category === category),
|
|
2193
|
+
[category, items],
|
|
2194
|
+
);
|
|
2195
|
+
const groups = useMemo(() => {
|
|
2196
|
+
const byDate = new Map<string, ApplicationHistoryItem[]>();
|
|
2197
|
+
for (const item of visibleItems) {
|
|
2198
|
+
const label = applicationHistoryDateLabel(item.timestamp);
|
|
2199
|
+
byDate.set(label, [...(byDate.get(label) ?? []), item]);
|
|
2200
|
+
}
|
|
2201
|
+
return [...byDate.entries()];
|
|
2202
|
+
}, [visibleItems]);
|
|
2203
|
+
const loading = sourceLoading || runtimeLoading;
|
|
2204
|
+
const empty = !loading && !runtimeError && items.length === 0;
|
|
2205
|
+
const hasRuntimeItems = items.some((item) => item.category !== "deployment");
|
|
1687
2206
|
|
|
1688
2207
|
return (
|
|
1689
2208
|
<div className="min-h-0 flex-1 overflow-auto">
|
|
1690
|
-
<div className="w-full max-w-[2400px]
|
|
1691
|
-
|
|
1692
|
-
<
|
|
1693
|
-
<div className="
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
))}
|
|
1704
|
-
</div>
|
|
1705
|
-
</ApplicationPanel>
|
|
1706
|
-
)}
|
|
1707
|
-
{incidents.length > 0 && (
|
|
1708
|
-
<ApplicationPanel title="Current incidents">
|
|
1709
|
-
<div className="divide-y divide-theme-border">
|
|
1710
|
-
{incidents.map((incident, idx) => {
|
|
1711
|
-
const workload = directWorkloadForEvent(
|
|
1712
|
-
{
|
|
1713
|
-
object: incident.object,
|
|
1714
|
-
reason: "",
|
|
1715
|
-
count: 1,
|
|
1716
|
-
type: "Warning",
|
|
1717
|
-
},
|
|
1718
|
-
workloads,
|
|
1719
|
-
);
|
|
1720
|
-
return (
|
|
1721
|
-
<HistoryIncidentLine
|
|
1722
|
-
key={`${incident.object}-${incident.title}-${idx}`}
|
|
1723
|
-
incident={incident}
|
|
1724
|
-
action={
|
|
1725
|
-
workload ? (
|
|
1726
|
-
<button
|
|
1727
|
-
type="button"
|
|
1728
|
-
onClick={() => onSelectWorkload(workload)}
|
|
1729
|
-
className="text-xs font-medium text-accent-text hover:underline"
|
|
1730
|
-
>
|
|
1731
|
-
Open workload
|
|
1732
|
-
</button>
|
|
1733
|
-
) : undefined
|
|
1734
|
-
}
|
|
2209
|
+
<div className="w-full max-w-[2400px] p-4 sm:p-6">
|
|
2210
|
+
<section className="overflow-hidden rounded-lg border border-theme-border bg-theme-surface shadow-theme-sm">
|
|
2211
|
+
<div className="flex flex-wrap items-start justify-between gap-3 border-b border-theme-border px-4 py-3">
|
|
2212
|
+
<div className="min-w-0">
|
|
2213
|
+
<div className="flex items-center gap-2">
|
|
2214
|
+
<Clock3 className="h-4 w-4 text-theme-text-secondary" />
|
|
2215
|
+
<h2 className="text-sm font-semibold text-theme-text-primary">
|
|
2216
|
+
Application history
|
|
2217
|
+
</h2>
|
|
2218
|
+
{loading && items.length > 0 && (
|
|
2219
|
+
<RefreshCw
|
|
2220
|
+
className="h-3.5 w-3.5 animate-spin text-theme-text-tertiary"
|
|
2221
|
+
aria-label="Refreshing history"
|
|
1735
2222
|
/>
|
|
1736
|
-
)
|
|
1737
|
-
|
|
2223
|
+
)}
|
|
2224
|
+
</div>
|
|
2225
|
+
<p className="mt-1 text-xs text-theme-text-tertiary">
|
|
2226
|
+
Deployments, configuration changes, and problems attributed to
|
|
2227
|
+
this application.
|
|
2228
|
+
</p>
|
|
1738
2229
|
</div>
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
2230
|
+
<div className="flex flex-wrap items-center justify-end gap-2">
|
|
2231
|
+
{range &&
|
|
2232
|
+
rangeOptions &&
|
|
2233
|
+
rangeOptions.length > 1 &&
|
|
2234
|
+
onRangeChange && (
|
|
2235
|
+
<select
|
|
2236
|
+
aria-label="Application history range"
|
|
2237
|
+
value={range}
|
|
2238
|
+
onChange={(event) =>
|
|
2239
|
+
onRangeChange(
|
|
2240
|
+
event.target.value as ApplicationHistoryRange,
|
|
2241
|
+
)
|
|
2242
|
+
}
|
|
2243
|
+
className="h-8 rounded-md border border-theme-border bg-theme-base px-2 text-xs text-theme-text-primary"
|
|
1751
2244
|
>
|
|
1752
|
-
{
|
|
1753
|
-
|
|
2245
|
+
{rangeOptions.map((option) => (
|
|
2246
|
+
<option key={option.value} value={option.value}>
|
|
2247
|
+
{option.label}
|
|
2248
|
+
</option>
|
|
2249
|
+
))}
|
|
2250
|
+
</select>
|
|
1754
2251
|
)}
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
2252
|
+
{onOpenTimeline && (
|
|
2253
|
+
<button
|
|
2254
|
+
type="button"
|
|
2255
|
+
onClick={() => onOpenTimeline(visibleItems[0]?.timestamp)}
|
|
2256
|
+
className="inline-flex h-8 items-center gap-1.5 px-2 text-xs font-medium text-accent-text hover:underline"
|
|
2257
|
+
>
|
|
2258
|
+
Open full Timeline
|
|
2259
|
+
<ExternalLink className="h-3.5 w-3.5" />
|
|
2260
|
+
</button>
|
|
2261
|
+
)}
|
|
2262
|
+
</div>
|
|
2263
|
+
</div>
|
|
2264
|
+
|
|
2265
|
+
{availableCategories.length > 1 && (
|
|
2266
|
+
<div
|
|
2267
|
+
className="flex flex-wrap gap-1 border-b border-theme-border px-4 py-2"
|
|
2268
|
+
role="group"
|
|
2269
|
+
aria-label="Application history categories"
|
|
2270
|
+
>
|
|
2271
|
+
{(["all", "deployment", "change", "problem"] as const).map(
|
|
2272
|
+
(value) => {
|
|
2273
|
+
if (value !== "all" && !availableCategories.includes(value))
|
|
2274
|
+
return null;
|
|
2275
|
+
return (
|
|
2276
|
+
<button
|
|
2277
|
+
key={value}
|
|
2278
|
+
type="button"
|
|
2279
|
+
aria-pressed={category === value}
|
|
2280
|
+
onClick={() => setCategory(value)}
|
|
2281
|
+
className={clsx(
|
|
2282
|
+
"rounded-md px-2.5 py-1 text-xs font-medium transition-colors",
|
|
2283
|
+
category === value
|
|
2284
|
+
? "bg-accent-subtle text-accent-text"
|
|
2285
|
+
: "text-theme-text-secondary hover:bg-theme-hover hover:text-theme-text-primary",
|
|
2286
|
+
)}
|
|
2287
|
+
>
|
|
2288
|
+
{applicationHistoryCategoryLabel(value)}
|
|
2289
|
+
</button>
|
|
2290
|
+
);
|
|
2291
|
+
},
|
|
2292
|
+
)}
|
|
2293
|
+
</div>
|
|
2294
|
+
)}
|
|
2295
|
+
|
|
2296
|
+
{(runtimeError || (history?.partialSources?.length ?? 0) > 0) && (
|
|
2297
|
+
<div
|
|
2298
|
+
className={clsx(
|
|
2299
|
+
"border-b border-theme-border bg-theme-elevated px-4 py-2 text-xs",
|
|
2300
|
+
HEALTH_META.degraded.text,
|
|
2301
|
+
)}
|
|
2302
|
+
>
|
|
2303
|
+
{runtimeError && (
|
|
2304
|
+
<div>
|
|
2305
|
+
{hasRuntimeItems
|
|
2306
|
+
? "Application changes could not be refreshed. Previously loaded changes may be stale."
|
|
2307
|
+
: "Application changes could not be loaded."}
|
|
2308
|
+
{!hasRuntimeItems &&
|
|
2309
|
+
items.some((item) => item.category === "deployment") &&
|
|
2310
|
+
" Deployment-source history is still shown."}
|
|
2311
|
+
</div>
|
|
2312
|
+
)}
|
|
2313
|
+
{history?.partialSources?.map((message, index) => (
|
|
2314
|
+
<div key={`${message}-${index}`}>{message}</div>
|
|
1764
2315
|
))}
|
|
1765
2316
|
</div>
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
2317
|
+
)}
|
|
2318
|
+
|
|
2319
|
+
{loading && items.length === 0 && (
|
|
2320
|
+
<div className="flex items-center gap-2 px-4 py-8 text-sm text-theme-text-tertiary">
|
|
2321
|
+
<RefreshCw className="h-4 w-4 animate-spin" />
|
|
2322
|
+
Loading application history...
|
|
2323
|
+
</div>
|
|
2324
|
+
)}
|
|
2325
|
+
|
|
2326
|
+
{groups.map(([label, dateItems]) => (
|
|
2327
|
+
<div key={label}>
|
|
2328
|
+
<div className="border-b border-theme-border bg-theme-base px-4 py-2 text-[10px] font-semibold uppercase tracking-wide text-theme-text-tertiary">
|
|
2329
|
+
{label}
|
|
2330
|
+
</div>
|
|
2331
|
+
<div className="divide-y divide-theme-border">
|
|
2332
|
+
{dateItems.map((item) => (
|
|
2333
|
+
<ApplicationHistoryLine
|
|
2334
|
+
key={item.id}
|
|
2335
|
+
item={item}
|
|
2336
|
+
workloads={workloads}
|
|
2337
|
+
onSelectWorkload={onSelectWorkload}
|
|
2338
|
+
onNavigateToResource={onNavigateToResource}
|
|
2339
|
+
onOpenSource={onOpenSource}
|
|
2340
|
+
/>
|
|
2341
|
+
))}
|
|
2342
|
+
</div>
|
|
2343
|
+
</div>
|
|
2344
|
+
))}
|
|
2345
|
+
|
|
2346
|
+
{empty && (
|
|
2347
|
+
<div className="px-4 py-8">
|
|
2348
|
+
<EmptyState
|
|
2349
|
+
variant="inline"
|
|
2350
|
+
headline="No application history in this range."
|
|
2351
|
+
body="Overview still shows the current application state."
|
|
2352
|
+
/>
|
|
2353
|
+
</div>
|
|
2354
|
+
)}
|
|
2355
|
+
|
|
2356
|
+
<div className="space-y-1 border-t border-theme-border px-4 py-2 text-xs text-theme-text-tertiary">
|
|
2357
|
+
{mode === "local" && (
|
|
2358
|
+
<div>
|
|
2359
|
+
Changes and problems reflect events retained by this Radar
|
|
2360
|
+
instance. Deployment-source history may cover a different
|
|
2361
|
+
period.
|
|
2362
|
+
</div>
|
|
2363
|
+
)}
|
|
2364
|
+
{mode === "retained" && (coverageRecordCount ?? 0) > 0 && (
|
|
2365
|
+
<div>
|
|
2366
|
+
Recording coverage is incomplete for part of this range.
|
|
2367
|
+
</div>
|
|
2368
|
+
)}
|
|
2369
|
+
{mode === "retained" && runtimeLimited && (
|
|
2370
|
+
<div>
|
|
2371
|
+
Showing the 10,000 most recent events in these namespaces. Older
|
|
2372
|
+
application activity in this range may not be included.
|
|
2373
|
+
</div>
|
|
2374
|
+
)}
|
|
2375
|
+
</div>
|
|
2376
|
+
</section>
|
|
1777
2377
|
</div>
|
|
1778
2378
|
</div>
|
|
1779
2379
|
);
|
|
1780
2380
|
}
|
|
1781
2381
|
|
|
1782
|
-
function
|
|
1783
|
-
|
|
2382
|
+
function ApplicationHistoryLine({
|
|
2383
|
+
item,
|
|
2384
|
+
workloads,
|
|
2385
|
+
onSelectWorkload,
|
|
2386
|
+
onNavigateToResource,
|
|
2387
|
+
onOpenSource,
|
|
1784
2388
|
}: {
|
|
1785
|
-
|
|
2389
|
+
item: ApplicationHistoryItem;
|
|
2390
|
+
workloads: AppWorkload[];
|
|
2391
|
+
onSelectWorkload: (workload: AppWorkload) => void;
|
|
2392
|
+
onNavigateToResource?: (resource: ResourceRef) => void;
|
|
2393
|
+
onOpenSource?: (source: AppSourceRef) => void;
|
|
1786
2394
|
}) {
|
|
2395
|
+
const workload = item.resource
|
|
2396
|
+
? workloads.find(
|
|
2397
|
+
(candidate) =>
|
|
2398
|
+
candidate.kind.toLowerCase() === item.resource!.kind.toLowerCase() &&
|
|
2399
|
+
candidate.namespace === item.resource!.namespace &&
|
|
2400
|
+
candidate.name === item.resource!.name,
|
|
2401
|
+
)
|
|
2402
|
+
: undefined;
|
|
2403
|
+
const Icon =
|
|
2404
|
+
item.category === "deployment"
|
|
2405
|
+
? GitCommit
|
|
2406
|
+
: item.category === "problem"
|
|
2407
|
+
? AlertTriangle
|
|
2408
|
+
: item.resource
|
|
2409
|
+
? getTopologyIcon(item.resource.kind)
|
|
2410
|
+
: Activity;
|
|
2411
|
+
const action =
|
|
2412
|
+
item.sourceRef && onOpenSource
|
|
2413
|
+
? {
|
|
2414
|
+
label: sourceLinkLabel(item.sourceRef),
|
|
2415
|
+
run: () => onOpenSource(item.sourceRef!),
|
|
2416
|
+
}
|
|
2417
|
+
: workload
|
|
2418
|
+
? { label: "Open workload", run: () => onSelectWorkload(workload) }
|
|
2419
|
+
: item.resource && onNavigateToResource
|
|
2420
|
+
? {
|
|
2421
|
+
label: "Open resource",
|
|
2422
|
+
run: () => onNavigateToResource(item.resource!),
|
|
2423
|
+
}
|
|
2424
|
+
: null;
|
|
2425
|
+
const revisionAlreadyInTitle = Boolean(
|
|
2426
|
+
item.revision &&
|
|
2427
|
+
item.title.toLowerCase().startsWith("helm revision ") &&
|
|
2428
|
+
item.title.endsWith(item.revision),
|
|
2429
|
+
);
|
|
2430
|
+
|
|
1787
2431
|
return (
|
|
1788
|
-
<div className="grid grid-cols-[
|
|
2432
|
+
<div className="grid grid-cols-[auto_minmax(0,1fr)_auto] gap-3 px-4 py-3">
|
|
2433
|
+
<div
|
|
2434
|
+
className={clsx(
|
|
2435
|
+
"flex h-8 w-8 shrink-0 items-center justify-center rounded-md border",
|
|
2436
|
+
item.category === "problem"
|
|
2437
|
+
? clsx(
|
|
2438
|
+
"border-theme-border bg-theme-elevated",
|
|
2439
|
+
HEALTH_META.degraded.text,
|
|
2440
|
+
)
|
|
2441
|
+
: "border-theme-border bg-theme-elevated text-theme-text-secondary",
|
|
2442
|
+
)}
|
|
2443
|
+
>
|
|
2444
|
+
<Icon className="h-4 w-4" />
|
|
2445
|
+
</div>
|
|
1789
2446
|
<div className="min-w-0">
|
|
1790
2447
|
<div className="flex flex-wrap items-center gap-2">
|
|
1791
2448
|
<span className="font-medium text-theme-text-primary">
|
|
1792
|
-
{
|
|
2449
|
+
{item.title}
|
|
1793
2450
|
</span>
|
|
1794
|
-
{
|
|
1795
|
-
<span className={`${CHIP} ${historyStatusTone(
|
|
1796
|
-
{
|
|
2451
|
+
{item.status && (
|
|
2452
|
+
<span className={`${CHIP} ${historyStatusTone(item.status)}`}>
|
|
2453
|
+
{item.status}
|
|
1797
2454
|
</span>
|
|
1798
2455
|
)}
|
|
2456
|
+
{item.count && item.count > 1 && (
|
|
2457
|
+
<span className={`${CHIP} ${CHIP_TONE.muted}`}>{item.count}x</span>
|
|
2458
|
+
)}
|
|
1799
2459
|
</div>
|
|
1800
|
-
{
|
|
2460
|
+
{item.revision && !revisionAlreadyInTitle && (
|
|
1801
2461
|
<div className="truncate font-mono text-sm text-theme-text-secondary">
|
|
1802
|
-
{
|
|
2462
|
+
{item.revision}
|
|
1803
2463
|
</div>
|
|
1804
2464
|
)}
|
|
1805
|
-
{
|
|
2465
|
+
{item.detail && (
|
|
1806
2466
|
<div className="mt-1 line-clamp-2 text-sm text-theme-text-tertiary">
|
|
1807
|
-
{
|
|
2467
|
+
{item.detail}
|
|
1808
2468
|
</div>
|
|
1809
2469
|
)}
|
|
1810
|
-
{
|
|
1811
|
-
<
|
|
1812
|
-
|
|
1813
|
-
|
|
2470
|
+
{item.resource && (
|
|
2471
|
+
<button
|
|
2472
|
+
type="button"
|
|
2473
|
+
onClick={action?.run}
|
|
2474
|
+
disabled={!action}
|
|
2475
|
+
className={clsx(
|
|
2476
|
+
"mt-1 truncate text-xs",
|
|
2477
|
+
action
|
|
2478
|
+
? "text-accent-text hover:underline"
|
|
2479
|
+
: "text-theme-text-tertiary",
|
|
2480
|
+
)}
|
|
2481
|
+
>
|
|
2482
|
+
{getKindLabel(item.resource.kind)} · {item.resource.namespace}/
|
|
2483
|
+
{item.resource.name}
|
|
2484
|
+
</button>
|
|
1814
2485
|
)}
|
|
1815
2486
|
</div>
|
|
1816
2487
|
<div className="text-right text-xs text-theme-text-tertiary">
|
|
1817
|
-
{
|
|
1818
|
-
{
|
|
1819
|
-
|
|
1820
|
-
</div>
|
|
1821
|
-
);
|
|
1822
|
-
}
|
|
1823
|
-
|
|
1824
|
-
function HistoryIncidentLine({
|
|
1825
|
-
incident,
|
|
1826
|
-
action,
|
|
1827
|
-
}: {
|
|
1828
|
-
incident: NonNullable<AppHistory["incidents"]>[number];
|
|
1829
|
-
action?: ReactNode;
|
|
1830
|
-
}) {
|
|
1831
|
-
return (
|
|
1832
|
-
<div className="grid grid-cols-[minmax(0,1fr)_auto] gap-3 py-3 first:pt-0 last:pb-0">
|
|
1833
|
-
<div className="min-w-0">
|
|
1834
|
-
<div className="truncate font-medium text-theme-text-primary">
|
|
1835
|
-
{incident.title}
|
|
1836
|
-
</div>
|
|
1837
|
-
{incident.object && (
|
|
1838
|
-
<div className="truncate text-sm text-theme-text-secondary">
|
|
1839
|
-
{incident.object}
|
|
1840
|
-
</div>
|
|
2488
|
+
<div>{applicationHistoryTimeLabel(item.timestamp)}</div>
|
|
2489
|
+
{item.initiatedBy && (
|
|
2490
|
+
<div className="mt-1 max-w-40 truncate">{item.initiatedBy}</div>
|
|
1841
2491
|
)}
|
|
1842
|
-
{
|
|
1843
|
-
<
|
|
1844
|
-
|
|
1845
|
-
|
|
2492
|
+
{action && item.sourceRef && (
|
|
2493
|
+
<button
|
|
2494
|
+
type="button"
|
|
2495
|
+
onClick={action.run}
|
|
2496
|
+
className="mt-1 font-medium text-accent-text hover:underline"
|
|
2497
|
+
>
|
|
2498
|
+
{action.label}
|
|
2499
|
+
</button>
|
|
1846
2500
|
)}
|
|
1847
2501
|
</div>
|
|
1848
|
-
<div className="text-right text-xs text-theme-text-tertiary">
|
|
1849
|
-
{formatAppEventTime(incident.lastSeen)}
|
|
1850
|
-
{incident.count && incident.count > 1 && <div>{incident.count}x</div>}
|
|
1851
|
-
{action && <div className="mt-1">{action}</div>}
|
|
1852
|
-
</div>
|
|
1853
2502
|
</div>
|
|
1854
2503
|
);
|
|
1855
2504
|
}
|
|
1856
2505
|
|
|
2506
|
+
function applicationHistoryCategoryLabel(
|
|
2507
|
+
category: ApplicationHistoryCategory | "all",
|
|
2508
|
+
): string {
|
|
2509
|
+
if (category === "all") return "All";
|
|
2510
|
+
if (category === "deployment") return "Deployments";
|
|
2511
|
+
if (category === "problem") return "Problems";
|
|
2512
|
+
return "Changes";
|
|
2513
|
+
}
|
|
2514
|
+
|
|
2515
|
+
function applicationHistoryDateLabel(timestamp: string): string {
|
|
2516
|
+
const date = new Date(timestamp);
|
|
2517
|
+
const today = new Date();
|
|
2518
|
+
const yesterday = new Date(today);
|
|
2519
|
+
yesterday.setDate(today.getDate() - 1);
|
|
2520
|
+
if (date.toDateString() === today.toDateString()) return "Today";
|
|
2521
|
+
if (date.toDateString() === yesterday.toDateString()) return "Yesterday";
|
|
2522
|
+
return new Intl.DateTimeFormat(undefined, {
|
|
2523
|
+
month: "short",
|
|
2524
|
+
day: "numeric",
|
|
2525
|
+
year: date.getFullYear() === today.getFullYear() ? undefined : "numeric",
|
|
2526
|
+
}).format(date);
|
|
2527
|
+
}
|
|
2528
|
+
|
|
2529
|
+
function applicationHistoryTimeLabel(timestamp: string): string {
|
|
2530
|
+
return new Intl.DateTimeFormat(undefined, {
|
|
2531
|
+
hour: "2-digit",
|
|
2532
|
+
minute: "2-digit",
|
|
2533
|
+
}).format(new Date(timestamp));
|
|
2534
|
+
}
|
|
2535
|
+
|
|
1857
2536
|
function buildAppIssues(
|
|
1858
2537
|
workloads: AppWorkload[],
|
|
1859
2538
|
events: NonNullable<AppRow["events"]>,
|
|
@@ -1968,41 +2647,61 @@ function historyStatusTone(status: string): string {
|
|
|
1968
2647
|
}
|
|
1969
2648
|
|
|
1970
2649
|
function sourceLinkLabel(source: AppSourceRef): string {
|
|
1971
|
-
return `View ${sourceObjectLabel(source)}
|
|
2650
|
+
return `View ${sourceObjectLabel(source)}`;
|
|
1972
2651
|
}
|
|
1973
2652
|
|
|
1974
2653
|
function sourceObjectLabel(source: AppSourceRef): string {
|
|
1975
|
-
if (source.type ===
|
|
1976
|
-
if (source.tool ===
|
|
1977
|
-
|
|
1978
|
-
if (source.kind.toLowerCase() ===
|
|
1979
|
-
|
|
2654
|
+
if (source.type === "helm") return "Helm release";
|
|
2655
|
+
if (source.tool === "argocd" || source.kind.toLowerCase() === "application")
|
|
2656
|
+
return "Argo CD application";
|
|
2657
|
+
if (source.kind.toLowerCase() === "helmrelease") return "Flux HelmRelease";
|
|
2658
|
+
if (source.kind.toLowerCase() === "kustomization")
|
|
2659
|
+
return "Flux Kustomization";
|
|
2660
|
+
return "GitOps source";
|
|
1980
2661
|
}
|
|
1981
2662
|
|
|
1982
2663
|
function sourceInventoryLabel(source: AppSourceRef): string {
|
|
1983
|
-
if (source.type ===
|
|
1984
|
-
if (source.tool ===
|
|
1985
|
-
|
|
1986
|
-
return
|
|
2664
|
+
if (source.type === "helm") return "Helm";
|
|
2665
|
+
if (source.tool === "argocd" || source.kind.toLowerCase() === "application")
|
|
2666
|
+
return "Argo CD";
|
|
2667
|
+
if (source.tool === "fluxcd") return "Flux";
|
|
2668
|
+
return "GitOps";
|
|
1987
2669
|
}
|
|
1988
2670
|
|
|
1989
|
-
function SourceDeliveryStatus({
|
|
2671
|
+
function SourceDeliveryStatus({
|
|
2672
|
+
status,
|
|
2673
|
+
}: {
|
|
2674
|
+
status: NonNullable<AppRow["sourceStatus"]>;
|
|
2675
|
+
}) {
|
|
1990
2676
|
const values = [
|
|
1991
|
-
status.sync
|
|
1992
|
-
|
|
1993
|
-
|
|
2677
|
+
status.sync
|
|
2678
|
+
? { label: status.sync, tone: sourceSyncHealth(status.sync) }
|
|
2679
|
+
: null,
|
|
2680
|
+
status.health
|
|
2681
|
+
? { label: status.health, tone: sourceReportedHealth(status.health) }
|
|
2682
|
+
: null,
|
|
2683
|
+
].filter(
|
|
2684
|
+
(value): value is { label: string; tone: AppHealth } => value !== null,
|
|
2685
|
+
);
|
|
1994
2686
|
|
|
1995
2687
|
return (
|
|
1996
2688
|
<span className="inline-flex min-w-0 items-center gap-2">
|
|
1997
2689
|
{values.map((value, index) => (
|
|
1998
|
-
<span
|
|
1999
|
-
{index
|
|
2690
|
+
<span
|
|
2691
|
+
key={`${value.label}-${index}`}
|
|
2692
|
+
className="inline-flex min-w-0 items-center gap-1.5"
|
|
2693
|
+
>
|
|
2694
|
+
{index > 0 && (
|
|
2695
|
+
<span className="text-theme-text-tertiary" aria-hidden>
|
|
2696
|
+
·
|
|
2697
|
+
</span>
|
|
2698
|
+
)}
|
|
2000
2699
|
<StatusDot tone={mapHealthToTone(value.tone)} />
|
|
2001
2700
|
<span className="truncate">{value.label}</span>
|
|
2002
2701
|
</span>
|
|
2003
2702
|
))}
|
|
2004
2703
|
</span>
|
|
2005
|
-
)
|
|
2704
|
+
);
|
|
2006
2705
|
}
|
|
2007
2706
|
|
|
2008
2707
|
function formatAppEventTime(value?: string): string {
|
|
@@ -2035,12 +2734,12 @@ function ApplicationFact({
|
|
|
2035
2734
|
monoDetail,
|
|
2036
2735
|
variant = "card",
|
|
2037
2736
|
}: {
|
|
2038
|
-
label: string
|
|
2039
|
-
value: ReactNode
|
|
2040
|
-
detail?: string
|
|
2041
|
-
monoValue?: boolean
|
|
2042
|
-
monoDetail?: boolean
|
|
2043
|
-
variant?:
|
|
2737
|
+
label: string;
|
|
2738
|
+
value: ReactNode;
|
|
2739
|
+
detail?: string;
|
|
2740
|
+
monoValue?: boolean;
|
|
2741
|
+
monoDetail?: boolean;
|
|
2742
|
+
variant?: "card" | "row" | "bare";
|
|
2044
2743
|
}) {
|
|
2045
2744
|
if (variant === "row") {
|
|
2046
2745
|
return (
|
|
@@ -2287,20 +2986,23 @@ function TopologyWorkloadLegend({
|
|
|
2287
2986
|
deploymentSource,
|
|
2288
2987
|
onOpenSource,
|
|
2289
2988
|
}: {
|
|
2290
|
-
workloads: AppWorkload[]
|
|
2291
|
-
colorByWorkload: Map<string, number> | null
|
|
2292
|
-
focusedOwnerId: WorkloadFocus
|
|
2293
|
-
showSharedOrUnscoped: boolean
|
|
2294
|
-
onFocus: (owner: WorkloadFocus) => void
|
|
2295
|
-
onSelectWorkload: (workload: AppWorkload) => void
|
|
2296
|
-
deploymentLayer: DeploymentTopologyLayer | null
|
|
2297
|
-
deploymentSource?: AppSourceRef
|
|
2298
|
-
onOpenSource?: (source: AppSourceRef) => void
|
|
2989
|
+
workloads: AppWorkload[];
|
|
2990
|
+
colorByWorkload: Map<string, number> | null;
|
|
2991
|
+
focusedOwnerId: WorkloadFocus;
|
|
2992
|
+
showSharedOrUnscoped: boolean;
|
|
2993
|
+
onFocus: (owner: WorkloadFocus) => void;
|
|
2994
|
+
onSelectWorkload: (workload: AppWorkload) => void;
|
|
2995
|
+
deploymentLayer: DeploymentTopologyLayer | null;
|
|
2996
|
+
deploymentSource?: AppSourceRef;
|
|
2997
|
+
onOpenSource?: (source: AppSourceRef) => void;
|
|
2299
2998
|
}) {
|
|
2300
|
-
const managedOnlyCount = deploymentLayer?.managedOnly.length ?? 0
|
|
2301
|
-
const runtimeOnlyCount = deploymentLayer?.runtimeOnlyCount ?? 0
|
|
2302
|
-
const showSourceDifferences =
|
|
2303
|
-
|
|
2999
|
+
const managedOnlyCount = deploymentLayer?.managedOnly.length ?? 0;
|
|
3000
|
+
const runtimeOnlyCount = deploymentLayer?.runtimeOnlyCount ?? 0;
|
|
3001
|
+
const showSourceDifferences =
|
|
3002
|
+
!!deploymentSource && managedOnlyCount + runtimeOnlyCount > 0;
|
|
3003
|
+
const sourceLabel = deploymentSource
|
|
3004
|
+
? sourceInventoryLabel(deploymentSource)
|
|
3005
|
+
: "";
|
|
2304
3006
|
|
|
2305
3007
|
return (
|
|
2306
3008
|
<div className="absolute left-4 top-4 z-10 w-[min(18.5rem,calc(100%-2rem))] 2xl:w-[min(22rem,calc(100%-2rem))] overflow-hidden rounded-lg border border-theme-border bg-theme-surface/95 shadow-theme-md backdrop-blur">
|
|
@@ -2347,20 +3049,30 @@ function TopologyWorkloadLegend({
|
|
|
2347
3049
|
</div>
|
|
2348
3050
|
{showSourceDifferences && deploymentSource && (
|
|
2349
3051
|
<div className="border-t border-theme-border px-3 py-2.5">
|
|
2350
|
-
<div className="mb-1.5 text-[10px] font-semibold uppercase tracking-wide text-theme-text-tertiary">
|
|
3052
|
+
<div className="mb-1.5 text-[10px] font-semibold uppercase tracking-wide text-theme-text-tertiary">
|
|
3053
|
+
Source differences
|
|
3054
|
+
</div>
|
|
2351
3055
|
<div className="space-y-1">
|
|
2352
3056
|
{managedOnlyCount > 0 && (
|
|
2353
3057
|
<div className="flex items-center gap-2 px-1.5 py-1 text-xs text-theme-text-secondary">
|
|
2354
3058
|
<DeploymentSourceLogo source={deploymentSource} />
|
|
2355
3059
|
<span className="min-w-0 flex-1">
|
|
2356
|
-
<span className="block">
|
|
3060
|
+
<span className="block">
|
|
3061
|
+
{managedOnlyCount} only in {sourceLabel}
|
|
3062
|
+
</span>
|
|
2357
3063
|
{deploymentLayer?.managedOnlySummary && (
|
|
2358
|
-
<span className="mt-0.5 block text-[10px] text-theme-text-tertiary">
|
|
3064
|
+
<span className="mt-0.5 block text-[10px] text-theme-text-tertiary">
|
|
3065
|
+
{deploymentLayer.managedOnlySummary}
|
|
3066
|
+
</span>
|
|
2359
3067
|
)}
|
|
2360
3068
|
</span>
|
|
2361
3069
|
{onOpenSource && (
|
|
2362
3070
|
<Tooltip content={`Open ${sourceLabel} view`} delay={150}>
|
|
2363
|
-
<button
|
|
3071
|
+
<button
|
|
3072
|
+
type="button"
|
|
3073
|
+
onClick={() => onOpenSource(deploymentSource)}
|
|
3074
|
+
className="rounded p-0.5 text-theme-text-tertiary hover:bg-theme-hover hover:text-theme-text-primary"
|
|
3075
|
+
>
|
|
2364
3076
|
<ExternalLink className="h-3 w-3" aria-hidden />
|
|
2365
3077
|
</button>
|
|
2366
3078
|
</Tooltip>
|
|
@@ -2369,8 +3081,13 @@ function TopologyWorkloadLegend({
|
|
|
2369
3081
|
)}
|
|
2370
3082
|
{runtimeOnlyCount > 0 && (
|
|
2371
3083
|
<div className="flex items-center gap-2 px-1.5 py-1 text-xs text-theme-text-secondary">
|
|
2372
|
-
<Radio
|
|
2373
|
-
|
|
3084
|
+
<Radio
|
|
3085
|
+
className="h-3.5 w-3.5 shrink-0 text-theme-text-tertiary"
|
|
3086
|
+
aria-hidden
|
|
3087
|
+
/>
|
|
3088
|
+
<span>
|
|
3089
|
+
{pluralize(runtimeOnlyCount, "runtime-only resource")}
|
|
3090
|
+
</span>
|
|
2374
3091
|
</div>
|
|
2375
3092
|
)}
|
|
2376
3093
|
</div>
|
|
@@ -2381,10 +3098,21 @@ function TopologyWorkloadLegend({
|
|
|
2381
3098
|
}
|
|
2382
3099
|
|
|
2383
3100
|
function DeploymentSourceLogo({ source }: { source: AppSourceRef }) {
|
|
2384
|
-
const sourceLabel = sourceInventoryLabel(source)
|
|
2385
|
-
const logo =
|
|
2386
|
-
|
|
2387
|
-
|
|
3101
|
+
const sourceLabel = sourceInventoryLabel(source);
|
|
3102
|
+
const logo =
|
|
3103
|
+
sourceLabel === "Argo CD"
|
|
3104
|
+
? argoCdLogo
|
|
3105
|
+
: sourceLabel === "Flux"
|
|
3106
|
+
? fluxLogo
|
|
3107
|
+
: null;
|
|
3108
|
+
if (!logo)
|
|
3109
|
+
return (
|
|
3110
|
+
<Layers
|
|
3111
|
+
className="h-3.5 w-3.5 shrink-0 text-theme-text-tertiary"
|
|
3112
|
+
aria-hidden
|
|
3113
|
+
/>
|
|
3114
|
+
);
|
|
3115
|
+
return <img src={logo} alt="" className="h-4 w-4 shrink-0 object-contain" />;
|
|
2388
3116
|
}
|
|
2389
3117
|
|
|
2390
3118
|
function StaticWorkloadScope({ workload }: { workload: AppWorkload }) {
|