@skyhook-io/k8s-ui 1.8.10 → 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/ApplicationsView.tsx +62 -10
- 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 +2 -3
- package/src/components/checks/severity.ts +14 -10
- 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 +3 -3
- 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 +51 -38
- package/src/components/issues/ResourceIssuesSection.test.tsx +6 -5
- package/src/components/issues/ResourceIssuesSection.tsx +28 -146
- package/src/components/issues/index.ts +3 -0
- package/src/components/issues/issue-timing.ts +53 -0
- package/src/components/issues/issues.test.ts +102 -1
- package/src/components/issues/severity.ts +2 -0
- 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 +71 -0
- package/src/components/resources/ResourcesSidebar.tsx +46 -46
- package/src/components/resources/ResourcesView.selected-kind.test.ts +143 -0
- package/src/components/resources/ResourcesView.tsx +233 -51
- 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/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/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/drawer-components.tsx +39 -26
- package/src/components/ui/index.ts +4 -0
- package/src/components/ui/severity-tone.ts +11 -4
- 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 +1 -1
- package/src/types/core.ts +57 -4
- 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
|
@@ -17,6 +17,7 @@ import { useRegisterShortcuts } from '../../hooks/useKeyboardShortcuts'
|
|
|
17
17
|
import { pluralize } from '../../utils/pluralize'
|
|
18
18
|
import {
|
|
19
19
|
type AppEntry,
|
|
20
|
+
type AppRow,
|
|
20
21
|
type AppHealth,
|
|
21
22
|
type AppWorkloadClass,
|
|
22
23
|
type AppSource,
|
|
@@ -36,12 +37,34 @@ import {
|
|
|
36
37
|
isSystemNamespace,
|
|
37
38
|
searchTextForEntry,
|
|
38
39
|
foldAppGroups,
|
|
40
|
+
batchSignalForApp,
|
|
41
|
+
batchRuntimeForApp,
|
|
39
42
|
type FoldedRow,
|
|
40
43
|
} from '../../utils/applications'
|
|
41
44
|
import { ReadyBar } from './ReadyBar'
|
|
42
|
-
import { ProvenanceBadge, ClassBadge, CategoryChip, VersionInfo } from './AppChips'
|
|
45
|
+
import { ProvenanceBadge, ClassBadge, CategoryChip, VersionInfo, BatchSignalChip } from './AppChips'
|
|
43
46
|
import { AppIdentityTooltip, EnvHint } from './AppTooltips'
|
|
44
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
|
+
|
|
45
68
|
// ApplicationsView — the shared, variant-agnostic core behind the Applications
|
|
46
69
|
// surface. It owns the entire list chassis: a health hero header (PageHeader +
|
|
47
70
|
// SummaryTiles + DistributionBar), a left facet rail (Availability / Class /
|
|
@@ -52,7 +75,7 @@ import { AppIdentityTooltip, EnvHint } from './AppTooltips'
|
|
|
52
75
|
// the per-instance row. Styling mirrors the Resources table so the surfaces
|
|
53
76
|
// read as one design.
|
|
54
77
|
|
|
55
|
-
//
|
|
78
|
+
// Status is the one health facet — map app health onto the shared facet
|
|
56
79
|
// tone so its dots read red/amber/green/grey like the GitOps sync+health facets.
|
|
57
80
|
const HEALTH_TONE: Record<AppHealth, FacetTone> = {
|
|
58
81
|
unhealthy: 'error',
|
|
@@ -72,7 +95,7 @@ function compareEntries(a: AppEntry, b: AppEntry, key: SortKey): number {
|
|
|
72
95
|
case 'name':
|
|
73
96
|
return a.row.name.localeCompare(b.row.name)
|
|
74
97
|
case 'ready':
|
|
75
|
-
return a
|
|
98
|
+
return runtimeSortValue(a) - runtimeSortValue(b)
|
|
76
99
|
case 'version': {
|
|
77
100
|
// Sort by distinct-version count first (skewed apps cluster), then the
|
|
78
101
|
// first tag for a stable, human-meaningful order.
|
|
@@ -83,6 +106,10 @@ function compareEntries(a: AppEntry, b: AppEntry, key: SortKey): number {
|
|
|
83
106
|
}
|
|
84
107
|
}
|
|
85
108
|
|
|
109
|
+
function runtimeSortValue(entry: AppEntry): number {
|
|
110
|
+
return entry.workloadClass === 'job' ? (HEALTH_RANK[entry.health] ?? 0) : entry.readyRatio
|
|
111
|
+
}
|
|
112
|
+
|
|
86
113
|
// The env token an entry filters under. Single entries carry one env; fleet
|
|
87
114
|
// entries carry several — a fleet row matches an env facet if ANY of its slices
|
|
88
115
|
// do (the same inclusive policy as the Class facet).
|
|
@@ -348,7 +375,7 @@ export function ApplicationsView({ entries: allEntries, variant, onSelect, title
|
|
|
348
375
|
{initialLoading ? (
|
|
349
376
|
<BoardRailSkeleton
|
|
350
377
|
sections={[
|
|
351
|
-
['
|
|
378
|
+
['Status', 5],
|
|
352
379
|
['Class', 3],
|
|
353
380
|
['Type', 4],
|
|
354
381
|
['Environment', 3],
|
|
@@ -357,7 +384,7 @@ export function ApplicationsView({ entries: allEntries, variant, onSelect, title
|
|
|
357
384
|
/>
|
|
358
385
|
) : (
|
|
359
386
|
<>
|
|
360
|
-
<Facet icon={HeartPulse} title="
|
|
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)} />
|
|
361
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)} />
|
|
362
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)} />
|
|
363
390
|
<Facet icon={Globe} title="Environment" info={<EnvHint />} options={envOptions} selected={fEnv} onToggle={(v) => filters.toggle('env', v)} />
|
|
@@ -430,7 +457,7 @@ export function ApplicationsView({ entries: allEntries, variant, onSelect, title
|
|
|
430
457
|
</>
|
|
431
458
|
)}
|
|
432
459
|
<th className={TH_CLASS}>Class</th>
|
|
433
|
-
<SortableTh label="
|
|
460
|
+
<SortableTh label="Runtime" sortKey="ready" activeKey={sort?.key ?? null} direction={sort?.dir ?? 'asc'} onSort={onSort} />
|
|
434
461
|
<SortableTh label="Version" sortKey="version" activeKey={sort?.key ?? null} direction={sort?.dir ?? 'asc'} onSort={onSort} />
|
|
435
462
|
<th className={TH_CLASS}>Workloads</th>
|
|
436
463
|
<th className={clsx(TH_CLASS, 'w-8')} />
|
|
@@ -452,11 +479,12 @@ export function ApplicationsView({ entries: allEntries, variant, onSelect, title
|
|
|
452
479
|
<span className="flex items-center gap-2">
|
|
453
480
|
{/* Left status stripe (worst-child health) — the row status
|
|
454
481
|
gutter shared with the GitOps table. */}
|
|
455
|
-
<Tooltip content={
|
|
482
|
+
<Tooltip content={applicationRuntimeLabel(r.members.map((member) => member.row), r.workloadClass, r.health)} delay={150}>
|
|
456
483
|
<span className={clsx('h-8 w-1 shrink-0 rounded-full', HEALTH_META[r.health].bar)} />
|
|
457
484
|
</Tooltip>
|
|
458
485
|
<ChevronRight className={clsx('h-3.5 w-3.5 shrink-0 text-theme-text-tertiary transition-transform', r.expanded && 'rotate-90')} aria-hidden />
|
|
459
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))} />}
|
|
460
488
|
<Tooltip
|
|
461
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 }))} />}
|
|
462
490
|
delay={150}
|
|
@@ -504,7 +532,7 @@ export function ApplicationsView({ entries: allEntries, variant, onSelect, title
|
|
|
504
532
|
</span>
|
|
505
533
|
</td>
|
|
506
534
|
<td className="px-2 py-2.5"><ClassBadge workloadClass={r.workloadClass} composition={r.classComposition} /></td>
|
|
507
|
-
<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>
|
|
508
536
|
<td className="px-2 py-2.5">
|
|
509
537
|
{r.lag ? (
|
|
510
538
|
<Tooltip content={`Promotion lag: ${r.lag} (${r.cells.filter((c) => c.version).map((c) => `${c.env}=${c.version}`).join(', ')})`} delay={150}>
|
|
@@ -531,10 +559,11 @@ export function ApplicationsView({ entries: allEntries, variant, onSelect, title
|
|
|
531
559
|
>
|
|
532
560
|
<td className={clsx('py-2.5 pr-2', r.child ? 'pl-10' : 'pl-3')}>
|
|
533
561
|
<span className="flex items-center gap-2">
|
|
534
|
-
<Tooltip content={
|
|
562
|
+
<Tooltip content={applicationRuntimeLabel([e.row], e.workloadClass, e.health)} delay={150}>
|
|
535
563
|
<span className={clsx('h-8 w-1 shrink-0 rounded-full', HEALTH_META[e.health].bar)} />
|
|
536
564
|
</Tooltip>
|
|
537
565
|
<span className="truncate font-medium text-theme-text-primary">{e.row.name}</span>
|
|
566
|
+
{e.workloadClass !== 'job' && <BatchSignalChip signal={batchSignalForApp(e.row)} />}
|
|
538
567
|
<ProvenanceBadge tier={e.row.tier} appKey={e.row.key} confidence={e.row.confidence} />
|
|
539
568
|
<CategoryChip category={e.category} addonReason={e.row.addonReason} />
|
|
540
569
|
</span>
|
|
@@ -601,7 +630,7 @@ export function ApplicationsView({ entries: allEntries, variant, onSelect, title
|
|
|
601
630
|
</>
|
|
602
631
|
)}
|
|
603
632
|
<td className="px-2 py-2.5"><ClassBadge workloadClass={e.workloadClass} composition={e.classComposition} /></td>
|
|
604
|
-
<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>
|
|
605
634
|
<td className="px-2 py-2.5">
|
|
606
635
|
{e.variant === 'fleet' ? (
|
|
607
636
|
e.versionSkew ? (
|
|
@@ -640,3 +669,26 @@ export function ApplicationsView({ entries: allEntries, variant, onSelect, title
|
|
|
640
669
|
</div>
|
|
641
670
|
)
|
|
642
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
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { useEffect, useMemo, useRef, useState, type ComponentType, type ReactNode } from 'react'
|
|
2
2
|
import { createPortal } from 'react-dom'
|
|
3
3
|
import { AlertCircle, AlertOctagon, AlertTriangle, ChevronDown, ChevronRight, ExternalLink, EyeOff, Info, Layers, MoreHorizontal, Search, ShieldCheck, Wrench, X } from 'lucide-react'
|
|
4
|
-
import { CardBody, CardSection, ClusterName, EmptyState, FilterPill, DistributionBar, DistributionLegendChip, NEUTRAL_CHIP_CLASS, renderProse } from '../ui'
|
|
4
|
+
import { CardBody, CardSection, ClusterName, EmptyState, FilterPill, DistributionBar, DistributionLegendChip, Input, NEUTRAL_CHIP_CLASS, renderProse } from '../ui'
|
|
5
5
|
import { useFilterState, defineFilterSchema } from '../../filter-state'
|
|
6
6
|
import type { CheckMeta, CheckReference } from '../audit'
|
|
7
7
|
import { CHECK_SEVERITIES, CHECK_SEVERITY_RANK, type Check, type CheckSeverity, type EffectiveCheckFinding, type CheckResourceRef } from './types'
|
|
@@ -265,8 +265,7 @@ export function ChecksView({ checks, catalog, anyData, resourceHref, onResourceC
|
|
|
265
265
|
</div>
|
|
266
266
|
<div className="relative">
|
|
267
267
|
<Search className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-theme-text-tertiary" />
|
|
268
|
-
<
|
|
269
|
-
type="text"
|
|
268
|
+
<Input
|
|
270
269
|
placeholder="Search checks…"
|
|
271
270
|
value={search}
|
|
272
271
|
onChange={(e) => filters.setString('q', e.target.value)}
|
|
@@ -10,14 +10,15 @@ import {
|
|
|
10
10
|
} from '../ui/severity-tone'
|
|
11
11
|
|
|
12
12
|
// The visual language for the 4-tier Checks severity ladder. One hue per tier:
|
|
13
|
-
// red=critical, orange=high,
|
|
14
|
-
// rail top-to-bottom and severity is obvious without reading a word.
|
|
15
|
-
//
|
|
16
|
-
//
|
|
13
|
+
// red=critical, orange=high, yellow=medium, neutral=low — read the queue's left
|
|
14
|
+
// rail top-to-bottom and severity is obvious without reading a word. Medium is
|
|
15
|
+
// yellow (not amber) so it sits a clear hue-step below high's orange rather than
|
|
16
|
+
// blurring into it. The color strings are shared with the Issue card via the
|
|
17
|
+
// tone module; here we only map each tier onto its tone.
|
|
17
18
|
const CHECK_SEVERITY_TONE: Record<CheckSeverity, SeverityTone> = {
|
|
18
19
|
critical: 'red',
|
|
19
20
|
high: 'orange',
|
|
20
|
-
medium: '
|
|
21
|
+
medium: 'yellow',
|
|
21
22
|
low: 'slate',
|
|
22
23
|
}
|
|
23
24
|
|
|
@@ -35,14 +36,17 @@ export const SEVERITY_LABEL: Record<CheckSeverity, string> = {
|
|
|
35
36
|
low: 'Low',
|
|
36
37
|
}
|
|
37
38
|
|
|
38
|
-
//
|
|
39
|
-
//
|
|
40
|
-
//
|
|
41
|
-
//
|
|
39
|
+
// Soft pill badge — the loud, explicit severity signal on rows + drawer header.
|
|
40
|
+
// Critical/high/low reuse the canonical Badge tones; medium takes an explicit
|
|
41
|
+
// yellow (matching its tone above) rather than the shared amber `warning` tone,
|
|
42
|
+
// so it reads a clear step below high's orange without shifting the amber
|
|
43
|
+
// `warning` badge used elsewhere (Issues, GitOps). yellow-800 on yellow-100
|
|
44
|
+
// clears WCAG for the pill text.
|
|
42
45
|
export const SEVERITY_BADGE_CLASS: Record<CheckSeverity, string> = {
|
|
43
46
|
critical: sev.error,
|
|
44
47
|
high: sev.alert,
|
|
45
|
-
medium:
|
|
48
|
+
medium:
|
|
49
|
+
'bg-yellow-100 text-yellow-800 border-yellow-300 dark:bg-yellow-950/50 dark:text-yellow-400 dark:border-yellow-700/40',
|
|
46
50
|
low: sev.neutral,
|
|
47
51
|
}
|
|
48
52
|
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
} from 'react'
|
|
10
10
|
import { ChevronDown, Check, FolderOpen, Loader2, Search, Server, X } from 'lucide-react'
|
|
11
11
|
import { ClusterName } from '../ui/ClusterName'
|
|
12
|
+
import { Input } from '../ui/Input'
|
|
12
13
|
import { MiddleEllipsis } from '../ui/MiddleEllipsis'
|
|
13
14
|
import { StatusDot, type StatusTone } from '../ui/status-tone'
|
|
14
15
|
|
|
@@ -269,9 +270,8 @@ export const ClusterSwitcher = forwardRef<ClusterSwitcherHandle, ClusterSwitcher
|
|
|
269
270
|
<div className="p-2 border-b border-theme-border">
|
|
270
271
|
<div className="relative">
|
|
271
272
|
<Search className="absolute left-2 top-1/2 -translate-y-1/2 w-3.5 h-3.5 text-theme-text-tertiary" />
|
|
272
|
-
<
|
|
273
|
+
<Input
|
|
273
274
|
ref={searchInputRef}
|
|
274
|
-
type="text"
|
|
275
275
|
value={search}
|
|
276
276
|
onChange={e => setSearch(e.target.value)}
|
|
277
277
|
onKeyDown={onSearchKeyDown}
|
|
@@ -2,6 +2,7 @@ import { useEffect, useMemo, useRef, useState } from 'react'
|
|
|
2
2
|
import { clsx } from 'clsx'
|
|
3
3
|
import { GitCompare, Search, X } from 'lucide-react'
|
|
4
4
|
import { DialogPortal } from '../ui/DialogPortal'
|
|
5
|
+
import { Input } from '../ui/Input'
|
|
5
6
|
import { pluralToKind } from '../../utils/navigation'
|
|
6
7
|
import type { CompareResourceRef } from './ResourceCompareView'
|
|
7
8
|
import { sortCandidates, filterCandidates } from './sort'
|
|
@@ -120,9 +121,8 @@ export function CompareResourcePicker({
|
|
|
120
121
|
</div>
|
|
121
122
|
<div className="relative">
|
|
122
123
|
<Search className="w-4 h-4 absolute left-3 top-1/2 -translate-y-1/2 text-theme-text-tertiary pointer-events-none" />
|
|
123
|
-
<
|
|
124
|
+
<Input
|
|
124
125
|
autoFocus
|
|
125
|
-
type="text"
|
|
126
126
|
value={query}
|
|
127
127
|
onChange={e => setQuery(e.target.value)}
|
|
128
128
|
onKeyDown={handleKeyDown}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { CheckCircle2, CircleDot, GitBranch, HeartPulse, LayoutGrid, List, Search } from 'lucide-react'
|
|
2
2
|
|
|
3
3
|
import { GitOpsFacetButton, GitOpsFilterSection } from './GitOpsTableView'
|
|
4
|
+
import { Input } from '../ui/Input'
|
|
4
5
|
import type { GitOpsResourceTree } from '../../types/gitops-tree'
|
|
5
6
|
import type { GitOpsTreePreset } from './tree'
|
|
6
7
|
|
|
@@ -79,7 +80,7 @@ export function GitOpsGraphFilterRail({
|
|
|
79
80
|
<div className="border-b border-theme-border px-3 py-3">
|
|
80
81
|
<div className="relative">
|
|
81
82
|
<Search className="pointer-events-none absolute left-2.5 top-1/2 h-3.5 w-3.5 -translate-y-1/2 text-theme-text-tertiary" />
|
|
82
|
-
<
|
|
83
|
+
<Input
|
|
83
84
|
value={search}
|
|
84
85
|
onChange={(event) => onSearchChange(event.target.value)}
|
|
85
86
|
placeholder="Filter resources..."
|
|
@@ -26,6 +26,7 @@ import {
|
|
|
26
26
|
|
|
27
27
|
import { HealthStatusBadge, SyncStatusBadge } from './GitOpsStatusBadge'
|
|
28
28
|
import { Tooltip } from '../ui/Tooltip'
|
|
29
|
+
import { Input } from '../ui/Input'
|
|
29
30
|
import { PageHeader } from '../ui/PageHeader'
|
|
30
31
|
import { SummaryTile, type SummaryTone } from '../ui/SummaryTile'
|
|
31
32
|
import { FacetSection, FacetButton } from '../ui/Facet'
|
|
@@ -687,7 +688,7 @@ export function GitOpsTableView({
|
|
|
687
688
|
<div className="mt-3 flex flex-wrap items-center gap-2">
|
|
688
689
|
<div className="relative w-full max-w-md">
|
|
689
690
|
<Search className="pointer-events-none absolute left-2.5 top-1/2 h-3.5 w-3.5 -translate-y-1/2 text-theme-text-tertiary" />
|
|
690
|
-
<
|
|
691
|
+
<Input
|
|
691
692
|
ref={searchInputRef}
|
|
692
693
|
value={search}
|
|
693
694
|
onChange={(e) => filters.setString('q', e.target.value)}
|
|
@@ -1129,8 +1130,7 @@ function LabelsDropdown({
|
|
|
1129
1130
|
<div className="flex items-center gap-2">
|
|
1130
1131
|
<div className="relative flex-1">
|
|
1131
1132
|
<Search className="pointer-events-none absolute left-2 top-1/2 h-3 w-3 -translate-y-1/2 text-theme-text-tertiary" />
|
|
1132
|
-
<
|
|
1133
|
-
type="text"
|
|
1133
|
+
<Input
|
|
1134
1134
|
value={search}
|
|
1135
1135
|
onChange={(e) => onSearchChange(e.target.value)}
|
|
1136
1136
|
placeholder="Search labels..."
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { useState } from 'react'
|
|
2
|
-
import {
|
|
2
|
+
import { Package, ExternalLink } from 'lucide-react'
|
|
3
3
|
import { clsx } from 'clsx'
|
|
4
4
|
import type { ManagedResource, GitOpsHealthStatus } from '../../types/gitops'
|
|
5
5
|
import { groupManagedResourcesByKind } from '../../types/gitops'
|
|
6
6
|
import { pluralize } from '../../utils/pluralize'
|
|
7
|
+
import { Collapse, CollapseChevron } from '../ui/Collapse'
|
|
7
8
|
|
|
8
9
|
interface ManagedResourcesListProps {
|
|
9
10
|
resources: ManagedResource[]
|
|
@@ -45,18 +46,14 @@ export function ManagedResourcesList({
|
|
|
45
46
|
onClick={() => setExpanded(!expanded)}
|
|
46
47
|
className="flex items-center gap-2 w-full text-left mb-2 hover:text-theme-text-primary transition-colors"
|
|
47
48
|
>
|
|
48
|
-
{expanded
|
|
49
|
-
<ChevronDown className="w-4 h-4 text-theme-text-tertiary" />
|
|
50
|
-
) : (
|
|
51
|
-
<ChevronRight className="w-4 h-4 text-theme-text-tertiary" />
|
|
52
|
-
)}
|
|
49
|
+
<CollapseChevron open={expanded} className="w-4 h-4" />
|
|
53
50
|
<Package className="w-4 h-4 text-theme-text-secondary" />
|
|
54
51
|
<span className="text-sm font-medium text-theme-text-secondary">
|
|
55
52
|
{title} ({resources.length})
|
|
56
53
|
</span>
|
|
57
54
|
</button>
|
|
58
55
|
|
|
59
|
-
{expanded
|
|
56
|
+
<Collapse open={expanded} mountLazily>
|
|
60
57
|
<div className="pl-6 space-y-3">
|
|
61
58
|
{sortedGroups.map(([kind, kindResources]) => (
|
|
62
59
|
<ResourceKindGroup
|
|
@@ -77,7 +74,7 @@ export function ManagedResourcesList({
|
|
|
77
74
|
</button>
|
|
78
75
|
)}
|
|
79
76
|
</div>
|
|
80
|
-
|
|
77
|
+
</Collapse>
|
|
81
78
|
</div>
|
|
82
79
|
)
|
|
83
80
|
}
|
|
@@ -98,12 +95,12 @@ function ResourceKindGroup({ kind, resources, onResourceClick, showHealth }: Res
|
|
|
98
95
|
onClick={() => setExpanded(!expanded)}
|
|
99
96
|
className="flex items-center gap-1.5 text-xs text-theme-text-tertiary hover:text-theme-text-secondary transition-colors mb-1"
|
|
100
97
|
>
|
|
101
|
-
|
|
98
|
+
<CollapseChevron open={expanded} className="w-3 h-3" />
|
|
102
99
|
<span className="font-medium">{kind}</span>
|
|
103
100
|
<span className="text-theme-text-tertiary">({resources.length})</span>
|
|
104
101
|
</button>
|
|
105
102
|
|
|
106
|
-
{expanded
|
|
103
|
+
<Collapse open={expanded} mountLazily>
|
|
107
104
|
<div className="ml-4 space-y-0.5">
|
|
108
105
|
{resources.map((resource, idx) => (
|
|
109
106
|
<ResourceItem
|
|
@@ -114,7 +111,7 @@ function ResourceKindGroup({ kind, resources, onResourceClick, showHealth }: Res
|
|
|
114
111
|
/>
|
|
115
112
|
))}
|
|
116
113
|
</div>
|
|
117
|
-
|
|
114
|
+
</Collapse>
|
|
118
115
|
</div>
|
|
119
116
|
)
|
|
120
117
|
}
|
|
@@ -93,23 +93,32 @@ function parseInterval(interval: string): number | null {
|
|
|
93
93
|
if (!interval) return null
|
|
94
94
|
|
|
95
95
|
let totalSeconds = 0
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
96
|
+
let index = 0
|
|
97
|
+
|
|
98
|
+
while (index < interval.length) {
|
|
99
|
+
const start = index
|
|
100
|
+
while (index < interval.length) {
|
|
101
|
+
const code = interval.charCodeAt(index)
|
|
102
|
+
if (code < 48 || code > 57) break
|
|
103
|
+
index += 1
|
|
104
|
+
}
|
|
105
|
+
if (start === index) {
|
|
106
|
+
index += 1
|
|
107
|
+
continue
|
|
108
|
+
}
|
|
109
|
+
const value = parseInt(interval.slice(start, index), 10)
|
|
110
|
+
const unit = interval[index]
|
|
111
|
+
if (unit === 'h') {
|
|
112
|
+
totalSeconds += value * 3600
|
|
113
|
+
index += 1
|
|
114
|
+
} else if (unit === 'm') {
|
|
115
|
+
totalSeconds += value * 60
|
|
116
|
+
index += 1
|
|
117
|
+
} else if (unit === 's') {
|
|
118
|
+
totalSeconds += value
|
|
119
|
+
index += 1
|
|
120
|
+
} else if (index < interval.length) {
|
|
121
|
+
index += 1
|
|
113
122
|
}
|
|
114
123
|
}
|
|
115
124
|
|
|
@@ -2,6 +2,7 @@ import { useState, useEffect, type ComponentType } from 'react'
|
|
|
2
2
|
import { Loader2, RefreshCw } from 'lucide-react'
|
|
3
3
|
|
|
4
4
|
import { DialogPortal } from '../ui/DialogPortal'
|
|
5
|
+
import { Input } from '../ui/Input'
|
|
5
6
|
|
|
6
7
|
// =============================================================================
|
|
7
8
|
// SyncOptionsDialog — Argo CD Sync drawer, shared between per-cluster
|
|
@@ -83,8 +84,7 @@ export function SyncOptionsDialog({ open, appLabel, pending, onCancel, onConfirm
|
|
|
83
84
|
<div className="space-y-4 px-4 py-4 text-sm">
|
|
84
85
|
<label className="block">
|
|
85
86
|
<span className="text-xs font-medium text-theme-text-secondary">Revision (optional)</span>
|
|
86
|
-
<
|
|
87
|
-
type="text"
|
|
87
|
+
<Input
|
|
88
88
|
value={revision}
|
|
89
89
|
onChange={(e) => setRevision(e.target.value)}
|
|
90
90
|
placeholder="HEAD"
|