From 208534c9d9a97ef059af221431e6c773f71693b1 Mon Sep 17 00:00:00 2001 From: Cara Ryan Date: Tue, 12 Aug 2025 10:23:27 +1200 Subject: [PATCH] fix(helm): helm apps do not combine in applications view if different namespace [R8S-420] (#988) --- .../ApplicationsDatatable.test.tsx | 161 ++++++++++++++++++ .../ApplicationsDatatable.tsx | 10 +- 2 files changed, 168 insertions(+), 3 deletions(-) create mode 100644 app/react/kubernetes/applications/ListView/ApplicationsDatatable/ApplicationsDatatable.test.tsx diff --git a/app/react/kubernetes/applications/ListView/ApplicationsDatatable/ApplicationsDatatable.test.tsx b/app/react/kubernetes/applications/ListView/ApplicationsDatatable/ApplicationsDatatable.test.tsx new file mode 100644 index 000000000..3c64708c8 --- /dev/null +++ b/app/react/kubernetes/applications/ListView/ApplicationsDatatable/ApplicationsDatatable.test.tsx @@ -0,0 +1,161 @@ +import { render, screen } from '@testing-library/react'; +import { vi } from 'vitest'; + +import { withTestQueryProvider } from '@/react/test-utils/withTestQuery'; +import { withTestRouter } from '@/react/test-utils/withRouter'; +import { UserViewModel } from '@/portainer/models/user'; +import { withUserProvider } from '@/react/test-utils/withUserProvider'; + +import { PodKubernetesInstanceLabel, PodManagedByLabel } from '../../constants'; + +import { ApplicationsDatatable } from './ApplicationsDatatable'; + +const mockUseCurrentStateAndParams = vi.fn(); +const mockUseEnvironmentId = vi.fn(); + +vi.mock('@uirouter/react', async (importOriginal: () => Promise) => ({ + ...(await importOriginal()), + useCurrentStateAndParams: () => mockUseCurrentStateAndParams(), +})); + +vi.mock('@/react/hooks/useEnvironmentId', () => ({ + useEnvironmentId: () => mockUseEnvironmentId(), +})); + +vi.mock('@/react/kubernetes/applications/queries/useApplications', () => ({ + useApplications: () => ({ + data: [ + { + Id: '1', + Name: 'app1', + CreationDate: '2021-10-01T00:00:00Z', + ResourcePool: 'namespace1', + Image: 'image1', + ApplicationType: 'Pod', + Kind: 'Pod', + DeploymentType: 'Replicated', + Status: 'status1', + TotalPodsCount: 1, + RunningPodsCount: 1, + Metadata: { + labels: { + [PodKubernetesInstanceLabel]: 'helm-release-1', + [PodManagedByLabel]: 'Helm', + }, + }, + }, + { + Id: '2', + Name: 'app2', + CreationDate: '2021-10-01T00:00:00Z', + ResourcePool: 'namespace1', + Image: 'image1', + ApplicationType: 'Pod', + Kind: 'Pod', + DeploymentType: 'Replicated', + Status: 'status1', + TotalPodsCount: 1, + RunningPodsCount: 1, + Metadata: { + labels: { + [PodKubernetesInstanceLabel]: 'helm-release-1', + [PodManagedByLabel]: 'Helm', + }, + }, + }, + { + Id: '3', + Name: 'app3', + CreationDate: '2021-10-01T00:00:00Z', + ResourcePool: 'namespace2', + Image: 'image1', + ApplicationType: 'Pod', + Kind: 'Pod', + DeploymentType: 'Replicated', + Status: 'status1', + TotalPodsCount: 1, + RunningPodsCount: 1, + Metadata: { + labels: { + [PodKubernetesInstanceLabel]: 'helm-release-1', + [PodManagedByLabel]: 'Helm', + }, + }, + }, + ], + isLoading: false, + }), +})); + +vi.mock('@@/Link', () => ({ + Link: ({ children }: { children: React.ReactNode }) => ( + {children} + ), +})); + +vi.mock('@/react/kubernetes/components/CreateFromManifestButton', () => ({ + CreateFromManifestButton: ({ + children, + ...props + }: { + children?: React.ReactNode; + 'data-cy'?: string; + }) => ( + + ), +})); + +function renderComponent() { + const user = new UserViewModel({ Username: 'user' }); + + const Wrapped = withTestQueryProvider( + withUserProvider(withTestRouter(ApplicationsDatatable), user) + ); + + return render( + {}, + namespace: '', + setNamespace: () => {}, + showSystemResources: false, + autoRefreshRate: 0, + setAutoRefreshRate: () => {}, + setShowSystemResources: () => {}, + sortBy: { id: 'Name', desc: false }, + setSortBy: () => {}, + pageSize: 10, + setPageSize: () => {}, + }} + /> + ); +} + +describe('ApplicationsDatatable', () => { + beforeEach(() => { + mockUseEnvironmentId.mockReturnValue(3); + mockUseCurrentStateAndParams.mockReturnValue({ + params: {}, + }); + }); + + it('should group helm apps by namespace and instance label', async () => { + renderComponent(); + + const helmReleases = await screen.findAllByText('helm-release-1'); + expect(helmReleases).toHaveLength(2); + + // Should show both namespaces in table cells + const namespace1Cells = await screen.findAllByRole('cell', { + name: 'namespace1', + }); + const namespace2Cells = await screen.findAllByRole('cell', { + name: 'namespace2', + }); + expect(namespace1Cells.length).toBeGreaterThan(0); + expect(namespace2Cells.length).toBeGreaterThan(0); + }); +}); diff --git a/app/react/kubernetes/applications/ListView/ApplicationsDatatable/ApplicationsDatatable.tsx b/app/react/kubernetes/applications/ListView/ApplicationsDatatable/ApplicationsDatatable.tsx index 091e2f97e..038d3bd82 100644 --- a/app/react/kubernetes/applications/ListView/ApplicationsDatatable/ApplicationsDatatable.tsx +++ b/app/react/kubernetes/applications/ListView/ApplicationsDatatable/ApplicationsDatatable.tsx @@ -170,14 +170,18 @@ function separateHelmApps(applications: Application[]): ApplicationRowData[] { const groupedHelmApps: Record = groupBy( helmApps, - (app) => app.Metadata?.labels[PodKubernetesInstanceLabel] ?? '' + (app) => + `${app.ResourcePool}/${ + app.Metadata?.labels[PodKubernetesInstanceLabel] ?? '' + }` ); // build the helm apps row data from the grouped helm apps const helmAppsRowData = Object.entries(groupedHelmApps).reduce< ApplicationRowData[] - >((helmApps, [appName, apps]) => { - const helmApp = buildHelmAppRowData(appName, apps); + >((helmApps, [groupKey, apps]) => { + const instanceLabel = groupKey.split('/')[1]; + const helmApp = buildHelmAppRowData(instanceLabel, apps); return [...helmApps, helmApp]; }, []);