@skyhook-io/k8s-ui 1.10.1 → 1.10.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json
CHANGED
|
@@ -122,6 +122,43 @@ export interface IssueRowSlotContext {
|
|
|
122
122
|
open: boolean;
|
|
123
123
|
}
|
|
124
124
|
|
|
125
|
+
/** Substitutes the SOURCE of the diagnosis prose in the expanded body.
|
|
126
|
+
*
|
|
127
|
+
* For hosts that can diagnose an issue better than the detectors can — today
|
|
128
|
+
* that means an AI investigation that actually read the logs and cross-checked
|
|
129
|
+
* the resource, where `issue.action` is a static template keyed off the reason
|
|
130
|
+
* code ("Open the resource drawer for events, logs, and YAML."). Showing both
|
|
131
|
+
* makes the reader arbitrate between navigation and an answer.
|
|
132
|
+
*
|
|
133
|
+
* Only the PROSE is the host's. The meta line (restarts, timing, change
|
|
134
|
+
* context), the raw detector text, the reason-code eyebrow, tone and spacing
|
|
135
|
+
* stay here — a host that rebuilt those would have to re-implement
|
|
136
|
+
* `issueTiming` and `changeContextText` and would drift from this file.
|
|
137
|
+
*
|
|
138
|
+
* Raw error is deliberately not overridable: it is the verbatim
|
|
139
|
+
* kubelet/containerd string operators grep for, and a summary is not a
|
|
140
|
+
* substitute for it.
|
|
141
|
+
*
|
|
142
|
+
* Pass a field as undefined to leave that section alone — an override with
|
|
143
|
+
* nothing to say is not an improvement over the detector's own text. */
|
|
144
|
+
export interface IssueDiagnosisSource {
|
|
145
|
+
/** Replaces the prose under "What's wrong". The meta line still follows it. */
|
|
146
|
+
cause?: ReactNode;
|
|
147
|
+
/** Replaces the body of "Next step", and makes the section render even when
|
|
148
|
+
* the detector supplied no `action` of its own. */
|
|
149
|
+
nextStep?: ReactNode;
|
|
150
|
+
/** Provenance — who is talking and how stale they are. Rendered once, in the
|
|
151
|
+
* header of the first overridden section, because it is a fact about the
|
|
152
|
+
* source rather than about either section. Name the source in words a reader
|
|
153
|
+
* already knows from elsewhere in the product; an initialism sitting beside a
|
|
154
|
+
* reason code reads as one more terse token, not as a claim about authorship. */
|
|
155
|
+
attribution?: ReactNode;
|
|
156
|
+
/** Section icon for the overridden sections. Usually leave unset: the stock
|
|
157
|
+
* icons carry the section's TONE (warn / fix), which the source doesn't
|
|
158
|
+
* change, and `attribution` is the better place to say who wrote the prose. */
|
|
159
|
+
icon?: ComponentType<{ className?: string }>;
|
|
160
|
+
}
|
|
161
|
+
|
|
125
162
|
export interface IssueRowProps {
|
|
126
163
|
issue: Issue;
|
|
127
164
|
clusterLabel?: (issue: Issue) => string | undefined;
|
|
@@ -139,6 +176,13 @@ export interface IssueRowProps {
|
|
|
139
176
|
renderBadges?: (ctx: IssueRowSlotContext) => ReactNode;
|
|
140
177
|
renderMeta?: (ctx: IssueRowSlotContext) => ReactNode;
|
|
141
178
|
renderActions?: (ctx: IssueRowSlotContext) => ReactNode;
|
|
179
|
+
/** See IssueDiagnosisSource — replaces the diagnosis prose, not its framing. */
|
|
180
|
+
diagnosisSource?: IssueDiagnosisSource;
|
|
181
|
+
/** Appends a section to the end of the expanded body's stack. Unlike
|
|
182
|
+
* `diagnosisSource` this only adds: it lands after Affected resources and
|
|
183
|
+
* inherits the stack's dividers and rhythm, so the host section needs no
|
|
184
|
+
* chrome of its own. */
|
|
185
|
+
renderDetailSection?: (ctx: IssueRowSlotContext) => ReactNode;
|
|
142
186
|
ResourceLinkIcon?: ComponentType<{ className?: string }>;
|
|
143
187
|
}
|
|
144
188
|
|
|
@@ -156,6 +200,8 @@ export function IssueRow({
|
|
|
156
200
|
renderBadges,
|
|
157
201
|
renderMeta,
|
|
158
202
|
renderActions,
|
|
203
|
+
diagnosisSource,
|
|
204
|
+
renderDetailSection,
|
|
159
205
|
ResourceLinkIcon = ExternalLink,
|
|
160
206
|
}: IssueRowProps) {
|
|
161
207
|
const cluster = clusterLabel?.(issue);
|
|
@@ -329,7 +375,7 @@ export function IssueRow({
|
|
|
329
375
|
text keeps enough contrast. */}
|
|
330
376
|
<div className="border-t border-theme-border bg-theme-surface py-4 pl-6 pr-4">
|
|
331
377
|
<div className="flex flex-col divide-y divide-theme-border/70 [&>*]:py-4 [&>*:first-child]:pt-0 [&>*:last-child]:pb-0">
|
|
332
|
-
<Diagnosis issue={issue} />
|
|
378
|
+
<Diagnosis issue={issue} source={diagnosisSource} />
|
|
333
379
|
{issue.incident_parent ? (
|
|
334
380
|
<section className="flex flex-col gap-1">
|
|
335
381
|
<h4 className="text-[11px] font-semibold uppercase tracking-wide text-theme-text-tertiary">
|
|
@@ -349,6 +395,7 @@ export function IssueRow({
|
|
|
349
395
|
) : null}
|
|
350
396
|
<DiagnosticContext issue={issue} resourceHref={resourceHref} onResourceClick={onResourceClick} ResourceLinkIcon={ResourceLinkIcon} />
|
|
351
397
|
<AffectedResources issue={issue} hideSubject={hideSubject} resourceHref={resourceHref} onResourceClick={onResourceClick} ResourceLinkIcon={ResourceLinkIcon} />
|
|
398
|
+
{renderDetailSection?.(slotCtx)}
|
|
352
399
|
</div>
|
|
353
400
|
</div>
|
|
354
401
|
</div>
|
|
@@ -361,7 +408,12 @@ export function IssueRow({
|
|
|
361
408
|
// Diagnosis: WHAT'S WRONG (amber) → NEXT STEP (emerald) → RAW ERROR (monochrome
|
|
362
409
|
// terminal block). Status/timing facts share one muted meta line under the
|
|
363
410
|
// diagnosis so the body reads as three beats rather than a stack of one-liners.
|
|
364
|
-
|
|
411
|
+
//
|
|
412
|
+
// `source` lets a host supply the prose of the first two beats (see
|
|
413
|
+
// IssueDiagnosisSource). It is a swap of WORDS ONLY: the meta line, the raw
|
|
414
|
+
// error, the reason-code eyebrow and the tones are computed here either way, so
|
|
415
|
+
// an overridden card loses none of the facts an un-overridden one shows.
|
|
416
|
+
function Diagnosis({ issue, source }: { issue: Issue; source?: IssueDiagnosisSource }) {
|
|
365
417
|
const crash =
|
|
366
418
|
issue.restart_count || issue.last_terminated_reason
|
|
367
419
|
? [issue.restart_count ? `${issue.restart_count} restart${issue.restart_count === 1 ? '' : 's'}` : null, issue.last_terminated_reason ? `last exit: ${issue.last_terminated_reason}` : null]
|
|
@@ -405,7 +457,36 @@ function Diagnosis({ issue }: { issue: Issue }) {
|
|
|
405
457
|
}
|
|
406
458
|
if (issue.change_context) meta.push(changeContextText(issue.change_context));
|
|
407
459
|
|
|
408
|
-
|
|
460
|
+
// A host's prose, when it has better. Each section falls back independently:
|
|
461
|
+
// supplying a cause and no next step leaves the detector's own action standing,
|
|
462
|
+
// which is the honest outcome when the host found a cause but no fix.
|
|
463
|
+
const sourceCause = source?.cause;
|
|
464
|
+
const sourceNextStep = source?.nextStep;
|
|
465
|
+
const SourceIcon = source?.icon;
|
|
466
|
+
|
|
467
|
+
// Stated once, on whichever overridden section comes first. Marking every
|
|
468
|
+
// overridden section reads as repetition rather than emphasis, and hosts tend
|
|
469
|
+
// to hang a control off the attribution — one that appeared twice in a single
|
|
470
|
+
// card would be a choice the reader has to make between identical options.
|
|
471
|
+
const attributionOn = sourceCause ? 'cause' : sourceNextStep ? 'nextStep' : null;
|
|
472
|
+
const causeAttribution = attributionOn === 'cause' ? source?.attribution : null;
|
|
473
|
+
const nextStepAttribution = attributionOn === 'nextStep' ? source?.attribution : null;
|
|
474
|
+
|
|
475
|
+
// The reason code rides the eyebrow whenever the prose above it isn't the
|
|
476
|
+
// detector's own — true for a parsed cause and for a host's alike, since in
|
|
477
|
+
// both cases the greppable token is otherwise nowhere on an open row.
|
|
478
|
+
const reasonEyebrow = (issue.cause || sourceCause) && issue.reason ? `· ${issue.reason}` : null;
|
|
479
|
+
const causeLabelExtra =
|
|
480
|
+
reasonEyebrow || causeAttribution ? (
|
|
481
|
+
<>
|
|
482
|
+
{reasonEyebrow}
|
|
483
|
+
{reasonEyebrow && causeAttribution ? ' ' : null}
|
|
484
|
+
{causeAttribution}
|
|
485
|
+
</>
|
|
486
|
+
) : undefined;
|
|
487
|
+
|
|
488
|
+
const hasNextStep =
|
|
489
|
+
!!issue.action || !!sourceNextStep || (issue.remediation_kind === 'create-namespace' && !!issue.remediation_target);
|
|
409
490
|
|
|
410
491
|
return (
|
|
411
492
|
<div className="flex flex-col divide-y divide-theme-border/70 [&>*]:py-4 [&>*:first-child]:pt-0 [&>*:last-child]:pb-0">
|
|
@@ -414,12 +495,14 @@ function Diagnosis({ issue }: { issue: Issue }) {
|
|
|
414
495
|
identifier operators anchor on, and the collapsed header drops it while
|
|
415
496
|
open. The non-cause branch already leads with it in the prose. */}
|
|
416
497
|
<CardSection
|
|
417
|
-
icon={AlertTriangle}
|
|
498
|
+
icon={sourceCause && SourceIcon ? SourceIcon : AlertTriangle}
|
|
418
499
|
label="What's wrong"
|
|
419
500
|
tone="warn"
|
|
420
|
-
labelExtra={
|
|
501
|
+
labelExtra={causeLabelExtra}
|
|
421
502
|
>
|
|
422
|
-
{
|
|
503
|
+
{sourceCause ? (
|
|
504
|
+
<div className="text-sm leading-relaxed text-theme-text-primary">{sourceCause}</div>
|
|
505
|
+
) : issue.cause ? (
|
|
423
506
|
<p className="text-sm leading-relaxed text-theme-text-primary">{issue.cause}</p>
|
|
424
507
|
) : (
|
|
425
508
|
<p className="text-sm leading-relaxed text-theme-text-primary">
|
|
@@ -427,12 +510,22 @@ function Diagnosis({ issue }: { issue: Issue }) {
|
|
|
427
510
|
{headline ? <span className="text-theme-text-secondary"> — {headline}</span> : null}
|
|
428
511
|
</p>
|
|
429
512
|
)}
|
|
513
|
+
{/* Always the detector's, never the host's: restarts, timing and change
|
|
514
|
+
context are measurements, and a narrative account of the issue is not
|
|
515
|
+
a substitute for them. */}
|
|
430
516
|
{meta.length > 0 ? <p className="text-xs leading-relaxed text-theme-text-tertiary tabular-nums">{meta.join(' · ')}</p> : null}
|
|
431
517
|
</CardSection>
|
|
432
518
|
|
|
433
519
|
{hasNextStep ? (
|
|
434
|
-
<CardSection
|
|
435
|
-
{
|
|
520
|
+
<CardSection
|
|
521
|
+
icon={sourceNextStep && SourceIcon ? SourceIcon : ArrowRight}
|
|
522
|
+
label="Next step"
|
|
523
|
+
tone="fix"
|
|
524
|
+
labelExtra={nextStepAttribution ?? undefined}
|
|
525
|
+
>
|
|
526
|
+
{sourceNextStep ?? (issue.action ? <CardBody>{issue.action}</CardBody> : null)}
|
|
527
|
+
{/* Survives an override: this is a structured one-click fix with an
|
|
528
|
+
apply path, not advice competing with the host's. */}
|
|
436
529
|
{issue.remediation_kind === 'create-namespace' && issue.remediation_target ? (
|
|
437
530
|
<p className="text-xs text-theme-text-tertiary">
|
|
438
531
|
Suggested fix: create namespace <code className="inline-code">{issue.remediation_target}</code> — apply it from the GitOps detail page.
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// queue's identically-named helpers when both land. Issue-prefixed public
|
|
4
4
|
// names are safe to surface.
|
|
5
5
|
export { IssueRow, IssuesView } from './IssuesView';
|
|
6
|
-
export type { IssueRowProps, IssueRowSlotContext, IssuesViewProps } from './IssuesView';
|
|
6
|
+
export type { IssueDiagnosisSource, IssueRowProps, IssueRowSlotContext, IssuesViewProps } from './IssuesView';
|
|
7
7
|
export { ResourceIssuesSection } from './ResourceIssuesSection';
|
|
8
8
|
export { issueTiming } from './issue-timing';
|
|
9
9
|
export type { IssueTimingDisplay, IssueTimingDisplayKind } from './issue-timing';
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
export { Tooltip, WithTooltip } from './Tooltip'
|
|
2
|
+
export { Badge } from './Badge'
|
|
3
|
+
export type { BadgeSeverity, BadgeSize, BadgeTone } from './Badge'
|
|
2
4
|
export { FreshnessControl } from './FreshnessControl'
|
|
3
5
|
export type { FreshnessMode, FreshnessConnection } from './FreshnessControl'
|
|
4
6
|
export { PaneLoader } from './PaneLoader'
|