1
0
Fork 0
mirror of https://github.com/portainer/portainer.git synced 2025-08-02 20:35:25 +02:00

refactor(kube/apps): migrate stacks table to react [EE-4661] (#10091)

This commit is contained in:
Chaim Lev-Ari 2023-09-20 09:04:26 +03:00 committed by GitHub
parent a5f60c64ef
commit 25d5e62f5c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
31 changed files with 516 additions and 565 deletions

View file

@ -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}`}
/>
);
}

View file

@ -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>
);
}

View file

@ -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>
);
}

View file

@ -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>
))}
</>
);
}

View file

@ -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>
);
}

View file

@ -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>
),
}),
];

View file

@ -0,0 +1 @@
export { ApplicationsStacksDatatable } from './ApplicationsStacksDatatable';

View file

@ -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;
}

View file

@ -34,3 +34,15 @@ type Patch = {
}[];
export type ApplicationPatch = Patch | RawExtension;
export type KubernetesStack = {
Name: string;
ResourcePool: string;
Applications: Array<
Application & {
Name: string;
ResourcePool: string;
}
>;
Highlighted: boolean;
};

View file

@ -91,17 +91,12 @@ export function ConfigMapsDatatable() {
)}
renderTableSettings={() => (
<TableSettingsMenu>
<DefaultDatatableSettings
settings={tableState}
hideShowSystemResources={!canAccessSystemResources}
/>
<DefaultDatatableSettings settings={tableState} />
</TableSettingsMenu>
)}
description={
<SystemResourceDescription
showSystemResources={
tableState.showSystemResources || !canAccessSystemResources
}
showSystemResources={tableState.showSystemResources}
/>
}
/>

View file

@ -91,17 +91,12 @@ export function SecretsDatatable() {
)}
renderTableSettings={() => (
<TableSettingsMenu>
<DefaultDatatableSettings
settings={tableState}
hideShowSystemResources={!canAccessSystemResources}
/>
<DefaultDatatableSettings settings={tableState} />
</TableSettingsMenu>
)}
description={
<SystemResourceDescription
showSystemResources={
tableState.showSystemResources || !canAccessSystemResources
}
showSystemResources={tableState.showSystemResources}
/>
}
/>

View file

@ -1,54 +1,31 @@
import { TableSettingsMenuAutoRefresh } from '@@/datatables/TableSettingsMenuAutoRefresh';
import { Checkbox } from '@@/form-components/Checkbox';
import {
BasicTableSettings,
RefreshableTableSettings,
ZustandSetFunc,
} from '@@/datatables/types';
interface SystemResourcesTableSettings {
showSystemResources: boolean;
setShowSystemResources: (value: boolean) => void;
}
import {
SystemResourcesSettings,
SystemResourcesTableSettings,
} from './SystemResourcesSettings';
export interface TableSettings
extends BasicTableSettings,
RefreshableTableSettings,
SystemResourcesTableSettings {}
export function systemResourcesSettings<T extends SystemResourcesTableSettings>(
set: ZustandSetFunc<T>
): SystemResourcesTableSettings {
return {
showSystemResources: false,
setShowSystemResources(showSystemResources: boolean) {
set((s) => ({
...s,
showSystemResources,
}));
},
};
}
interface Props {
settings: TableSettings;
hideShowSystemResources?: boolean;
}
export function DefaultDatatableSettings({
settings,
hideShowSystemResources = false,
}: Props) {
}: {
settings: TableSettings;
}) {
return (
<>
{!hideShowSystemResources && (
<Checkbox
id="show-system-resources"
label="Show system resources"
checked={settings.showSystemResources}
onChange={(e) => settings.setShowSystemResources(e.target.checked)}
/>
)}
<SystemResourcesSettings
value={settings.showSystemResources}
onChange={(value) => settings.setShowSystemResources(value)}
/>
<TableSettingsMenuAutoRefresh
value={settings.autoRefreshRate}
onChange={handleRefreshRateChange}

View file

@ -1,3 +1,5 @@
import { Authorized } from '@/react/hooks/useUser';
import { TextTip } from '@@/Tip/TextTip';
interface Props {
@ -5,13 +7,11 @@ interface Props {
}
export function SystemResourceDescription({ showSystemResources }: Props) {
return (
<div className="w-full">
{!showSystemResources && (
<TextTip color="blue" className="!mb-0">
System resources are hidden, this can be changed in the table settings
</TextTip>
)}
</div>
);
return !showSystemResources ? (
<Authorized authorizations="K8sAccessSystemNamespaces" adminOnlyCE>
<TextTip color="blue" className="!mb-0">
System resources are hidden, this can be changed in the table settings
</TextTip>
</Authorized>
) : null;
}

View file

@ -0,0 +1,42 @@
import { Authorized } from '@/react/hooks/useUser';
import { ZustandSetFunc } from '@@/datatables/types';
import { Checkbox } from '@@/form-components/Checkbox';
export function SystemResourcesSettings({
value,
onChange,
}: {
value: boolean;
onChange: (value: boolean) => void;
}) {
return (
<Authorized authorizations="K8sAccessSystemNamespaces" adminOnlyCE>
<Checkbox
id="show-system-resources"
label="Show system resources"
checked={value}
onChange={(e) => onChange(e.target.checked)}
/>
</Authorized>
);
}
export interface SystemResourcesTableSettings {
showSystemResources: boolean;
setShowSystemResources: (value: boolean) => void;
}
export function systemResourcesSettings<T extends SystemResourcesTableSettings>(
set: ZustandSetFunc<T>
): SystemResourcesTableSettings {
return {
showSystemResources: false,
setShowSystemResources(showSystemResources: boolean) {
set((s) => ({
...s,
showSystemResources,
}));
},
};
}

View file

@ -1,9 +1,7 @@
import { refreshableSettings, createPersistedStore } from '@@/datatables/types';
import {
systemResourcesSettings,
TableSettings,
} from './DefaultDatatableSettings';
import { TableSettings } from './DefaultDatatableSettings';
import { systemResourcesSettings } from './SystemResourcesSettings';
export function createStore(
storageKey: string,

View file

@ -75,17 +75,12 @@ export function IngressDatatable() {
renderTableActions={tableActions}
renderTableSettings={() => (
<TableSettingsMenu>
<DefaultDatatableSettings
settings={tableState}
hideShowSystemResources={!canAccessSystemResources}
/>
<DefaultDatatableSettings settings={tableState} />
</TableSettingsMenu>
)}
description={
<SystemResourceDescription
showSystemResources={
tableState.showSystemResources || !canAccessSystemResources
}
showSystemResources={tableState.showSystemResources}
/>
}
disableSelect={useCheckboxes()}

View file

@ -7,6 +7,10 @@ import { useEnvironmentId } from '@/react/hooks/useEnvironmentId';
import { Authorized, useAuthorizations } from '@/react/hooks/useUser';
import { notifyError, notifySuccess } from '@/portainer/services/notifications';
import { pluralize } from '@/portainer/helpers/strings';
import { DefaultDatatableSettings } from '@/react/kubernetes/datatables/DefaultDatatableSettings';
import { isSystemNamespace } from '@/react/kubernetes/namespaces/utils';
import { useNamespaces } from '@/react/kubernetes/namespaces/queries';
import { SystemResourceDescription } from '@/react/kubernetes/datatables/SystemResourceDescription';
import { Datatable, Table, TableSettingsMenu } from '@@/datatables';
import { confirmDelete } from '@@/modals/confirm';
@ -19,10 +23,6 @@ import {
useServicesForCluster,
} from '../../service';
import { Service } from '../../types';
import { DefaultDatatableSettings } from '../../../datatables/DefaultDatatableSettings';
import { isSystemNamespace } from '../../../namespaces/utils';
import { useNamespaces } from '../../../namespaces/queries';
import { SystemResourceDescription } from '../../../datatables/SystemResourceDescription';
import { columns } from './columns';
import { createStore } from './datatable-store';
@ -71,10 +71,7 @@ export function ServicesDatatable() {
)}
renderTableSettings={() => (
<TableSettingsMenu>
<DefaultDatatableSettings
settings={tableState}
hideShowSystemResources={!canAccessSystemResources}
/>
<DefaultDatatableSettings settings={tableState} />
</TableSettingsMenu>
)}
description={

View file

@ -1,9 +1,7 @@
import { refreshableSettings, createPersistedStore } from '@@/datatables/types';
import { TableSettings } from '@/react/kubernetes/datatables/DefaultDatatableSettings';
import { systemResourcesSettings } from '@/react/kubernetes/datatables/SystemResourcesSettings';
import {
systemResourcesSettings,
TableSettings,
} from '../../../datatables/DefaultDatatableSettings';
import { refreshableSettings, createPersistedStore } from '@@/datatables/types';
export function createStore(storageKey: string) {
return createPersistedStore<TableSettings>(storageKey, 'name', (set) => ({