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:
parent
bf79ef7d89
commit
37ece734f0
15 changed files with 379 additions and 309 deletions
|
@ -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} />
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
|
@ -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>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
import { createColumnHelper } from '@tanstack/react-table';
|
||||
|
||||
import { Node } from '../../types';
|
||||
|
||||
export const columnHelper = createColumnHelper<Node>();
|
|
@ -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',
|
||||
}),
|
||||
];
|
|
@ -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,
|
||||
});
|
|
@ -0,0 +1 @@
|
|||
export { PlacementsDatatable } from './PlacementsDatatable';
|
Loading…
Add table
Add a link
Reference in a new issue