@skyhook-io/k8s-ui 1.8.8 → 1.8.10
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/ApplicationsList.tsx +4 -1
- package/src/components/applications/ApplicationsView.tsx +73 -38
- package/src/components/checks/ChecksView.tsx +105 -63
- package/src/components/checks/severity.ts +29 -39
- package/src/components/gitops/GitOpsTableView.tsx +72 -43
- package/src/components/issues/IssuesView.tsx +186 -123
- package/src/components/issues/ResourceIssuesSection.test.tsx +39 -0
- package/src/components/issues/ResourceIssuesSection.tsx +21 -3
- package/src/components/issues/index.ts +0 -2
- package/src/components/issues/issues.test.ts +45 -4
- package/src/components/issues/severity.ts +27 -32
- package/src/components/resources/ResourcesSidebar.test.tsx +36 -0
- package/src/components/resources/ResourcesSidebar.tsx +24 -6
- package/src/components/resources/ResourcesView.tsx +10 -2
- 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/ui/BoardSkeleton.tsx +47 -0
- package/src/components/ui/CardSection.tsx +117 -0
- package/src/components/ui/SummaryTile.tsx +9 -1
- package/src/components/ui/Toast.tsx +1 -1
- package/src/components/ui/index.ts +2 -0
- package/src/components/ui/severity-tone.ts +56 -0
- package/src/filter-state/filter-state-core.test.ts +98 -0
- package/src/filter-state/filter-state-core.ts +138 -0
- package/src/filter-state/filter-state.tsx +127 -0
- package/src/filter-state/index.ts +16 -0
- package/src/index.ts +4 -0
- package/src/theme/variables.css +14 -0
- package/src/utils/api-resources.test.ts +54 -0
- package/src/utils/api-resources.ts +68 -1
- package/tsconfig.json +2 -2
|
@@ -32,10 +32,11 @@ import { FacetSection, FacetButton } from '../ui/Facet'
|
|
|
32
32
|
import { SortableTh, TH_CLASS, type SortDir } from '../ui/SortableTh'
|
|
33
33
|
import { DistributionBar } from '../ui/DistributionBar'
|
|
34
34
|
import { RowActionMenu, type RowActionItem } from '../ui/RowActionMenu'
|
|
35
|
-
import {
|
|
35
|
+
import { BoardTableSkeleton, BoardRailSkeleton } from '../ui/BoardSkeleton'
|
|
36
36
|
import { getGitOpsResourceStatus } from './detail-helpers'
|
|
37
37
|
import { isArgoSuspendedByRadar } from '../resources/resource-utils-argo'
|
|
38
38
|
import { toggleSet } from './GitOpsGraphFilterRail'
|
|
39
|
+
import { useFilterState, defineFilterSchema } from '../../filter-state'
|
|
39
40
|
import { parseContextName } from '../../utils/context-name'
|
|
40
41
|
|
|
41
42
|
// =============================================================================
|
|
@@ -273,6 +274,16 @@ export interface GitOpsTableViewProps {
|
|
|
273
274
|
|
|
274
275
|
// ----- Main component --------------------------------------------------------
|
|
275
276
|
|
|
277
|
+
const GITOPS_FILTER_SCHEMA = defineFilterSchema({
|
|
278
|
+
q: { param: 'q', type: 'text' },
|
|
279
|
+
sync: { param: 'sync', type: 'set' },
|
|
280
|
+
health: { param: 'health', type: 'set' },
|
|
281
|
+
project: { param: 'project', type: 'set' },
|
|
282
|
+
automation: { param: 'automation', type: 'set' },
|
|
283
|
+
labels: { param: 'labels', type: 'set' },
|
|
284
|
+
lifecycle: { param: 'lifecycle', type: 'single', default: 'all' },
|
|
285
|
+
})
|
|
286
|
+
|
|
276
287
|
export function GitOpsTableView({
|
|
277
288
|
rows: allRowsInput,
|
|
278
289
|
loading,
|
|
@@ -300,27 +311,24 @@ export function GitOpsTableView({
|
|
|
300
311
|
const searchInputRef = useRef<HTMLInputElement>(null)
|
|
301
312
|
const [mode, setMode] = useState<GitOpsMode>('applications')
|
|
302
313
|
const [viewMode, setViewMode] = useState<GitOpsViewMode>('table')
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
314
|
+
// Sync / health / project / automation / labels / lifecycle + search live in
|
|
315
|
+
// the URL (shareable, bookmarkable) via the shared filter-state contract.
|
|
316
|
+
// Deliberate divergences kept on their own state: `namespace` (entangled with
|
|
317
|
+
// the host globalNamespaces seam + the header scope pill — a separate product
|
|
318
|
+
// decision), `sort` (a compound view-preference, not a filter), and the
|
|
319
|
+
// destination filter (host-controlled, below).
|
|
320
|
+
const filters = useFilterState(GITOPS_FILTER_SCHEMA)
|
|
321
|
+
const search = filters.values.q
|
|
322
|
+
const syncFilters = filters.values.sync
|
|
323
|
+
const healthFilters = filters.values.health
|
|
324
|
+
const projectFilters = filters.values.project
|
|
325
|
+
const labelFilters = filters.values.labels
|
|
326
|
+
const automationFilters = filters.values.automation as Set<'auto' | 'manual' | 'suspended'>
|
|
327
|
+
const lifecycleFilter = filters.values.lifecycle as 'all' | 'terminating' | 'active'
|
|
328
|
+
const toggleAutomation = useCallback((value: 'auto' | 'manual' | 'suspended') => filters.toggle('automation', value), [filters.toggle])
|
|
307
329
|
const [namespaceFilters, setNamespaceFilters] = useState<Set<string>>(new Set())
|
|
308
|
-
const [labelFilters, setLabelFilters] = useState<Set<string>>(new Set())
|
|
309
330
|
const [showLabelsDropdown, setShowLabelsDropdown] = useState(false)
|
|
310
331
|
const [labelSearch, setLabelSearch] = useState('')
|
|
311
|
-
// Auto-sync / Manual / Suspended are INDEPENDENT row attributes (a Flux app can
|
|
312
|
-
// be auto-reconciling AND suspended), so this is a multi-select facet like Sync
|
|
313
|
-
// and Health — not the old single-select that conflated the mode (auto vs
|
|
314
|
-
// manual) with the orthogonal suspended state.
|
|
315
|
-
const [automationFilters, setAutomationFilters] = useState<Set<'auto' | 'manual' | 'suspended'>>(new Set())
|
|
316
|
-
const toggleAutomation = useCallback((value: 'auto' | 'manual' | 'suspended') => {
|
|
317
|
-
setAutomationFilters((prev) => {
|
|
318
|
-
const next = new Set(prev)
|
|
319
|
-
next.has(value) ? next.delete(value) : next.add(value)
|
|
320
|
-
return next
|
|
321
|
-
})
|
|
322
|
-
}, [])
|
|
323
|
-
const [lifecycleFilter, setLifecycleFilter] = useState<'all' | 'terminating' | 'active'>('all')
|
|
324
332
|
const [sort, setSort] = useState<{ key: SortKey; dir: SortDir } | null>({ key: 'urgency', dir: 'asc' })
|
|
325
333
|
// 3-state cycle: natural direction → reversed → off. The first click uses each
|
|
326
334
|
// column's natural direction (SORT_DEFAULT_DIR — e.g. Last Sync is newest-first)
|
|
@@ -468,16 +476,12 @@ export function GitOpsTableView({
|
|
|
468
476
|
const terminatingCount = useMemo(() => allRows.filter((row) => row.terminating).length, [allRows])
|
|
469
477
|
|
|
470
478
|
const clearAllFilters = useCallback(() => {
|
|
471
|
-
|
|
472
|
-
setSyncFilters(new Set())
|
|
473
|
-
setHealthFilters(new Set())
|
|
474
|
-
setProjectFilters(new Set())
|
|
479
|
+
filters.clearAll()
|
|
475
480
|
setNamespaceFilters(new Set())
|
|
476
|
-
setLabelFilters(new Set())
|
|
477
|
-
setAutomationFilters(new Set())
|
|
478
|
-
setLifecycleFilter('all')
|
|
479
481
|
onClearNamespaces?.()
|
|
480
482
|
onDestinationFilterChange?.('all')
|
|
483
|
+
// filters.clearAll is a stable ref from the hook.
|
|
484
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
481
485
|
}, [onClearNamespaces, onDestinationFilterChange])
|
|
482
486
|
|
|
483
487
|
// True when nothing is filtered at all — backs the Total tile's active state.
|
|
@@ -531,6 +535,12 @@ export function GitOpsTableView({
|
|
|
531
535
|
|
|
532
536
|
const showCrossClusterTile = typeof crossClusterCount === 'number' && mode === 'applications'
|
|
533
537
|
|
|
538
|
+
// First fetch, nothing to show yet. Drives the shape-stable loading
|
|
539
|
+
// treatment: tiles pulse instead of reading "0" (a false zero), the table
|
|
540
|
+
// area renders skeleton rows, and the filter rail holds its loaded height
|
|
541
|
+
// with section stubs — so the settled layout doesn't reflow into place.
|
|
542
|
+
const initialLoading = Boolean(loading) && allRowsInput.length === 0
|
|
543
|
+
|
|
534
544
|
// Header tiles unify with the facet rail: each STATUS tile toggles its own
|
|
535
545
|
// dimension and composes with the other facets + search (clicking "Out of
|
|
536
546
|
// sync" adds sync=OutOfSync without wiping an active health filter or your
|
|
@@ -552,8 +562,8 @@ export function GitOpsTableView({
|
|
|
552
562
|
value: statusSummary.outOfSync,
|
|
553
563
|
tone: 'warning',
|
|
554
564
|
active: syncFilters.size === 1 && syncFilters.has('OutOfSync'),
|
|
555
|
-
apply: () =>
|
|
556
|
-
clear: () =>
|
|
565
|
+
apply: () => filters.setSet('sync', ['OutOfSync']),
|
|
566
|
+
clear: () => filters.setSet('sync', []),
|
|
557
567
|
},
|
|
558
568
|
{
|
|
559
569
|
key: 'degraded',
|
|
@@ -561,8 +571,8 @@ export function GitOpsTableView({
|
|
|
561
571
|
value: statusSummary.degraded,
|
|
562
572
|
tone: 'error',
|
|
563
573
|
active: healthFilters.size === 1 && healthFilters.has('Degraded'),
|
|
564
|
-
apply: () =>
|
|
565
|
-
clear: () =>
|
|
574
|
+
apply: () => filters.setSet('health', ['Degraded']),
|
|
575
|
+
clear: () => filters.setSet('health', []),
|
|
566
576
|
},
|
|
567
577
|
{
|
|
568
578
|
key: 'suspended',
|
|
@@ -570,8 +580,8 @@ export function GitOpsTableView({
|
|
|
570
580
|
value: statusSummary.suspended,
|
|
571
581
|
tone: 'warning',
|
|
572
582
|
active: automationFilters.size === 1 && automationFilters.has('suspended'),
|
|
573
|
-
apply: () =>
|
|
574
|
-
clear: () =>
|
|
583
|
+
apply: () => filters.setSet('automation', ['suspended']),
|
|
584
|
+
clear: () => filters.setSet('automation', []),
|
|
575
585
|
},
|
|
576
586
|
{
|
|
577
587
|
key: 'reconciling',
|
|
@@ -579,8 +589,8 @@ export function GitOpsTableView({
|
|
|
579
589
|
value: syncCounts.get('Reconciling') ?? 0,
|
|
580
590
|
tone: 'info',
|
|
581
591
|
active: syncFilters.size === 1 && syncFilters.has('Reconciling'),
|
|
582
|
-
apply: () =>
|
|
583
|
-
clear: () =>
|
|
592
|
+
apply: () => filters.setSet('sync', ['Reconciling']),
|
|
593
|
+
clear: () => filters.setSet('sync', []),
|
|
584
594
|
},
|
|
585
595
|
...(showCrossClusterTile
|
|
586
596
|
? [
|
|
@@ -615,6 +625,7 @@ export function GitOpsTableView({
|
|
|
615
625
|
value={tile.value}
|
|
616
626
|
tone={tile.tone}
|
|
617
627
|
active={tile.active}
|
|
628
|
+
loading={initialLoading}
|
|
618
629
|
onClick={() => {
|
|
619
630
|
if (tile.active) tile.clear?.()
|
|
620
631
|
else tile.apply?.()
|
|
@@ -631,25 +642,26 @@ export function GitOpsTableView({
|
|
|
631
642
|
}`}
|
|
632
643
|
>
|
|
633
644
|
<GitOpsFilterSidebar
|
|
645
|
+
loading={initialLoading}
|
|
634
646
|
side={filtersSide}
|
|
635
647
|
mode={mode}
|
|
636
648
|
onModeChange={setMode}
|
|
637
649
|
modeCounts={modeCounts}
|
|
638
650
|
syncCounts={syncCounts}
|
|
639
651
|
syncFilters={syncFilters}
|
|
640
|
-
onToggleSync={(value) =>
|
|
652
|
+
onToggleSync={(value) => filters.toggle('sync', value)}
|
|
641
653
|
healthCounts={healthCounts}
|
|
642
654
|
healthFilters={healthFilters}
|
|
643
|
-
onToggleHealth={(value) =>
|
|
655
|
+
onToggleHealth={(value) => filters.toggle('health', value)}
|
|
644
656
|
automationFilters={automationFilters}
|
|
645
657
|
automationCounts={automationCounts}
|
|
646
658
|
onToggleAutomation={toggleAutomation}
|
|
647
659
|
lifecycleFilter={lifecycleFilter}
|
|
648
|
-
onLifecycleFilterChange={
|
|
660
|
+
onLifecycleFilterChange={(v) => filters.setString('lifecycle', v)}
|
|
649
661
|
terminatingCount={terminatingCount}
|
|
650
662
|
projects={projects}
|
|
651
663
|
projectFilters={projectFilters}
|
|
652
|
-
onToggleProject={(value) =>
|
|
664
|
+
onToggleProject={(value) => filters.toggle('project', value)}
|
|
653
665
|
namespaces={rowNamespaces}
|
|
654
666
|
namespaceFilters={namespaceFilters}
|
|
655
667
|
onToggleNamespace={(value) => toggleSet(namespaceFilters, setNamespaceFilters, value)}
|
|
@@ -678,7 +690,7 @@ export function GitOpsTableView({
|
|
|
678
690
|
<input
|
|
679
691
|
ref={searchInputRef}
|
|
680
692
|
value={search}
|
|
681
|
-
onChange={(e) =>
|
|
693
|
+
onChange={(e) => filters.setString('q', e.target.value)}
|
|
682
694
|
placeholder="Search applications, repos, paths..."
|
|
683
695
|
className="h-8 w-full rounded-md border border-theme-border bg-theme-base pl-8 pr-3 text-sm text-theme-text-primary placeholder:text-theme-text-tertiary focus:outline-none focus:ring-1 focus:ring-blue-500/50"
|
|
684
696
|
/>
|
|
@@ -698,8 +710,8 @@ export function GitOpsTableView({
|
|
|
698
710
|
<LabelsDropdown
|
|
699
711
|
labels={labels}
|
|
700
712
|
activeLabels={labelFilters}
|
|
701
|
-
onToggle={(value) =>
|
|
702
|
-
onClear={() =>
|
|
713
|
+
onToggle={(value) => filters.toggle('labels', value)}
|
|
714
|
+
onClear={() => filters.setSet('labels', [])}
|
|
703
715
|
open={showLabelsDropdown}
|
|
704
716
|
onOpenChange={setShowLabelsDropdown}
|
|
705
717
|
search={labelSearch}
|
|
@@ -754,8 +766,8 @@ export function GitOpsTableView({
|
|
|
754
766
|
<div className="flex h-full items-center justify-center text-sm text-theme-text-secondary">
|
|
755
767
|
{modeLabel(mode)} view is queued behind the application list.
|
|
756
768
|
</div>
|
|
757
|
-
) :
|
|
758
|
-
<
|
|
769
|
+
) : initialLoading ? (
|
|
770
|
+
<BoardTableSkeleton />
|
|
759
771
|
) : error ? (
|
|
760
772
|
<div className="p-4 text-sm text-red-500">Failed to load GitOps applications: {error.message}</div>
|
|
761
773
|
) : filteredRows.length === 0 ? (
|
|
@@ -806,6 +818,7 @@ export function GitOpsTableView({
|
|
|
806
818
|
// =============================================================================
|
|
807
819
|
|
|
808
820
|
function GitOpsFilterSidebar({
|
|
821
|
+
loading,
|
|
809
822
|
side,
|
|
810
823
|
mode,
|
|
811
824
|
onModeChange,
|
|
@@ -853,6 +866,8 @@ function GitOpsFilterSidebar({
|
|
|
853
866
|
namespaceFilters: Set<string>
|
|
854
867
|
onToggleNamespace: (value: string) => void
|
|
855
868
|
onClear: () => void
|
|
869
|
+
/** Initial fetch in flight — hold the rail's shape with section stubs. */
|
|
870
|
+
loading?: boolean
|
|
856
871
|
}) {
|
|
857
872
|
return (
|
|
858
873
|
<aside
|
|
@@ -867,6 +882,18 @@ function GitOpsFilterSidebar({
|
|
|
867
882
|
</button>
|
|
868
883
|
</div>
|
|
869
884
|
<div className="flex-1 overflow-y-auto">
|
|
885
|
+
{loading ? (
|
|
886
|
+
<BoardRailSkeleton
|
|
887
|
+
sections={[
|
|
888
|
+
['Sync', 4],
|
|
889
|
+
['Health', 5],
|
|
890
|
+
['Automation', 3],
|
|
891
|
+
['Projects', 4],
|
|
892
|
+
['Namespaces', 4],
|
|
893
|
+
]}
|
|
894
|
+
/>
|
|
895
|
+
) : (
|
|
896
|
+
<>
|
|
870
897
|
{AVAILABLE_MODES.length > 1 && (
|
|
871
898
|
<GitOpsFilterSection icon={GitBranch} title="Scope">
|
|
872
899
|
<div className="grid grid-cols-2 gap-1">
|
|
@@ -960,6 +987,8 @@ function GitOpsFilterSidebar({
|
|
|
960
987
|
/>
|
|
961
988
|
))}
|
|
962
989
|
</GitOpsFilterSection>
|
|
990
|
+
</>
|
|
991
|
+
)}
|
|
963
992
|
</div>
|
|
964
993
|
</aside>
|
|
965
994
|
)
|