From 57e10dc911ce4d9b047a48827b7c918cf8ab1934 Mon Sep 17 00:00:00 2001 From: Ali <83188384+testA113@users.noreply.github.com> Date: Mon, 14 Oct 2024 10:28:56 +1300 Subject: [PATCH] fix(apps): group helm apps together [r8s-102] (#24) --- app/kubernetes/helpers/application/index.js | 4 +- .../ApplicationsDatatable.tsx | 78 ++++++++++++++++++- .../ListView/ApplicationsDatatable/SubRow.tsx | 7 +- .../ApplicationsDatatable/columns.helper.tsx | 4 +- .../ApplicationsDatatable/columns.name.tsx | 6 +- .../ApplicationsDatatable/columns.status.tsx | 6 +- .../ListView/ApplicationsDatatable/types.ts | 5 +- .../kubernetes/applications/constants.ts | 2 + 8 files changed, 95 insertions(+), 17 deletions(-) diff --git a/app/kubernetes/helpers/application/index.js b/app/kubernetes/helpers/application/index.js index c01e18192..987560d76 100644 --- a/app/kubernetes/helpers/application/index.js +++ b/app/kubernetes/helpers/application/index.js @@ -31,9 +31,7 @@ import { KubernetesPodNodeAffinityPayload, KubernetesPreferredSchedulingTermPayload, } from 'Kubernetes/pod/payloads/affinities'; - -export const PodKubernetesInstanceLabel = 'app.kubernetes.io/instance'; -export const PodManagedByLabel = 'app.kubernetes.io/managed-by'; +import { PodKubernetesInstanceLabel, PodManagedByLabel } from '@/react/kubernetes/applications/constants'; class KubernetesApplicationHelper { /* #region UTILITY FUNCTIONS */ diff --git a/app/react/kubernetes/applications/ListView/ApplicationsDatatable/ApplicationsDatatable.tsx b/app/react/kubernetes/applications/ListView/ApplicationsDatatable/ApplicationsDatatable.tsx index bedcadc38..7d926b002 100644 --- a/app/react/kubernetes/applications/ListView/ApplicationsDatatable/ApplicationsDatatable.tsx +++ b/app/react/kubernetes/applications/ListView/ApplicationsDatatable/ApplicationsDatatable.tsx @@ -1,5 +1,6 @@ -import { useEffect } from 'react'; +import { useEffect, useMemo } from 'react'; import { BoxIcon } from 'lucide-react'; +import { groupBy, partition } from 'lodash'; import { useKubeStore } from '@/react/kubernetes/datatables/default-kube-datatable-store'; import { DefaultDatatableSettings } from '@/react/kubernetes/datatables/DefaultDatatableSettings'; @@ -10,6 +11,7 @@ import { useEnvironmentId } from '@/react/hooks/useEnvironmentId'; import { useCurrentEnvironment } from '@/react/hooks/useCurrentEnvironment'; import { useAuthorizations } from '@/react/hooks/useUser'; import { isSystemNamespace } from '@/react/kubernetes/namespaces/queries/useIsSystemNamespace'; +import { KubernetesApplicationTypes } from '@/kubernetes/models/application/models/appConstants'; import { TableSettingsMenu } from '@@/datatables'; import { useRepeater } from '@@/datatables/useRepeater'; @@ -20,8 +22,9 @@ import { ExpandableDatatable } from '@@/datatables/ExpandableDatatable'; import { NamespaceFilter } from '../ApplicationsStacksDatatable/NamespaceFilter'; import { Namespace } from '../ApplicationsStacksDatatable/types'; import { useApplications } from '../../application.queries'; +import { PodKubernetesInstanceLabel, PodManagedByLabel } from '../../constants'; -import { Application, ConfigKind } from './types'; +import { Application, ApplicationRowData, ConfigKind } from './types'; import { useColumns } from './useColumns'; import { getPublishedUrls } from './PublishedPorts'; import { SubRow } from './SubRow'; @@ -70,7 +73,7 @@ export function ApplicationsDatatable({ namespace, withDependencies: true, }); - const applications = applicationsQuery.data ?? []; + const applications = useApplicationsRowData(applicationsQuery.data); const filteredApplications = showSystem ? applications : applications.filter( @@ -156,7 +159,74 @@ export function ApplicationsDatatable({ ); } -function isExpandable(item: Application) { +function useApplicationsRowData( + applications?: Application[] +): ApplicationRowData[] { + return useMemo(() => separateHelmApps(applications ?? []), [applications]); +} + +function separateHelmApps(applications: Application[]): ApplicationRowData[] { + const [helmApps, nonHelmApps] = partition( + applications, + (app) => + app.Metadata?.labels && + app.Metadata.labels[PodKubernetesInstanceLabel] && + app.Metadata.labels[PodManagedByLabel] === 'Helm' + ); + + const groupedHelmApps: Record = groupBy( + helmApps, + (app) => 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); + return [...helmApps, helmApp]; + }, []); + + return [...helmAppsRowData, ...nonHelmApps]; +} + +function buildHelmAppRowData( + appName: string, + apps: Application[] +): ApplicationRowData { + const id = `${apps[0].ResourcePool}-${appName + .toLowerCase() + .replaceAll(' ', '-')}`; + const { earliestCreationDate, runningPods, totalPods } = apps.reduce( + (acc, app) => ({ + earliestCreationDate: + new Date(app.CreationDate) < new Date(acc.earliestCreationDate) + ? app.CreationDate + : acc.earliestCreationDate, + runningPods: acc.runningPods + app.RunningPodsCount, + totalPods: acc.totalPods + app.TotalPodsCount, + }), + { + earliestCreationDate: apps[0].CreationDate, + runningPods: 0, + totalPods: 0, + } + ); + const helmApp: ApplicationRowData = { + ...apps[0], + Name: appName, + Id: id, + KubernetesApplications: apps, + ApplicationType: KubernetesApplicationTypes.Helm, + Status: runningPods < totalPods ? 'Not ready' : 'Ready', + CreationDate: earliestCreationDate, + RunningPodsCount: runningPods, + TotalPodsCount: totalPods, + }; + return helmApp; +} + +function isExpandable(item: ApplicationRowData) { return ( !!item.KubernetesApplications || !!getPublishedUrls(item).length || diff --git a/app/react/kubernetes/applications/ListView/ApplicationsDatatable/SubRow.tsx b/app/react/kubernetes/applications/ListView/ApplicationsDatatable/SubRow.tsx index 8b1a9d2d3..e6c1d8786 100644 --- a/app/react/kubernetes/applications/ListView/ApplicationsDatatable/SubRow.tsx +++ b/app/react/kubernetes/applications/ListView/ApplicationsDatatable/SubRow.tsx @@ -5,25 +5,26 @@ import { useCurrentUser } from '@/react/hooks/useUser'; import { ConfigurationDetails } from './ConfigurationDetails'; import { InnerTable } from './InnerTable'; import { PublishedPorts } from './PublishedPorts'; -import { Application } from './types'; +import { ApplicationRowData } from './types'; export function SubRow({ item, hideStacks, areSecretsRestricted, }: { - item: Application; + item: ApplicationRowData; hideStacks: boolean; areSecretsRestricted: boolean; }) { const { user: { Username: username }, } = useCurrentUser(); + const colSpan = hideStacks ? 8 : 9; return ( - + {item.KubernetesApplications ? ( (); +export const helper = createColumnHelper(); diff --git a/app/react/kubernetes/applications/ListView/ApplicationsDatatable/columns.name.tsx b/app/react/kubernetes/applications/ListView/ApplicationsDatatable/columns.name.tsx index 00c1029e2..e0801cb8f 100644 --- a/app/react/kubernetes/applications/ListView/ApplicationsDatatable/columns.name.tsx +++ b/app/react/kubernetes/applications/ListView/ApplicationsDatatable/columns.name.tsx @@ -7,14 +7,16 @@ import { SystemBadge } from '@@/Badge/SystemBadge'; import { ExternalBadge } from '@@/Badge/ExternalBadge'; import { helper } from './columns.helper'; -import { Application } from './types'; +import { ApplicationRowData } from './types'; export const name = helper.accessor('Name', { header: 'Name', cell: Cell, }); -function Cell({ row: { original: item } }: CellContext) { +function Cell({ + row: { original: item }, +}: CellContext) { const isSystem = useIsSystemNamespace(item.ResourcePool); return ( diff --git a/app/react/kubernetes/applications/ListView/ApplicationsDatatable/columns.status.tsx b/app/react/kubernetes/applications/ListView/ApplicationsDatatable/columns.status.tsx index a099b48de..4a3a4c6b8 100644 --- a/app/react/kubernetes/applications/ListView/ApplicationsDatatable/columns.status.tsx +++ b/app/react/kubernetes/applications/ListView/ApplicationsDatatable/columns.status.tsx @@ -8,7 +8,7 @@ import { import styles from './columns.status.module.css'; import { helper } from './columns.helper'; -import { Application } from './types'; +import { ApplicationRowData } from './types'; export const status = helper.accessor('Status', { header: 'Status', @@ -16,7 +16,9 @@ export const status = helper.accessor('Status', { enableSorting: false, }); -function Cell({ row: { original: item } }: CellContext) { +function Cell({ + row: { original: item }, +}: CellContext) { if ( item.ApplicationType === KubernetesApplicationTypes.Pod && item.Pods && diff --git a/app/react/kubernetes/applications/ListView/ApplicationsDatatable/types.ts b/app/react/kubernetes/applications/ListView/ApplicationsDatatable/types.ts index 35758466a..52cb374c0 100644 --- a/app/react/kubernetes/applications/ListView/ApplicationsDatatable/types.ts +++ b/app/react/kubernetes/applications/ListView/ApplicationsDatatable/types.ts @@ -1,5 +1,9 @@ import { AppType, DeploymentType } from '../../types'; +export interface ApplicationRowData extends Application { + KubernetesApplications?: Array; +} + export interface Application { Id: string; Name: string; @@ -11,7 +15,6 @@ export interface Application { StackName?: string; ResourcePool: string; ApplicationType: AppType; - KubernetesApplications?: Array; Metadata?: { labels: Record; }; diff --git a/app/react/kubernetes/applications/constants.ts b/app/react/kubernetes/applications/constants.ts index c8175010b..19e1eca56 100644 --- a/app/react/kubernetes/applications/constants.ts +++ b/app/react/kubernetes/applications/constants.ts @@ -8,6 +8,8 @@ export const appNoteAnnotation = 'io.portainer.kubernetes.application.note'; export const appDeployMethodLabel = 'io.portainer.kubernetes.application.kind'; export const defaultDeploymentUniqueLabel = 'pod-template-hash'; export const appNameLabel = 'io.portainer.kubernetes.application.name'; +export const PodKubernetesInstanceLabel = 'app.kubernetes.io/instance'; +export const PodManagedByLabel = 'app.kubernetes.io/managed-by'; export const appRevisionAnnotation = 'deployment.kubernetes.io/revision';