@skyhook-io/k8s-ui 1.8.9 → 1.8.11
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 +5 -4
- package/src/assets/gitops/argocd.png +0 -0
- package/src/assets/gitops/flux.svg +7 -0
- package/src/components/applications/AppChips.tsx +10 -0
- package/src/components/applications/ApplicationDetail.test.tsx +569 -0
- package/src/components/applications/ApplicationDetail.tsx +2517 -450
- package/src/components/applications/ApplicationsList.tsx +4 -1
- package/src/components/applications/ApplicationsView.tsx +101 -18
- package/src/components/applications/applications-view.test.tsx +47 -0
- package/src/components/applications/index.ts +1 -1
- package/src/components/audit/AuditAlerts.tsx +14 -10
- package/src/components/checks/ChecksView.tsx +81 -51
- package/src/components/checks/severity.ts +39 -45
- package/src/components/cluster-switcher/ClusterSwitcher.tsx +2 -2
- package/src/components/compare/CompareResourcePicker.tsx +2 -2
- package/src/components/gitops/GitOpsGraphFilterRail.tsx +2 -1
- package/src/components/gitops/GitOpsTableView.tsx +14 -59
- package/src/components/gitops/ManagedResourcesList.tsx +8 -11
- package/src/components/gitops/SyncCountdown.tsx +26 -17
- package/src/components/gitops/SyncOptionsDialog.tsx +2 -2
- package/src/components/gitops/insights/GitOpsInsightViews.tsx +277 -104
- package/src/components/gitops/insights/insights-helpers.test.ts +86 -1
- package/src/components/gitops/insights/insights-helpers.ts +69 -1
- package/src/components/gitops/tree/GitOpsTreeGraph.tsx +5 -2
- package/src/components/issues/IssuesView.tsx +219 -143
- package/src/components/issues/ResourceIssuesSection.test.tsx +40 -0
- package/src/components/issues/ResourceIssuesSection.tsx +28 -128
- package/src/components/issues/index.ts +3 -2
- package/src/components/issues/issue-timing.ts +53 -0
- package/src/components/issues/issues.test.ts +146 -4
- package/src/components/issues/severity.ts +29 -32
- package/src/components/logs/LogCore.tsx +14 -4
- package/src/components/logs/WorkloadLogsViewer.tsx +31 -6
- package/src/components/logs/useLogStream.ts +8 -1
- package/src/components/namespace-switcher/NamespacePicker.tsx +86 -107
- package/src/components/resources/ResourcesSidebar.test.tsx +107 -0
- package/src/components/resources/ResourcesSidebar.tsx +70 -52
- package/src/components/resources/ResourcesView.selected-kind.test.ts +143 -0
- package/src/components/resources/ResourcesView.tsx +243 -53
- package/src/components/resources/renderers/ClusterComplianceReportRenderer.tsx +2 -2
- package/src/components/resources/renderers/CronWorkflowRenderer.tsx +99 -0
- package/src/components/resources/renderers/JobRenderer.tsx +3 -1
- package/src/components/resources/renderers/MetricsUnavailableNotice.tsx +47 -0
- package/src/components/resources/renderers/NodeRenderer.test.tsx +103 -0
- package/src/components/resources/renderers/NodeRenderer.tsx +20 -12
- package/src/components/resources/renderers/PVCRenderer.test.tsx +23 -0
- package/src/components/resources/renderers/PVCRenderer.tsx +2 -2
- package/src/components/resources/renderers/PodRenderer.test.tsx +101 -0
- package/src/components/resources/renderers/PodRenderer.tsx +81 -70
- package/src/components/resources/renderers/PrometheusRuleRenderer.tsx +2 -2
- package/src/components/resources/renderers/SbomReportRenderer.tsx +2 -2
- package/src/components/resources/renderers/SecretRenderer.tsx +2 -0
- package/src/components/resources/renderers/ServiceRenderer.tsx +58 -40
- package/src/components/resources/renderers/VulnerabilityReportRenderer.tsx +2 -2
- package/src/components/resources/renderers/WorkflowRenderer.test.tsx +43 -0
- package/src/components/resources/renderers/WorkflowRenderer.tsx +60 -78
- package/src/components/resources/renderers/index.ts +1 -0
- package/src/components/resources/resource-utils-capi.ts +1 -1
- package/src/components/resources/resource-utils.ts +31 -0
- package/src/components/shared/DetailShell.tsx +2 -2
- package/src/components/shared/ResourceActionsBar.tsx +140 -108
- package/src/components/shared/ResourceRendererDispatch.tsx +33 -4
- package/src/components/timeline/TimelineList.tsx +371 -354
- package/src/components/timeline/TimelineStrip.test.tsx +194 -0
- package/src/components/timeline/TimelineStrip.tsx +836 -0
- package/src/components/timeline/TimelineSwimlanes.test.tsx +1028 -0
- package/src/components/timeline/TimelineSwimlanes.tsx +2498 -593
- package/src/components/timeline/TimelineToolbar.test.tsx +320 -0
- package/src/components/timeline/TimelineToolbar.tsx +759 -0
- package/src/components/timeline/TimelineToolbarState.test.tsx +64 -0
- package/src/components/timeline/index.ts +23 -0
- package/src/components/timeline/scrubber-math.test.ts +235 -0
- package/src/components/timeline/scrubber-math.ts +249 -0
- package/src/components/timeline/shared.test.tsx +219 -0
- package/src/components/timeline/shared.tsx +232 -447
- package/src/components/timeline/timeline-filters.test.ts +196 -0
- package/src/components/timeline/timeline-filters.ts +180 -0
- package/src/components/timeline/timeline-lane-sort.test.ts +97 -0
- package/src/components/timeline/timeline-lane-sort.ts +77 -0
- package/src/components/timeline/timeline-live.test.ts +131 -0
- package/src/components/timeline/timeline-live.ts +77 -0
- package/src/components/topology/GroupNode.tsx +9 -2
- package/src/components/topology/K8sResourceNode.test.ts +11 -0
- package/src/components/topology/K8sResourceNode.tsx +387 -243
- package/src/components/topology/TopologyFilterSidebar.tsx +9 -0
- package/src/components/topology/TopologyGraph.tsx +89 -16
- package/src/components/topology/TopologySearch.tsx +2 -2
- package/src/components/topology/layout.ts +10 -2
- package/src/components/topology/topology.css +0 -92
- package/src/components/ui/Badge.tsx +5 -0
- package/src/components/ui/BoardSkeleton.tsx +47 -0
- package/src/components/ui/CardSection.tsx +117 -0
- package/src/components/ui/CodeViewer.tsx +2 -2
- package/src/components/ui/Collapse.tsx +59 -0
- package/src/components/ui/ConfirmDialog.tsx +7 -3
- package/src/components/ui/DialogPortal.tsx +31 -9
- package/src/components/ui/Input.tsx +24 -0
- package/src/components/ui/MultiSelectPicker.test.tsx +101 -0
- package/src/components/ui/MultiSelectPicker.tsx +167 -0
- package/src/components/ui/PaneLoader.tsx +10 -2
- package/src/components/ui/ResourceBar.tsx +68 -25
- package/src/components/ui/SearchBox.tsx +18 -4
- package/src/components/ui/SearchPillInput.tsx +2 -2
- package/src/components/ui/Toast.tsx +1 -1
- package/src/components/ui/drawer-components.tsx +39 -26
- package/src/components/ui/index.ts +6 -0
- package/src/components/ui/severity-tone.ts +63 -0
- package/src/components/workload/ResourceDetailDrawer.tsx +6 -2
- package/src/components/workload/WorkloadView.tsx +2682 -472
- package/src/components/workload/index.ts +1 -1
- package/src/theme/variables.css +15 -1
- package/src/types/core.ts +57 -4
- package/src/utils/api-resources.test.ts +54 -0
- package/src/utils/api-resources.ts +68 -1
- package/src/utils/application-topology.test.ts +198 -0
- package/src/utils/application-topology.ts +261 -0
- package/src/utils/applications.test.ts +167 -1
- package/src/utils/applications.ts +1157 -393
- package/src/utils/index.ts +2 -0
- package/src/utils/navigation.test.ts +32 -1
- package/src/utils/navigation.ts +70 -0
- package/src/utils/resource-hierarchy.test.ts +990 -1
- package/src/utils/resource-hierarchy.ts +905 -267
- package/src/utils/resource-icons.ts +2 -0
- package/src/utils/topology-neighborhood.test.ts +202 -1
- package/src/utils/topology-neighborhood.ts +194 -13
- package/src/utils/workflow-execution.test.ts +556 -0
- package/src/utils/workflow-execution.ts +814 -0
- package/src/utils/workload-colors.ts +2 -3
- package/tsconfig.json +2 -2
|
@@ -13,9 +13,11 @@ export interface ApplicationsListProps {
|
|
|
13
13
|
onSelect: (key: string) => void
|
|
14
14
|
/** Leading element in the header actions (e.g. a freshness control). */
|
|
15
15
|
headerActions?: ReactNode
|
|
16
|
+
/** First fetch in flight — chassis renders its shape-stable skeleton. */
|
|
17
|
+
loading?: boolean
|
|
16
18
|
}
|
|
17
19
|
|
|
18
|
-
export function ApplicationsList({ apps, onSelect, headerActions }: ApplicationsListProps) {
|
|
20
|
+
export function ApplicationsList({ apps, onSelect, headerActions, loading }: ApplicationsListProps) {
|
|
19
21
|
// Env tokens this CLUSTER proved (identity classifications on the wire) feed
|
|
20
22
|
// the namespace heuristic, so sibling-less rows in discovered env namespaces
|
|
21
23
|
// still label without any hardcoded vocabulary.
|
|
@@ -24,6 +26,7 @@ export function ApplicationsList({ apps, onSelect, headerActions }: Applications
|
|
|
24
26
|
|
|
25
27
|
return (
|
|
26
28
|
<ApplicationsView
|
|
29
|
+
loading={loading}
|
|
27
30
|
variant="single"
|
|
28
31
|
entries={entries}
|
|
29
32
|
onSelect={onSelect}
|
|
@@ -5,6 +5,7 @@ import { clsx } from 'clsx'
|
|
|
5
5
|
import { StatusDot, mapHealthToTone } from '../ui/status-tone'
|
|
6
6
|
import { Tooltip } from '../ui/Tooltip'
|
|
7
7
|
import { EmptyState } from '../ui/EmptyState'
|
|
8
|
+
import { BoardTableSkeleton, BoardRailSkeleton } from '../ui/BoardSkeleton'
|
|
8
9
|
import { SearchBox } from '../ui/SearchBox'
|
|
9
10
|
import { useFilterState, defineFilterSchema } from '../../filter-state'
|
|
10
11
|
import { PageHeader } from '../ui/PageHeader'
|
|
@@ -16,6 +17,7 @@ import { useRegisterShortcuts } from '../../hooks/useKeyboardShortcuts'
|
|
|
16
17
|
import { pluralize } from '../../utils/pluralize'
|
|
17
18
|
import {
|
|
18
19
|
type AppEntry,
|
|
20
|
+
type AppRow,
|
|
19
21
|
type AppHealth,
|
|
20
22
|
type AppWorkloadClass,
|
|
21
23
|
type AppSource,
|
|
@@ -35,12 +37,34 @@ import {
|
|
|
35
37
|
isSystemNamespace,
|
|
36
38
|
searchTextForEntry,
|
|
37
39
|
foldAppGroups,
|
|
40
|
+
batchSignalForApp,
|
|
41
|
+
batchRuntimeForApp,
|
|
38
42
|
type FoldedRow,
|
|
39
43
|
} from '../../utils/applications'
|
|
40
44
|
import { ReadyBar } from './ReadyBar'
|
|
41
|
-
import { ProvenanceBadge, ClassBadge, CategoryChip, VersionInfo } from './AppChips'
|
|
45
|
+
import { ProvenanceBadge, ClassBadge, CategoryChip, VersionInfo, BatchSignalChip } from './AppChips'
|
|
42
46
|
import { AppIdentityTooltip, EnvHint } from './AppTooltips'
|
|
43
47
|
|
|
48
|
+
function ApplicationRuntimeCell({ apps, workloadClass, ready, desired }: { apps: AppRow[]; workloadClass: AppWorkloadClass; ready: number; desired: number }) {
|
|
49
|
+
if (workloadClass !== 'job') return <ReadyBar ready={ready} desired={desired} />
|
|
50
|
+
const runtimes = apps.map(batchRuntimeForApp)
|
|
51
|
+
const worst = runtimes.reduce((current, runtime) => (HEALTH_RANK[runtime.health] ?? 0) > (HEALTH_RANK[current.health] ?? 0) ? runtime : current, runtimes[0])
|
|
52
|
+
if (!worst) return <span className="text-xs text-theme-text-tertiary">Idle</span>
|
|
53
|
+
const same = runtimes.every((runtime) => runtime.label === worst.label)
|
|
54
|
+
const label = same ? worst.label : `${runtimes.length} states`
|
|
55
|
+
return (
|
|
56
|
+
<Tooltip content={runtimes.map((runtime, index) => `${apps[index]?.name ?? 'app'}: ${runtime.label} — ${runtime.detail}`).join('\n')} delay={150}>
|
|
57
|
+
<span className="inline-flex items-center gap-1.5 text-xs text-theme-text-secondary"><StatusDot tone={mapHealthToTone(worst.health)} />{label}</span>
|
|
58
|
+
</Tooltip>
|
|
59
|
+
)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function applicationRuntimeLabel(apps: AppRow[], workloadClass: AppWorkloadClass, health: AppHealth): string {
|
|
63
|
+
if (workloadClass !== 'job') return HEALTH_META[health].label
|
|
64
|
+
const labels = Array.from(new Set(apps.map((app) => batchRuntimeForApp(app).label)))
|
|
65
|
+
return labels.length === 1 ? labels[0] : 'Multiple batch states'
|
|
66
|
+
}
|
|
67
|
+
|
|
44
68
|
// ApplicationsView — the shared, variant-agnostic core behind the Applications
|
|
45
69
|
// surface. It owns the entire list chassis: a health hero header (PageHeader +
|
|
46
70
|
// SummaryTiles + DistributionBar), a left facet rail (Availability / Class /
|
|
@@ -51,7 +75,7 @@ import { AppIdentityTooltip, EnvHint } from './AppTooltips'
|
|
|
51
75
|
// the per-instance row. Styling mirrors the Resources table so the surfaces
|
|
52
76
|
// read as one design.
|
|
53
77
|
|
|
54
|
-
//
|
|
78
|
+
// Status is the one health facet — map app health onto the shared facet
|
|
55
79
|
// tone so its dots read red/amber/green/grey like the GitOps sync+health facets.
|
|
56
80
|
const HEALTH_TONE: Record<AppHealth, FacetTone> = {
|
|
57
81
|
unhealthy: 'error',
|
|
@@ -71,7 +95,7 @@ function compareEntries(a: AppEntry, b: AppEntry, key: SortKey): number {
|
|
|
71
95
|
case 'name':
|
|
72
96
|
return a.row.name.localeCompare(b.row.name)
|
|
73
97
|
case 'ready':
|
|
74
|
-
return a
|
|
98
|
+
return runtimeSortValue(a) - runtimeSortValue(b)
|
|
75
99
|
case 'version': {
|
|
76
100
|
// Sort by distinct-version count first (skewed apps cluster), then the
|
|
77
101
|
// first tag for a stable, human-meaningful order.
|
|
@@ -82,6 +106,10 @@ function compareEntries(a: AppEntry, b: AppEntry, key: SortKey): number {
|
|
|
82
106
|
}
|
|
83
107
|
}
|
|
84
108
|
|
|
109
|
+
function runtimeSortValue(entry: AppEntry): number {
|
|
110
|
+
return entry.workloadClass === 'job' ? (HEALTH_RANK[entry.health] ?? 0) : entry.readyRatio
|
|
111
|
+
}
|
|
112
|
+
|
|
85
113
|
// The env token an entry filters under. Single entries carry one env; fleet
|
|
86
114
|
// entries carry several — a fleet row matches an env facet if ANY of its slices
|
|
87
115
|
// do (the same inclusive policy as the Class facet).
|
|
@@ -100,6 +128,9 @@ export interface ApplicationsViewProps {
|
|
|
100
128
|
emptySlot?: ReactNode
|
|
101
129
|
/** Leading element in the header actions cluster (e.g. a freshness control). */
|
|
102
130
|
headerActions?: ReactNode
|
|
131
|
+
/** First fetch in flight — render the shape-stable skeleton (pulsing tiles,
|
|
132
|
+
* rail stubs, table rows) instead of collapsing the chassis. */
|
|
133
|
+
loading?: boolean
|
|
103
134
|
}
|
|
104
135
|
|
|
105
136
|
const APPS_FILTER_SCHEMA = defineFilterSchema({
|
|
@@ -112,7 +143,7 @@ const APPS_FILTER_SCHEMA = defineFilterSchema({
|
|
|
112
143
|
system: { param: 'system', type: 'boolean' },
|
|
113
144
|
})
|
|
114
145
|
|
|
115
|
-
export function ApplicationsView({ entries: allEntries, variant, onSelect, title = 'Applications', description, emptySlot, headerActions }: ApplicationsViewProps) {
|
|
146
|
+
export function ApplicationsView({ entries: allEntries, variant, onSelect, title = 'Applications', description, emptySlot, headerActions, loading }: ApplicationsViewProps) {
|
|
116
147
|
// Facets + search + show-system live in the URL (shareable, bookmarkable) via
|
|
117
148
|
// the shared filter-state contract. Sort stays local: it's a compound
|
|
118
149
|
// {key, dir} view-preference, not a result-narrowing filter, so it isn't
|
|
@@ -274,9 +305,16 @@ export function ApplicationsView({ entries: allEntries, variant, onSelect, title
|
|
|
274
305
|
.sort((a, b) => (envRank(b[0] === 'none' ? undefined : b[0]) ?? -1) - (envRank(a[0] === 'none' ? undefined : a[0]) ?? -1))
|
|
275
306
|
.map(([env, count]) => ({ value: env, label: env === 'none' ? 'unlabeled' : env, count }))
|
|
276
307
|
|
|
308
|
+
// First fetch, nothing to show yet: drive the shape-stable skeleton. The
|
|
309
|
+
// loaded state hides zero-count health tiles, so without this the header
|
|
310
|
+
// (tiles + distribution bar), rail, and table would all pop in at once.
|
|
311
|
+
const initialLoading = Boolean(loading) && allEntries.length === 0
|
|
312
|
+
|
|
277
313
|
// Clickable status tile wired to the health facet — tap to filter to that tier.
|
|
278
314
|
const healthTile = (h: AppHealth, tone: SummaryTone) =>
|
|
279
|
-
|
|
315
|
+
initialLoading && (h === 'healthy' || h === 'degraded' || h === 'unhealthy') ? (
|
|
316
|
+
<SummaryTile key={h} label={HEALTH_META[h].label} value={0} tone={tone} loading />
|
|
317
|
+
) : counts.health[h] ? (
|
|
280
318
|
<SummaryTile key={h} label={HEALTH_META[h].label} value={counts.health[h]} tone={tone} active={fHealth.has(h)} onClick={() => filters.toggle('health', h)} />
|
|
281
319
|
) : null
|
|
282
320
|
|
|
@@ -301,7 +339,7 @@ export function ApplicationsView({ entries: allEntries, variant, onSelect, title
|
|
|
301
339
|
actions={
|
|
302
340
|
<>
|
|
303
341
|
{headerActions}
|
|
304
|
-
<SummaryTile label={total === 1 ? 'application' : 'applications'} value={total} />
|
|
342
|
+
<SummaryTile label={total === 1 ? 'application' : 'applications'} value={total} loading={initialLoading} />
|
|
305
343
|
{healthTile('unhealthy', 'error')}
|
|
306
344
|
{healthTile('degraded', 'warning')}
|
|
307
345
|
{healthTile('healthy', 'success')}
|
|
@@ -310,11 +348,15 @@ export function ApplicationsView({ entries: allEntries, variant, onSelect, title
|
|
|
310
348
|
</>
|
|
311
349
|
}
|
|
312
350
|
/>
|
|
313
|
-
|
|
314
|
-
className="mt-3"
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
351
|
+
{initialLoading ? (
|
|
352
|
+
<div className="mt-3 h-1.5 w-full animate-pulse rounded-full bg-theme-hover" aria-hidden />
|
|
353
|
+
) : (
|
|
354
|
+
<DistributionBar
|
|
355
|
+
className="mt-3"
|
|
356
|
+
ariaLabel="Health distribution"
|
|
357
|
+
segments={HEALTH_ORDER.map((h) => ({ key: h, count: counts.health[h] ?? 0, fillClass: HEALTH_META[h].bar }))}
|
|
358
|
+
/>
|
|
359
|
+
)}
|
|
318
360
|
</div>
|
|
319
361
|
|
|
320
362
|
{/* Body: filter sidebar | content (toolbar + table). */}
|
|
@@ -330,7 +372,19 @@ export function ApplicationsView({ entries: allEntries, variant, onSelect, title
|
|
|
330
372
|
)}
|
|
331
373
|
</div>
|
|
332
374
|
<div className="flex-1 overflow-y-auto">
|
|
333
|
-
|
|
375
|
+
{initialLoading ? (
|
|
376
|
+
<BoardRailSkeleton
|
|
377
|
+
sections={[
|
|
378
|
+
['Status', 5],
|
|
379
|
+
['Class', 3],
|
|
380
|
+
['Type', 4],
|
|
381
|
+
['Environment', 3],
|
|
382
|
+
['Source', 3],
|
|
383
|
+
]}
|
|
384
|
+
/>
|
|
385
|
+
) : (
|
|
386
|
+
<>
|
|
387
|
+
<Facet icon={HeartPulse} title="Status" options={HEALTH_ORDER.map((h) => ({ value: h, label: HEALTH_META[h].label, count: counts.health[h] ?? 0, tone: HEALTH_TONE[h] }))} selected={fHealth} onToggle={(v) => filters.toggle('health', v)} />
|
|
334
388
|
<Facet icon={Layers} title="Class" options={CLASS_ORDER.map((c) => ({ value: c, label: CLASS_META[c].label, count: counts.workloadClass[c] ?? 0 }))} selected={fClass} onToggle={(v) => filters.toggle('class', v)} />
|
|
335
389
|
<Facet icon={Shapes} title="Type" options={CATEGORY_ORDER.map((c) => ({ value: c, label: CATEGORY_META[c].label, count: counts.category[c] ?? 0, tooltip: CATEGORY_META[c].tooltip }))} selected={fType} onToggle={(v) => filters.toggle('type', v)} />
|
|
336
390
|
<Facet icon={Globe} title="Environment" info={<EnvHint />} options={envOptions} selected={fEnv} onToggle={(v) => filters.toggle('env', v)} />
|
|
@@ -342,6 +396,8 @@ export function ApplicationsView({ entries: allEntries, variant, onSelect, title
|
|
|
342
396
|
<span className="ml-auto tabular-nums text-theme-text-tertiary">{systemCount}</span>
|
|
343
397
|
</label>
|
|
344
398
|
)}
|
|
399
|
+
</>
|
|
400
|
+
)}
|
|
345
401
|
</div>
|
|
346
402
|
</aside>
|
|
347
403
|
|
|
@@ -369,7 +425,9 @@ export function ApplicationsView({ entries: allEntries, variant, onSelect, title
|
|
|
369
425
|
</div>
|
|
370
426
|
|
|
371
427
|
<div className="min-w-0 flex-1 overflow-auto bg-theme-base">
|
|
372
|
-
{
|
|
428
|
+
{initialLoading ? (
|
|
429
|
+
<BoardTableSkeleton />
|
|
430
|
+
) : entries.length === 0 ? (
|
|
373
431
|
emptySlot && allEntries.length === 0 ? (
|
|
374
432
|
emptySlot
|
|
375
433
|
) : (
|
|
@@ -399,7 +457,7 @@ export function ApplicationsView({ entries: allEntries, variant, onSelect, title
|
|
|
399
457
|
</>
|
|
400
458
|
)}
|
|
401
459
|
<th className={TH_CLASS}>Class</th>
|
|
402
|
-
<SortableTh label="
|
|
460
|
+
<SortableTh label="Runtime" sortKey="ready" activeKey={sort?.key ?? null} direction={sort?.dir ?? 'asc'} onSort={onSort} />
|
|
403
461
|
<SortableTh label="Version" sortKey="version" activeKey={sort?.key ?? null} direction={sort?.dir ?? 'asc'} onSort={onSort} />
|
|
404
462
|
<th className={TH_CLASS}>Workloads</th>
|
|
405
463
|
<th className={clsx(TH_CLASS, 'w-8')} />
|
|
@@ -421,11 +479,12 @@ export function ApplicationsView({ entries: allEntries, variant, onSelect, title
|
|
|
421
479
|
<span className="flex items-center gap-2">
|
|
422
480
|
{/* Left status stripe (worst-child health) — the row status
|
|
423
481
|
gutter shared with the GitOps table. */}
|
|
424
|
-
<Tooltip content={
|
|
482
|
+
<Tooltip content={applicationRuntimeLabel(r.members.map((member) => member.row), r.workloadClass, r.health)} delay={150}>
|
|
425
483
|
<span className={clsx('h-8 w-1 shrink-0 rounded-full', HEALTH_META[r.health].bar)} />
|
|
426
484
|
</Tooltip>
|
|
427
485
|
<ChevronRight className={clsx('h-3.5 w-3.5 shrink-0 text-theme-text-tertiary transition-transform', r.expanded && 'rotate-90')} aria-hidden />
|
|
428
486
|
<span className="truncate font-semibold text-theme-text-primary">{r.label}</span>
|
|
487
|
+
{r.workloadClass !== 'job' && <BatchSignalChip signal={firstBatchSignal(r.members.map((m) => m.row))} />}
|
|
429
488
|
<Tooltip
|
|
430
489
|
content={<AppIdentityTooltip identityKey={r.label} source={r.members[0]?.row.identity?.source} portable={r.members[0]?.row.identity?.portable} fleet={variant === 'fleet'} members={r.members.map((m) => ({ name: m.row.name, env: m.row.identity!.env, confidence: m.row.identity!.confidence, evidence: m.row.identity!.evidence }))} />}
|
|
431
490
|
delay={150}
|
|
@@ -473,7 +532,7 @@ export function ApplicationsView({ entries: allEntries, variant, onSelect, title
|
|
|
473
532
|
</span>
|
|
474
533
|
</td>
|
|
475
534
|
<td className="px-2 py-2.5"><ClassBadge workloadClass={r.workloadClass} composition={r.classComposition} /></td>
|
|
476
|
-
<td className="px-2 py-2.5"><
|
|
535
|
+
<td className="px-2 py-2.5"><ApplicationRuntimeCell apps={r.members.map((member) => member.row)} workloadClass={r.workloadClass} ready={r.ready} desired={r.desired} /></td>
|
|
477
536
|
<td className="px-2 py-2.5">
|
|
478
537
|
{r.lag ? (
|
|
479
538
|
<Tooltip content={`Promotion lag: ${r.lag} (${r.cells.filter((c) => c.version).map((c) => `${c.env}=${c.version}`).join(', ')})`} delay={150}>
|
|
@@ -500,10 +559,11 @@ export function ApplicationsView({ entries: allEntries, variant, onSelect, title
|
|
|
500
559
|
>
|
|
501
560
|
<td className={clsx('py-2.5 pr-2', r.child ? 'pl-10' : 'pl-3')}>
|
|
502
561
|
<span className="flex items-center gap-2">
|
|
503
|
-
<Tooltip content={
|
|
562
|
+
<Tooltip content={applicationRuntimeLabel([e.row], e.workloadClass, e.health)} delay={150}>
|
|
504
563
|
<span className={clsx('h-8 w-1 shrink-0 rounded-full', HEALTH_META[e.health].bar)} />
|
|
505
564
|
</Tooltip>
|
|
506
565
|
<span className="truncate font-medium text-theme-text-primary">{e.row.name}</span>
|
|
566
|
+
{e.workloadClass !== 'job' && <BatchSignalChip signal={batchSignalForApp(e.row)} />}
|
|
507
567
|
<ProvenanceBadge tier={e.row.tier} appKey={e.row.key} confidence={e.row.confidence} />
|
|
508
568
|
<CategoryChip category={e.category} addonReason={e.row.addonReason} />
|
|
509
569
|
</span>
|
|
@@ -570,7 +630,7 @@ export function ApplicationsView({ entries: allEntries, variant, onSelect, title
|
|
|
570
630
|
</>
|
|
571
631
|
)}
|
|
572
632
|
<td className="px-2 py-2.5"><ClassBadge workloadClass={e.workloadClass} composition={e.classComposition} /></td>
|
|
573
|
-
<td className="px-2 py-2.5"><
|
|
633
|
+
<td className="px-2 py-2.5"><ApplicationRuntimeCell apps={[e.row]} workloadClass={e.workloadClass} ready={e.ready} desired={e.desired} /></td>
|
|
574
634
|
<td className="px-2 py-2.5">
|
|
575
635
|
{e.variant === 'fleet' ? (
|
|
576
636
|
e.versionSkew ? (
|
|
@@ -609,3 +669,26 @@ export function ApplicationsView({ entries: allEntries, variant, onSelect, title
|
|
|
609
669
|
</div>
|
|
610
670
|
)
|
|
611
671
|
}
|
|
672
|
+
|
|
673
|
+
function firstBatchSignal(rows: AppRow[]) {
|
|
674
|
+
let best = null as ReturnType<typeof batchSignalForApp>
|
|
675
|
+
const rank = (tone: string | undefined) => {
|
|
676
|
+
switch (tone) {
|
|
677
|
+
case 'rose':
|
|
678
|
+
return 5
|
|
679
|
+
case 'amber':
|
|
680
|
+
return 4
|
|
681
|
+
case 'sky':
|
|
682
|
+
return 3
|
|
683
|
+
case 'emerald':
|
|
684
|
+
return 2
|
|
685
|
+
default:
|
|
686
|
+
return 0
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
for (const row of rows) {
|
|
690
|
+
const signal = batchSignalForApp(row)
|
|
691
|
+
if (signal && rank(signal.tone) > rank(best?.tone)) best = signal
|
|
692
|
+
}
|
|
693
|
+
return best
|
|
694
|
+
}
|
|
@@ -67,6 +67,53 @@ describe('buildSingleAppEntry', () => {
|
|
|
67
67
|
expect(e.env).toBe('staging')
|
|
68
68
|
expect(e.envInferred).toBe(false)
|
|
69
69
|
})
|
|
70
|
+
|
|
71
|
+
it('uses delivery degradation in the displayed application health', () => {
|
|
72
|
+
const e = buildSingleAppEntry(app({
|
|
73
|
+
health: 'degraded',
|
|
74
|
+
runtimeHealth: 'healthy',
|
|
75
|
+
sourceStatus: { sync: 'Synced', health: 'Degraded' },
|
|
76
|
+
}))
|
|
77
|
+
|
|
78
|
+
expect(e.health).toBe('degraded')
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
it('preserves the latest retained batch outcome when delivery adds no degradation', () => {
|
|
82
|
+
const e = buildSingleAppEntry(app({
|
|
83
|
+
health: 'unhealthy',
|
|
84
|
+
runtimeHealth: 'unhealthy',
|
|
85
|
+
workload_class: 'job',
|
|
86
|
+
workloads: [wl({
|
|
87
|
+
kind: 'CronJob',
|
|
88
|
+
workload_class: 'job',
|
|
89
|
+
health: 'unhealthy',
|
|
90
|
+
ready: 0,
|
|
91
|
+
desired: 0,
|
|
92
|
+
batch: { retainedRuns: 2, failedRuns: 1, succeededRuns: 1, latestRunPhase: 'Succeeded' },
|
|
93
|
+
})],
|
|
94
|
+
}))
|
|
95
|
+
|
|
96
|
+
expect(e.health).toBe('healthy')
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
it('applies delivery degradation to a batch app with a successful latest run', () => {
|
|
100
|
+
const e = buildSingleAppEntry(app({
|
|
101
|
+
health: 'unhealthy',
|
|
102
|
+
runtimeHealth: 'unhealthy',
|
|
103
|
+
workload_class: 'job',
|
|
104
|
+
sourceStatus: { sync: 'Synced', health: 'Degraded' },
|
|
105
|
+
workloads: [wl({
|
|
106
|
+
kind: 'CronJob',
|
|
107
|
+
workload_class: 'job',
|
|
108
|
+
health: 'unhealthy',
|
|
109
|
+
ready: 0,
|
|
110
|
+
desired: 0,
|
|
111
|
+
batch: { retainedRuns: 2, failedRuns: 1, succeededRuns: 1, latestRunPhase: 'Succeeded' },
|
|
112
|
+
})],
|
|
113
|
+
}))
|
|
114
|
+
|
|
115
|
+
expect(e.health).toBe('degraded')
|
|
116
|
+
})
|
|
70
117
|
})
|
|
71
118
|
|
|
72
119
|
describe('searchTextForEntry (single)', () => {
|
|
@@ -7,7 +7,7 @@ export { buildSingleAppEntry } from '../../utils/applications'
|
|
|
7
7
|
// Facet moved to the shared ui/ primitives; re-exported here for compatibility.
|
|
8
8
|
export { Facet } from '../ui/Facet'
|
|
9
9
|
export { ApplicationDetail } from './ApplicationDetail'
|
|
10
|
-
export type { ApplicationDetailProps, SelectedAppWorkload, AppIdentityInstance } from './ApplicationDetail'
|
|
10
|
+
export type { ApplicationDetailProps, SelectedAppWorkload, AppIdentityInstance, ApplicationView } from './ApplicationDetail'
|
|
11
11
|
export { CenteredEmpty } from '../ui/CenteredEmpty'
|
|
12
12
|
export { ProvenanceBadge, ClassBadge, CategoryChip, VersionInfo } from './AppChips'
|
|
13
13
|
export { ProvenanceTooltip, CategoryTooltip, VersionTooltip, AppIdentityTooltip } from './AppTooltips'
|
|
@@ -41,14 +41,15 @@ export function AuditAlerts({ findings, onViewAll }: AuditAlertsProps) {
|
|
|
41
41
|
const warnings = findings.filter(f => f.severity === 'warning').length
|
|
42
42
|
|
|
43
43
|
return (
|
|
44
|
-
<
|
|
44
|
+
<section className="rounded-lg border border-theme-border bg-theme-surface p-4 shadow-theme-sm">
|
|
45
45
|
<button
|
|
46
|
+
type="button"
|
|
46
47
|
onClick={() => setExpanded(!expanded)}
|
|
47
|
-
className="flex items-center gap-2
|
|
48
|
+
className="flex w-full items-center gap-2 rounded-md text-left transition-colors hover:text-theme-text-primary"
|
|
48
49
|
>
|
|
49
50
|
<ChevronRight className={clsx('w-4 h-4 text-theme-text-tertiary transition-transform duration-200', expanded && 'rotate-90')} />
|
|
50
51
|
<ClipboardCheck className="w-4 h-4 text-theme-text-secondary" />
|
|
51
|
-
<span className="text-sm font-
|
|
52
|
+
<span className="text-sm font-semibold text-theme-text-primary">Audit Findings</span>
|
|
52
53
|
<div className="flex items-center gap-2 ml-1">
|
|
53
54
|
{dangers > 0 && (
|
|
54
55
|
<span className={clsx('text-xs font-medium tabular-nums', SEVERITY_TEXT.error)}>{dangers} critical</span>
|
|
@@ -64,7 +65,7 @@ export function AuditAlerts({ findings, onViewAll }: AuditAlertsProps) {
|
|
|
64
65
|
style={{ gridTemplateRows: expanded ? '1fr' : '0fr' }}
|
|
65
66
|
>
|
|
66
67
|
<div className="overflow-hidden">
|
|
67
|
-
<div className="
|
|
68
|
+
<div className="flex flex-col gap-0.5 pt-3">
|
|
68
69
|
{findings.map((f, i) => {
|
|
69
70
|
const isDanger = f.severity === 'danger'
|
|
70
71
|
return (
|
|
@@ -74,17 +75,20 @@ export function AuditAlerts({ findings, onViewAll }: AuditAlertsProps) {
|
|
|
74
75
|
) : (
|
|
75
76
|
<AlertTriangle className={clsx('w-3.5 h-3.5 shrink-0 mt-0.5', SEVERITY_TEXT.warning)} />
|
|
76
77
|
)}
|
|
77
|
-
<
|
|
78
|
-
|
|
79
|
-
{f.category}
|
|
80
|
-
|
|
78
|
+
<div className="flex min-w-0 flex-1 flex-wrap items-center gap-x-2 gap-y-1">
|
|
79
|
+
<span className="text-xs text-theme-text-secondary">{f.message}</span>
|
|
80
|
+
<span className={clsx('badge-sm text-[10px] shrink-0', BP_CATEGORY_BADGE[f.category] || DEFAULT_BADGE_COLOR)}>
|
|
81
|
+
{f.category}
|
|
82
|
+
</span>
|
|
83
|
+
</div>
|
|
81
84
|
</div>
|
|
82
85
|
)
|
|
83
86
|
})}
|
|
84
87
|
{onViewAll && (
|
|
85
88
|
<button
|
|
89
|
+
type="button"
|
|
86
90
|
onClick={(e) => { e.stopPropagation(); onViewAll() }}
|
|
87
|
-
className="flex items-center gap-1 text-xs text-
|
|
91
|
+
className="mt-1 flex items-center gap-1 py-1 text-xs text-accent-text transition-colors hover:text-accent"
|
|
88
92
|
>
|
|
89
93
|
View all findings
|
|
90
94
|
<ArrowRight className="w-3 h-3" />
|
|
@@ -93,6 +97,6 @@ export function AuditAlerts({ findings, onViewAll }: AuditAlertsProps) {
|
|
|
93
97
|
</div>
|
|
94
98
|
</div>
|
|
95
99
|
</div>
|
|
96
|
-
</
|
|
100
|
+
</section>
|
|
97
101
|
)
|
|
98
102
|
}
|