@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.
Files changed (37) hide show
  1. package/package.json +1 -1
  2. package/src/components/applications/ApplicationsList.tsx +4 -1
  3. package/src/components/applications/ApplicationsView.tsx +73 -38
  4. package/src/components/checks/ChecksView.tsx +105 -63
  5. package/src/components/checks/severity.ts +29 -39
  6. package/src/components/gitops/GitOpsTableView.tsx +72 -43
  7. package/src/components/issues/IssuesView.tsx +186 -123
  8. package/src/components/issues/ResourceIssuesSection.test.tsx +39 -0
  9. package/src/components/issues/ResourceIssuesSection.tsx +21 -3
  10. package/src/components/issues/index.ts +0 -2
  11. package/src/components/issues/issues.test.ts +45 -4
  12. package/src/components/issues/severity.ts +27 -32
  13. package/src/components/resources/ResourcesSidebar.test.tsx +36 -0
  14. package/src/components/resources/ResourcesSidebar.tsx +24 -6
  15. package/src/components/resources/ResourcesView.tsx +10 -2
  16. package/src/components/resources/renderers/MetricsUnavailableNotice.tsx +47 -0
  17. package/src/components/resources/renderers/NodeRenderer.test.tsx +103 -0
  18. package/src/components/resources/renderers/NodeRenderer.tsx +20 -12
  19. package/src/components/resources/renderers/PVCRenderer.test.tsx +23 -0
  20. package/src/components/resources/renderers/PVCRenderer.tsx +2 -2
  21. package/src/components/resources/renderers/PodRenderer.test.tsx +101 -0
  22. package/src/components/resources/renderers/PodRenderer.tsx +81 -70
  23. package/src/components/ui/BoardSkeleton.tsx +47 -0
  24. package/src/components/ui/CardSection.tsx +117 -0
  25. package/src/components/ui/SummaryTile.tsx +9 -1
  26. package/src/components/ui/Toast.tsx +1 -1
  27. package/src/components/ui/index.ts +2 -0
  28. package/src/components/ui/severity-tone.ts +56 -0
  29. package/src/filter-state/filter-state-core.test.ts +98 -0
  30. package/src/filter-state/filter-state-core.ts +138 -0
  31. package/src/filter-state/filter-state.tsx +127 -0
  32. package/src/filter-state/index.ts +16 -0
  33. package/src/index.ts +4 -0
  34. package/src/theme/variables.css +14 -0
  35. package/src/utils/api-resources.test.ts +54 -0
  36. package/src/utils/api-resources.ts +68 -1
  37. 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 { PaneLoader } from '../ui/PaneLoader'
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
- const [search, setSearch] = useState('')
304
- const [syncFilters, setSyncFilters] = useState<Set<string>>(new Set())
305
- const [healthFilters, setHealthFilters] = useState<Set<string>>(new Set())
306
- const [projectFilters, setProjectFilters] = useState<Set<string>>(new Set())
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
- setSearch('')
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: () => setSyncFilters(new Set(['OutOfSync'])),
556
- clear: () => setSyncFilters(new Set()),
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: () => setHealthFilters(new Set(['Degraded'])),
565
- clear: () => setHealthFilters(new Set()),
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: () => setAutomationFilters(new Set(['suspended'])),
574
- clear: () => setAutomationFilters(new Set()),
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: () => setSyncFilters(new Set(['Reconciling'])),
583
- clear: () => setSyncFilters(new Set()),
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) => toggleSet(syncFilters, setSyncFilters, value)}
652
+ onToggleSync={(value) => filters.toggle('sync', value)}
641
653
  healthCounts={healthCounts}
642
654
  healthFilters={healthFilters}
643
- onToggleHealth={(value) => toggleSet(healthFilters, setHealthFilters, 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={setLifecycleFilter}
660
+ onLifecycleFilterChange={(v) => filters.setString('lifecycle', v)}
649
661
  terminatingCount={terminatingCount}
650
662
  projects={projects}
651
663
  projectFilters={projectFilters}
652
- onToggleProject={(value) => toggleSet(projectFilters, setProjectFilters, 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) => setSearch(e.target.value)}
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) => toggleSet(labelFilters, setLabelFilters, value)}
702
- onClear={() => setLabelFilters(new Set())}
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
- ) : loading && filteredRows.length === 0 ? (
758
- <PaneLoader label="Loading GitOps applications…" className="h-full" />
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
  )