From 61d6ac035d22e258007cbf070bbd87bff76b76c8 Mon Sep 17 00:00:00 2001 From: Ali <83188384+testA113@users.noreply.github.com> Date: Wed, 23 Apr 2025 08:58:21 +1200 Subject: [PATCH] feat(helm): auto refresh helm resources [r8s-298] (#672) --- .../components/datatables/Datatable.test.tsx | 16 +++--- app/react/components/datatables/types.ts | 4 ++ .../ListView/ConfigsDatatable/store.ts | 17 +++--- .../ApplicationsStacksDatatable/types.ts | 12 ----- .../ReleaseDetails/ReleaseTabs.tsx | 2 +- .../ResourcesTable/ResourcesTable.test.tsx | 38 +++++++++++++- .../ResourcesTable/ResourcesTable.tsx | 52 +++++++++++++++---- .../ResourcesTable/columns/name.tsx | 14 +++-- .../ResourcesTable/useResourceRows.ts | 8 ++- .../queries/useHelmRelease.ts | 7 ++- 10 files changed, 120 insertions(+), 50 deletions(-) diff --git a/app/react/components/datatables/Datatable.test.tsx b/app/react/components/datatables/Datatable.test.tsx index 67ff85f70..a40822848 100644 --- a/app/react/components/datatables/Datatable.test.tsx +++ b/app/react/components/datatables/Datatable.test.tsx @@ -9,10 +9,9 @@ import { import { Datatable, defaultGlobalFilterFn, Props } from './Datatable'; import { - BasicTableSettings, createPersistedStore, refreshableSettings, - RefreshableTableSettings, + TableSettingsWithRefreshable, } from './types'; import { useTableState } from './useTableState'; @@ -30,13 +29,14 @@ const mockColumns = [ ]; // mock table settings / state -export interface TableSettings - extends BasicTableSettings, - RefreshableTableSettings {} function createStore(storageKey: string) { - return createPersistedStore(storageKey, 'name', (set) => ({ - ...refreshableSettings(set), - })); + return createPersistedStore( + storageKey, + 'name', + (set) => ({ + ...refreshableSettings(set), + }) + ); } const storageKey = 'test-table'; const settingsStore = createStore(storageKey); diff --git a/app/react/components/datatables/types.ts b/app/react/components/datatables/types.ts index 11dcac867..45264f337 100644 --- a/app/react/components/datatables/types.ts +++ b/app/react/components/datatables/types.ts @@ -99,6 +99,10 @@ export interface BasicTableSettings extends SortableTableSettings, PaginationTableSettings {} +export interface TableSettingsWithRefreshable + extends BasicTableSettings, + RefreshableTableSettings {} + export function createPersistedStore( storageKey: string, initialSortBy?: string | { id: string; desc: boolean }, diff --git a/app/react/docker/configs/ListView/ConfigsDatatable/store.ts b/app/react/docker/configs/ListView/ConfigsDatatable/store.ts index 66672630a..95a649f89 100644 --- a/app/react/docker/configs/ListView/ConfigsDatatable/store.ts +++ b/app/react/docker/configs/ListView/ConfigsDatatable/store.ts @@ -1,16 +1,15 @@ import { - BasicTableSettings, createPersistedStore, refreshableSettings, - RefreshableTableSettings, + TableSettingsWithRefreshable, } from '@@/datatables/types'; -export interface TableSettings - extends BasicTableSettings, - RefreshableTableSettings {} - export function createStore(storageKey: string) { - return createPersistedStore(storageKey, 'name', (set) => ({ - ...refreshableSettings(set), - })); + return createPersistedStore( + storageKey, + 'name', + (set) => ({ + ...refreshableSettings(set), + }) + ); } diff --git a/app/react/kubernetes/applications/ListView/ApplicationsStacksDatatable/types.ts b/app/react/kubernetes/applications/ListView/ApplicationsStacksDatatable/types.ts index 5f6f4736b..75f0a882e 100644 --- a/app/react/kubernetes/applications/ListView/ApplicationsStacksDatatable/types.ts +++ b/app/react/kubernetes/applications/ListView/ApplicationsStacksDatatable/types.ts @@ -1,17 +1,5 @@ -import { SystemResourcesTableSettings } from '@/react/kubernetes/datatables/SystemResourcesSettings'; - -import { - BasicTableSettings, - RefreshableTableSettings, -} from '@@/datatables/types'; - import { Application } from '../ApplicationsDatatable/types'; -export interface TableSettings - extends BasicTableSettings, - RefreshableTableSettings, - SystemResourcesTableSettings {} - export type Stack = { Name: string; ResourcePool: string; diff --git a/app/react/kubernetes/helm/HelmApplicationView/ReleaseDetails/ReleaseTabs.tsx b/app/react/kubernetes/helm/HelmApplicationView/ReleaseDetails/ReleaseTabs.tsx index 6a954d1fc..efa36c0c2 100644 --- a/app/react/kubernetes/helm/HelmApplicationView/ReleaseDetails/ReleaseTabs.tsx +++ b/app/react/kubernetes/helm/HelmApplicationView/ReleaseDetails/ReleaseTabs.tsx @@ -25,7 +25,7 @@ function helmTabs( { label: 'Resources', id: 'resources', - children: , + children: , }, { label: 'Values', diff --git a/app/react/kubernetes/helm/HelmApplicationView/ReleaseDetails/ResourcesTable/ResourcesTable.test.tsx b/app/react/kubernetes/helm/HelmApplicationView/ReleaseDetails/ResourcesTable/ResourcesTable.test.tsx index cf0dfde71..113776344 100644 --- a/app/react/kubernetes/helm/HelmApplicationView/ReleaseDetails/ResourcesTable/ResourcesTable.test.tsx +++ b/app/react/kubernetes/helm/HelmApplicationView/ReleaseDetails/ResourcesTable/ResourcesTable.test.tsx @@ -8,6 +8,24 @@ import { GenericResource } from '../../../types'; import { ResourcesTable } from './ResourcesTable'; +// Mock the necessary hooks +const mockUseCurrentStateAndParams = vi.fn(); +const mockUseEnvironmentId = vi.fn(); +const mockUseHelmRelease = vi.fn(); + +vi.mock('@uirouter/react', async (importOriginal: () => Promise) => ({ + ...(await importOriginal()), + useCurrentStateAndParams: () => mockUseCurrentStateAndParams(), +})); + +vi.mock('@/react/hooks/useEnvironmentId', () => ({ + useEnvironmentId: () => mockUseEnvironmentId(), +})); + +vi.mock('../../queries/useHelmRelease', () => ({ + useHelmRelease: () => mockUseHelmRelease(), +})); + const successResources = [ { kind: 'ValidatingWebhookConfiguration', @@ -108,8 +126,26 @@ const failedResources = [ ]; function renderResourcesTable(resources: GenericResource[]) { + // Setup mock return values + mockUseEnvironmentId.mockReturnValue(3); + mockUseCurrentStateAndParams.mockReturnValue({ + params: { + name: 'test-release', + namespace: 'default', + }, + }); + mockUseHelmRelease.mockReturnValue({ + data: { + info: { + resources, + }, + }, + isLoading: false, + error: null, + }); + const Wrapped = withTestQueryProvider(withTestRouter(ResourcesTable)); - return render(); + return render(); } afterEach(() => { diff --git a/app/react/kubernetes/helm/HelmApplicationView/ReleaseDetails/ResourcesTable/ResourcesTable.tsx b/app/react/kubernetes/helm/HelmApplicationView/ReleaseDetails/ResourcesTable/ResourcesTable.tsx index c8922ca04..6a3b35674 100644 --- a/app/react/kubernetes/helm/HelmApplicationView/ReleaseDetails/ResourcesTable/ResourcesTable.tsx +++ b/app/react/kubernetes/helm/HelmApplicationView/ReleaseDetails/ResourcesTable/ResourcesTable.tsx @@ -1,23 +1,47 @@ -import { Datatable } from '@@/datatables'; -import { createPersistedStore } from '@@/datatables/types'; +import { useCurrentStateAndParams } from '@uirouter/react'; + +import { useEnvironmentId } from '@/react/hooks/useEnvironmentId'; + +import { Datatable, TableSettingsMenu } from '@@/datatables'; +import { + createPersistedStore, + refreshableSettings, + TableSettingsWithRefreshable, +} from '@@/datatables/types'; import { useTableState } from '@@/datatables/useTableState'; import { Widget } from '@@/Widget'; +import { TableSettingsMenuAutoRefresh } from '@@/datatables/TableSettingsMenuAutoRefresh'; -import { GenericResource } from '../../../types'; +import { useHelmRelease } from '../../queries/useHelmRelease'; import { columns } from './columns'; import { useResourceRows } from './useResourceRows'; -type Props = { - resources: GenericResource[]; -}; - const storageKey = 'helm-resources'; -const settingsStore = createPersistedStore(storageKey, 'resourceType'); -export function ResourcesTable({ resources }: Props) { +export function createStore(storageKey: string) { + return createPersistedStore( + storageKey, + 'name', + (set) => ({ + ...refreshableSettings(set), + }) + ); +} + +const settingsStore = createStore('helm-resources'); + +export function ResourcesTable() { + const environmentId = useEnvironmentId(); + const { params } = useCurrentStateAndParams(); + const { name, namespace } = params; + const tableState = useTableState(settingsStore, storageKey); - const rows = useResourceRows(resources); + const helmReleaseQuery = useHelmRelease(environmentId, name, namespace, { + showResources: true, + refetchInterval: tableState.autoRefreshRate * 1000, + }); + const rows = useResourceRows(helmReleaseQuery.data?.info?.resources); return ( @@ -32,6 +56,14 @@ export function ResourcesTable({ resources }: Props) { disableSelect getRowId={(row) => row.id} data-cy="helm-resources-datatable" + renderTableSettings={() => ( + + tableState.setAutoRefreshRate(value)} + /> + + )} /> ); diff --git a/app/react/kubernetes/helm/HelmApplicationView/ReleaseDetails/ResourcesTable/columns/name.tsx b/app/react/kubernetes/helm/HelmApplicationView/ReleaseDetails/ResourcesTable/columns/name.tsx index 332e49f1c..0b8c2aaae 100644 --- a/app/react/kubernetes/helm/HelmApplicationView/ReleaseDetails/ResourcesTable/columns/name.tsx +++ b/app/react/kubernetes/helm/HelmApplicationView/ReleaseDetails/ResourcesTable/columns/name.tsx @@ -6,11 +6,15 @@ import { ResourceRow } from '../types'; import { columnHelper } from './helper'; -export const name = columnHelper.accessor((row) => row.name.label, { - header: 'Name', - cell: Cell, - id: 'name', -}); +// `${row.name.label}/${row.resourceType}` reduces shuffling when the name is the same but the resourceType is different +export const name = columnHelper.accessor( + (row) => `${row.name.label}/${row.resourceType}`, + { + header: 'Name', + cell: Cell, + id: 'name', + } +); function Cell({ row }: CellContext) { const { name } = row.original; diff --git a/app/react/kubernetes/helm/HelmApplicationView/ReleaseDetails/ResourcesTable/useResourceRows.ts b/app/react/kubernetes/helm/HelmApplicationView/ReleaseDetails/ResourcesTable/useResourceRows.ts index e5301dc05..ac90271f8 100644 --- a/app/react/kubernetes/helm/HelmApplicationView/ReleaseDetails/ResourcesTable/useResourceRows.ts +++ b/app/react/kubernetes/helm/HelmApplicationView/ReleaseDetails/ResourcesTable/useResourceRows.ts @@ -27,11 +27,15 @@ const statusToColorMap: Record = { Unknown: 'mutedLite', }; -export function useResourceRows(resources: GenericResource[]): ResourceRow[] { +export function useResourceRows(resources?: GenericResource[]): ResourceRow[] { return useMemo(() => getResourceRows(resources), [resources]); } -function getResourceRows(resources: GenericResource[]): ResourceRow[] { +function getResourceRows(resources?: GenericResource[]): ResourceRow[] { + if (!resources) { + return []; + } + return resources.map(getResourceRow); } diff --git a/app/react/kubernetes/helm/HelmApplicationView/queries/useHelmRelease.ts b/app/react/kubernetes/helm/HelmApplicationView/queries/useHelmRelease.ts index f503aec7f..8d41e6544 100644 --- a/app/react/kubernetes/helm/HelmApplicationView/queries/useHelmRelease.ts +++ b/app/react/kubernetes/helm/HelmApplicationView/queries/useHelmRelease.ts @@ -16,19 +16,22 @@ export function useHelmRelease( options: { select?: (data: HelmRelease) => T; showResources?: boolean; + refetchInterval?: number; } = {} ) { + const { select, showResources, refetchInterval } = options; return useQuery( [environmentId, 'helm', 'releases', namespace, name, options.showResources], () => getHelmRelease(environmentId, name, { namespace, - showResources: options.showResources, + showResources, }), { enabled: !!environmentId && !!name && !!namespace, ...withGlobalError('Unable to retrieve helm application details'), - select: options.select, + select, + refetchInterval, } ); }