mirror of
https://github.com/portainer/portainer.git
synced 2025-07-24 07:49:41 +02:00
refactor(kube/apps): migrate stacks table to react [EE-4661] (#10091)
This commit is contained in:
parent
a5f60c64ef
commit
25d5e62f5c
31 changed files with 516 additions and 565 deletions
|
@ -0,0 +1,105 @@
|
|||
import { List } from 'lucide-react';
|
||||
|
||||
import { useAuthorizations } from '@/react/hooks/useUser';
|
||||
import { SystemResourceDescription } from '@/react/kubernetes/datatables/SystemResourceDescription';
|
||||
import { systemResourcesSettings } from '@/react/kubernetes/datatables/SystemResourcesSettings';
|
||||
|
||||
import { ExpandableDatatable } from '@@/datatables/ExpandableDatatable';
|
||||
import { createPersistedStore, refreshableSettings } from '@@/datatables/types';
|
||||
import { useRepeater } from '@@/datatables/useRepeater';
|
||||
import { useTableState } from '@@/datatables/useTableState';
|
||||
import { InsightsBox } from '@@/InsightsBox';
|
||||
|
||||
import { KubernetesStack } from '../../types';
|
||||
|
||||
import { columns } from './columns';
|
||||
import { SubRows } from './SubRows';
|
||||
import { Namespace, TableSettings } from './types';
|
||||
import { StacksSettingsMenu } from './StacksSettingsMenu';
|
||||
import { NamespaceFilter } from './NamespaceFilter';
|
||||
import { TableActions } from './TableActions';
|
||||
|
||||
const storageKey = 'kubernetes.applications.stacks';
|
||||
|
||||
const settingsStore = createPersistedStore<TableSettings>(
|
||||
storageKey,
|
||||
'name',
|
||||
(set) => ({
|
||||
...systemResourcesSettings(set),
|
||||
...refreshableSettings(set),
|
||||
})
|
||||
);
|
||||
|
||||
interface Props {
|
||||
dataset: Array<KubernetesStack>;
|
||||
onRemove(selectedItems: Array<KubernetesStack>): void;
|
||||
onRefresh(): Promise<void>;
|
||||
namespace?: string;
|
||||
namespaces: Array<Namespace>;
|
||||
onNamespaceChange(namespace: string): void;
|
||||
isLoading?: boolean;
|
||||
}
|
||||
|
||||
export function ApplicationsStacksDatatable({
|
||||
dataset,
|
||||
onRemove,
|
||||
onRefresh,
|
||||
namespace = '',
|
||||
namespaces,
|
||||
onNamespaceChange,
|
||||
isLoading,
|
||||
}: Props) {
|
||||
const tableState = useTableState(settingsStore, storageKey);
|
||||
|
||||
const authorized = useAuthorizations('K8sApplicationsW');
|
||||
useRepeater(tableState.autoRefreshRate, onRefresh);
|
||||
|
||||
return (
|
||||
<ExpandableDatatable
|
||||
getRowCanExpand={(row) => row.original.Applications.length > 0}
|
||||
title="Stacks"
|
||||
titleIcon={List}
|
||||
dataset={dataset}
|
||||
isLoading={isLoading}
|
||||
columns={columns}
|
||||
settingsManager={tableState}
|
||||
disableSelect={!authorized}
|
||||
renderSubRow={(row) => (
|
||||
<SubRows stack={row.original} span={row.getVisibleCells().length} />
|
||||
)}
|
||||
noWidget
|
||||
emptyContentLabel="No stack available."
|
||||
description={
|
||||
<div className="w-full">
|
||||
<div className="min-w-[140px] float-right">
|
||||
<NamespaceFilter
|
||||
namespaces={namespaces}
|
||||
value={namespace}
|
||||
onChange={onNamespaceChange}
|
||||
showSystem={tableState.showSystemResources}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<SystemResourceDescription
|
||||
showSystemResources={tableState.showSystemResources}
|
||||
/>
|
||||
|
||||
<div className="w-fit">
|
||||
<InsightsBox
|
||||
type="slim"
|
||||
header="From 2.18 on, you can filter this view by namespace."
|
||||
insightCloseId="k8s-namespace-filtering"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
renderTableActions={(selectedItems) => (
|
||||
<TableActions selectedItems={selectedItems} onRemove={onRemove} />
|
||||
)}
|
||||
renderTableSettings={() => <StacksSettingsMenu settings={tableState} />}
|
||||
getRowId={(row) => `${row.Name}-${row.ResourcePool}`}
|
||||
/>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,61 @@
|
|||
import { Filter } from 'lucide-react';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
import { Icon } from '@@/Icon';
|
||||
import { Select } from '@@/form-components/Input';
|
||||
import { InputGroup } from '@@/form-components/InputGroup';
|
||||
|
||||
import { Namespace } from './types';
|
||||
|
||||
function transformNamespaces(namespaces: Namespace[], showSystem: boolean) {
|
||||
return namespaces
|
||||
.filter((ns) => showSystem || !ns.IsSystem)
|
||||
.map(({ Name, IsSystem }) => ({
|
||||
label: IsSystem ? `${Name} - system` : Name,
|
||||
value: Name,
|
||||
}));
|
||||
}
|
||||
|
||||
export function NamespaceFilter({
|
||||
namespaces,
|
||||
value,
|
||||
onChange,
|
||||
showSystem,
|
||||
}: {
|
||||
namespaces: Namespace[];
|
||||
value: string;
|
||||
onChange: (value: string) => void;
|
||||
showSystem: boolean;
|
||||
}) {
|
||||
const transformedNamespaces = transformNamespaces(namespaces, showSystem);
|
||||
|
||||
// sync value with displayed namespaces
|
||||
useEffect(() => {
|
||||
const names = transformedNamespaces.map((ns) => ns.value);
|
||||
if (value && !names.find((ns) => ns === value)) {
|
||||
onChange(
|
||||
names.length > 0 ? names.find((ns) => ns === 'default') || names[0] : ''
|
||||
);
|
||||
}
|
||||
}, [value, onChange, transformedNamespaces]);
|
||||
|
||||
return (
|
||||
<InputGroup>
|
||||
<InputGroup.Addon>
|
||||
<div className="flex items-center gap-1">
|
||||
<Icon icon={Filter} />
|
||||
Namespace
|
||||
</div>
|
||||
</InputGroup.Addon>
|
||||
<Select
|
||||
className="!h-[30px] py-1"
|
||||
value={value || ''}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
options={[
|
||||
{ label: 'All namespaces', value: '' },
|
||||
...transformedNamespaces,
|
||||
]}
|
||||
/>
|
||||
</InputGroup>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
import { SystemResourcesSettings } from '@/react/kubernetes/datatables/SystemResourcesSettings';
|
||||
|
||||
import { TableSettingsMenu } from '@@/datatables';
|
||||
import { TableSettingsMenuAutoRefresh } from '@@/datatables/TableSettingsMenuAutoRefresh';
|
||||
|
||||
import { type TableSettings } from './types';
|
||||
|
||||
export function StacksSettingsMenu({ settings }: { settings: TableSettings }) {
|
||||
return (
|
||||
<TableSettingsMenu>
|
||||
<SystemResourcesSettings
|
||||
value={settings.showSystemResources}
|
||||
onChange={(value) => settings.setShowSystemResources(value)}
|
||||
/>
|
||||
|
||||
<TableSettingsMenuAutoRefresh
|
||||
onChange={settings.setAutoRefreshRate}
|
||||
value={settings.autoRefreshRate}
|
||||
/>
|
||||
</TableSettingsMenu>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
import clsx from 'clsx';
|
||||
|
||||
import KubernetesApplicationHelper from '@/kubernetes/helpers/application';
|
||||
import KubernetesNamespaceHelper from '@/kubernetes/helpers/namespaceHelper';
|
||||
|
||||
import { Link } from '@@/Link';
|
||||
|
||||
import { KubernetesStack } from '../../types';
|
||||
|
||||
export function SubRows({
|
||||
stack,
|
||||
span,
|
||||
}: {
|
||||
stack: KubernetesStack;
|
||||
span: number;
|
||||
}) {
|
||||
return (
|
||||
<>
|
||||
{stack.Applications.map((app) => (
|
||||
<tr
|
||||
className={clsx({
|
||||
'datatable-highlighted': stack.Highlighted,
|
||||
'datatable-unhighlighted': !stack.Highlighted,
|
||||
})}
|
||||
key={app.Name}
|
||||
>
|
||||
<td />
|
||||
<td colSpan={span - 1}>
|
||||
<Link
|
||||
to="kubernetes.applications.application"
|
||||
params={{ name: app.Name, namespace: app.ResourcePool }}
|
||||
>
|
||||
{app.Name}
|
||||
</Link>
|
||||
{KubernetesNamespaceHelper.isSystemNamespace(app.ResourcePool) &&
|
||||
KubernetesApplicationHelper.isExternalApplication(app) && (
|
||||
<span className="space-left label label-primary image-tag">
|
||||
external
|
||||
</span>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
import { Trash2 } from 'lucide-react';
|
||||
|
||||
import { Authorized } from '@/react/hooks/useUser';
|
||||
|
||||
import { Button } from '@@/buttons';
|
||||
|
||||
import { KubernetesStack } from '../../types';
|
||||
|
||||
export function TableActions({
|
||||
selectedItems,
|
||||
onRemove,
|
||||
}: {
|
||||
selectedItems: Array<KubernetesStack>;
|
||||
onRemove: (selectedItems: Array<KubernetesStack>) => void;
|
||||
}) {
|
||||
return (
|
||||
<Authorized authorizations="K8sApplicationsW">
|
||||
<Button
|
||||
disabled={selectedItems.length === 0}
|
||||
color="dangerlight"
|
||||
onClick={() => onRemove(selectedItems)}
|
||||
icon={Trash2}
|
||||
data-cy="k8sApp-removeStackButton"
|
||||
>
|
||||
Remove
|
||||
</Button>
|
||||
</Authorized>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,62 @@
|
|||
import { FileText } from 'lucide-react';
|
||||
import { createColumnHelper } from '@tanstack/react-table';
|
||||
|
||||
import KubernetesNamespaceHelper from '@/kubernetes/helpers/namespaceHelper';
|
||||
|
||||
import { buildExpandColumn } from '@@/datatables/expand-column';
|
||||
import { Link } from '@@/Link';
|
||||
import { Icon } from '@@/Icon';
|
||||
|
||||
import { KubernetesStack } from '../../types';
|
||||
|
||||
export const columnHelper = createColumnHelper<KubernetesStack>();
|
||||
|
||||
export const columns = [
|
||||
buildExpandColumn<KubernetesStack>(),
|
||||
columnHelper.accessor('Name', {
|
||||
id: 'name',
|
||||
header: 'Stack',
|
||||
}),
|
||||
columnHelper.accessor('ResourcePool', {
|
||||
id: 'namespace',
|
||||
header: 'Namespace',
|
||||
cell: ({ getValue }) => {
|
||||
const value = getValue();
|
||||
return (
|
||||
<>
|
||||
<Link
|
||||
to="kubernetes.resourcePools.resourcePool"
|
||||
params={{ id: value }}
|
||||
>
|
||||
{value}
|
||||
</Link>
|
||||
{KubernetesNamespaceHelper.isSystemNamespace(value) && (
|
||||
<span className="label label-info image-tag label-margins">
|
||||
system
|
||||
</span>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
},
|
||||
}),
|
||||
|
||||
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"
|
||||
>
|
||||
<Icon icon={FileText} />
|
||||
Logs
|
||||
</Link>
|
||||
),
|
||||
}),
|
||||
];
|
|
@ -0,0 +1 @@
|
|||
export { ApplicationsStacksDatatable } from './ApplicationsStacksDatatable';
|
|
@ -0,0 +1,18 @@
|
|||
import { SystemResourcesTableSettings } from '@/react/kubernetes/datatables/SystemResourcesSettings';
|
||||
|
||||
import {
|
||||
BasicTableSettings,
|
||||
RefreshableTableSettings,
|
||||
} from '@@/datatables/types';
|
||||
|
||||
export interface TableSettings
|
||||
extends BasicTableSettings,
|
||||
RefreshableTableSettings,
|
||||
SystemResourcesTableSettings {}
|
||||
|
||||
export interface Namespace {
|
||||
Id: string;
|
||||
Name: string;
|
||||
Yaml: string;
|
||||
IsSystem?: boolean;
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue