@skyhook-io/k8s-ui 1.8.6 → 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-problems.test.ts +18 -0
- package/src/components/resources/health-golden.test.ts +66 -0
- package/src/components/resources/renderers/JobRenderer.tsx +6 -2
- package/src/components/resources/renderers/KedaScaledObjectRenderer.tsx +2 -2
- 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/WorkloadRenderer.tsx +6 -3
- 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-keda.ts +12 -8
- package/src/components/resources/resource-utils.ts +34 -14
- 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/layout.ts +11 -5
- package/src/components/ui/PaneLoader.tsx +24 -6
- 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 +100 -3
- package/src/utils/applications.test.ts +55 -1
- package/src/utils/applications.ts +28 -7
- package/src/utils/badge-colors.ts +7 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, it, expect } from 'vitest'
|
|
2
|
-
import { compareVersions, appGroupingExplainer, APP_IDENTITY_ANNOTATION, appGroupLagMessage, matchWorkloadAcrossInstances, foldAppGroups, identityEnvInferred, type AppGroupFoldEntry } from './applications'
|
|
2
|
+
import { compareVersions, appGroupingExplainer, APP_IDENTITY_ANNOTATION, appGroupLagMessage, matchWorkloadAcrossInstances, foldAppGroups, identityEnvInferred, worstHealth, type AppGroupFoldEntry } from './applications'
|
|
3
3
|
|
|
4
4
|
describe('compareVersions', () => {
|
|
5
5
|
it('orders semver', () => {
|
|
@@ -206,6 +206,60 @@ describe('foldAppGroups', () => {
|
|
|
206
206
|
})
|
|
207
207
|
})
|
|
208
208
|
|
|
209
|
+
describe('worstHealth', () => {
|
|
210
|
+
// Mirrors pkg/health.WorseOf: unhealthy > degraded > unknown > healthy > neutral,
|
|
211
|
+
// with neutral as the most-benign identity. Regression for the rank-inversion
|
|
212
|
+
// fix — seeding the fold with `unknown` (now rank 2) made all-healthy/all-idle
|
|
213
|
+
// sets wrongly return `unknown`.
|
|
214
|
+
it('all-healthy stays healthy (not unknown)', () => {
|
|
215
|
+
expect(worstHealth(['healthy', 'healthy'])).toBe('healthy')
|
|
216
|
+
})
|
|
217
|
+
it('all-neutral stays neutral', () => {
|
|
218
|
+
expect(worstHealth(['neutral', 'neutral'])).toBe('neutral')
|
|
219
|
+
})
|
|
220
|
+
it('healthy + neutral resolves to healthy (healthy out-ranks idle)', () => {
|
|
221
|
+
expect(worstHealth(['healthy', 'neutral'])).toBe('healthy')
|
|
222
|
+
expect(worstHealth(['neutral', 'healthy'])).toBe('healthy')
|
|
223
|
+
})
|
|
224
|
+
it('unknown out-ranks healthy (a node-lost workload is worse than a running one)', () => {
|
|
225
|
+
expect(worstHealth(['unknown', 'healthy'])).toBe('unknown')
|
|
226
|
+
})
|
|
227
|
+
it('unhealthy dominates everything', () => {
|
|
228
|
+
expect(worstHealth(['unhealthy', 'degraded', 'unknown', 'healthy', 'neutral'])).toBe('unhealthy')
|
|
229
|
+
})
|
|
230
|
+
it('empty set is the most-benign identity', () => {
|
|
231
|
+
expect(worstHealth([])).toBe('neutral')
|
|
232
|
+
})
|
|
233
|
+
})
|
|
234
|
+
|
|
235
|
+
describe('foldAppGroups health rollup', () => {
|
|
236
|
+
const grp = (env: string, health: string): AppGroupFoldEntry => ({
|
|
237
|
+
row: { key: `k-${env}`, name: `billing-${env}`, identity: { key: 'billing', env, confidence: 'medium', evidence: 'e' } },
|
|
238
|
+
health: health as AppGroupFoldEntry['health'],
|
|
239
|
+
versions: [],
|
|
240
|
+
ready: 1,
|
|
241
|
+
desired: 1,
|
|
242
|
+
kinds: { Deployment: 1 },
|
|
243
|
+
classComposition: [{ cls: 'service', count: 1 }],
|
|
244
|
+
})
|
|
245
|
+
const rollup = (...hs: string[]) => {
|
|
246
|
+
const rows = foldAppGroups(hs.map((h, i) => grp(`env${i}`, h)), new Set(), false)
|
|
247
|
+
return (rows[0] as Extract<(typeof rows)[0], { kind: 'group' }>).health
|
|
248
|
+
}
|
|
249
|
+
it('all-healthy group rolls up healthy (regression: was unknown)', () => {
|
|
250
|
+
expect(rollup('healthy', 'healthy')).toBe('healthy')
|
|
251
|
+
})
|
|
252
|
+
it('all-idle group rolls up neutral (Idle), not green', () => {
|
|
253
|
+
expect(rollup('neutral', 'neutral')).toBe('neutral')
|
|
254
|
+
})
|
|
255
|
+
it('mixed healthy + idle reads healthy', () => {
|
|
256
|
+
expect(rollup('healthy', 'neutral')).toBe('healthy')
|
|
257
|
+
})
|
|
258
|
+
it('healthy + unknown reads unknown (node-lost dominates)', () => {
|
|
259
|
+
expect(rollup('healthy', 'unknown')).toBe('unknown')
|
|
260
|
+
})
|
|
261
|
+
})
|
|
262
|
+
|
|
209
263
|
describe('appGroupingExplainer', () => {
|
|
210
264
|
it('declared origins fold across clusters with no fix needed', () => {
|
|
211
265
|
for (const source of ['explicit', 'argo-path', 'argo-appset', 'flux-source']) {
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
// (internal/server/applications.go). Field names match the Go json tags.
|
|
7
7
|
|
|
8
8
|
export type AppWorkloadClass = 'service' | 'worker' | 'job' | 'mixed' | 'unknown'
|
|
9
|
-
export type AppHealth = 'healthy' | 'degraded' | 'unhealthy' | 'unknown'
|
|
9
|
+
export type AppHealth = 'healthy' | 'degraded' | 'unhealthy' | 'neutral' | 'unknown'
|
|
10
10
|
|
|
11
11
|
export interface AppWorkload {
|
|
12
12
|
kind: string
|
|
@@ -582,7 +582,9 @@ export function foldAppGroups<T extends AppGroupFoldEntry>(
|
|
|
582
582
|
const compMap = new Map<AppWorkloadClass, number>()
|
|
583
583
|
let ready = 0
|
|
584
584
|
let desired = 0
|
|
585
|
-
|
|
585
|
+
// Seed with the most-benign tier (rank 0) so the max-fold below has a valid
|
|
586
|
+
// identity now that `unknown` ranks above healthy — see worstHealth.
|
|
587
|
+
let health: AppHealth = 'neutral'
|
|
586
588
|
for (const m of members) {
|
|
587
589
|
const v = newest(m)
|
|
588
590
|
// A fleet member spans several per-cluster envs; the host supplies them so
|
|
@@ -640,7 +642,9 @@ export function foldAppGroups<T extends AppGroupFoldEntry>(
|
|
|
640
642
|
/** Normalize a wire health string to the AppHealth union (the health twin of
|
|
641
643
|
* workloadClassOf — keeps `as AppHealth` casts out of components). */
|
|
642
644
|
export function healthOf(value: string | undefined): AppHealth {
|
|
643
|
-
return value === 'unhealthy' || value === 'degraded' || value === 'healthy'
|
|
645
|
+
return value === 'unhealthy' || value === 'degraded' || value === 'healthy' || value === 'neutral'
|
|
646
|
+
? value
|
|
647
|
+
: 'unknown'
|
|
644
648
|
}
|
|
645
649
|
|
|
646
650
|
// -----------------------------------------------------------------------------
|
|
@@ -648,8 +652,16 @@ export function healthOf(value: string | undefined): AppHealth {
|
|
|
648
652
|
// pale-pastel pills (which have no theme token) for the colored tiers.
|
|
649
653
|
// -----------------------------------------------------------------------------
|
|
650
654
|
|
|
651
|
-
|
|
652
|
-
|
|
655
|
+
// Worst-first display + aggregation order. Mirrors the backend's
|
|
656
|
+
// `pkg/health` Rank ordering (unhealthy > degraded > unknown > healthy ≈ neutral)
|
|
657
|
+
// so the app rollup agrees with every other surface. `unknown` ranks ABOVE
|
|
658
|
+
// healthy (a node-lost workload is worse than a running one); `neutral`
|
|
659
|
+
// (intentional/idle) is the most-benign tier, so an all-idle app rolls up to
|
|
660
|
+
// "Idle" while a mixed healthy+idle app still reads Healthy (healthy out-ranks
|
|
661
|
+
// neutral in the max). NOTE: the previous map inverted this — it ranked
|
|
662
|
+
// `unknown: 0` below `healthy`, disagreeing with the backend rollup.
|
|
663
|
+
export const HEALTH_ORDER: AppHealth[] = ['unhealthy', 'degraded', 'unknown', 'healthy', 'neutral']
|
|
664
|
+
export const HEALTH_RANK: Record<string, number> = { unhealthy: 4, degraded: 3, unknown: 2, healthy: 1, neutral: 0 }
|
|
653
665
|
|
|
654
666
|
export interface HealthMeta {
|
|
655
667
|
label: string
|
|
@@ -670,6 +682,7 @@ export const CHIP_TONE = {
|
|
|
670
682
|
amber: 'bg-amber-50 text-amber-700 ring-amber-200 dark:bg-amber-950/40 dark:text-amber-300 dark:ring-amber-900',
|
|
671
683
|
emerald: 'bg-emerald-50 text-emerald-700 ring-emerald-200 dark:bg-emerald-950/40 dark:text-emerald-300 dark:ring-emerald-900',
|
|
672
684
|
blue: 'bg-blue-50 text-blue-700 ring-blue-200 dark:bg-blue-950/40 dark:text-blue-300 dark:ring-blue-900',
|
|
685
|
+
sky: 'bg-sky-50 text-sky-700 ring-sky-200 dark:bg-sky-950/40 dark:text-sky-300 dark:ring-sky-900',
|
|
673
686
|
violet: 'bg-violet-50 text-violet-700 ring-violet-200 dark:bg-violet-950/40 dark:text-violet-300 dark:ring-violet-900',
|
|
674
687
|
neutral: 'bg-theme-hover text-theme-text-secondary ring-theme-border',
|
|
675
688
|
muted: 'bg-theme-hover text-theme-text-tertiary ring-theme-border',
|
|
@@ -679,6 +692,9 @@ export const HEALTH_META: Record<AppHealth, HealthMeta> = {
|
|
|
679
692
|
unhealthy: { label: 'Down', bar: 'bg-rose-500', text: 'text-rose-600 dark:text-rose-400', pill: CHIP_TONE.rose },
|
|
680
693
|
degraded: { label: 'Degraded', bar: 'bg-amber-500', text: 'text-amber-600 dark:text-amber-400', pill: CHIP_TONE.amber },
|
|
681
694
|
healthy: { label: 'Healthy', bar: 'bg-emerald-500', text: 'text-emerald-600 dark:text-emerald-400', pill: CHIP_TONE.emerald },
|
|
695
|
+
// neutral = intentionally idle/off (every workload suspended or scaled to 0).
|
|
696
|
+
// Sky, calm — not "Healthy" (it isn't serving) and not a problem to act on.
|
|
697
|
+
neutral: { label: 'Idle', bar: 'bg-sky-500', text: 'text-sky-600 dark:text-sky-400', pill: CHIP_TONE.sky },
|
|
682
698
|
unknown: { label: 'Unknown', bar: 'bg-slate-400', text: 'text-theme-text-tertiary', pill: CHIP_TONE.muted },
|
|
683
699
|
}
|
|
684
700
|
|
|
@@ -726,9 +742,14 @@ export function workloadClassOf(value?: AppWorkloadClass): AppWorkloadClass {
|
|
|
726
742
|
}
|
|
727
743
|
}
|
|
728
744
|
|
|
729
|
-
/** Worst health across a set of raw health strings.
|
|
745
|
+
/** Worst health across a set of raw health strings. Seeds with `neutral` — the
|
|
746
|
+
* most-benign tier (rank 0) — so it acts as the max-fold identity: any real
|
|
747
|
+
* value out-ranks it, an all-neutral set stays neutral, and healthy+neutral
|
|
748
|
+
* resolves to healthy. (Seeding with `unknown` would be wrong now that `unknown`
|
|
749
|
+
* ranks ABOVE healthy — an all-healthy set would never beat it and return
|
|
750
|
+
* `unknown`. Mirrors pkg/health.WorseOf.) */
|
|
730
751
|
export function worstHealth(hs: string[]): AppHealth {
|
|
731
|
-
let w: AppHealth = '
|
|
752
|
+
let w: AppHealth = 'neutral'
|
|
732
753
|
for (const h of hs) if ((HEALTH_RANK[h] ?? 0) > (HEALTH_RANK[w] ?? 0)) w = h as AppHealth
|
|
733
754
|
return w
|
|
734
755
|
}
|
|
@@ -38,6 +38,10 @@ export const HEALTH_BADGE_COLORS: Record<string, string> = {
|
|
|
38
38
|
degraded: BADGE_SEVERITY_COLORS.warning,
|
|
39
39
|
alert: BADGE_SEVERITY_COLORS.alert,
|
|
40
40
|
unhealthy: BADGE_SEVERITY_COLORS.error,
|
|
41
|
+
// neutral = intentional/idle (suspended, scaled-to-0, completed) — sky, calm
|
|
42
|
+
// and distinct from `unknown` (gray, "can't determine"). Reuses the `info`
|
|
43
|
+
// sky palette; see `.status-neutral` in theme/components.css.
|
|
44
|
+
neutral: BADGE_SEVERITY_COLORS.info,
|
|
41
45
|
unknown: BADGE_SEVERITY_COLORS.neutral,
|
|
42
46
|
}
|
|
43
47
|
|
|
@@ -225,6 +229,9 @@ export function healthToSeverity(health: string): Severity {
|
|
|
225
229
|
case 'error':
|
|
226
230
|
case 'failed':
|
|
227
231
|
return 'error'
|
|
232
|
+
// health 'neutral' (intentional/idle) renders sky via the `info` palette —
|
|
233
|
+
// calm, and visually distinct from 'unknown' which falls through to gray.
|
|
234
|
+
case 'neutral':
|
|
228
235
|
case 'info':
|
|
229
236
|
return 'info'
|
|
230
237
|
default:
|