fix(apps): group helm apps together [r8s-102] (#24)

pull/12346/head
Ali 2024-10-14 10:28:56 +13:00 committed by GitHub
parent 1110f745e1
commit 57e10dc911
8 changed files with 95 additions and 17 deletions

View File

@ -31,9 +31,7 @@ import {
KubernetesPodNodeAffinityPayload, KubernetesPodNodeAffinityPayload,
KubernetesPreferredSchedulingTermPayload, KubernetesPreferredSchedulingTermPayload,
} from 'Kubernetes/pod/payloads/affinities'; } from 'Kubernetes/pod/payloads/affinities';
import { PodKubernetesInstanceLabel, PodManagedByLabel } from '@/react/kubernetes/applications/constants';
export const PodKubernetesInstanceLabel = 'app.kubernetes.io/instance';
export const PodManagedByLabel = 'app.kubernetes.io/managed-by';
class KubernetesApplicationHelper { class KubernetesApplicationHelper {
/* #region UTILITY FUNCTIONS */ /* #region UTILITY FUNCTIONS */

View File

@ -1,5 +1,6 @@
import { useEffect } from 'react'; import { useEffect, useMemo } from 'react';
import { BoxIcon } from 'lucide-react'; import { BoxIcon } from 'lucide-react';
import { groupBy, partition } from 'lodash';
import { useKubeStore } from '@/react/kubernetes/datatables/default-kube-datatable-store'; import { useKubeStore } from '@/react/kubernetes/datatables/default-kube-datatable-store';
import { DefaultDatatableSettings } from '@/react/kubernetes/datatables/DefaultDatatableSettings'; import { DefaultDatatableSettings } from '@/react/kubernetes/datatables/DefaultDatatableSettings';
@ -10,6 +11,7 @@ import { useEnvironmentId } from '@/react/hooks/useEnvironmentId';
import { useCurrentEnvironment } from '@/react/hooks/useCurrentEnvironment'; import { useCurrentEnvironment } from '@/react/hooks/useCurrentEnvironment';
import { useAuthorizations } from '@/react/hooks/useUser'; import { useAuthorizations } from '@/react/hooks/useUser';
import { isSystemNamespace } from '@/react/kubernetes/namespaces/queries/useIsSystemNamespace'; import { isSystemNamespace } from '@/react/kubernetes/namespaces/queries/useIsSystemNamespace';
import { KubernetesApplicationTypes } from '@/kubernetes/models/application/models/appConstants';
import { TableSettingsMenu } from '@@/datatables'; import { TableSettingsMenu } from '@@/datatables';
import { useRepeater } from '@@/datatables/useRepeater'; import { useRepeater } from '@@/datatables/useRepeater';
@ -20,8 +22,9 @@ import { ExpandableDatatable } from '@@/datatables/ExpandableDatatable';
import { NamespaceFilter } from '../ApplicationsStacksDatatable/NamespaceFilter'; import { NamespaceFilter } from '../ApplicationsStacksDatatable/NamespaceFilter';
import { Namespace } from '../ApplicationsStacksDatatable/types'; import { Namespace } from '../ApplicationsStacksDatatable/types';
import { useApplications } from '../../application.queries'; 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 { useColumns } from './useColumns';
import { getPublishedUrls } from './PublishedPorts'; import { getPublishedUrls } from './PublishedPorts';
import { SubRow } from './SubRow'; import { SubRow } from './SubRow';
@ -70,7 +73,7 @@ export function ApplicationsDatatable({
namespace, namespace,
withDependencies: true, withDependencies: true,
}); });
const applications = applicationsQuery.data ?? []; const applications = useApplicationsRowData(applicationsQuery.data);
const filteredApplications = showSystem const filteredApplications = showSystem
? applications ? applications
: applications.filter( : 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<string, Application[]> = 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 ( return (
!!item.KubernetesApplications || !!item.KubernetesApplications ||
!!getPublishedUrls(item).length || !!getPublishedUrls(item).length ||

View File

@ -5,25 +5,26 @@ import { useCurrentUser } from '@/react/hooks/useUser';
import { ConfigurationDetails } from './ConfigurationDetails'; import { ConfigurationDetails } from './ConfigurationDetails';
import { InnerTable } from './InnerTable'; import { InnerTable } from './InnerTable';
import { PublishedPorts } from './PublishedPorts'; import { PublishedPorts } from './PublishedPorts';
import { Application } from './types'; import { ApplicationRowData } from './types';
export function SubRow({ export function SubRow({
item, item,
hideStacks, hideStacks,
areSecretsRestricted, areSecretsRestricted,
}: { }: {
item: Application; item: ApplicationRowData;
hideStacks: boolean; hideStacks: boolean;
areSecretsRestricted: boolean; areSecretsRestricted: boolean;
}) { }) {
const { const {
user: { Username: username }, user: { Username: username },
} = useCurrentUser(); } = useCurrentUser();
const colSpan = hideStacks ? 8 : 9;
return ( return (
<tr className={clsx({ 'secondary-body': !item.KubernetesApplications })}> <tr className={clsx({ 'secondary-body': !item.KubernetesApplications })}>
<td /> <td />
<td colSpan={8} className="datatable-padding-vertical"> <td colSpan={colSpan} className="datatable-padding-vertical">
{item.KubernetesApplications ? ( {item.KubernetesApplications ? (
<InnerTable <InnerTable
dataset={item.KubernetesApplications} dataset={item.KubernetesApplications}

View File

@ -1,5 +1,5 @@
import { createColumnHelper } from '@tanstack/react-table'; import { createColumnHelper } from '@tanstack/react-table';
import { Application } from './types'; import { ApplicationRowData } from './types';
export const helper = createColumnHelper<Application>(); export const helper = createColumnHelper<ApplicationRowData>();

View File

@ -7,14 +7,16 @@ import { SystemBadge } from '@@/Badge/SystemBadge';
import { ExternalBadge } from '@@/Badge/ExternalBadge'; import { ExternalBadge } from '@@/Badge/ExternalBadge';
import { helper } from './columns.helper'; import { helper } from './columns.helper';
import { Application } from './types'; import { ApplicationRowData } from './types';
export const name = helper.accessor('Name', { export const name = helper.accessor('Name', {
header: 'Name', header: 'Name',
cell: Cell, cell: Cell,
}); });
function Cell({ row: { original: item } }: CellContext<Application, string>) { function Cell({
row: { original: item },
}: CellContext<ApplicationRowData, string>) {
const isSystem = useIsSystemNamespace(item.ResourcePool); const isSystem = useIsSystemNamespace(item.ResourcePool);
return ( return (

View File

@ -8,7 +8,7 @@ import {
import styles from './columns.status.module.css'; import styles from './columns.status.module.css';
import { helper } from './columns.helper'; import { helper } from './columns.helper';
import { Application } from './types'; import { ApplicationRowData } from './types';
export const status = helper.accessor('Status', { export const status = helper.accessor('Status', {
header: 'Status', header: 'Status',
@ -16,7 +16,9 @@ export const status = helper.accessor('Status', {
enableSorting: false, enableSorting: false,
}); });
function Cell({ row: { original: item } }: CellContext<Application, string>) { function Cell({
row: { original: item },
}: CellContext<ApplicationRowData, string>) {
if ( if (
item.ApplicationType === KubernetesApplicationTypes.Pod && item.ApplicationType === KubernetesApplicationTypes.Pod &&
item.Pods && item.Pods &&

View File

@ -1,5 +1,9 @@
import { AppType, DeploymentType } from '../../types'; import { AppType, DeploymentType } from '../../types';
export interface ApplicationRowData extends Application {
KubernetesApplications?: Array<Application>;
}
export interface Application { export interface Application {
Id: string; Id: string;
Name: string; Name: string;
@ -11,7 +15,6 @@ export interface Application {
StackName?: string; StackName?: string;
ResourcePool: string; ResourcePool: string;
ApplicationType: AppType; ApplicationType: AppType;
KubernetesApplications?: Array<Application>;
Metadata?: { Metadata?: {
labels: Record<string, string>; labels: Record<string, string>;
}; };

View File

@ -8,6 +8,8 @@ export const appNoteAnnotation = 'io.portainer.kubernetes.application.note';
export const appDeployMethodLabel = 'io.portainer.kubernetes.application.kind'; export const appDeployMethodLabel = 'io.portainer.kubernetes.application.kind';
export const defaultDeploymentUniqueLabel = 'pod-template-hash'; export const defaultDeploymentUniqueLabel = 'pod-template-hash';
export const appNameLabel = 'io.portainer.kubernetes.application.name'; 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'; export const appRevisionAnnotation = 'deployment.kubernetes.io/revision';