@skyhook-io/k8s-ui 1.8.9 → 1.8.11
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 +5 -4
- package/src/assets/gitops/argocd.png +0 -0
- package/src/assets/gitops/flux.svg +7 -0
- package/src/components/applications/AppChips.tsx +10 -0
- package/src/components/applications/ApplicationDetail.test.tsx +569 -0
- package/src/components/applications/ApplicationDetail.tsx +2517 -450
- package/src/components/applications/ApplicationsList.tsx +4 -1
- package/src/components/applications/ApplicationsView.tsx +101 -18
- package/src/components/applications/applications-view.test.tsx +47 -0
- package/src/components/applications/index.ts +1 -1
- package/src/components/audit/AuditAlerts.tsx +14 -10
- package/src/components/checks/ChecksView.tsx +81 -51
- package/src/components/checks/severity.ts +39 -45
- package/src/components/cluster-switcher/ClusterSwitcher.tsx +2 -2
- package/src/components/compare/CompareResourcePicker.tsx +2 -2
- package/src/components/gitops/GitOpsGraphFilterRail.tsx +2 -1
- package/src/components/gitops/GitOpsTableView.tsx +14 -59
- package/src/components/gitops/ManagedResourcesList.tsx +8 -11
- package/src/components/gitops/SyncCountdown.tsx +26 -17
- package/src/components/gitops/SyncOptionsDialog.tsx +2 -2
- package/src/components/gitops/insights/GitOpsInsightViews.tsx +277 -104
- package/src/components/gitops/insights/insights-helpers.test.ts +86 -1
- package/src/components/gitops/insights/insights-helpers.ts +69 -1
- package/src/components/gitops/tree/GitOpsTreeGraph.tsx +5 -2
- package/src/components/issues/IssuesView.tsx +219 -143
- package/src/components/issues/ResourceIssuesSection.test.tsx +40 -0
- package/src/components/issues/ResourceIssuesSection.tsx +28 -128
- package/src/components/issues/index.ts +3 -2
- package/src/components/issues/issue-timing.ts +53 -0
- package/src/components/issues/issues.test.ts +146 -4
- package/src/components/issues/severity.ts +29 -32
- package/src/components/logs/LogCore.tsx +14 -4
- package/src/components/logs/WorkloadLogsViewer.tsx +31 -6
- package/src/components/logs/useLogStream.ts +8 -1
- package/src/components/namespace-switcher/NamespacePicker.tsx +86 -107
- package/src/components/resources/ResourcesSidebar.test.tsx +107 -0
- package/src/components/resources/ResourcesSidebar.tsx +70 -52
- package/src/components/resources/ResourcesView.selected-kind.test.ts +143 -0
- package/src/components/resources/ResourcesView.tsx +243 -53
- package/src/components/resources/renderers/ClusterComplianceReportRenderer.tsx +2 -2
- package/src/components/resources/renderers/CronWorkflowRenderer.tsx +99 -0
- package/src/components/resources/renderers/JobRenderer.tsx +3 -1
- package/src/components/resources/renderers/MetricsUnavailableNotice.tsx +47 -0
- package/src/components/resources/renderers/NodeRenderer.test.tsx +103 -0
- package/src/components/resources/renderers/NodeRenderer.tsx +20 -12
- package/src/components/resources/renderers/PVCRenderer.test.tsx +23 -0
- package/src/components/resources/renderers/PVCRenderer.tsx +2 -2
- package/src/components/resources/renderers/PodRenderer.test.tsx +101 -0
- package/src/components/resources/renderers/PodRenderer.tsx +81 -70
- package/src/components/resources/renderers/PrometheusRuleRenderer.tsx +2 -2
- package/src/components/resources/renderers/SbomReportRenderer.tsx +2 -2
- package/src/components/resources/renderers/SecretRenderer.tsx +2 -0
- package/src/components/resources/renderers/ServiceRenderer.tsx +58 -40
- package/src/components/resources/renderers/VulnerabilityReportRenderer.tsx +2 -2
- package/src/components/resources/renderers/WorkflowRenderer.test.tsx +43 -0
- package/src/components/resources/renderers/WorkflowRenderer.tsx +60 -78
- package/src/components/resources/renderers/index.ts +1 -0
- package/src/components/resources/resource-utils-capi.ts +1 -1
- package/src/components/resources/resource-utils.ts +31 -0
- package/src/components/shared/DetailShell.tsx +2 -2
- package/src/components/shared/ResourceActionsBar.tsx +140 -108
- package/src/components/shared/ResourceRendererDispatch.tsx +33 -4
- package/src/components/timeline/TimelineList.tsx +371 -354
- package/src/components/timeline/TimelineStrip.test.tsx +194 -0
- package/src/components/timeline/TimelineStrip.tsx +836 -0
- package/src/components/timeline/TimelineSwimlanes.test.tsx +1028 -0
- package/src/components/timeline/TimelineSwimlanes.tsx +2498 -593
- package/src/components/timeline/TimelineToolbar.test.tsx +320 -0
- package/src/components/timeline/TimelineToolbar.tsx +759 -0
- package/src/components/timeline/TimelineToolbarState.test.tsx +64 -0
- package/src/components/timeline/index.ts +23 -0
- package/src/components/timeline/scrubber-math.test.ts +235 -0
- package/src/components/timeline/scrubber-math.ts +249 -0
- package/src/components/timeline/shared.test.tsx +219 -0
- package/src/components/timeline/shared.tsx +232 -447
- package/src/components/timeline/timeline-filters.test.ts +196 -0
- package/src/components/timeline/timeline-filters.ts +180 -0
- package/src/components/timeline/timeline-lane-sort.test.ts +97 -0
- package/src/components/timeline/timeline-lane-sort.ts +77 -0
- package/src/components/timeline/timeline-live.test.ts +131 -0
- package/src/components/timeline/timeline-live.ts +77 -0
- package/src/components/topology/GroupNode.tsx +9 -2
- package/src/components/topology/K8sResourceNode.test.ts +11 -0
- package/src/components/topology/K8sResourceNode.tsx +387 -243
- package/src/components/topology/TopologyFilterSidebar.tsx +9 -0
- package/src/components/topology/TopologyGraph.tsx +89 -16
- package/src/components/topology/TopologySearch.tsx +2 -2
- package/src/components/topology/layout.ts +10 -2
- package/src/components/topology/topology.css +0 -92
- package/src/components/ui/Badge.tsx +5 -0
- package/src/components/ui/BoardSkeleton.tsx +47 -0
- package/src/components/ui/CardSection.tsx +117 -0
- package/src/components/ui/CodeViewer.tsx +2 -2
- package/src/components/ui/Collapse.tsx +59 -0
- package/src/components/ui/ConfirmDialog.tsx +7 -3
- package/src/components/ui/DialogPortal.tsx +31 -9
- package/src/components/ui/Input.tsx +24 -0
- package/src/components/ui/MultiSelectPicker.test.tsx +101 -0
- package/src/components/ui/MultiSelectPicker.tsx +167 -0
- package/src/components/ui/PaneLoader.tsx +10 -2
- package/src/components/ui/ResourceBar.tsx +68 -25
- package/src/components/ui/SearchBox.tsx +18 -4
- package/src/components/ui/SearchPillInput.tsx +2 -2
- package/src/components/ui/Toast.tsx +1 -1
- package/src/components/ui/drawer-components.tsx +39 -26
- package/src/components/ui/index.ts +6 -0
- package/src/components/ui/severity-tone.ts +63 -0
- package/src/components/workload/ResourceDetailDrawer.tsx +6 -2
- package/src/components/workload/WorkloadView.tsx +2682 -472
- package/src/components/workload/index.ts +1 -1
- package/src/theme/variables.css +15 -1
- package/src/types/core.ts +57 -4
- package/src/utils/api-resources.test.ts +54 -0
- package/src/utils/api-resources.ts +68 -1
- package/src/utils/application-topology.test.ts +198 -0
- package/src/utils/application-topology.ts +261 -0
- package/src/utils/applications.test.ts +167 -1
- package/src/utils/applications.ts +1157 -393
- package/src/utils/index.ts +2 -0
- package/src/utils/navigation.test.ts +32 -1
- package/src/utils/navigation.ts +70 -0
- package/src/utils/resource-hierarchy.test.ts +990 -1
- package/src/utils/resource-hierarchy.ts +905 -267
- package/src/utils/resource-icons.ts +2 -0
- package/src/utils/topology-neighborhood.test.ts +202 -1
- package/src/utils/topology-neighborhood.ts +194 -13
- package/src/utils/workflow-execution.test.ts +556 -0
- package/src/utils/workflow-execution.ts +814 -0
- package/src/utils/workload-colors.ts +2 -3
- package/tsconfig.json +2 -2
|
@@ -1,26 +1,8 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import type
|
|
5
|
-
import { categoryLabel } from './severity'
|
|
6
|
-
import { diagnosticRoleLabel, diagnosticFactLabel, confidenceTitle } from './diagnostic'
|
|
1
|
+
import { useMemo, useState } from 'react'
|
|
2
|
+
import { AlertTriangle } from 'lucide-react'
|
|
3
|
+
import { IssueRow } from './IssuesView'
|
|
4
|
+
import { compareIssues, type Issue, type IssueResourceRef } from './types'
|
|
7
5
|
|
|
8
|
-
/**
|
|
9
|
-
* ResourceIssuesSection — the compact "Operational Issues" block for the resource
|
|
10
|
-
* detail. Renders the live, diagnosed Issues that touch one resource (its own +,
|
|
11
|
-
* for a workload, its owned pods' issues, server-rolled-up via RelatedIssues).
|
|
12
|
-
* The diagnosis sibling of IssuesView's queue row — same Cause/Action vocabulary,
|
|
13
|
-
* stripped of the queue chrome (accordion, scope copy, fan-out expansion).
|
|
14
|
-
*
|
|
15
|
-
* Header mirrors the queue: the plain `categoryLabel` is the operator-facing
|
|
16
|
-
* headline and the raw `reason` rides alongside as a muted signal (so the K8s
|
|
17
|
-
* jargon is available but not the lead). Body is intentionally just the plain
|
|
18
|
-
* `cause` (which names the offending object) + the `Next step` — the diagnosis
|
|
19
|
-
* and the fix. The raw `message`/evidence stays in the queue + MCP where the
|
|
20
|
-
* locator detail is wanted; inline it mostly restated the cause or the category,
|
|
21
|
-
* so it's omitted to keep the card scannable. (`message` is the body fallback
|
|
22
|
-
* only for categories that don't yet emit a `cause`.)
|
|
23
|
-
*/
|
|
24
6
|
export function ResourceIssuesSection({
|
|
25
7
|
issues,
|
|
26
8
|
onResourceClick,
|
|
@@ -29,117 +11,35 @@ export function ResourceIssuesSection({
|
|
|
29
11
|
/** When provided, related resources in a causal link become clickable. */
|
|
30
12
|
onResourceClick?: (ref: IssueResourceRef) => void
|
|
31
13
|
}) {
|
|
32
|
-
|
|
14
|
+
const sorted = useMemo(() => [...(issues ?? [])].sort(compareIssues), [issues])
|
|
15
|
+
const [openId, setOpenId] = useState<string | null>(null)
|
|
16
|
+
|
|
17
|
+
if (sorted.length === 0) return null
|
|
18
|
+
|
|
33
19
|
return (
|
|
34
|
-
<
|
|
35
|
-
<div className="
|
|
36
|
-
|
|
20
|
+
<section className="space-y-2">
|
|
21
|
+
<div className="flex items-center gap-2 px-1 text-xs font-semibold uppercase tracking-wide text-theme-text-secondary">
|
|
22
|
+
<AlertTriangle className="h-4 w-4 text-theme-text-tertiary" aria-hidden />
|
|
23
|
+
<span>Operational issues ({sorted.length})</span>
|
|
24
|
+
</div>
|
|
25
|
+
<ol className="flex flex-col gap-1.5">
|
|
26
|
+
{sorted.map((issue) => {
|
|
27
|
+
const key = issueKey(issue)
|
|
37
28
|
return (
|
|
38
|
-
<
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
<span className="min-w-0 flex-1 truncate text-xs text-theme-text-tertiary">{issue.reason}</span>
|
|
46
|
-
) : null}
|
|
47
|
-
{issue.count ? (
|
|
48
|
-
<span className="shrink-0 text-xs text-theme-text-tertiary tabular-nums">· {issue.count} affected</span>
|
|
49
|
-
) : null}
|
|
50
|
-
</div>
|
|
51
|
-
{issue.cause ? (
|
|
52
|
-
<p className="text-sm leading-relaxed text-theme-text-secondary">{issue.cause}</p>
|
|
53
|
-
) : issue.message ? (
|
|
54
|
-
<p className="text-sm leading-relaxed text-theme-text-secondary">{issue.message}</p>
|
|
55
|
-
) : null}
|
|
56
|
-
{issue.action ? (
|
|
57
|
-
<p className="mt-1 text-sm leading-relaxed text-theme-text-secondary">
|
|
58
|
-
<span className="font-medium text-theme-text-primary">Next step: </span>
|
|
59
|
-
{issue.action}
|
|
60
|
-
</p>
|
|
61
|
-
) : null}
|
|
62
|
-
{issue.remediation_kind === 'create-namespace' && issue.remediation_target ? (
|
|
63
|
-
<p className="mt-1 text-xs text-theme-text-tertiary">
|
|
64
|
-
Suggested fix: create namespace{' '}
|
|
65
|
-
<code className="rounded bg-theme-elevated px-1 font-mono">{issue.remediation_target}</code> — apply it from the GitOps detail page.
|
|
66
|
-
</p>
|
|
67
|
-
) : null}
|
|
68
|
-
<CausalContext issue={issue} onResourceClick={onResourceClick} />
|
|
69
|
-
</div>
|
|
29
|
+
<IssueRow
|
|
30
|
+
key={key}
|
|
31
|
+
issue={issue}
|
|
32
|
+
open={openId === key}
|
|
33
|
+
onToggle={() => setOpenId((cur) => (cur === key ? null : key))}
|
|
34
|
+
onResourceClick={onResourceClick}
|
|
35
|
+
/>
|
|
70
36
|
)
|
|
71
37
|
})}
|
|
72
|
-
</
|
|
73
|
-
</
|
|
38
|
+
</ol>
|
|
39
|
+
</section>
|
|
74
40
|
)
|
|
75
41
|
}
|
|
76
42
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
* cross-subject causal links (DiagnosticContext). Shows only the linking facts
|
|
80
|
-
* (those carrying a confidence tier or related issues) — the queue's IssuesView
|
|
81
|
-
* renders the fuller context with clickable resource navigation; here the related
|
|
82
|
-
* resources are shown as plain identifiers to keep the resource panel scannable.
|
|
83
|
-
*/
|
|
84
|
-
function CausalContext({ issue, onResourceClick }: { issue: Issue; onResourceClick?: (ref: IssueResourceRef) => void }) {
|
|
85
|
-
const ctx = issue.diagnostic_context
|
|
86
|
-
const links = ctx?.facts?.filter((f) => f.confidence || (f.related_issues && f.related_issues.length > 0)) ?? []
|
|
87
|
-
if (!ctx || links.length === 0) return null
|
|
88
|
-
return (
|
|
89
|
-
<div className="mt-2 border-t border-theme-border/60 pt-2">
|
|
90
|
-
<div className="mb-1 flex items-center gap-2">
|
|
91
|
-
<span className="text-[10px] font-semibold uppercase tracking-wide text-theme-text-tertiary">Context</span>
|
|
92
|
-
{ctx.role ? <span className="badge-sm text-[10px] text-theme-text-secondary">{diagnosticRoleLabel(ctx.role)}</span> : null}
|
|
93
|
-
</div>
|
|
94
|
-
<ul className="space-y-1.5">
|
|
95
|
-
{links.map((fact, idx) => (
|
|
96
|
-
<li key={`${fact.type}-${idx}`} className="text-xs">
|
|
97
|
-
<div className="flex flex-wrap items-baseline gap-x-2 gap-y-0.5">
|
|
98
|
-
<span className="font-medium text-theme-text-secondary">{diagnosticFactLabel(fact.type)}</span>
|
|
99
|
-
{fact.confidence ? (
|
|
100
|
-
<span className="badge-sm text-[10px] text-theme-text-tertiary" title={confidenceTitle(fact.confidence)}>
|
|
101
|
-
{fact.confidence} confidence
|
|
102
|
-
</span>
|
|
103
|
-
) : null}
|
|
104
|
-
{fact.message ? <span className="text-theme-text-tertiary">{fact.message}</span> : null}
|
|
105
|
-
</div>
|
|
106
|
-
{fact.related_issues && fact.related_issues.length > 0 ? (
|
|
107
|
-
<ul className="mt-0.5 space-y-0.5 pl-3">
|
|
108
|
-
{fact.related_issues.map((rel, ri) => {
|
|
109
|
-
const label = (
|
|
110
|
-
<>
|
|
111
|
-
<span className="text-[10px] uppercase tracking-wide text-theme-text-tertiary">{rel.ref.kind}</span>{' '}
|
|
112
|
-
<span className="font-mono">
|
|
113
|
-
{rel.ref.namespace ? `${rel.ref.namespace} / ` : ''}
|
|
114
|
-
{rel.ref.name}
|
|
115
|
-
</span>
|
|
116
|
-
{rel.count && rel.count > 1 ? (
|
|
117
|
-
<span className="ml-1 tabular-nums" title={`${rel.count} affected resources grouped under this issue`}>· {rel.count} affected</span>
|
|
118
|
-
) : null}
|
|
119
|
-
</>
|
|
120
|
-
)
|
|
121
|
-
return (
|
|
122
|
-
<li key={ri} className="text-theme-text-tertiary">
|
|
123
|
-
{onResourceClick ? (
|
|
124
|
-
<button
|
|
125
|
-
type="button"
|
|
126
|
-
onClick={() => onResourceClick(rel.ref)}
|
|
127
|
-
className="group inline-flex items-center gap-1 text-left hover:text-theme-text-secondary"
|
|
128
|
-
>
|
|
129
|
-
{label}
|
|
130
|
-
<ExternalLink className="h-3 w-3 opacity-0 group-hover:opacity-100" />
|
|
131
|
-
</button>
|
|
132
|
-
) : (
|
|
133
|
-
label
|
|
134
|
-
)}
|
|
135
|
-
</li>
|
|
136
|
-
)
|
|
137
|
-
})}
|
|
138
|
-
</ul>
|
|
139
|
-
) : null}
|
|
140
|
-
</li>
|
|
141
|
-
))}
|
|
142
|
-
</ul>
|
|
143
|
-
</div>
|
|
144
|
-
)
|
|
43
|
+
function issueKey(issue: Issue): string {
|
|
44
|
+
return `${issue.cluster_id ?? ''}:${issue.id}`
|
|
145
45
|
}
|
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
export { IssueRow, IssuesView } from './IssuesView';
|
|
6
6
|
export type { IssueRowProps, IssueRowSlotContext, IssuesViewProps } from './IssuesView';
|
|
7
7
|
export { ResourceIssuesSection } from './ResourceIssuesSection';
|
|
8
|
+
export { issueTiming } from './issue-timing';
|
|
9
|
+
export type { IssueTimingDisplay, IssueTimingDisplayKind } from './issue-timing';
|
|
8
10
|
export {
|
|
9
11
|
ISSUE_SEVERITIES,
|
|
10
12
|
ISSUE_SEVERITY_RANK,
|
|
@@ -16,10 +18,9 @@ export type { Issue, IssueSeverity, IssueAffected, IssueResourceRef, IssueDiagno
|
|
|
16
18
|
export {
|
|
17
19
|
ISSUE_SEVERITY_LABEL,
|
|
18
20
|
ISSUE_SEVERITY_BADGE_CLASS,
|
|
19
|
-
ISSUE_SEVERITY_FILL_CLASS,
|
|
20
21
|
ISSUE_SEVERITY_TEXT_CLASS,
|
|
22
|
+
ISSUE_SEVERITY_FILL_CLASS,
|
|
21
23
|
ISSUE_SEVERITY_RAIL_CLASS,
|
|
22
|
-
groupBadgeClass,
|
|
23
24
|
categoryLabel,
|
|
24
25
|
groupLabel,
|
|
25
26
|
} from './severity';
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { formatRelativeAgeTime } from '../../utils/format';
|
|
2
|
+
import { isDeploymentLikeWorkloadKind } from '../../types';
|
|
3
|
+
import type { Issue } from './types';
|
|
4
|
+
|
|
5
|
+
export type IssueTimingDisplayKind = 'creation' | 'regression';
|
|
6
|
+
|
|
7
|
+
export interface IssueTimingDisplay {
|
|
8
|
+
kind: IssueTimingDisplayKind;
|
|
9
|
+
chip: string;
|
|
10
|
+
meta: string;
|
|
11
|
+
tooltip: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function isDeploymentLikeCreation(issue: Issue): boolean {
|
|
15
|
+
const group = issue.group ?? '';
|
|
16
|
+
if (issue.kind === 'Pod') {
|
|
17
|
+
return issue.issue_timing_basis === 'pod_creation'
|
|
18
|
+
|| issue.issue_timing_basis === 'owner_condition';
|
|
19
|
+
}
|
|
20
|
+
return isDeploymentLikeWorkloadKind(issue.kind, group);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function issueTiming(issue: Issue): IssueTimingDisplay | null {
|
|
24
|
+
switch (issue.issue_timing) {
|
|
25
|
+
case 'started_after_resource_was_healthy': {
|
|
26
|
+
const started = issue.first_seen ? `started ${formatRelativeAgeTime(issue.first_seen)} after being healthy` : 'started after being healthy';
|
|
27
|
+
return {
|
|
28
|
+
kind: 'regression',
|
|
29
|
+
chip: 'after healthy',
|
|
30
|
+
meta: started,
|
|
31
|
+
tooltip: 'Previously healthy before this failing signal.',
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
case 'started_at_resource_creation': {
|
|
35
|
+
if (isDeploymentLikeCreation(issue)) {
|
|
36
|
+
return {
|
|
37
|
+
kind: 'creation',
|
|
38
|
+
chip: 'since deploy',
|
|
39
|
+
meta: 'present since deployment or first reconciliation',
|
|
40
|
+
tooltip: 'Failing signal began during deployment or first reconciliation.',
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
return {
|
|
44
|
+
kind: 'creation',
|
|
45
|
+
chip: 'since creation',
|
|
46
|
+
meta: 'present since creation or first reconciliation',
|
|
47
|
+
tooltip: 'Failing signal began during resource creation or first reconciliation.',
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
default:
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest'
|
|
1
|
+
import { afterEach, describe, it, expect, vi } from 'vitest'
|
|
2
2
|
import { createElement } from 'react'
|
|
3
3
|
import { renderToString } from 'react-dom/server'
|
|
4
4
|
import { compareIssues, subjectRef, memberRef, normalizeImagePullMessage, issueMessageParts, type Issue } from './types'
|
|
5
|
-
import { categoryLabel,
|
|
5
|
+
import { categoryLabel, groupBadgeClass, groupLabel } from './severity'
|
|
6
6
|
import { IssueRow } from './IssuesView'
|
|
7
|
+
import { issueTiming } from './issue-timing'
|
|
7
8
|
|
|
8
9
|
const base: Issue = {
|
|
9
10
|
id: 'id-0',
|
|
@@ -18,6 +19,10 @@ const base: Issue = {
|
|
|
18
19
|
}
|
|
19
20
|
const mk = (o: Partial<Issue>): Issue => ({ ...base, ...o })
|
|
20
21
|
|
|
22
|
+
afterEach(() => {
|
|
23
|
+
vi.useRealTimers()
|
|
24
|
+
})
|
|
25
|
+
|
|
21
26
|
describe('compareIssues', () => {
|
|
22
27
|
it('orders critical before warning regardless of observed age', () => {
|
|
23
28
|
const warn = mk({ id: 'w', severity: 'warning', first_seen: '2026-05-01T00:00:00Z' }) // newer
|
|
@@ -62,8 +67,145 @@ describe('category/group label fallbacks', () => {
|
|
|
62
67
|
expect(groupLabel('runtime')).toBe('Runtime')
|
|
63
68
|
expect(groupLabel('some_future_group')).toBe('Some future group')
|
|
64
69
|
})
|
|
65
|
-
it('
|
|
66
|
-
expect(groupBadgeClass('
|
|
70
|
+
it('keeps ordinary group chips neutral and emphasizes control-plane/unknown groups', () => {
|
|
71
|
+
expect(groupBadgeClass('runtime')).toContain('text-theme-text-secondary')
|
|
72
|
+
expect(groupBadgeClass('runtime')).not.toContain('ring-theme-border-light')
|
|
73
|
+
expect(groupBadgeClass('control_plane')).toContain('text-theme-text-primary')
|
|
74
|
+
expect(groupBadgeClass('control_plane')).toContain('ring-theme-border-light')
|
|
75
|
+
expect(groupBadgeClass('unknown')).toContain('ring-theme-border-light')
|
|
76
|
+
expect(groupBadgeClass('some_future_group')).toContain('text-theme-text-secondary')
|
|
77
|
+
expect(groupBadgeClass('some_future_group')).not.toContain('ring-theme-border-light')
|
|
78
|
+
})
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
describe('IssueRow', () => {
|
|
82
|
+
it('keeps relative age visible and adds deployment timing as a secondary tag', () => {
|
|
83
|
+
vi.useFakeTimers()
|
|
84
|
+
vi.setSystemTime(new Date('2026-06-30T12:00:00Z'))
|
|
85
|
+
|
|
86
|
+
const html = renderToString(createElement(IssueRow, {
|
|
87
|
+
issue: mk({
|
|
88
|
+
id: 'baseline',
|
|
89
|
+
first_seen: '2026-06-28T12:00:00Z',
|
|
90
|
+
issue_timing: 'started_at_resource_creation',
|
|
91
|
+
}),
|
|
92
|
+
open: false,
|
|
93
|
+
onToggle: () => undefined,
|
|
94
|
+
}))
|
|
95
|
+
|
|
96
|
+
expect(html).toContain('2d')
|
|
97
|
+
expect(html).toContain('since deploy')
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
it('promotes after-healthy timing in the collapsed row', () => {
|
|
101
|
+
vi.useFakeTimers()
|
|
102
|
+
vi.setSystemTime(new Date('2026-06-30T12:00:00Z'))
|
|
103
|
+
|
|
104
|
+
const html = renderToString(createElement(IssueRow, {
|
|
105
|
+
issue: mk({
|
|
106
|
+
id: 'regression',
|
|
107
|
+
first_seen: '2026-06-30T10:00:00Z',
|
|
108
|
+
issue_timing: 'started_after_resource_was_healthy',
|
|
109
|
+
issue_timing_basis: 'condition',
|
|
110
|
+
}),
|
|
111
|
+
open: false,
|
|
112
|
+
onToggle: () => undefined,
|
|
113
|
+
}))
|
|
114
|
+
|
|
115
|
+
expect(html).toContain('2h')
|
|
116
|
+
expect(html).toContain('after healthy')
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
it('uses the category-group chip class in the queue row', () => {
|
|
120
|
+
const html = renderToString(createElement(IssueRow, {
|
|
121
|
+
issue: mk({ category_group: 'control_plane' }),
|
|
122
|
+
open: false,
|
|
123
|
+
onToggle: () => undefined,
|
|
124
|
+
}))
|
|
125
|
+
|
|
126
|
+
expect(html).toContain('Control plane')
|
|
127
|
+
expect(html).toContain(groupBadgeClass('control_plane'))
|
|
128
|
+
})
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
describe('issueTiming', () => {
|
|
132
|
+
it('keeps deployment wording for deployment-like creation failures', () => {
|
|
133
|
+
expect(issueTiming(mk({
|
|
134
|
+
kind: 'Deployment',
|
|
135
|
+
group: 'apps',
|
|
136
|
+
issue_timing: 'started_at_resource_creation',
|
|
137
|
+
issue_timing_basis: 'condition',
|
|
138
|
+
}))).toMatchObject({
|
|
139
|
+
kind: 'creation',
|
|
140
|
+
chip: 'since deploy',
|
|
141
|
+
meta: 'present since deployment or first reconciliation',
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
expect(issueTiming(mk({
|
|
145
|
+
kind: 'Rollout',
|
|
146
|
+
group: 'argoproj.io',
|
|
147
|
+
issue_timing: 'started_at_resource_creation',
|
|
148
|
+
issue_timing_basis: 'condition',
|
|
149
|
+
}))).toMatchObject({
|
|
150
|
+
kind: 'creation',
|
|
151
|
+
chip: 'since deploy',
|
|
152
|
+
meta: 'present since deployment or first reconciliation',
|
|
153
|
+
})
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
it('uses deployment wording for pod issues only when workload timing evidence is present', () => {
|
|
157
|
+
expect(issueTiming(mk({
|
|
158
|
+
kind: 'Pod',
|
|
159
|
+
issue_timing: 'started_at_resource_creation',
|
|
160
|
+
issue_timing_basis: 'pod_creation',
|
|
161
|
+
}))).toMatchObject({
|
|
162
|
+
kind: 'creation',
|
|
163
|
+
chip: 'since deploy',
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
expect(issueTiming(mk({
|
|
167
|
+
kind: 'Pod',
|
|
168
|
+
issue_timing: 'started_at_resource_creation',
|
|
169
|
+
issue_timing_basis: 'phase',
|
|
170
|
+
}))).toMatchObject({
|
|
171
|
+
kind: 'creation',
|
|
172
|
+
chip: 'since creation',
|
|
173
|
+
})
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
it('uses resource creation wording for non-deployment creation failures', () => {
|
|
177
|
+
expect(issueTiming(mk({
|
|
178
|
+
kind: 'PersistentVolumeClaim',
|
|
179
|
+
issue_timing: 'started_at_resource_creation',
|
|
180
|
+
issue_timing_basis: 'phase',
|
|
181
|
+
}))).toMatchObject({
|
|
182
|
+
kind: 'creation',
|
|
183
|
+
chip: 'since creation',
|
|
184
|
+
meta: 'present since creation or first reconciliation',
|
|
185
|
+
})
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
it('describes after-healthy timing without implying root cause or safety', () => {
|
|
189
|
+
vi.useFakeTimers()
|
|
190
|
+
vi.setSystemTime(new Date('2026-06-30T12:00:00Z'))
|
|
191
|
+
|
|
192
|
+
const display = issueTiming(mk({
|
|
193
|
+
first_seen: '2026-06-30T10:00:00Z',
|
|
194
|
+
issue_timing: 'started_after_resource_was_healthy',
|
|
195
|
+
issue_timing_basis: 'condition',
|
|
196
|
+
}))
|
|
197
|
+
|
|
198
|
+
expect(display).toMatchObject({
|
|
199
|
+
kind: 'regression',
|
|
200
|
+
chip: 'after healthy',
|
|
201
|
+
meta: 'started 2h ago after being healthy',
|
|
202
|
+
tooltip: 'Previously healthy before this failing signal.',
|
|
203
|
+
})
|
|
204
|
+
expect(`${display?.chip} ${display?.meta} ${display?.tooltip}`).not.toMatch(/baseline|safe|ignore/i)
|
|
205
|
+
})
|
|
206
|
+
|
|
207
|
+
it('returns null when there is no confident timing signal', () => {
|
|
208
|
+
expect(issueTiming(mk({ issue_timing: undefined, issue_timing_basis: undefined }))).toBeNull()
|
|
67
209
|
})
|
|
68
210
|
})
|
|
69
211
|
|
|
@@ -1,11 +1,29 @@
|
|
|
1
1
|
import type { IssueSeverity } from './types';
|
|
2
2
|
import { BADGE_SEVERITY_COLORS as sev } from '../ui/Badge';
|
|
3
|
+
import { NEUTRAL_CHIP_CLASS } from '../ui/CardSection';
|
|
4
|
+
import {
|
|
5
|
+
TONE_FILL_CLASS,
|
|
6
|
+
TONE_HEADER_BAND_CLASS,
|
|
7
|
+
TONE_RAIL_CLASS,
|
|
8
|
+
TONE_SOLID_CLASS,
|
|
9
|
+
TONE_TEXT_CLASS,
|
|
10
|
+
type SeverityTone,
|
|
11
|
+
} from '../ui/severity-tone';
|
|
3
12
|
|
|
4
13
|
// Visual language for the 2-tier Issues severity. critical = red, warning =
|
|
5
14
|
// amber. Issues and Checks are different severity axes but must read as one
|
|
6
|
-
// product
|
|
7
|
-
//
|
|
8
|
-
// badge elsewhere.
|
|
15
|
+
// product, so the color strings are shared via the tone module; here we only map
|
|
16
|
+
// each tier onto its tone. The pill still pulls from the canonical Badge tones
|
|
17
|
+
// so it matches every status badge elsewhere.
|
|
18
|
+
const ISSUE_SEVERITY_TONE: Record<IssueSeverity, SeverityTone> = {
|
|
19
|
+
critical: 'red',
|
|
20
|
+
warning: 'amber',
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const byTone = <T,>(toneMap: Record<SeverityTone, T>): Record<IssueSeverity, T> => ({
|
|
24
|
+
critical: toneMap[ISSUE_SEVERITY_TONE.critical],
|
|
25
|
+
warning: toneMap[ISSUE_SEVERITY_TONE.warning],
|
|
26
|
+
});
|
|
9
27
|
|
|
10
28
|
export const ISSUE_SEVERITY_LABEL: Record<IssueSeverity, string> = {
|
|
11
29
|
critical: 'Critical',
|
|
@@ -18,39 +36,18 @@ export const ISSUE_SEVERITY_BADGE_CLASS: Record<IssueSeverity, string> = {
|
|
|
18
36
|
warning: sev.warning,
|
|
19
37
|
};
|
|
20
38
|
|
|
21
|
-
|
|
22
|
-
export const ISSUE_SEVERITY_FILL_CLASS
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
39
|
+
export const ISSUE_SEVERITY_TEXT_CLASS = byTone(TONE_TEXT_CLASS);
|
|
40
|
+
export const ISSUE_SEVERITY_FILL_CLASS = byTone(TONE_FILL_CLASS);
|
|
41
|
+
export const ISSUE_SEVERITY_RAIL_CLASS = byTone(TONE_RAIL_CLASS);
|
|
42
|
+
export const ISSUE_SEVERITY_SOLID_CLASS = byTone(TONE_SOLID_CLASS);
|
|
43
|
+
export const ISSUE_SEVERITY_HEADER_BAND_CLASS = byTone(TONE_HEADER_BAND_CLASS);
|
|
26
44
|
|
|
27
|
-
|
|
28
|
-
critical: 'text-red-600 dark:text-red-400',
|
|
29
|
-
warning: 'text-amber-600 dark:text-amber-400',
|
|
30
|
-
};
|
|
45
|
+
const GROUP_CHIP_EMPHASIS_CLASS = 'inline-flex items-center rounded-md px-2 py-0.5 text-[11px] font-semibold leading-tight bg-theme-elevated text-theme-text-primary ring-1 ring-theme-border-light';
|
|
31
46
|
|
|
32
|
-
|
|
33
|
-
export const ISSUE_SEVERITY_RAIL_CLASS: Record<IssueSeverity, string> = {
|
|
34
|
-
critical: 'border-l-red-500 hover:bg-red-50/40 dark:hover:bg-red-950/20',
|
|
35
|
-
warning: 'border-l-amber-500 hover:bg-amber-50/30 dark:hover:bg-amber-950/15',
|
|
36
|
-
};
|
|
37
|
-
|
|
38
|
-
// Category-group accent — the quiet classification tag (severity is the loud
|
|
39
|
-
// one). One hue per group; unknown/unmapped falls back to a neutral theme tag.
|
|
40
|
-
const GROUP_BADGE_CLASS: Record<string, string> = {
|
|
41
|
-
scheduling: 'bg-violet-50 text-violet-700 ring-1 ring-violet-200 dark:bg-violet-950/40 dark:text-violet-300 dark:ring-violet-900',
|
|
42
|
-
startup: 'bg-sky-50 text-sky-700 ring-1 ring-sky-200 dark:bg-sky-950/40 dark:text-sky-300 dark:ring-sky-900',
|
|
43
|
-
runtime: 'bg-rose-50 text-rose-700 ring-1 ring-rose-200 dark:bg-rose-950/40 dark:text-rose-300 dark:ring-rose-900',
|
|
44
|
-
configuration: 'bg-teal-50 text-teal-700 ring-1 ring-teal-200 dark:bg-teal-950/40 dark:text-teal-300 dark:ring-teal-900',
|
|
45
|
-
networking: 'bg-indigo-50 text-indigo-700 ring-1 ring-indigo-200 dark:bg-indigo-950/40 dark:text-indigo-300 dark:ring-indigo-900',
|
|
46
|
-
storage: 'bg-cyan-50 text-cyan-700 ring-1 ring-cyan-200 dark:bg-cyan-950/40 dark:text-cyan-300 dark:ring-cyan-900',
|
|
47
|
-
scaling: 'bg-fuchsia-50 text-fuchsia-700 ring-1 ring-fuchsia-200 dark:bg-fuchsia-950/40 dark:text-fuchsia-300 dark:ring-fuchsia-900',
|
|
48
|
-
security: 'bg-amber-50 text-amber-700 ring-1 ring-amber-200 dark:bg-amber-950/40 dark:text-amber-300 dark:ring-amber-900',
|
|
49
|
-
control_plane: 'bg-slate-100 text-slate-600 ring-1 ring-slate-200 dark:bg-slate-800/60 dark:text-slate-300 dark:ring-slate-700',
|
|
50
|
-
};
|
|
47
|
+
const EMPHASIZED_GROUPS = new Set(['control_plane', 'unknown']);
|
|
51
48
|
|
|
52
49
|
export function groupBadgeClass(group: string): string {
|
|
53
|
-
return
|
|
50
|
+
return EMPHASIZED_GROUPS.has(group) ? GROUP_CHIP_EMPHASIS_CLASS : NEUTRAL_CHIP_CLASS;
|
|
54
51
|
}
|
|
55
52
|
|
|
56
53
|
// Display labels. The server emits raw snake_case category/group enums (so a
|
|
@@ -5,6 +5,7 @@ import type { LogEntry, LogLevel } from './useLogBuffer'
|
|
|
5
5
|
import { useLogSearch } from './useLogSearch'
|
|
6
6
|
import { StructuredLogLine } from './StructuredLogLine'
|
|
7
7
|
import { Tooltip } from '../ui/Tooltip'
|
|
8
|
+
import { Input } from '../ui/Input'
|
|
8
9
|
import {
|
|
9
10
|
formatLogTimestamp,
|
|
10
11
|
highlightSearchMatches,
|
|
@@ -37,6 +38,7 @@ interface LogCoreProps {
|
|
|
37
38
|
toolbarExtra?: ToolbarExtraRenderer
|
|
38
39
|
showPodName?: boolean
|
|
39
40
|
emptyMessage?: string
|
|
41
|
+
emptyCommand?: string | null
|
|
40
42
|
errorMessage?: string | null
|
|
41
43
|
/**
|
|
42
44
|
* Hard override for the viewer palette. When set, the viewer stays pinned to
|
|
@@ -88,6 +90,8 @@ const STRUCTURED_MODE_DESCRIPTIONS: Record<StructuredMode, string> = {
|
|
|
88
90
|
raw: 'Original log line, unparsed',
|
|
89
91
|
}
|
|
90
92
|
|
|
93
|
+
const EMPTY_STATE_CLASS = 'flex-1 flex flex-col items-center justify-center gap-2 px-4 text-center'
|
|
94
|
+
|
|
91
95
|
const TIMESTAMP_FORMAT_SHORT_LABELS: Record<TimestampFormat, string> = {
|
|
92
96
|
'time-local': 'Local time',
|
|
93
97
|
'time-utc': 'UTC time',
|
|
@@ -124,6 +128,7 @@ export function LogCore({
|
|
|
124
128
|
toolbarExtra,
|
|
125
129
|
showPodName = false,
|
|
126
130
|
emptyMessage = 'No logs available',
|
|
131
|
+
emptyCommand,
|
|
127
132
|
errorMessage,
|
|
128
133
|
forceDark,
|
|
129
134
|
}: LogCoreProps) {
|
|
@@ -694,8 +699,7 @@ export function LogCore({
|
|
|
694
699
|
{search.isOpen && (
|
|
695
700
|
<div className={`flex items-center gap-2 px-3 py-2 border-b ${palette.border} ${palette.toolbarBgMuted}`}>
|
|
696
701
|
<Search className={`w-4 h-4 ${palette.textSecondary} shrink-0`} />
|
|
697
|
-
<
|
|
698
|
-
type="text"
|
|
702
|
+
<Input
|
|
699
703
|
value={search.query}
|
|
700
704
|
onChange={(e) => search.setQuery(e.target.value)}
|
|
701
705
|
onKeyDown={(e) => {
|
|
@@ -800,14 +804,20 @@ export function LogCore({
|
|
|
800
804
|
</div>
|
|
801
805
|
</div>
|
|
802
806
|
) : errorMessage ? (
|
|
803
|
-
<div className={
|
|
807
|
+
<div className={`${EMPTY_STATE_CLASS} ${palette.textError}`}>
|
|
804
808
|
<Terminal className="w-8 h-8" />
|
|
805
809
|
<span>{errorMessage}</span>
|
|
806
810
|
</div>
|
|
807
811
|
) : groupedEntries.length === 0 ? (
|
|
808
|
-
<div className={
|
|
812
|
+
<div className={`${EMPTY_STATE_CLASS} ${palette.textTertiary}`}>
|
|
809
813
|
<Terminal className="w-8 h-8" />
|
|
810
814
|
<span>{emptyMessage}</span>
|
|
815
|
+
{emptyCommand && (
|
|
816
|
+
<button type="button" onClick={() => navigator.clipboard.writeText(emptyCommand).catch(() => {})} className={`mt-2 inline-flex max-w-[80%] items-center gap-2 rounded border px-3 py-2 font-mono text-xs ${palette.border} ${palette.toolbarBg}`} title="Copy recovery command">
|
|
817
|
+
<code className="truncate">{emptyCommand}</code>
|
|
818
|
+
<Copy className="h-3.5 w-3.5 shrink-0" />
|
|
819
|
+
</button>
|
|
820
|
+
)}
|
|
811
821
|
</div>
|
|
812
822
|
) : (
|
|
813
823
|
<div className="flex-1 relative">
|
|
@@ -27,6 +27,9 @@ export interface WorkloadLogsFetchParams {
|
|
|
27
27
|
export interface WorkloadLogsResult {
|
|
28
28
|
pods: WorkloadPodInfo[]
|
|
29
29
|
logs: WorkloadRawLog[]
|
|
30
|
+
emptyReason?: string
|
|
31
|
+
emptyMessage?: string
|
|
32
|
+
command?: string
|
|
30
33
|
}
|
|
31
34
|
|
|
32
35
|
export interface WorkloadLogsViewerProps {
|
|
@@ -61,6 +64,8 @@ export function WorkloadLogsViewer({ name, fetchAll, createStream, overrideDownl
|
|
|
61
64
|
const [selectedPods, setSelectedPods] = useState<Set<string>>(new Set())
|
|
62
65
|
const [isLoading, setIsLoading] = useState(false)
|
|
63
66
|
const [fetchError, setFetchError] = useState<string | null>(null)
|
|
67
|
+
const [emptyMessage, setEmptyMessage] = useState<string | null>(null)
|
|
68
|
+
const [emptyCommand, setEmptyCommand] = useState<string | null>(null)
|
|
64
69
|
const [showPodFilter, setShowPodFilter] = useState(false)
|
|
65
70
|
const [logRange, setLogRange] = useState('100')
|
|
66
71
|
const { showError, showSuccess } = useToast()
|
|
@@ -95,9 +100,10 @@ export function WorkloadLogsViewer({ name, fetchAll, createStream, overrideDownl
|
|
|
95
100
|
setFetchError(null)
|
|
96
101
|
try {
|
|
97
102
|
const result = await fetchAll({ container: selectedContainer || undefined, tailLines, sinceSeconds })
|
|
98
|
-
// Older backends marshal empty results as null rather than [].
|
|
99
103
|
const resultPods = result.pods ?? []
|
|
100
104
|
const resultLogs = result.logs ?? []
|
|
105
|
+
setEmptyMessage(result.emptyMessage || null)
|
|
106
|
+
setEmptyCommand(result.command || null)
|
|
101
107
|
|
|
102
108
|
podColorIndexRef.current = new Map(resultPods.map((pod, i) => [pod.name, i]))
|
|
103
109
|
setPods(resultPods)
|
|
@@ -157,6 +163,8 @@ export function WorkloadLogsViewer({ name, fetchAll, createStream, overrideDownl
|
|
|
157
163
|
const nextPods = data.pods as WorkloadPodInfo[]
|
|
158
164
|
podColorIndexRef.current = new Map(nextPods.map((pod, i) => [pod.name, i]))
|
|
159
165
|
setPods(nextPods)
|
|
166
|
+
setEmptyMessage(data.emptyMessage || null)
|
|
167
|
+
setEmptyCommand(data.command || null)
|
|
160
168
|
setSelectedPods(prev => (
|
|
161
169
|
prev.size === 0 ? new Set(nextPods.map((p: WorkloadPodInfo) => p.name)) : prev
|
|
162
170
|
))
|
|
@@ -180,8 +188,12 @@ export function WorkloadLogsViewer({ name, fetchAll, createStream, overrideDownl
|
|
|
180
188
|
const existing = new Set(prev.map(p => p.name))
|
|
181
189
|
const toAdd = newPods.filter(p => !existing.has(p.name))
|
|
182
190
|
if (toAdd.length === 0) return prev
|
|
183
|
-
|
|
191
|
+
const next = [...prev, ...toAdd]
|
|
192
|
+
podColorIndexRef.current = new Map(next.map((pod, i) => [pod.name, i]))
|
|
193
|
+
return next
|
|
184
194
|
})
|
|
195
|
+
setEmptyMessage(null)
|
|
196
|
+
setEmptyCommand(null)
|
|
185
197
|
setSelectedPods(prev => {
|
|
186
198
|
const next = new Set(prev)
|
|
187
199
|
newPods.forEach(p => next.add(p.name))
|
|
@@ -197,6 +209,10 @@ export function WorkloadLogsViewer({ name, fetchAll, createStream, overrideDownl
|
|
|
197
209
|
// while new pod logs start flowing in
|
|
198
210
|
}
|
|
199
211
|
},
|
|
212
|
+
onEnd: (data: any) => {
|
|
213
|
+
if (data?.emptyMessage) setEmptyMessage(data.emptyMessage)
|
|
214
|
+
if (data?.command) setEmptyCommand(data.command)
|
|
215
|
+
},
|
|
200
216
|
},
|
|
201
217
|
'Workload log stream connection failed',
|
|
202
218
|
)
|
|
@@ -299,8 +315,13 @@ export function WorkloadLogsViewer({ name, fetchAll, createStream, overrideDownl
|
|
|
299
315
|
</div>
|
|
300
316
|
{pods.map(pod => {
|
|
301
317
|
const dotBg = palette.podColors[(podColorIndex.get(pod.name) ?? 0) % palette.podColors.length].bg
|
|
318
|
+
const primaryLabel = pod.stepName || pod.name
|
|
319
|
+
const secondaryLabel = pod.stepName ? pod.name : ''
|
|
320
|
+
const stateLabel = pod.stepPhase || pod.phase || (pod.ready ? 'Ready' : 'Not Ready')
|
|
302
321
|
let readyColor: string
|
|
303
|
-
if (
|
|
322
|
+
if (stateLabel === 'Failed' || stateLabel === 'Error') {
|
|
323
|
+
readyColor = isDark ? 'text-red-400' : 'text-red-700'
|
|
324
|
+
} else if (pod.ready || stateLabel === 'Succeeded') {
|
|
304
325
|
readyColor = isDark ? 'text-emerald-400' : 'text-emerald-700'
|
|
305
326
|
} else {
|
|
306
327
|
readyColor = isDark ? 'text-amber-400' : 'text-amber-700'
|
|
@@ -314,9 +335,12 @@ export function WorkloadLogsViewer({ name, fetchAll, createStream, overrideDownl
|
|
|
314
335
|
className={`w-3 h-3 rounded ${palette.borderLight} ${palette.elevatedBg} text-blue-500 focus:ring-blue-500 focus:ring-offset-0`}
|
|
315
336
|
/>
|
|
316
337
|
<span className={`w-2 h-2 rounded-full ${dotBg}`} />
|
|
317
|
-
<span className=
|
|
338
|
+
<span className="min-w-0 flex-1">
|
|
339
|
+
<span className={`block truncate text-xs ${palette.textPrimary}`}>{primaryLabel}</span>
|
|
340
|
+
{secondaryLabel && <span className={`block truncate text-[10px] ${palette.textTertiary}`}>{secondaryLabel}</span>}
|
|
341
|
+
</span>
|
|
318
342
|
<span className={`text-xs ${readyColor}`}>
|
|
319
|
-
{
|
|
343
|
+
{stateLabel}
|
|
320
344
|
</span>
|
|
321
345
|
</label>
|
|
322
346
|
)
|
|
@@ -360,7 +384,8 @@ export function WorkloadLogsViewer({ name, fetchAll, createStream, overrideDownl
|
|
|
360
384
|
onClear={clear}
|
|
361
385
|
toolbarExtra={renderToolbarExtra}
|
|
362
386
|
showPodName
|
|
363
|
-
emptyMessage={pods.length === 0 ? 'No pods found' : 'No logs available'}
|
|
387
|
+
emptyMessage={emptyMessage || (pods.length === 0 ? 'No pods found' : 'No logs available')}
|
|
388
|
+
emptyCommand={emptyCommand}
|
|
364
389
|
errorMessage={fetchError || (entries.length === 0 ? streamError : null)}
|
|
365
390
|
forceDark={forceDark}
|
|
366
391
|
/>
|