@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.
Files changed (43) hide show
  1. package/package.json +1 -1
  2. package/src/components/applications/ApplicationsView.tsx +2 -0
  3. package/src/components/audit/AuditAlerts.tsx +4 -0
  4. package/src/components/audit/AuditBadgeTooltip.test.tsx +30 -0
  5. package/src/components/audit/AuditBadgeTooltip.tsx +47 -0
  6. package/src/components/audit/AuditFindingsTable.tsx +4 -0
  7. package/src/components/audit/index.ts +1 -0
  8. package/src/components/gitops/GitOpsDetailLayout.tsx +3 -3
  9. package/src/components/gitops/GitOpsStatusBadge.tsx +9 -3
  10. package/src/components/gitops/GitOpsTableView.tsx +3 -1
  11. package/src/components/issues/IssuesView.tsx +9 -36
  12. package/src/components/issues/ResourceIssuesSection.tsx +142 -0
  13. package/src/components/issues/diagnostic.ts +64 -0
  14. package/src/components/issues/index.ts +2 -1
  15. package/src/components/issues/severity.ts +10 -9
  16. package/src/components/issues/types.ts +5 -0
  17. package/src/components/resources/ResourcesView.tsx +38 -2
  18. package/src/components/resources/cron-to-human.test.ts +41 -0
  19. package/src/components/resources/get-pod-problems.test.ts +18 -0
  20. package/src/components/resources/health-golden.test.ts +66 -0
  21. package/src/components/resources/renderers/JobRenderer.tsx +6 -2
  22. package/src/components/resources/renderers/KedaScaledObjectRenderer.tsx +2 -2
  23. package/src/components/resources/renderers/NodeRenderer.tsx +17 -8
  24. package/src/components/resources/renderers/PVCRenderer.tsx +7 -7
  25. package/src/components/resources/renderers/PodRenderer.tsx +28 -9
  26. package/src/components/resources/renderers/ServiceRenderer.tsx +23 -9
  27. package/src/components/resources/renderers/WorkloadRenderer.tsx +6 -3
  28. package/src/components/resources/resource-utils-argo.test.ts +23 -0
  29. package/src/components/resources/resource-utils-argo.ts +5 -1
  30. package/src/components/resources/resource-utils-keda.ts +12 -8
  31. package/src/components/resources/resource-utils.ts +34 -14
  32. package/src/components/timeline/TimelineSwimlanes.tsx +1 -0
  33. package/src/components/timeline/shared.tsx +15 -4
  34. package/src/components/topology/K8sResourceNode.tsx +28 -1
  35. package/src/components/topology/layout.ts +11 -5
  36. package/src/components/ui/PaneLoader.tsx +24 -6
  37. package/src/components/ui/drawer-components.test.tsx +35 -0
  38. package/src/components/ui/drawer-components.tsx +13 -1
  39. package/src/components/workload/WorkloadView.tsx +35 -5
  40. package/src/types/core.ts +100 -3
  41. package/src/utils/applications.test.ts +55 -1
  42. package/src/utils/applications.ts +28 -7
  43. 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
- let health: AppHealth = 'unknown'
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' ? value : 'unknown'
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
- export const HEALTH_ORDER: AppHealth[] = ['unhealthy', 'degraded', 'healthy', 'unknown']
652
- export const HEALTH_RANK: Record<string, number> = { unhealthy: 3, degraded: 2, healthy: 1, unknown: 0 }
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 = 'unknown'
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: