@skyhook-io/k8s-ui 1.8.5 → 1.8.7
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 +1 -1
- package/src/components/applications/ApplicationsView.tsx +2 -0
- package/src/components/audit/AuditAlerts.tsx +4 -0
- package/src/components/audit/AuditBadgeTooltip.test.tsx +30 -0
- package/src/components/audit/AuditBadgeTooltip.tsx +47 -0
- package/src/components/audit/AuditFindingsTable.tsx +4 -0
- package/src/components/audit/index.ts +1 -0
- package/src/components/gitops/GitOpsDetailLayout.tsx +3 -3
- package/src/components/gitops/GitOpsStatusBadge.tsx +9 -3
- package/src/components/gitops/GitOpsTableView.tsx +3 -1
- package/src/components/issues/IssuesView.tsx +9 -36
- package/src/components/issues/ResourceIssuesSection.tsx +142 -0
- package/src/components/issues/diagnostic.ts +64 -0
- package/src/components/issues/index.ts +2 -1
- package/src/components/issues/severity.ts +10 -9
- package/src/components/issues/types.ts +5 -0
- package/src/components/resources/ResourcesView.tsx +38 -2
- package/src/components/resources/cron-to-human.test.ts +41 -0
- package/src/components/resources/get-pod-phase-display.test.ts +115 -3
- package/src/components/resources/get-pod-problems.test.ts +18 -0
- package/src/components/resources/health-golden.test.ts +66 -0
- package/src/components/resources/renderers/CronJobRenderer.tsx +14 -7
- package/src/components/resources/renderers/EndpointSliceRenderer.tsx +4 -3
- package/src/components/resources/renderers/GRPCRouteRenderer.tsx +9 -15
- package/src/components/resources/renderers/GatewayClassRenderer.tsx +3 -11
- package/src/components/resources/renderers/GatewayRenderer.tsx +11 -35
- package/src/components/resources/renderers/HTTPRouteRenderer.tsx +10 -16
- package/src/components/resources/renderers/HelmRepositoryRenderer.tsx +3 -6
- package/src/components/resources/renderers/IstioAuthorizationPolicyRenderer.tsx +24 -28
- package/src/components/resources/renderers/IstioGatewayRenderer.tsx +19 -26
- package/src/components/resources/renderers/IstioVirtualServiceRenderer.tsx +20 -19
- package/src/components/resources/renderers/JobRenderer.tsx +12 -7
- package/src/components/resources/renderers/KedaScaledObjectRenderer.tsx +2 -2
- package/src/components/resources/renderers/KnativeNetworkingRenderer.tsx +9 -11
- package/src/components/resources/renderers/NodeRenderer.tsx +17 -8
- package/src/components/resources/renderers/PVCRenderer.tsx +7 -7
- package/src/components/resources/renderers/PodRenderer.tsx +28 -9
- package/src/components/resources/renderers/ServiceRenderer.tsx +23 -9
- package/src/components/resources/renderers/SimpleRouteRenderer.tsx +6 -12
- package/src/components/resources/renderers/TraefikIngressRouteRenderer.tsx +11 -10
- package/src/components/resources/renderers/TraefikMiddlewareRenderer.tsx +185 -0
- package/src/components/resources/renderers/TraefikServersTransportRenderer.tsx +84 -0
- package/src/components/resources/renderers/TraefikServiceRenderer.tsx +118 -0
- package/src/components/resources/renderers/TraefikTLSOptionRenderer.tsx +62 -0
- package/src/components/resources/renderers/WorkflowRenderer.tsx +1 -1
- package/src/components/resources/renderers/WorkloadRenderer.tsx +6 -3
- package/src/components/resources/renderers/badge-no-handrolled.test.tsx +66 -0
- package/src/components/resources/renderers/contour-cells.tsx +6 -5
- package/src/components/resources/renderers/index.ts +4 -0
- package/src/components/resources/renderers/istio-cells.tsx +16 -25
- package/src/components/resources/resource-utils-argo.test.ts +23 -0
- package/src/components/resources/resource-utils-argo.ts +5 -1
- package/src/components/resources/resource-utils-cnpg.ts +1 -1
- package/src/components/resources/resource-utils-keda.ts +17 -7
- package/src/components/resources/resource-utils.ts +206 -71
- package/src/components/shared/ResourceRendererDispatch.tsx +10 -0
- package/src/components/timeline/TimelineSwimlanes.tsx +1 -0
- package/src/components/timeline/shared.tsx +15 -4
- package/src/components/topology/K8sResourceNode.tsx +28 -1
- package/src/components/topology/TopologyControls.tsx +41 -9
- package/src/components/topology/layout.ts +11 -5
- package/src/components/ui/Badge.tsx +66 -3
- package/src/components/ui/ClusterName.tsx +12 -4
- package/src/components/ui/PaneLoader.tsx +30 -7
- package/src/components/ui/drawer-components.test.tsx +35 -0
- package/src/components/ui/drawer-components.tsx +13 -1
- package/src/components/workload/WorkloadView.tsx +35 -5
- package/src/types/core.ts +136 -2
- package/src/utils/applications.test.ts +55 -1
- package/src/utils/applications.ts +28 -7
- package/src/utils/asset-url.ts +13 -0
- package/src/utils/badge-colors.ts +7 -0
|
@@ -731,11 +731,14 @@ export function buildHierarchicalElkGraph(
|
|
|
731
731
|
}
|
|
732
732
|
}
|
|
733
733
|
|
|
734
|
-
// Lower number = higher severity
|
|
735
|
-
|
|
734
|
+
// Lower number = higher severity. `neutral` (intentional/idle) is the
|
|
735
|
+
// most-benign tier, so a group whose every member is idle rolls up to neutral,
|
|
736
|
+
// while a mixed healthy+idle group still reads healthy (healthy out-ranks
|
|
737
|
+
// neutral). An empty/unresolved group defaults to healthy.
|
|
738
|
+
const HEALTH_PRIORITY: Record<HealthStatus, number> = { unhealthy: 0, degraded: 1, unknown: 2, healthy: 3, neutral: 4 }
|
|
736
739
|
|
|
737
740
|
function computeGroupHealth(memberIds: string[], nodeMap: Map<string, TopologyNode>): { worstStatus: HealthStatus; unhealthyCount: number } {
|
|
738
|
-
let worstPriority =
|
|
741
|
+
let worstPriority = Infinity
|
|
739
742
|
let worstStatus: HealthStatus = 'healthy'
|
|
740
743
|
let unhealthyCount = 0
|
|
741
744
|
for (const id of memberIds) {
|
|
@@ -804,8 +807,11 @@ function computeWorkloadCards(
|
|
|
804
807
|
})
|
|
805
808
|
const primary = sorted[0]
|
|
806
809
|
|
|
807
|
-
// Compute worst health
|
|
808
|
-
|
|
810
|
+
// Compute worst health. Seed with Infinity (not 3) so an all-neutral
|
|
811
|
+
// component can surface neutral — `neutral` is priority 4 (most benign), so a
|
|
812
|
+
// `< 3` seed would never accept it and the card would stay green. Matches
|
|
813
|
+
// computeGroupHealth.
|
|
814
|
+
let worstPriority = Infinity
|
|
809
815
|
let worstStatus: HealthStatus = 'healthy'
|
|
810
816
|
for (const node of comp) {
|
|
811
817
|
const p = HEALTH_PRIORITY[node.status] ?? 2
|
|
@@ -9,11 +9,31 @@ import type { ReactNode } from 'react'
|
|
|
9
9
|
export type BadgeSeverity = 'success' | 'warning' | 'alert' | 'error' | 'info' | 'neutral'
|
|
10
10
|
export type BadgeSize = 'sm' | 'default'
|
|
11
11
|
|
|
12
|
+
// Intent tokens beyond severity/kind. Named by PURPOSE, not hue: two tokens may
|
|
13
|
+
// share a color value today yet stay separate names so a future retune moves
|
|
14
|
+
// exactly the right ones. See DESIGN.md "Badge decision tree".
|
|
15
|
+
//
|
|
16
|
+
// - protocol: transport/scheme tags (HTTP/TCP/UDP/…). Its OWN family because it
|
|
17
|
+
// is a recurring, meaningful axis and must NOT collide with severity hues
|
|
18
|
+
// (e.g. a green "HTTPS" must not read as "success").
|
|
19
|
+
// - note: neutral attention/FYI markers (cross-namespace, wildcard, default,
|
|
20
|
+
// immutable). Distinct from severity-info — it flags "noteworthy", not a status.
|
|
21
|
+
// - accent1/2/3: LOCAL categorical distinction (rw/ro, spot/on-demand,
|
|
22
|
+
// control-plane/worker) where the hue carries no cross-screen meaning — just
|
|
23
|
+
// "tell these sibling options apart". The only place sharing is intended.
|
|
24
|
+
// - structural: ports/paths/hosts/weights/names — neutral data fragments, not a
|
|
25
|
+
// status. Its own name so it can diverge from severity-neutral later.
|
|
26
|
+
export type BadgeTone = 'note' | 'accent1' | 'accent2' | 'accent3' | 'structural'
|
|
27
|
+
|
|
12
28
|
interface BadgeProps {
|
|
13
29
|
/** Severity-based coloring (status badges) */
|
|
14
30
|
severity?: BadgeSeverity
|
|
15
31
|
/** K8s resource kind coloring (kind badges) */
|
|
16
32
|
kind?: string
|
|
33
|
+
/** Transport protocol/scheme tag (http/https/tls/tcp/udp/grpc/h2) */
|
|
34
|
+
protocol?: string
|
|
35
|
+
/** Categorical/accent intent tone (see BadgeTone) */
|
|
36
|
+
tone?: BadgeTone
|
|
17
37
|
/** Explicit color class override (bypasses severity/kind lookup) */
|
|
18
38
|
colorClass?: string
|
|
19
39
|
/** Size variant */
|
|
@@ -135,12 +155,49 @@ const KIND: Record<string, string> = {
|
|
|
135
155
|
|
|
136
156
|
const DEFAULT_KIND_COLOR = 'bg-fuchsia-50 text-fuchsia-700 border-fuchsia-200 dark:bg-fuchsia-950/40 dark:text-fuchsia-400 dark:border-fuchsia-800/40'
|
|
137
157
|
|
|
158
|
+
// ---------------------------------------------------------------------------
|
|
159
|
+
// PROTOCOL COLORS — transport/scheme tags. Deliberately avoid severity hues
|
|
160
|
+
// (emerald/amber/orange/red/sky) so a protocol never reads as a status.
|
|
161
|
+
// ---------------------------------------------------------------------------
|
|
162
|
+
const PROTOCOL: Record<string, string> = {
|
|
163
|
+
http: 'bg-blue-100 text-blue-700 border-blue-300 dark:bg-blue-950/50 dark:text-blue-400 dark:border-blue-700/40',
|
|
164
|
+
http2: 'bg-blue-100 text-blue-700 border-blue-300 dark:bg-blue-950/50 dark:text-blue-400 dark:border-blue-700/40',
|
|
165
|
+
h2: 'bg-blue-100 text-blue-700 border-blue-300 dark:bg-blue-950/50 dark:text-blue-400 dark:border-blue-700/40',
|
|
166
|
+
h2c: 'bg-blue-100 text-blue-700 border-blue-300 dark:bg-blue-950/50 dark:text-blue-400 dark:border-blue-700/40',
|
|
167
|
+
https: 'bg-teal-100 text-teal-800 border-teal-300 dark:bg-teal-950/50 dark:text-teal-400 dark:border-teal-700/40',
|
|
168
|
+
tls: 'bg-teal-100 text-teal-800 border-teal-300 dark:bg-teal-950/50 dark:text-teal-400 dark:border-teal-700/40',
|
|
169
|
+
tcp: 'bg-indigo-100 text-indigo-700 border-indigo-300 dark:bg-indigo-950/50 dark:text-indigo-400 dark:border-indigo-700/40',
|
|
170
|
+
udp: 'bg-violet-100 text-violet-700 border-violet-300 dark:bg-violet-950/50 dark:text-violet-400 dark:border-violet-700/40',
|
|
171
|
+
grpc: 'bg-fuchsia-100 text-fuchsia-700 border-fuchsia-300 dark:bg-fuchsia-950/50 dark:text-fuchsia-400 dark:border-fuchsia-700/40',
|
|
172
|
+
'grpc-web': 'bg-fuchsia-100 text-fuchsia-700 border-fuchsia-300 dark:bg-fuchsia-950/50 dark:text-fuchsia-400 dark:border-fuchsia-700/40',
|
|
173
|
+
}
|
|
174
|
+
const DEFAULT_PROTOCOL_COLOR = 'bg-slate-100 text-slate-700 border-slate-300 dark:bg-slate-950/50 dark:text-slate-400 dark:border-slate-700/40'
|
|
175
|
+
|
|
176
|
+
// ---------------------------------------------------------------------------
|
|
177
|
+
// TONE COLORS — note / local-categorical accents / structural.
|
|
178
|
+
// ---------------------------------------------------------------------------
|
|
179
|
+
const TONE: Record<BadgeTone, string> = {
|
|
180
|
+
// attention/FYI — distinct from severity so it doesn't read as warning/info
|
|
181
|
+
note: 'bg-violet-100 text-violet-700 border-violet-300 dark:bg-violet-950/50 dark:text-violet-400 dark:border-violet-700/40',
|
|
182
|
+
// three visually-distinct accents for local "tell siblings apart" use
|
|
183
|
+
accent1: 'bg-blue-100 text-blue-700 border-blue-300 dark:bg-blue-950/50 dark:text-blue-400 dark:border-blue-700/40',
|
|
184
|
+
accent2: 'bg-fuchsia-100 text-fuchsia-700 border-fuchsia-300 dark:bg-fuchsia-950/50 dark:text-fuchsia-400 dark:border-fuchsia-700/40',
|
|
185
|
+
accent3: 'bg-teal-100 text-teal-800 border-teal-300 dark:bg-teal-950/50 dark:text-teal-400 dark:border-teal-700/40',
|
|
186
|
+
// neutral data fragment (ports/paths/hosts/names)
|
|
187
|
+
structural: 'bg-theme-elevated text-theme-text-secondary border-theme-border',
|
|
188
|
+
}
|
|
189
|
+
|
|
138
190
|
// Structure classes
|
|
139
191
|
const SIZE_CLASSES: Record<BadgeSize, string> = {
|
|
140
192
|
default: 'badge',
|
|
141
193
|
sm: 'badge-sm',
|
|
142
194
|
}
|
|
143
195
|
|
|
196
|
+
/** Resolve a protocol/scheme label (case-insensitive) to its color class. */
|
|
197
|
+
export function getProtocolColorClass(protocol: string): string {
|
|
198
|
+
return PROTOCOL[protocol.toLowerCase().trim()] ?? DEFAULT_PROTOCOL_COLOR
|
|
199
|
+
}
|
|
200
|
+
|
|
144
201
|
/** Resolve kind color with fuzzy matching for plural/lowercase forms */
|
|
145
202
|
export function getKindColorClass(kind: string): string {
|
|
146
203
|
// Direct match
|
|
@@ -185,8 +242,14 @@ export function getSeverityColorClass(severity: BadgeSeverity): string {
|
|
|
185
242
|
* Badge component — the ONE source of truth for badge rendering.
|
|
186
243
|
* Use severity for status badges, kind for resource type badges, or colorClass for custom.
|
|
187
244
|
*/
|
|
188
|
-
export function Badge({ severity, kind, colorClass, size = 'default', className, onClick, title, children }: BadgeProps) {
|
|
189
|
-
const color =
|
|
245
|
+
export function Badge({ severity, kind, protocol, tone, colorClass, size = 'default', className, onClick, title, children }: BadgeProps) {
|
|
246
|
+
const color =
|
|
247
|
+
colorClass ??
|
|
248
|
+
(severity ? SEVERITY[severity]
|
|
249
|
+
: protocol ? getProtocolColorClass(protocol)
|
|
250
|
+
: tone ? TONE[tone]
|
|
251
|
+
: kind ? getKindColorClass(kind)
|
|
252
|
+
: '')
|
|
190
253
|
const cls = clsx(SIZE_CLASSES[size], color, className)
|
|
191
254
|
|
|
192
255
|
if (onClick) {
|
|
@@ -196,4 +259,4 @@ export function Badge({ severity, kind, colorClass, size = 'default', className,
|
|
|
196
259
|
}
|
|
197
260
|
|
|
198
261
|
// Re-export the raw color maps for backwards compat (used by badge-colors.ts consumers)
|
|
199
|
-
export { SEVERITY as BADGE_SEVERITY_COLORS, KIND as BADGE_KIND_COLORS }
|
|
262
|
+
export { SEVERITY as BADGE_SEVERITY_COLORS, KIND as BADGE_KIND_COLORS, PROTOCOL as BADGE_PROTOCOL_COLORS, TONE as BADGE_TONE_COLORS }
|
|
@@ -3,10 +3,18 @@ import { MiddleEllipsis } from './MiddleEllipsis'
|
|
|
3
3
|
import { Tooltip } from './Tooltip'
|
|
4
4
|
import { parseContextName } from '../../utils/context-name'
|
|
5
5
|
import type { ParsedContextName } from '../../utils/context-name'
|
|
6
|
-
import
|
|
7
|
-
import
|
|
8
|
-
import
|
|
9
|
-
import
|
|
6
|
+
import { assetUrl } from '../../utils/asset-url'
|
|
7
|
+
import awsLogoAsset from './provider-logos/aws.png'
|
|
8
|
+
import awsLogoDarkAsset from './provider-logos/aws-dark.png'
|
|
9
|
+
import gcpLogoAsset from './provider-logos/gcp.png'
|
|
10
|
+
import azureLogoAsset from './provider-logos/azure.svg'
|
|
11
|
+
|
|
12
|
+
// assetUrl normalizes the bundler-specific asset-import type (string under Vite,
|
|
13
|
+
// StaticImageData under webpack/Next) to a URL string usable in `<img src>`.
|
|
14
|
+
const awsLogo = assetUrl(awsLogoAsset)
|
|
15
|
+
const awsLogoDark = assetUrl(awsLogoDarkAsset)
|
|
16
|
+
const gcpLogo = assetUrl(gcpLogoAsset)
|
|
17
|
+
const azureLogo = assetUrl(azureLogoAsset)
|
|
10
18
|
|
|
11
19
|
// ClusterName renders a kubectl context string with the meaningful
|
|
12
20
|
// cluster identity surfaced as primary text and provider/region pushed
|
|
@@ -1,8 +1,18 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { assetUrl } from '../../utils/asset-url'
|
|
2
|
+
import radarLoadingIconAsset from '../../assets/radar/radar-icon-loading.svg'
|
|
2
3
|
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
|
|
4
|
+
// assetUrl normalizes the bundler-specific asset-import type (string under Vite,
|
|
5
|
+
// StaticImageData under webpack/Next) to a URL string usable in `<img src>`.
|
|
6
|
+
const radarLoadingIcon = assetUrl(radarLoadingIconAsset)
|
|
7
|
+
|
|
8
|
+
// PaneLoader — center-of-pane loading state. The animated radar icon is
|
|
9
|
+
// pinned to the pane's exact center; the label hangs at a fixed offset
|
|
10
|
+
// BELOW it, absolutely positioned (not a flex sibling), so the label never
|
|
11
|
+
// affects where the icon sits. The icon therefore holds a single position
|
|
12
|
+
// while only the text under it appears/changes — and it lands at the same
|
|
13
|
+
// point as the host/connect splash surfaces (which center the icon at 50%
|
|
14
|
+
// with the label decoupled below), so a splash → PaneLoader hand-off no
|
|
15
|
+
// longer makes the logo jump. Pin to the parent's fill via `className`
|
|
6
16
|
// (`flex-1`, `h-full`, `h-32`, `absolute inset-0`, etc.). The SVG self-
|
|
7
17
|
// animates (sweep arm + blips, `prefers-reduced-motion` honored).
|
|
8
18
|
export function PaneLoader({
|
|
@@ -12,10 +22,23 @@ export function PaneLoader({
|
|
|
12
22
|
label?: string
|
|
13
23
|
className?: string
|
|
14
24
|
}) {
|
|
25
|
+
// No `relative` on the root: the label anchors to the inner `relative` span
|
|
26
|
+
// below, and callers may pass a positioning class (e.g. `absolute inset-0`,
|
|
27
|
+
// for topology panes) — a root `relative` would conflict with it.
|
|
15
28
|
return (
|
|
16
|
-
<div className={`flex
|
|
17
|
-
|
|
18
|
-
|
|
29
|
+
<div className={`flex items-center justify-center ${className}`} aria-live="polite">
|
|
30
|
+
{/* The icon is the only in-flow child, so it centers in the pane. The
|
|
31
|
+
label is absolutely positioned below the icon and so never shifts it. */}
|
|
32
|
+
<span className="relative">
|
|
33
|
+
<img src={radarLoadingIcon} alt="" aria-hidden className="w-11 h-11" />
|
|
34
|
+
{/* Label style matches the splash surfaces (17px semibold tracking-tight,
|
|
35
|
+
primary) so the whole loading family — boot splash, connect splash,
|
|
36
|
+
PaneLoader — reads as one continuous state, not a font change at the
|
|
37
|
+
hand-off. */}
|
|
38
|
+
<span className="absolute left-1/2 top-full mt-3 -translate-x-1/2 whitespace-nowrap text-[17px] font-semibold tracking-tight text-theme-text-primary">
|
|
39
|
+
{label}
|
|
40
|
+
</span>
|
|
41
|
+
</span>
|
|
19
42
|
</div>
|
|
20
43
|
)
|
|
21
44
|
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { renderToString } from 'react-dom/server'
|
|
3
|
+
import { ProblemAlerts, OperationalIssuesShownContext } from './drawer-components'
|
|
4
|
+
|
|
5
|
+
const problems = [
|
|
6
|
+
{ color: 'red' as const, message: 'Application is Degraded' },
|
|
7
|
+
{ color: 'yellow' as const, message: 'Application is OutOfSync' },
|
|
8
|
+
]
|
|
9
|
+
|
|
10
|
+
describe('ProblemAlerts', () => {
|
|
11
|
+
it('renders every problem', () => {
|
|
12
|
+
const html = renderToString(<ProblemAlerts problems={problems} />)
|
|
13
|
+
expect(html).toContain('Application is Degraded')
|
|
14
|
+
expect(html).toContain('Application is OutOfSync')
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
it('renders nothing when there are no problems', () => {
|
|
18
|
+
expect(renderToString(<ProblemAlerts problems={[]} />)).toBe('')
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
// Regression guard: ProblemAlerts is used only by GitOps renderers, whose
|
|
22
|
+
// problems the live-Issues pipeline does not comprehensively emit. It must NOT
|
|
23
|
+
// suppress itself under the Operational-Issues context — doing so hid real
|
|
24
|
+
// GitOps warnings (e.g. a manual Argo app's OutOfSync). Pod/Workload renderers
|
|
25
|
+
// self-gate their own arrays; this component never should.
|
|
26
|
+
it('still renders under OperationalIssuesShownContext (does not self-suppress)', () => {
|
|
27
|
+
const html = renderToString(
|
|
28
|
+
<OperationalIssuesShownContext.Provider value={true}>
|
|
29
|
+
<ProblemAlerts problems={problems} />
|
|
30
|
+
</OperationalIssuesShownContext.Provider>
|
|
31
|
+
)
|
|
32
|
+
expect(html).toContain('Application is Degraded')
|
|
33
|
+
expect(html).toContain('Application is OutOfSync')
|
|
34
|
+
})
|
|
35
|
+
})
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useState } from 'react'
|
|
1
|
+
import { useState, createContext, useContext } from 'react'
|
|
2
2
|
import { ChevronRight, Copy, Check, Tag, AlertTriangle, CheckCircle, ExternalLink, Layers, X, Minus } from 'lucide-react'
|
|
3
3
|
import { clsx } from 'clsx'
|
|
4
4
|
import { formatAge, formatDuration, formatResources } from '../resources/resource-utils'
|
|
@@ -446,6 +446,18 @@ export interface Problem {
|
|
|
446
446
|
}
|
|
447
447
|
|
|
448
448
|
/** Displays a list of problem alerts (warnings and errors) */
|
|
449
|
+
// True when the resource detail is already rendering a dedicated, authoritative
|
|
450
|
+
// "Operational Issues" section (the Issues pipeline — richer cause/action). Only
|
|
451
|
+
// renderers whose problems the pipeline COMPREHENSIVELY covers read this and drop
|
|
452
|
+
// their own problem array so the same failure isn't shown twice — today that's
|
|
453
|
+
// PodRenderer and WorkloadRenderer (workload + pod runtime). It is deliberately
|
|
454
|
+
// NOT wired into ProblemAlerts: that component is used only by GitOps renderers
|
|
455
|
+
// (Argo/Flux), whose Degraded/OutOfSync/revision-mismatch banners the pipeline
|
|
456
|
+
// does not fully emit (e.g. OutOfSync only for automated Argo apps) — suppressing
|
|
457
|
+
// them would hide real problems, which is worse than an occasional duplicate.
|
|
458
|
+
export const OperationalIssuesShownContext = createContext(false)
|
|
459
|
+
export const useOperationalIssuesShown = () => useContext(OperationalIssuesShownContext)
|
|
460
|
+
|
|
449
461
|
export function ProblemAlerts({ problems }: { problems: Problem[] }) {
|
|
450
462
|
if (problems.length === 0) return null
|
|
451
463
|
|
|
@@ -52,7 +52,7 @@ import { ResourceRendererDispatch, getResourceStatus, type RendererOverrides } f
|
|
|
52
52
|
import type { ScalerDiagnosis } from '../resources/renderers/WorkloadRenderer'
|
|
53
53
|
import { DetailShell, type DetailShellTab } from '../shared/DetailShell'
|
|
54
54
|
import { HelmManagedByChip, ManagedByChip, type HelmOwnerRef } from '../shared/ManagedByChip'
|
|
55
|
-
import { getKindColorOutline, displayKindName } from '../ui/drawer-components'
|
|
55
|
+
import { getKindColorOutline, displayKindName, OperationalIssuesShownContext } from '../ui/drawer-components'
|
|
56
56
|
import { midTruncate } from '../../utils/format'
|
|
57
57
|
|
|
58
58
|
export type WorkloadTabType = 'overview' | 'topology' | 'timeline' | 'logs' | 'metrics' | 'yaml'
|
|
@@ -211,6 +211,17 @@ interface WorkloadViewProps {
|
|
|
211
211
|
isMetricsAvailable?: (kind: string, resource: any) => boolean
|
|
212
212
|
/** Render extra content at the bottom of the overview tab (e.g. audit findings) */
|
|
213
213
|
renderOverviewExtra?: (props: { kind: string; namespace: string; name: string }) => ReactNode
|
|
214
|
+
/** Render content at the TOP of the overview tab, above the renderer (e.g. live
|
|
215
|
+
* Operational Issues). Optional + additive — consumers that don't pass it are
|
|
216
|
+
* unaffected. Only rendered when `hasOperationalIssues` is true: the lead
|
|
217
|
+
* component returns null when empty, but its padded wrapper can't tell, so
|
|
218
|
+
* gating on the flag avoids an empty top gap on healthy resources. */
|
|
219
|
+
renderOverviewLead?: (props: { kind: string; namespace: string; name: string }) => ReactNode
|
|
220
|
+
/** When true, renderers suppress their own status-derived problem displays
|
|
221
|
+
* because a dedicated Operational Issues section is shown (the host fetched
|
|
222
|
+
* live issues for this resource). Avoids showing the same failure twice.
|
|
223
|
+
* Also gates the `renderOverviewLead` wrapper (see above). */
|
|
224
|
+
hasOperationalIssues?: boolean
|
|
214
225
|
|
|
215
226
|
// ── Duplicate ────────────────────────────────────────────────────────────
|
|
216
227
|
/** Duplicate handler — opens create dialog with this resource's YAML */
|
|
@@ -282,6 +293,8 @@ export function WorkloadView({
|
|
|
282
293
|
onDuplicate,
|
|
283
294
|
onDownload,
|
|
284
295
|
renderOverviewExtra,
|
|
296
|
+
renderOverviewLead,
|
|
297
|
+
hasOperationalIssues,
|
|
285
298
|
// Actions bar
|
|
286
299
|
actionsBarProps,
|
|
287
300
|
// Renderer overrides
|
|
@@ -636,7 +649,9 @@ export function WorkloadView({
|
|
|
636
649
|
{/* Content — viewTransitionName scopes View Transitions API cross-fade to this element */}
|
|
637
650
|
<div className="flex-1 overflow-y-auto" style={{ viewTransitionName: 'drawer-content' }}>
|
|
638
651
|
{!resource ? (
|
|
639
|
-
|
|
652
|
+
// Fill the drawer body so the loading logo centers in it, not in a
|
|
653
|
+
// 128px box pinned to the top (matches the splash/PaneLoader centering).
|
|
654
|
+
<FetchResult loading={resourceLoading} error={resourceError} className="h-full" />
|
|
640
655
|
) : showYaml ? (
|
|
641
656
|
<EditableYamlView
|
|
642
657
|
resource={selectedResource}
|
|
@@ -652,7 +667,12 @@ export function WorkloadView({
|
|
|
652
667
|
onDownload={onDownload}
|
|
653
668
|
/>
|
|
654
669
|
) : (
|
|
655
|
-
|
|
670
|
+
<OperationalIssuesShownContext.Provider value={!!hasOperationalIssues}>
|
|
671
|
+
{renderOverviewLead && hasOperationalIssues && (
|
|
672
|
+
<div className="px-4 pt-4">
|
|
673
|
+
{renderOverviewLead({ kind, namespace, name })}
|
|
674
|
+
</div>
|
|
675
|
+
)}
|
|
656
676
|
<ResourceRendererDispatch
|
|
657
677
|
resource={selectedResource}
|
|
658
678
|
data={resource}
|
|
@@ -679,7 +699,7 @@ export function WorkloadView({
|
|
|
679
699
|
{renderOverviewExtra({ kind, namespace, name })}
|
|
680
700
|
</div>
|
|
681
701
|
)}
|
|
682
|
-
|
|
702
|
+
</OperationalIssuesShownContext.Provider>
|
|
683
703
|
)}
|
|
684
704
|
</div>
|
|
685
705
|
</div>
|
|
@@ -688,6 +708,7 @@ export function WorkloadView({
|
|
|
688
708
|
|
|
689
709
|
// ── Expanded (full) mode ─────────────────────────────────────────────────
|
|
690
710
|
return (
|
|
711
|
+
<OperationalIssuesShownContext.Provider value={!!hasOperationalIssues}>
|
|
691
712
|
<DetailShell
|
|
692
713
|
breadcrumb={breadcrumb}
|
|
693
714
|
nav={
|
|
@@ -802,6 +823,7 @@ export function WorkloadView({
|
|
|
802
823
|
eventsError={overviewEventsError}
|
|
803
824
|
updatesError={resourceFocusedUpdatesError}
|
|
804
825
|
extraContent={renderOverviewExtra && renderOverviewExtra({ kind, namespace, name })}
|
|
826
|
+
leadContent={hasOperationalIssues && renderOverviewLead ? renderOverviewLead({ kind, namespace, name }) : undefined}
|
|
805
827
|
/>
|
|
806
828
|
)}
|
|
807
829
|
{effectiveTab === 'topology' && (
|
|
@@ -881,7 +903,7 @@ export function WorkloadView({
|
|
|
881
903
|
{yamlObject && !yamlObject.primary && renderRelatedYaml ? (
|
|
882
904
|
renderRelatedYaml(yamlObject)
|
|
883
905
|
) : !resource ? (
|
|
884
|
-
<FetchResult loading={resourceLoading} error={resourceError} className="h-
|
|
906
|
+
<FetchResult loading={resourceLoading} error={resourceError} className="h-full" />
|
|
885
907
|
) : (
|
|
886
908
|
<EditableYamlView
|
|
887
909
|
resource={selectedResource}
|
|
@@ -901,6 +923,7 @@ export function WorkloadView({
|
|
|
901
923
|
</div>
|
|
902
924
|
)}
|
|
903
925
|
</DetailShell>
|
|
926
|
+
</OperationalIssuesShownContext.Provider>
|
|
904
927
|
)
|
|
905
928
|
}
|
|
906
929
|
|
|
@@ -1335,6 +1358,7 @@ function InfoTab({
|
|
|
1335
1358
|
eventsError,
|
|
1336
1359
|
updatesError,
|
|
1337
1360
|
extraContent,
|
|
1361
|
+
leadContent,
|
|
1338
1362
|
}: {
|
|
1339
1363
|
resource: any
|
|
1340
1364
|
selectedResource: SelectedResource
|
|
@@ -1358,6 +1382,7 @@ function InfoTab({
|
|
|
1358
1382
|
eventsError?: Error | null
|
|
1359
1383
|
updatesError?: Error | null
|
|
1360
1384
|
extraContent?: ReactNode
|
|
1385
|
+
leadContent?: ReactNode
|
|
1361
1386
|
}) {
|
|
1362
1387
|
if (!resource) {
|
|
1363
1388
|
return <FetchResult loading={isLoading} error={error} className="h-full" />
|
|
@@ -1365,6 +1390,11 @@ function InfoTab({
|
|
|
1365
1390
|
|
|
1366
1391
|
return (
|
|
1367
1392
|
<div className="h-full overflow-auto">
|
|
1393
|
+
{leadContent && (
|
|
1394
|
+
<div className="px-4 pt-4">
|
|
1395
|
+
{leadContent}
|
|
1396
|
+
</div>
|
|
1397
|
+
)}
|
|
1368
1398
|
<ResourceRendererDispatch
|
|
1369
1399
|
resource={selectedResource}
|
|
1370
1400
|
data={resource}
|
package/src/types/core.ts
CHANGED
|
@@ -193,7 +193,7 @@ export function displayKind(kind: string): string {
|
|
|
193
193
|
return shortNames[kind] || kind
|
|
194
194
|
}
|
|
195
195
|
|
|
196
|
-
export type HealthStatus = 'healthy' | 'degraded' | 'unhealthy' | 'unknown'
|
|
196
|
+
export type HealthStatus = 'healthy' | 'degraded' | 'unhealthy' | 'neutral' | 'unknown'
|
|
197
197
|
|
|
198
198
|
export type EdgeType = 'routes-to' | 'exposes' | 'manages' | 'uses' | 'configures' | 'protects'
|
|
199
199
|
|
|
@@ -554,8 +554,10 @@ export interface HelmRelease {
|
|
|
554
554
|
status: string
|
|
555
555
|
revision: number
|
|
556
556
|
updated: string // ISO date string
|
|
557
|
+
lastOperation?: HelmOperation
|
|
558
|
+
operations?: HelmOperation[]
|
|
557
559
|
// Health summary from owned resources
|
|
558
|
-
resourceHealth?: 'healthy' | 'degraded' | 'unhealthy' | 'unknown'
|
|
560
|
+
resourceHealth?: 'healthy' | 'degraded' | 'unhealthy' | 'neutral' | 'unknown'
|
|
559
561
|
healthIssue?: string // Primary issue if unhealthy (e.g., "OOMKilled")
|
|
560
562
|
healthSummary?: string // Brief summary like "2/3 pods ready"
|
|
561
563
|
// When set, this release was installed by Flux's helm-controller — the
|
|
@@ -574,6 +576,27 @@ export interface HelmRevision {
|
|
|
574
576
|
updated: string // ISO date string
|
|
575
577
|
}
|
|
576
578
|
|
|
579
|
+
export type HelmOperationKind = 'release_failed' | 'upgrade_failed' | 'upgrade_rolled_back' | 'rollback' | 'pending'
|
|
580
|
+
export type HelmOperationStatus = 'failed' | 'rolled_back' | 'completed' | 'stuck_pending'
|
|
581
|
+
export type HelmOperationConfidence = 'high' | 'medium' | 'low'
|
|
582
|
+
export type HelmOperationSource = 'helm_status' | 'helm_history'
|
|
583
|
+
|
|
584
|
+
export interface HelmOperation {
|
|
585
|
+
kind: HelmOperationKind
|
|
586
|
+
status: HelmOperationStatus
|
|
587
|
+
source: HelmOperationSource
|
|
588
|
+
confidence: HelmOperationConfidence
|
|
589
|
+
message: string
|
|
590
|
+
evidence?: string
|
|
591
|
+
failureDescription?: string
|
|
592
|
+
revision?: number
|
|
593
|
+
failedRevision?: number
|
|
594
|
+
rollbackRevision?: number
|
|
595
|
+
targetRevision?: number
|
|
596
|
+
pendingStatus?: string
|
|
597
|
+
updated?: string
|
|
598
|
+
}
|
|
599
|
+
|
|
577
600
|
export interface HelmReleaseDetail {
|
|
578
601
|
name: string
|
|
579
602
|
namespace: string
|
|
@@ -589,9 +612,15 @@ export interface HelmReleaseDetail {
|
|
|
589
612
|
notes: string
|
|
590
613
|
history: HelmRevision[]
|
|
591
614
|
resources: HelmOwnedResource[]
|
|
615
|
+
resourceHealth?: 'healthy' | 'degraded' | 'unhealthy' | 'neutral' | 'unknown'
|
|
616
|
+
healthIssue?: string
|
|
617
|
+
healthSummary?: string
|
|
592
618
|
hooks?: HelmHook[]
|
|
619
|
+
hookDiagnostics?: HookDiagnostic[]
|
|
593
620
|
readme?: string
|
|
594
621
|
dependencies?: ChartDependency[]
|
|
622
|
+
lastOperation?: HelmOperation
|
|
623
|
+
operations?: HelmOperation[]
|
|
595
624
|
// When set, this release was installed by Flux's helm-controller — see
|
|
596
625
|
// HelmRelease.managedByFluxHelmRelease for context. Format: "namespace/name".
|
|
597
626
|
managedByFluxHelmRelease?: string
|
|
@@ -599,10 +628,78 @@ export interface HelmReleaseDetail {
|
|
|
599
628
|
|
|
600
629
|
export interface HelmHook {
|
|
601
630
|
name: string
|
|
631
|
+
namespace?: string
|
|
602
632
|
kind: string
|
|
633
|
+
path?: string
|
|
603
634
|
events: string[]
|
|
604
635
|
weight: number
|
|
605
636
|
status?: string
|
|
637
|
+
startedAt?: string
|
|
638
|
+
completedAt?: string
|
|
639
|
+
deletePolicies?: string[]
|
|
640
|
+
outputLogPolicies?: string[]
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
export interface HookDiagnostic {
|
|
644
|
+
name: string
|
|
645
|
+
namespace?: string
|
|
646
|
+
kind: string
|
|
647
|
+
events?: string[]
|
|
648
|
+
phase: string
|
|
649
|
+
message: string
|
|
650
|
+
evidence?: HookEvidence
|
|
651
|
+
evidenceUnavailable?: boolean
|
|
652
|
+
evidenceUnavailableReason?: string
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
export interface HookEvidence {
|
|
656
|
+
summary?: string
|
|
657
|
+
jobs?: HookJobEvidence[]
|
|
658
|
+
pods?: HookPodEvidence[]
|
|
659
|
+
events?: HookEventEvidence[]
|
|
660
|
+
logs?: HookLogEvidence[]
|
|
661
|
+
errors?: string[]
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
export interface HookJobEvidence {
|
|
665
|
+
name: string
|
|
666
|
+
namespace?: string
|
|
667
|
+
status?: string
|
|
668
|
+
active?: number
|
|
669
|
+
succeeded?: number
|
|
670
|
+
failed?: number
|
|
671
|
+
conditions?: string[]
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
export interface HookPodEvidence {
|
|
675
|
+
name: string
|
|
676
|
+
namespace?: string
|
|
677
|
+
phase?: string
|
|
678
|
+
ready?: string
|
|
679
|
+
restartCount?: number
|
|
680
|
+
reason?: string
|
|
681
|
+
message?: string
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
export interface HookEventEvidence {
|
|
685
|
+
involvedKind: string
|
|
686
|
+
involvedName: string
|
|
687
|
+
type?: string
|
|
688
|
+
reason?: string
|
|
689
|
+
message?: string
|
|
690
|
+
count?: number
|
|
691
|
+
lastSeen?: string
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
export interface HookLogEvidence {
|
|
695
|
+
pod: string
|
|
696
|
+
container: string
|
|
697
|
+
previous?: boolean
|
|
698
|
+
lines?: string[]
|
|
699
|
+
totalLines?: number
|
|
700
|
+
matchedLines?: number
|
|
701
|
+
fallback?: boolean
|
|
702
|
+
error?: string
|
|
606
703
|
}
|
|
607
704
|
|
|
608
705
|
export interface ChartDependency {
|
|
@@ -630,12 +727,40 @@ export interface HelmValues {
|
|
|
630
727
|
computed?: Record<string, unknown>
|
|
631
728
|
}
|
|
632
729
|
|
|
730
|
+
export interface ValuesDiff {
|
|
731
|
+
revision1: number
|
|
732
|
+
revision2: number
|
|
733
|
+
allValues: boolean
|
|
734
|
+
diff: string
|
|
735
|
+
}
|
|
736
|
+
|
|
633
737
|
export interface ManifestDiff {
|
|
634
738
|
revision1: number
|
|
635
739
|
revision2: number
|
|
636
740
|
diff: string
|
|
637
741
|
}
|
|
638
742
|
|
|
743
|
+
export interface NotesDiff {
|
|
744
|
+
revision1: number
|
|
745
|
+
revision2: number
|
|
746
|
+
diff: string
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
export interface HelmResourceRef {
|
|
750
|
+
kind: string
|
|
751
|
+
apiVersion?: string
|
|
752
|
+
name: string
|
|
753
|
+
namespace: string
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
export interface ResourceDiff {
|
|
757
|
+
revision1: number
|
|
758
|
+
revision2: number
|
|
759
|
+
added: HelmResourceRef[]
|
|
760
|
+
removed: HelmResourceRef[]
|
|
761
|
+
unchanged: HelmResourceRef[]
|
|
762
|
+
}
|
|
763
|
+
|
|
639
764
|
// Selected Helm release (for drawer state)
|
|
640
765
|
export interface SelectedHelmRelease {
|
|
641
766
|
namespace: string
|
|
@@ -649,7 +774,16 @@ export interface UpgradeInfo {
|
|
|
649
774
|
latestVersion?: string
|
|
650
775
|
updateAvailable: boolean
|
|
651
776
|
repositoryName?: string
|
|
777
|
+
// 'repository' for classic HTTP-repo matches, 'oci' when discovered via a
|
|
778
|
+
// registered OCI chart source. Absent when the source couldn't be determined.
|
|
779
|
+
sourceType?: 'repository' | 'oci'
|
|
780
|
+
// oci:// chart reference an OCI-sourced upgrade lives at (display only).
|
|
781
|
+
chartRef?: string
|
|
652
782
|
error?: string
|
|
783
|
+
// True only when the error is a genuinely untracked source (registering a
|
|
784
|
+
// chart source could fix it) — NOT for repo-side errors like a stale index or
|
|
785
|
+
// classic ambiguity. Gates the "track source" affordance.
|
|
786
|
+
untracked?: boolean
|
|
653
787
|
}
|
|
654
788
|
|
|
655
789
|
// Batch upgrade info keyed by "storageNamespace/name".
|