1
0
Fork 0
mirror of https://github.com/portainer/portainer.git synced 2025-07-24 15:59:41 +02:00

refactor(k8s): namespace core logic (#12142)

Co-authored-by: testA113 <aliharriss1995@gmail.com>
Co-authored-by: Anthony Lapenna <anthony.lapenna@portainer.io>
Co-authored-by: James Carppe <85850129+jamescarppe@users.noreply.github.com>
Co-authored-by: Ali <83188384+testA113@users.noreply.github.com>
This commit is contained in:
Steven Kang 2024-10-01 14:15:51 +13:00 committed by GitHub
parent da010f3d08
commit ea228c3d6d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
276 changed files with 9241 additions and 3361 deletions

View file

@ -9,6 +9,7 @@ import { useNamespacesQuery } from '@/react/kubernetes/namespaces/queries/useNam
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 { TableSettingsMenu } from '@@/datatables';
import { useRepeater } from '@@/datatables/useRepeater';
@ -18,6 +19,7 @@ import { ExpandableDatatable } from '@@/datatables/ExpandableDatatable';
import { NamespaceFilter } from '../ApplicationsStacksDatatable/NamespaceFilter';
import { Namespace } from '../ApplicationsStacksDatatable/types';
import { useApplications } from '../../application.queries';
import { Application, ConfigKind } from './types';
import { useColumns } from './useColumns';
@ -26,9 +28,7 @@ import { SubRow } from './SubRow';
import { HelmInsightsBox } from './HelmInsightsBox';
export function ApplicationsDatatable({
dataset,
onRefresh,
isLoading,
onRemove,
namespace = '',
namespaces,
@ -37,9 +37,7 @@ export function ApplicationsDatatable({
onShowSystemChange,
hideStacks,
}: {
dataset: Array<Application>;
onRefresh: () => void;
isLoading: boolean;
onRemove: (selectedItems: Application[]) => void;
namespace?: string;
namespaces: Array<Namespace>;
@ -50,7 +48,7 @@ export function ApplicationsDatatable({
}) {
const envId = useEnvironmentId();
const envQuery = useCurrentEnvironment();
const namespaceMetaListQuery = useNamespacesQuery(envId);
const namespaceListQuery = useNamespacesQuery(envId);
const tableState = useKubeStore('kubernetes.applications', 'Name');
useRepeater(tableState.autoRefreshRate, onRefresh);
@ -58,7 +56,7 @@ export function ApplicationsDatatable({
const hasWriteAuthQuery = useAuthorizations(
'K8sApplicationsW',
undefined,
true
false
);
const { setShowSystemResources } = tableState;
@ -67,27 +65,34 @@ export function ApplicationsDatatable({
setShowSystemResources(showSystem || false);
}, [showSystem, setShowSystemResources]);
const columns = useColumns(hideStacks);
const applicationsQuery = useApplications(envId, {
refetchInterval: tableState.autoRefreshRate * 1000,
namespace,
withDependencies: true,
});
const applications = applicationsQuery.data ?? [];
const filteredApplications = showSystem
? applications
: applications.filter(
(application) =>
!isSystemNamespace(application.ResourcePool, namespaceListQuery.data)
);
const filteredDataset = !showSystem
? dataset.filter(
(item) => !namespaceMetaListQuery.data?.[item.ResourcePool]?.IsSystem
)
: dataset;
const columns = useColumns(hideStacks);
return (
<ExpandableDatatable
data-cy="k8sApp-appTable"
noWidget
dataset={filteredDataset}
dataset={filteredApplications ?? []}
settingsManager={tableState}
columns={columns}
title="Applications"
titleIcon={BoxIcon}
isLoading={isLoading}
isLoading={applicationsQuery.isLoading}
disableSelect={!hasWriteAuthQuery.authorized}
isRowSelectable={(row) =>
!namespaceMetaListQuery.data?.[row.original.ResourcePool]?.IsSystem
!isSystemNamespace(row.original.ResourcePool, namespaceListQuery.data)
}
getRowCanExpand={(row) => isExpandable(row.original)}
renderSubRow={(row) => (

View file

@ -1,5 +1,12 @@
import { isoDate, truncate } from '@/portainer/filters/filters';
import { CellContext } from '@tanstack/react-table';
import { isoDate, truncate } from '@/portainer/filters/filters';
import { useIsSystemNamespace } from '@/react/kubernetes/namespaces/queries/useIsSystemNamespace';
import { Link } from '@@/Link';
import { SystemBadge } from '@@/Badge/SystemBadge';
import { Application } from './types';
import { helper } from './columns.helper';
export const stackName = helper.accessor('StackName', {
@ -9,9 +16,26 @@ export const stackName = helper.accessor('StackName', {
export const namespace = helper.accessor('ResourcePool', {
header: 'Namespace',
cell: ({ getValue }) => getValue() || '-',
cell: NamespaceCell,
});
function NamespaceCell({ row, getValue }: CellContext<Application, string>) {
const value = getValue();
const isSystem = useIsSystemNamespace(value);
return (
<div className="flex gap-2">
<Link
to="kubernetes.resourcePools.resourcePool"
params={{ id: value }}
data-cy={`app-namespace-link-${row.original.Name}`}
>
{value}
</Link>
{isSystem && <SystemBadge />}
</div>
);
}
export const image = helper.accessor('Image', {
header: 'Image',
cell: ({ row: { original: item } }) => (

View file

@ -39,6 +39,12 @@ export interface Application {
}>;
Port: number;
}>;
Resource?: {
CpuLimit?: number;
CpuRequest?: number;
MemoryLimit?: number;
MemoryRequest?: number;
};
}
export enum ConfigKind {

View file

@ -4,44 +4,41 @@ import { useEffect } from 'react';
import { useAuthorizations } from '@/react/hooks/useUser';
import { SystemResourceDescription } from '@/react/kubernetes/datatables/SystemResourceDescription';
import { createStore } from '@/react/kubernetes/datatables/default-kube-datatable-store';
import { useEnvironmentId } from '@/react/hooks/useEnvironmentId';
import { isSystemNamespace } from '@/react/kubernetes/namespaces/queries/useIsSystemNamespace';
import { useNamespacesQuery } from '@/react/kubernetes/namespaces/queries/useNamespacesQuery';
import { ExpandableDatatable } from '@@/datatables/ExpandableDatatable';
import { useRepeater } from '@@/datatables/useRepeater';
import { useTableState } from '@@/datatables/useTableState';
import { KubernetesStack } from '../../types';
import { useApplications } from '../../application.queries';
import { columns } from './columns';
import { SubRows } from './SubRows';
import { Namespace } from './types';
import { Namespace, Stack } from './types';
import { StacksSettingsMenu } from './StacksSettingsMenu';
import { NamespaceFilter } from './NamespaceFilter';
import { TableActions } from './TableActions';
import { getStacksFromApplications } from './getStacksFromApplications';
const storageKey = 'kubernetes.applications.stacks';
const settingsStore = createStore(storageKey);
interface Props {
dataset: Array<KubernetesStack>;
onRemove(selectedItems: Array<KubernetesStack>): void;
onRefresh(): Promise<void>;
onRemove(selectedItems: Array<Stack>): void;
namespace?: string;
namespaces: Array<Namespace>;
onNamespaceChange(namespace: string): void;
isLoading?: boolean;
showSystem?: boolean;
setSystemResources(showSystem: boolean): void;
}
export function ApplicationsStacksDatatable({
dataset,
onRemove,
onRefresh,
namespace = '',
namespaces,
onNamespaceChange,
isLoading,
showSystem,
setSystemResources,
}: Props) {
@ -53,16 +50,32 @@ export function ApplicationsStacksDatatable({
setShowSystemResources(showSystem || false);
}, [showSystem, setShowSystemResources]);
const envId = useEnvironmentId();
const applicationsQuery = useApplications(envId, {
refetchInterval: tableState.autoRefreshRate * 1000,
namespace,
withDependencies: true,
});
const namespaceListQuery = useNamespacesQuery(envId);
const applications = applicationsQuery.data ?? [];
const filteredApplications = showSystem
? applications
: applications.filter(
(item) =>
!isSystemNamespace(item.ResourcePool, namespaceListQuery.data ?? [])
);
const { authorized } = useAuthorizations('K8sApplicationsW');
useRepeater(tableState.autoRefreshRate, onRefresh);
const stacks = getStacksFromApplications(filteredApplications);
return (
<ExpandableDatatable
getRowCanExpand={(row) => row.original.Applications.length > 0}
title="Stacks"
titleIcon={List}
dataset={dataset}
isLoading={isLoading}
dataset={stacks}
isLoading={applicationsQuery.isLoading || namespaceListQuery.isLoading}
columns={columns}
settingsManager={tableState}
disableSelect={!authorized}

View file

@ -6,15 +6,9 @@ import KubernetesNamespaceHelper from '@/kubernetes/helpers/namespaceHelper';
import { Link } from '@@/Link';
import { ExternalBadge } from '@@/Badge/ExternalBadge';
import { KubernetesStack } from '../../types';
import { Stack } from './types';
export function SubRows({
stack,
span,
}: {
stack: KubernetesStack;
span: number;
}) {
export function SubRows({ stack, span }: { stack: Stack; span: number }) {
return (
<>
{stack.Applications.map((app) => (

View file

@ -2,14 +2,14 @@ import { Authorized } from '@/react/hooks/useUser';
import { DeleteButton } from '@@/buttons/DeleteButton';
import { KubernetesStack } from '../../types';
import { Stack } from './types';
export function TableActions({
selectedItems,
onRemove,
}: {
selectedItems: Array<KubernetesStack>;
onRemove: (selectedItems: Array<KubernetesStack>) => void;
selectedItems: Array<Stack>;
onRemove: (selectedItems: Array<Stack>) => void;
}) {
return (
<Authorized authorizations="K8sApplicationsW">

View file

@ -1,63 +1,70 @@
import { FileText } from 'lucide-react';
import { createColumnHelper } from '@tanstack/react-table';
import { CellContext, createColumnHelper } from '@tanstack/react-table';
import KubernetesNamespaceHelper from '@/kubernetes/helpers/namespaceHelper';
import { useIsSystemNamespace } from '@/react/kubernetes/namespaces/queries/useIsSystemNamespace';
import { buildExpandColumn } from '@@/datatables/expand-column';
import { Link } from '@@/Link';
import { Icon } from '@@/Icon';
import { SystemBadge } from '@@/Badge/SystemBadge';
import { KubernetesStack } from '../../types';
import { Stack } from './types';
export const columnHelper = createColumnHelper<KubernetesStack>();
export const columnHelper = createColumnHelper<Stack>();
const namespace = columnHelper.accessor('ResourcePool', {
id: 'namespace',
header: 'Namespace',
cell: NamespaceCell,
});
function NamespaceCell({ row, getValue }: CellContext<Stack, string>) {
const value = getValue();
const isSystem = useIsSystemNamespace(value);
return (
<div className="flex gap-2">
<Link
to="kubernetes.resourcePools.resourcePool"
params={{ id: value }}
data-cy={`app-stack-namespace-link-${row.original.Name}`}
>
{value}
</Link>
{isSystem && <SystemBadge />}
</div>
);
}
const name = columnHelper.accessor('Name', {
id: 'name',
header: 'Stack',
});
const applications = columnHelper.accessor((row) => row.Applications.length, {
id: 'applications',
header: 'Applications',
});
const actions = columnHelper.display({
id: 'actions',
header: 'Actions',
cell: ({ row: { original: item } }) => (
<Link
to="kubernetes.stacks.stack.logs"
params={{ namespace: item.ResourcePool, name: item.Name }}
className="flex items-center gap-1"
data-cy={`app-stack-logs-link-${item.Name}`}
>
<Icon icon={FileText} />
Logs
</Link>
),
});
export const columns = [
buildExpandColumn<KubernetesStack>(),
columnHelper.accessor('Name', {
id: 'name',
header: 'Stack',
}),
columnHelper.accessor('ResourcePool', {
id: 'namespace',
header: 'Namespace',
cell: ({ getValue, row }) => {
const value = getValue();
return (
<div className="flex gap-2">
<Link
to="kubernetes.resourcePools.resourcePool"
params={{ id: value }}
data-cy={`app-stack-namespace-link-${row.original.Name}`}
>
{value}
</Link>
{KubernetesNamespaceHelper.isSystemNamespace(value) && (
<SystemBadge />
)}
</div>
);
},
}),
columnHelper.accessor((row) => row.Applications.length, {
id: 'applications',
header: 'Applications',
}),
columnHelper.display({
id: 'actions',
header: 'Actions',
cell: ({ row: { original: item } }) => (
<Link
to="kubernetes.stacks.stack.logs"
params={{ namespace: item.ResourcePool, name: item.Name }}
className="flex items-center gap-1"
data-cy={`app-stack-logs-link-${item.Name}`}
>
<Icon icon={FileText} />
Logs
</Link>
),
}),
buildExpandColumn<Stack>(),
name,
namespace,
applications,
actions,
];

View file

@ -0,0 +1,159 @@
import { Application } from '../ApplicationsDatatable/types';
import { getStacksFromApplications } from './getStacksFromApplications';
import { Stack } from './types';
describe('getStacksFromApplications', () => {
test('should return an empty array when passed an empty array', () => {
expect(getStacksFromApplications([])).toHaveLength(0);
});
test('should return an empty array when passed a list of applications without stacks', () => {
const appsWithoutStacks: Application[] = [
{
StackName: '',
Id: '1',
Name: 'app1',
CreationDate: '2021-10-01T00:00:00Z',
ResourcePool: 'namespace1',
Image: 'image1',
ApplicationType: 'Pod',
DeploymentType: 'Replicated',
Status: 'status1',
TotalPodsCount: 1,
RunningPodsCount: 1,
},
{
StackName: '',
Id: '1',
Name: 'app2',
CreationDate: '2021-10-01T00:00:00Z',
ResourcePool: 'namespace1',
Image: 'image1',
ApplicationType: 'Pod',
DeploymentType: 'Replicated',
Status: 'status1',
TotalPodsCount: 1,
RunningPodsCount: 1,
},
{
StackName: '',
Id: '1',
Name: 'app3',
CreationDate: '2021-10-01T00:00:00Z',
ResourcePool: 'namespace1',
Image: 'image1',
ApplicationType: 'Pod',
DeploymentType: 'Replicated',
Status: 'status1',
TotalPodsCount: 1,
RunningPodsCount: 1,
},
];
expect(getStacksFromApplications(appsWithoutStacks)).toHaveLength(0);
});
test('should return a list of stacks when passed a list of applications with stacks', () => {
const appsWithStacks: Application[] = [
{
StackName: 'stack1',
Id: '1',
Name: 'app1',
CreationDate: '2021-10-01T00:00:00Z',
ResourcePool: 'namespace1',
Image: 'image1',
ApplicationType: 'Pod',
DeploymentType: 'Replicated',
Status: 'status1',
TotalPodsCount: 1,
RunningPodsCount: 1,
},
{
StackName: 'stack1',
Id: '1',
Name: 'app2',
CreationDate: '2021-10-01T00:00:00Z',
ResourcePool: 'namespace1',
Image: 'image1',
ApplicationType: 'Pod',
DeploymentType: 'Replicated',
Status: 'status1',
TotalPodsCount: 1,
RunningPodsCount: 1,
},
{
StackName: 'stack2',
Id: '1',
Name: 'app3',
CreationDate: '2021-10-01T00:00:00Z',
ResourcePool: 'namespace1',
Image: 'image1',
ApplicationType: 'Pod',
DeploymentType: 'Replicated',
Status: 'status1',
TotalPodsCount: 1,
RunningPodsCount: 1,
},
];
const expectedStacksWithApps: Stack[] = [
{
Name: 'stack1',
ResourcePool: 'namespace1',
Applications: [
{
StackName: 'stack1',
Id: '1',
Name: 'app1',
CreationDate: '2021-10-01T00:00:00Z',
ResourcePool: 'namespace1',
Image: 'image1',
ApplicationType: 'Pod',
DeploymentType: 'Replicated',
Status: 'status1',
TotalPodsCount: 1,
RunningPodsCount: 1,
},
{
StackName: 'stack1',
Id: '1',
Name: 'app2',
CreationDate: '2021-10-01T00:00:00Z',
ResourcePool: 'namespace1',
Image: 'image1',
ApplicationType: 'Pod',
DeploymentType: 'Replicated',
Status: 'status1',
TotalPodsCount: 1,
RunningPodsCount: 1,
},
],
Highlighted: false,
},
{
Name: 'stack2',
ResourcePool: 'namespace1',
Applications: [
{
StackName: 'stack2',
Id: '1',
Name: 'app3',
CreationDate: '2021-10-01T00:00:00Z',
ResourcePool: 'namespace1',
Image: 'image1',
ApplicationType: 'Pod',
DeploymentType: 'Replicated',
Status: 'status1',
TotalPodsCount: 1,
RunningPodsCount: 1,
},
],
Highlighted: false,
},
];
expect(getStacksFromApplications(appsWithStacks)).toEqual(
expectedStacksWithApps
);
});
});

View file

@ -0,0 +1,36 @@
import { Application } from '../ApplicationsDatatable/types';
import { Stack } from './types';
export function getStacksFromApplications(applications: Application[]) {
const res = applications.reduce<Stack[]>((stacks, app) => {
const updatedStacks = stacks.map((stack) => {
if (
stack.Name === app.StackName &&
stack.ResourcePool === app.ResourcePool
) {
return {
...stack,
Applications: [...stack.Applications, app],
};
}
return stack;
});
const stackExists = updatedStacks.some(
(stack) =>
stack.Name === app.StackName && stack.ResourcePool === app.ResourcePool
);
if (!stackExists && app.StackName) {
updatedStacks.push({
Name: app.StackName,
ResourcePool: app.ResourcePool,
Applications: [app],
Highlighted: false,
});
}
return updatedStacks;
}, []);
return res;
}

View file

@ -5,6 +5,8 @@ import {
RefreshableTableSettings,
} from '@@/datatables/types';
import { Application } from '../ApplicationsDatatable/types';
export interface TableSettings
extends BasicTableSettings,
RefreshableTableSettings,
@ -16,3 +18,10 @@ export interface Namespace {
Yaml: string;
IsSystem?: boolean;
}
export type Stack = {
Name: string;
ResourcePool: string;
Applications: Application[];
Highlighted: boolean;
};