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

refactor(kube/apps): convert placement table to react [EE-4662] (#8938)

This commit is contained in:
Chaim Lev-Ari 2023-07-29 17:08:41 +02:00 committed by GitHub
parent bf79ef7d89
commit 37ece734f0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 379 additions and 309 deletions

View file

@ -0,0 +1,66 @@
import { Minimize2 } from 'lucide-react';
import {
BasicTableSettings,
createPersistedStore,
refreshableSettings,
RefreshableTableSettings,
} from '@@/datatables/types';
import { ExpandableDatatable } from '@@/datatables/ExpandableDatatable';
import { useRepeater } from '@@/datatables/useRepeater';
import { TableSettingsMenu } from '@@/datatables';
import { TableSettingsMenuAutoRefresh } from '@@/datatables/TableSettingsMenuAutoRefresh';
import { useTableState } from '@@/datatables/useTableState';
import { Node } from '../types';
import { SubRow } from './PlacementsDatatableSubRow';
import { columns } from './columns';
interface TableSettings extends BasicTableSettings, RefreshableTableSettings {}
function createStore(storageKey: string) {
return createPersistedStore<TableSettings>(storageKey, 'node', (set) => ({
...refreshableSettings(set),
}));
}
const storageKey = 'kubernetes.application.placements';
const settingsStore = createStore(storageKey);
export function PlacementsDatatable({
dataset,
onRefresh,
}: {
dataset: Node[];
onRefresh: () => Promise<void>;
}) {
const tableState = useTableState(settingsStore, storageKey);
useRepeater(tableState.autoRefreshRate, onRefresh);
return (
<ExpandableDatatable
getRowCanExpand={(row) => !row.original.AcceptsApplication}
title="Placement constraints/preferences"
titleIcon={Minimize2}
dataset={dataset}
settingsManager={tableState}
columns={columns}
disableSelect
noWidget
renderTableSettings={() => (
<TableSettingsMenu>
<TableSettingsMenuAutoRefresh
value={tableState.autoRefreshRate}
onChange={tableState.setAutoRefreshRate}
/>
</TableSettingsMenu>
)}
emptyContentLabel="No node available."
renderSubRow={(row) => (
<SubRow node={row.original} cellCount={row.getVisibleCells().length} />
)}
/>
);
}

View file

@ -0,0 +1,189 @@
import clsx from 'clsx';
import { Fragment } from 'react';
import { nodeAffinityValues } from '@/kubernetes/filters/application';
import { useAuthorizations } from '@/react/hooks/useUser';
import { Affinity, Label, Node, Taint } from '../types';
interface SubRowProps {
node: Node;
cellCount: number;
}
export function SubRow({ node, cellCount }: SubRowProps) {
const authorized = useAuthorizations(
'K8sApplicationErrorDetailsR',
undefined,
true
);
if (!authorized) {
<>
{isDefined(node.UnmetTaints) && (
<tr
className={clsx({
'datatable-highlighted': node.Highlighted,
'datatable-unhighlighted': !node.Highlighted,
})}
>
<td colSpan={cellCount}>
Placement constraint not respected for that node.
</td>
</tr>
)}
{(isDefined(node.UnmatchedNodeSelectorLabels) ||
isDefined(node.UnmatchedNodeAffinities)) && (
<tr
className={clsx({
'datatable-highlighted': node.Highlighted,
'datatable-unhighlighted': !node.Highlighted,
})}
>
<td colSpan={cellCount}>
Placement label not respected for that node.
</td>
</tr>
)}
</>;
}
return (
<>
{isDefined(node.UnmetTaints) && (
<UnmetTaintsInfo
taints={node.UnmetTaints}
cellCount={cellCount}
isHighlighted={node.Highlighted}
/>
)}
{isDefined(node.UnmatchedNodeSelectorLabels) && (
<UnmatchedLabelsInfo
labels={node.UnmatchedNodeSelectorLabels}
cellCount={cellCount}
isHighlighted={node.Highlighted}
/>
)}
{isDefined(node.UnmatchedNodeAffinities) && (
<UnmatchedAffinitiesInfo
affinities={node.UnmatchedNodeAffinities}
cellCount={cellCount}
isHighlighted={node.Highlighted}
/>
)}
</>
);
}
function isDefined<T>(arr?: Array<T>): arr is Array<T> {
return !!arr && arr.length > 0;
}
function UnmetTaintsInfo({
taints,
isHighlighted,
cellCount,
}: {
taints: Array<Taint>;
isHighlighted: boolean;
cellCount: number;
}) {
return (
<>
{taints.map((taint) => (
<tr
className={clsx({
'datatable-highlighted': isHighlighted,
'datatable-unhighlighted': !isHighlighted,
})}
key={taint.Key}
>
<td colSpan={cellCount}>
This application is missing a toleration for the taint
<code className="space-left">
{taint.Key}
{taint.Value ? `=${taint.Value}` : ''}:{taint.Effect}
</code>
</td>
</tr>
))}
</>
);
}
function UnmatchedLabelsInfo({
labels,
isHighlighted,
cellCount,
}: {
labels: Array<Label>;
isHighlighted: boolean;
cellCount: number;
}) {
return (
<>
{labels.map((label) => (
<tr
className={clsx({
'datatable-highlighted': isHighlighted,
'datatable-unhighlighted': !isHighlighted,
})}
key={label.key}
>
<td colSpan={cellCount}>
This application can only be scheduled on a node where the label{' '}
<code>{label.key}</code> is set to <code>{label.value}</code>
</td>
</tr>
))}
</>
);
}
function UnmatchedAffinitiesInfo({
affinities,
isHighlighted,
cellCount,
}: {
affinities: Array<Affinity>;
isHighlighted: boolean;
cellCount: number;
}) {
return (
<>
<tr
className={clsx({
'datatable-highlighted': isHighlighted,
'datatable-unhighlighted': !isHighlighted,
})}
>
<td colSpan={cellCount}>
This application can only be scheduled on nodes respecting one of the
following labels combination:
</td>
</tr>
{affinities.map((aff) => (
<tr
className={clsx({
'datatable-highlighted': isHighlighted,
'datatable-unhighlighted': !isHighlighted,
})}
>
<td />
<td colSpan={cellCount - 1}>
{aff.map((term, index) => (
<Fragment key={index}>
<code>
{term.key} {term.operator}{' '}
{nodeAffinityValues(term.values, term.operator)}
</code>
<span>{index === aff.length - 1 ? '' : ' + '}</span>
</Fragment>
))}
</td>
</tr>
))}
</>
);
}

View file

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

View file

@ -0,0 +1,15 @@
import { buildExpandColumn } from '@@/datatables/expand-column';
import { Node } from '../../types';
import { columnHelper } from './helper';
import { status } from './status';
export const columns = [
buildExpandColumn<Node>(),
status,
columnHelper.accessor('Name', {
header: 'Node',
id: 'node',
}),
];

View file

@ -0,0 +1,23 @@
import { Check, X } from 'lucide-react';
import { Icon } from '@@/Icon';
import { columnHelper } from './helper';
export const status = columnHelper.accessor('AcceptsApplication', {
id: 'status',
cell: ({ getValue }) => {
const acceptsApplication = getValue();
return (
<Icon
icon={acceptsApplication ? Check : X}
mode={acceptsApplication ? 'success' : 'danger'}
size="sm"
/>
);
},
meta: {
width: 30,
},
enableResizing: false,
});

View file

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