mirror of https://github.com/portainer/portainer
fix(apps): group helm apps together [r8s-102] (#24)
parent
1110f745e1
commit
57e10dc911
|
@ -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 */
|
||||||
|
|
|
@ -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 ||
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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>();
|
||||||
|
|
|
@ -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 (
|
||||||
|
|
|
@ -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 &&
|
||||||
|
|
|
@ -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>;
|
||||||
};
|
};
|
||||||
|
|
|
@ -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';
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue