@skyhook-io/k8s-ui 1.7.13 → 1.7.15
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 +6 -7
- package/src/components/audit/index.ts +1 -1
- package/src/components/checks/ChecksView.tsx +269 -142
- package/src/components/checks/index.ts +14 -1
- package/src/components/issues/IssuesView.tsx +60 -23
- package/src/components/issues/index.ts +2 -2
- package/src/components/issues/issues.test.ts +7 -0
- package/src/components/issues/types.ts +37 -7
- package/src/components/logs/WorkloadLogsViewer.tsx +16 -7
- package/src/components/resources/ResourcesView.tsx +167 -21
- package/src/components/resources/get-pod-problems.test.ts +127 -0
- package/src/components/resources/renderers/PodRenderer.test.tsx +56 -0
- package/src/components/resources/renderers/PodRenderer.tsx +1 -1
- package/src/components/resources/resource-utils.ts +39 -18
- package/src/components/shared/ResourceActionsBar.tsx +6 -6
- package/src/components/ui/ForceDeleteConfirmDialog.tsx +1 -2
- package/src/components/ui/drawer-components.tsx +8 -0
- package/src/components/workload/WorkloadView.tsx +4 -4
- package/src/types/core.ts +10 -0
- package/src/utils/bulk-workload-actions.test.ts +78 -0
- package/src/utils/bulk-workload-actions.ts +50 -0
- package/src/utils/index.ts +1 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useEffect, useMemo, useState, type ReactNode } from 'react';
|
|
1
|
+
import { useEffect, useMemo, useState, type ComponentType, type ReactNode } from 'react';
|
|
2
2
|
import { ChevronRight, CircleCheck, Clock, ExternalLink } from 'lucide-react';
|
|
3
3
|
import { ClusterName, EmptyState } from '../ui';
|
|
4
4
|
import { formatCompactAge, formatRelativeAgeTime } from '../../utils/format';
|
|
@@ -90,25 +90,48 @@ export function IssuesView({ issues, anyData, resourceHref, onResourceClick, clu
|
|
|
90
90
|
);
|
|
91
91
|
}
|
|
92
92
|
|
|
93
|
-
|
|
94
|
-
issue
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
onResourceClick,
|
|
100
|
-
}: {
|
|
93
|
+
export interface IssueRowSlotContext {
|
|
94
|
+
issue: Issue;
|
|
95
|
+
open: boolean;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export interface IssueRowProps {
|
|
101
99
|
issue: Issue;
|
|
102
100
|
clusterLabel?: (issue: Issue) => string | undefined;
|
|
103
101
|
open: boolean;
|
|
104
102
|
onToggle: () => void;
|
|
105
103
|
resourceHref?: (ref: IssueResourceRef) => string;
|
|
106
104
|
onResourceClick?: (ref: IssueResourceRef) => void;
|
|
107
|
-
|
|
105
|
+
as?: 'li' | 'div';
|
|
106
|
+
className?: string;
|
|
107
|
+
dimmed?: boolean;
|
|
108
|
+
renderBadges?: (ctx: IssueRowSlotContext) => ReactNode;
|
|
109
|
+
renderMeta?: (ctx: IssueRowSlotContext) => ReactNode;
|
|
110
|
+
renderActions?: (ctx: IssueRowSlotContext) => ReactNode;
|
|
111
|
+
ResourceLinkIcon?: ComponentType<{ className?: string }>;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function IssueRow({
|
|
115
|
+
issue,
|
|
116
|
+
clusterLabel,
|
|
117
|
+
open,
|
|
118
|
+
onToggle,
|
|
119
|
+
resourceHref,
|
|
120
|
+
onResourceClick,
|
|
121
|
+
as = 'li',
|
|
122
|
+
className,
|
|
123
|
+
dimmed,
|
|
124
|
+
renderBadges,
|
|
125
|
+
renderMeta,
|
|
126
|
+
renderActions,
|
|
127
|
+
ResourceLinkIcon = ExternalLink,
|
|
128
|
+
}: IssueRowProps) {
|
|
108
129
|
const cluster = clusterLabel?.(issue);
|
|
109
130
|
const affected = affectedSummary(issue.affected);
|
|
110
131
|
const { headline } = issueMessageParts(issue);
|
|
111
132
|
const [renderDetails, setRenderDetails] = useState(open);
|
|
133
|
+
const Container = as;
|
|
134
|
+
const slotCtx = { issue, open };
|
|
112
135
|
|
|
113
136
|
useEffect(() => {
|
|
114
137
|
if (open) {
|
|
@@ -122,8 +145,12 @@ function IssueRow({
|
|
|
122
145
|
}, [open, renderDetails]);
|
|
123
146
|
|
|
124
147
|
return (
|
|
125
|
-
<
|
|
126
|
-
className=
|
|
148
|
+
<Container
|
|
149
|
+
className={[
|
|
150
|
+
'overflow-hidden rounded-xl border border-theme-border bg-theme-surface shadow-theme-sm',
|
|
151
|
+
dimmed ? 'opacity-60' : '',
|
|
152
|
+
className ?? '',
|
|
153
|
+
].filter(Boolean).join(' ')}
|
|
127
154
|
style={{ contentVisibility: 'auto', containIntrinsicSize: 'auto 72px' }}
|
|
128
155
|
>
|
|
129
156
|
{/* The whole header is the single toggle target — chevron is just the
|
|
@@ -149,6 +176,7 @@ function IssueRow({
|
|
|
149
176
|
<div className="flex min-w-0 items-baseline gap-2">
|
|
150
177
|
<span className="shrink-0 text-sm font-medium text-theme-text-primary">{categoryLabel(issue.category)}</span>
|
|
151
178
|
<span className={`badge-sm shrink-0 self-center text-[10px] ${groupBadgeClass(issue.category_group)}`}>{groupLabel(issue.category_group)}</span>
|
|
179
|
+
{renderBadges?.(slotCtx)}
|
|
152
180
|
{/* The detector reason/message rides the title row so the most
|
|
153
181
|
useful triage signal is visible without expanding — it fills
|
|
154
182
|
the otherwise-empty band between the title and the severity
|
|
@@ -180,6 +208,7 @@ function IssueRow({
|
|
|
180
208
|
<span className="shrink-0 tabular-nums">{affected}</span>
|
|
181
209
|
</>
|
|
182
210
|
) : null}
|
|
211
|
+
{renderMeta?.(slotCtx)}
|
|
183
212
|
</div>
|
|
184
213
|
</div>
|
|
185
214
|
|
|
@@ -203,6 +232,7 @@ function IssueRow({
|
|
|
203
232
|
<span className={`badge-sm shrink-0 text-[10px] font-semibold ${ISSUE_SEVERITY_BADGE_CLASS[issue.severity]}`}>
|
|
204
233
|
{ISSUE_SEVERITY_LABEL[issue.severity]}
|
|
205
234
|
</span>
|
|
235
|
+
{renderActions?.(slotCtx)}
|
|
206
236
|
</div>
|
|
207
237
|
|
|
208
238
|
{renderDetails ? (
|
|
@@ -218,16 +248,16 @@ function IssueRow({
|
|
|
218
248
|
<div className="border-t border-theme-border bg-theme-base/40 px-4 py-4 pl-11">
|
|
219
249
|
<div className="flex flex-col gap-4">
|
|
220
250
|
<Diagnosis issue={issue} />
|
|
221
|
-
<DiagnosticContext issue={issue} resourceHref={resourceHref} onResourceClick={onResourceClick} />
|
|
251
|
+
<DiagnosticContext issue={issue} resourceHref={resourceHref} onResourceClick={onResourceClick} ResourceLinkIcon={ResourceLinkIcon} />
|
|
222
252
|
<div className="border-t border-theme-border/70 pt-3">
|
|
223
|
-
<AffectedResources issue={issue} resourceHref={resourceHref} onResourceClick={onResourceClick} />
|
|
253
|
+
<AffectedResources issue={issue} resourceHref={resourceHref} onResourceClick={onResourceClick} ResourceLinkIcon={ResourceLinkIcon} />
|
|
224
254
|
</div>
|
|
225
255
|
</div>
|
|
226
256
|
</div>
|
|
227
257
|
</div>
|
|
228
258
|
</div>
|
|
229
259
|
) : null}
|
|
230
|
-
</
|
|
260
|
+
</Container>
|
|
231
261
|
);
|
|
232
262
|
}
|
|
233
263
|
|
|
@@ -241,11 +271,9 @@ function Diagnosis({ issue }: { issue: Issue }) {
|
|
|
241
271
|
.join(' · ')
|
|
242
272
|
: null;
|
|
243
273
|
const { headline, detail } = issueMessageParts(issue);
|
|
244
|
-
// When the issue carries a parsed plain-English cause
|
|
245
|
-
//
|
|
246
|
-
|
|
247
|
-
// category chip. The raw message is kept below as de-emphasized detail.
|
|
248
|
-
const rawMessage = [headline, detail].filter(Boolean).join(' ');
|
|
274
|
+
// When the issue carries a parsed plain-English cause, lead with it. The raw
|
|
275
|
+
// detector message is kept below as de-emphasized detail.
|
|
276
|
+
const rawMessage = issue.cause ? issue.message ?? '' : [headline, detail].filter(Boolean).join(' ');
|
|
249
277
|
return (
|
|
250
278
|
<section className="flex flex-col gap-1">
|
|
251
279
|
<h4 className="text-[11px] font-semibold uppercase tracking-wide text-theme-text-tertiary">What's wrong</h4>
|
|
@@ -280,7 +308,7 @@ function Diagnosis({ issue }: { issue: Issue }) {
|
|
|
280
308
|
{issue.operation_retry_count ? ` · retried ${issue.operation_retry_count}×` : ''}
|
|
281
309
|
</p>
|
|
282
310
|
) : null}
|
|
283
|
-
{/* Raw
|
|
311
|
+
{/* Raw detector message, de-emphasized — shown below the parsed cause so
|
|
284
312
|
the precise error (URLs, resource names) is available without leading. */}
|
|
285
313
|
{issue.cause && rawMessage ? (
|
|
286
314
|
<p className="break-words font-mono text-[11px] leading-relaxed text-theme-text-tertiary">{rawMessage}</p>
|
|
@@ -316,10 +344,12 @@ function DiagnosticContext({
|
|
|
316
344
|
issue,
|
|
317
345
|
resourceHref,
|
|
318
346
|
onResourceClick,
|
|
347
|
+
ResourceLinkIcon,
|
|
319
348
|
}: {
|
|
320
349
|
issue: Issue;
|
|
321
350
|
resourceHref?: (ref: IssueResourceRef) => string;
|
|
322
351
|
onResourceClick?: (ref: IssueResourceRef) => void;
|
|
352
|
+
ResourceLinkIcon: ComponentType<{ className?: string }>;
|
|
323
353
|
}) {
|
|
324
354
|
const ctx = issue.diagnostic_context;
|
|
325
355
|
const facts = ctx?.facts?.filter((fact) => fact.message || fact.refs?.length || fact.related_issues?.length) ?? [];
|
|
@@ -347,6 +377,7 @@ function DiagnosticContext({
|
|
|
347
377
|
refForLink={memberRef(issue, related.ref)}
|
|
348
378
|
resourceHref={resourceHref}
|
|
349
379
|
onResourceClick={onResourceClick}
|
|
380
|
+
ResourceLinkIcon={ResourceLinkIcon}
|
|
350
381
|
/>
|
|
351
382
|
))}
|
|
352
383
|
</ul>
|
|
@@ -359,6 +390,7 @@ function DiagnosticContext({
|
|
|
359
390
|
refForLink={memberRef(issue, ref)}
|
|
360
391
|
resourceHref={resourceHref}
|
|
361
392
|
onResourceClick={onResourceClick}
|
|
393
|
+
ResourceLinkIcon={ResourceLinkIcon}
|
|
362
394
|
/>
|
|
363
395
|
))}
|
|
364
396
|
</ul>
|
|
@@ -424,10 +456,12 @@ function AffectedResources({
|
|
|
424
456
|
issue,
|
|
425
457
|
resourceHref,
|
|
426
458
|
onResourceClick,
|
|
459
|
+
ResourceLinkIcon,
|
|
427
460
|
}: {
|
|
428
461
|
issue: Issue;
|
|
429
462
|
resourceHref?: (ref: IssueResourceRef) => string;
|
|
430
463
|
onResourceClick?: (ref: IssueResourceRef) => void;
|
|
464
|
+
ResourceLinkIcon: ComponentType<{ className?: string }>;
|
|
431
465
|
}) {
|
|
432
466
|
const members = issue.members ?? [];
|
|
433
467
|
// count is the backend fan-out size (members, subject excluded — see
|
|
@@ -439,7 +473,7 @@ function AffectedResources({
|
|
|
439
473
|
first deep-link; members (the folded pods) follow. ResourceLine emits
|
|
440
474
|
an <li>, so it needs a list parent of its own. */}
|
|
441
475
|
<ul className="flex flex-col gap-px">
|
|
442
|
-
<ResourceLine label="Subject" refForLink={subjectRef(issue)} resourceHref={resourceHref} onResourceClick={onResourceClick} />
|
|
476
|
+
<ResourceLine label="Subject" refForLink={subjectRef(issue)} resourceHref={resourceHref} onResourceClick={onResourceClick} ResourceLinkIcon={ResourceLinkIcon} />
|
|
443
477
|
</ul>
|
|
444
478
|
{members.length > 0 && (
|
|
445
479
|
<>
|
|
@@ -453,6 +487,7 @@ function AffectedResources({
|
|
|
453
487
|
refForLink={memberRef(issue, m)}
|
|
454
488
|
resourceHref={resourceHref}
|
|
455
489
|
onResourceClick={onResourceClick}
|
|
490
|
+
ResourceLinkIcon={ResourceLinkIcon}
|
|
456
491
|
/>
|
|
457
492
|
))}
|
|
458
493
|
</ul>
|
|
@@ -472,11 +507,13 @@ function ResourceLine({
|
|
|
472
507
|
refForLink,
|
|
473
508
|
resourceHref,
|
|
474
509
|
onResourceClick,
|
|
510
|
+
ResourceLinkIcon,
|
|
475
511
|
}: {
|
|
476
512
|
label?: string;
|
|
477
513
|
refForLink: IssueResourceRef;
|
|
478
514
|
resourceHref?: (ref: IssueResourceRef) => string;
|
|
479
515
|
onResourceClick?: (ref: IssueResourceRef) => void;
|
|
516
|
+
ResourceLinkIcon: ComponentType<{ className?: string }>;
|
|
480
517
|
}) {
|
|
481
518
|
const r = refForLink;
|
|
482
519
|
const linkable = !!(onResourceClick || resourceHref);
|
|
@@ -488,7 +525,7 @@ function ResourceLine({
|
|
|
488
525
|
{r.namespace ? `${r.namespace} / ` : ''}
|
|
489
526
|
{r.name}
|
|
490
527
|
</span>
|
|
491
|
-
{linkable && <
|
|
528
|
+
{linkable && <ResourceLinkIcon className="h-3 w-3 shrink-0 text-theme-text-tertiary opacity-0 transition-opacity group-hover/r:opacity-100" />}
|
|
492
529
|
</>
|
|
493
530
|
);
|
|
494
531
|
const cls = 'group/r flex w-full items-center gap-2 rounded-md px-2 py-1 text-left text-sm transition-colors hover:bg-theme-hover/60';
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
// module-internal and don't collide at the top-level barrel with the Checks
|
|
3
3
|
// queue's identically-named helpers when both land. Issue-prefixed public
|
|
4
4
|
// names are safe to surface.
|
|
5
|
-
export { IssuesView } from './IssuesView';
|
|
6
|
-
export type { IssuesViewProps } from './IssuesView';
|
|
5
|
+
export { IssueRow, IssuesView } from './IssuesView';
|
|
6
|
+
export type { IssueRowProps, IssueRowSlotContext, IssuesViewProps } from './IssuesView';
|
|
7
7
|
export {
|
|
8
8
|
ISSUE_SEVERITIES,
|
|
9
9
|
ISSUE_SEVERITY_RANK,
|
|
@@ -28,6 +28,13 @@ describe('compareIssues', () => {
|
|
|
28
28
|
expect([older, newer].sort(compareIssues).map((i) => i.id)).toEqual(['n', 'o'])
|
|
29
29
|
})
|
|
30
30
|
|
|
31
|
+
it('orders direct startup blockers before generic problem rows at same severity', () => {
|
|
32
|
+
const generic = mk({ id: 'generic', source: 'problem', first_seen: '2026-05-01T00:00:00Z' })
|
|
33
|
+
const blocker = mk({ id: 'blocker', source: 'scheduling', first_seen: '2026-01-01T00:00:00Z' })
|
|
34
|
+
const missing = mk({ id: 'missing', source: 'missing_ref', first_seen: '2026-03-01T00:00:00Z' })
|
|
35
|
+
expect([generic, blocker, missing].sort(compareIssues).map((i) => i.id)).toEqual(['blocker', 'missing', 'generic'])
|
|
36
|
+
})
|
|
37
|
+
|
|
31
38
|
it('does NOT reshuffle same-severity rows when only last_seen changes (anti-churn)', () => {
|
|
32
39
|
// Two same-severity rows, same first_seen — order is the deterministic name tiebreak.
|
|
33
40
|
const a = mk({ id: 'id-a', name: 'a', first_seen: '2026-01-01T00:00:00Z', last_seen: '2026-05-01T00:00:00Z' })
|
|
@@ -24,6 +24,13 @@ export const ISSUE_SEVERITY_RANK: Record<IssueSeverity, number> = {
|
|
|
24
24
|
warning: 1,
|
|
25
25
|
};
|
|
26
26
|
|
|
27
|
+
const ISSUE_SOURCE_RANK: Record<string, number> = {
|
|
28
|
+
scheduling: 4,
|
|
29
|
+
missing_ref: 3,
|
|
30
|
+
condition: 2,
|
|
31
|
+
problem: 1,
|
|
32
|
+
};
|
|
33
|
+
|
|
27
34
|
export function isIssueSeverity(s: string): s is IssueSeverity {
|
|
28
35
|
return s === 'critical' || s === 'warning';
|
|
29
36
|
}
|
|
@@ -101,6 +108,16 @@ export interface IssueRecentChange {
|
|
|
101
108
|
change_category?: 'spec_config' | 'lifecycle' | 'runtime_status' | string;
|
|
102
109
|
rank_reason?: string;
|
|
103
110
|
fields?: IssueRecentChangeField[];
|
|
111
|
+
/** Workloads that mount/reference this ConfigMap directly ("Deployment/flagd").
|
|
112
|
+
* Direct spec references only — runtime consumers via an intermediary
|
|
113
|
+
* service are not captured. */
|
|
114
|
+
consumed_by?: string[];
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** "No tracked non-status changes in the window" — the claim is scoped by
|
|
118
|
+
* window_seconds so a change just outside it can't be misread as absent. */
|
|
119
|
+
export interface IssueNoRecentChanges {
|
|
120
|
+
window_seconds: number;
|
|
104
121
|
}
|
|
105
122
|
|
|
106
123
|
/**
|
|
@@ -136,8 +153,8 @@ export interface Issue {
|
|
|
136
153
|
|
|
137
154
|
reason: string;
|
|
138
155
|
message?: string;
|
|
139
|
-
/** Parsed domain diagnosis
|
|
140
|
-
*
|
|
156
|
+
/** Parsed domain diagnosis: plain-English cause, suggested next step, and
|
|
157
|
+
* an optional structured one-click fix.
|
|
141
158
|
* Server-emitted (omitempty); empty for issues without a parser. */
|
|
142
159
|
cause?: string;
|
|
143
160
|
action?: string;
|
|
@@ -178,6 +195,16 @@ export interface Issue {
|
|
|
178
195
|
issue_timing?: 'started_at_resource_creation' | 'started_after_resource_was_healthy';
|
|
179
196
|
/** The evidence that determined issue_timing (for auditability). */
|
|
180
197
|
issue_timing_basis?: 'condition' | 'owner_condition' | 'pod_creation' | 'deletion' | 'phase' | 'spec';
|
|
198
|
+
|
|
199
|
+
/** Recent non-status changes (spec/config and lifecycle) on this issue's
|
|
200
|
+
* subject (and, for workload subjects, its referenced ConfigMaps) —
|
|
201
|
+
* deterministic evidence, not a causal claim. Populated only on
|
|
202
|
+
* single-namespace MCP issue responses; never set on /api/issues today. */
|
|
203
|
+
correlated_changes?: IssueRecentChange[];
|
|
204
|
+
/** Explicit "no tracked changes in the window" evidence. An issue with
|
|
205
|
+
* NEITHER correlation field was not checked — absence must not be read as
|
|
206
|
+
* "no changes". MCP-only, like correlated_changes. */
|
|
207
|
+
no_recent_changes?: IssueNoRecentChanges;
|
|
181
208
|
}
|
|
182
209
|
|
|
183
210
|
/** subjectRef builds a deep-linkable ref for an issue's subject — the row's
|
|
@@ -209,15 +236,18 @@ export function memberRef(issue: Issue, member: IssueResourceRef): IssueResource
|
|
|
209
236
|
|
|
210
237
|
/**
|
|
211
238
|
* compareIssues is the queue's stable sort order (extracted from IssuesView so
|
|
212
|
-
* it can be unit-tested). Severity first (critical before warning), then
|
|
213
|
-
*
|
|
214
|
-
* on every poll, so sorting by
|
|
215
|
-
*
|
|
216
|
-
*
|
|
239
|
+
* it can be unit-tested). Severity first (critical before warning), then
|
|
240
|
+
* direct-blocker source priority, then ONSET — first_seen DESC, deliberately
|
|
241
|
+
* NOT last_seen: last_seen bumps to compose-time on every poll, so sorting by
|
|
242
|
+
* it would reshuffle same-severity rows on each refetch. The remaining keys
|
|
243
|
+
* (cluster → namespace → name → id) are a fully deterministic tiebreak so the
|
|
244
|
+
* order never churns under auto-refresh.
|
|
217
245
|
*/
|
|
218
246
|
export function compareIssues(a: Issue, b: Issue): number {
|
|
219
247
|
const r = ISSUE_SEVERITY_RANK[b.severity] - ISSUE_SEVERITY_RANK[a.severity];
|
|
220
248
|
if (r !== 0) return r;
|
|
249
|
+
const sr = (ISSUE_SOURCE_RANK[b.source] ?? 0) - (ISSUE_SOURCE_RANK[a.source] ?? 0);
|
|
250
|
+
if (sr !== 0) return sr;
|
|
221
251
|
const fa = a.first_seen ?? '';
|
|
222
252
|
const fb = b.first_seen ?? '';
|
|
223
253
|
if (fa !== fb) return fb.localeCompare(fa);
|
|
@@ -82,6 +82,11 @@ export function WorkloadLogsViewer({ name, fetchAll, createStream, overrideDownl
|
|
|
82
82
|
pods.forEach((pod, i) => m.set(pod.name, i))
|
|
83
83
|
return m
|
|
84
84
|
}, [pods])
|
|
85
|
+
const podColorIndexRef = useRef<Map<string, number>>(new Map())
|
|
86
|
+
|
|
87
|
+
useEffect(() => {
|
|
88
|
+
podColorIndexRef.current = podColorIndex
|
|
89
|
+
}, [podColorIndex])
|
|
85
90
|
|
|
86
91
|
const podsInitialized = useRef(false)
|
|
87
92
|
|
|
@@ -94,6 +99,7 @@ export function WorkloadLogsViewer({ name, fetchAll, createStream, overrideDownl
|
|
|
94
99
|
const resultPods = result.pods ?? []
|
|
95
100
|
const resultLogs = result.logs ?? []
|
|
96
101
|
|
|
102
|
+
podColorIndexRef.current = new Map(resultPods.map((pod, i) => [pod.name, i]))
|
|
97
103
|
setPods(resultPods)
|
|
98
104
|
|
|
99
105
|
if (!podsInitialized.current && resultPods.length > 0) {
|
|
@@ -148,10 +154,12 @@ export function WorkloadLogsViewer({ name, fetchAll, createStream, overrideDownl
|
|
|
148
154
|
{
|
|
149
155
|
onConnected: (data: any) => {
|
|
150
156
|
if (data?.pods) {
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
157
|
+
const nextPods = data.pods as WorkloadPodInfo[]
|
|
158
|
+
podColorIndexRef.current = new Map(nextPods.map((pod, i) => [pod.name, i]))
|
|
159
|
+
setPods(nextPods)
|
|
160
|
+
setSelectedPods(prev => (
|
|
161
|
+
prev.size === 0 ? new Set(nextPods.map((p: WorkloadPodInfo) => p.name)) : prev
|
|
162
|
+
))
|
|
155
163
|
}
|
|
156
164
|
},
|
|
157
165
|
onLog: (data: any) => {
|
|
@@ -161,7 +169,7 @@ export function WorkloadLogsViewer({ name, fetchAll, createStream, overrideDownl
|
|
|
161
169
|
content: data.content || '',
|
|
162
170
|
container: data.container || '',
|
|
163
171
|
pod: data.pod || '',
|
|
164
|
-
podColorIndex:
|
|
172
|
+
podColorIndex: podColorIndexRef.current.get(data.pod || ''),
|
|
165
173
|
})
|
|
166
174
|
}
|
|
167
175
|
},
|
|
@@ -171,7 +179,8 @@ export function WorkloadLogsViewer({ name, fetchAll, createStream, overrideDownl
|
|
|
171
179
|
setPods(prev => {
|
|
172
180
|
const existing = new Set(prev.map(p => p.name))
|
|
173
181
|
const toAdd = newPods.filter(p => !existing.has(p.name))
|
|
174
|
-
|
|
182
|
+
if (toAdd.length === 0) return prev
|
|
183
|
+
return [...prev, ...toAdd]
|
|
175
184
|
})
|
|
176
185
|
setSelectedPods(prev => {
|
|
177
186
|
const next = new Set(prev)
|
|
@@ -191,7 +200,7 @@ export function WorkloadLogsViewer({ name, fetchAll, createStream, overrideDownl
|
|
|
191
200
|
},
|
|
192
201
|
'Workload log stream connection failed',
|
|
193
202
|
)
|
|
194
|
-
}, [createStream, startStreaming, selectedContainer, sinceSeconds, append,
|
|
203
|
+
}, [createStream, startStreaming, selectedContainer, sinceSeconds, append, clear])
|
|
195
204
|
|
|
196
205
|
const handleStopStreaming = useCallback(() => {
|
|
197
206
|
userStoppedRef.current = true
|