1
0
Fork 0
mirror of https://github.com/portainer/portainer.git synced 2025-07-23 15:29:42 +02:00

refactor(containers): replace containers datatable with react component [EE-1815] (#6059)

This commit is contained in:
Chaim Lev-Ari 2022-01-04 14:16:09 +02:00 committed by GitHub
parent 65821aaccc
commit 07e7fbd270
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
80 changed files with 3614 additions and 1084 deletions

View file

@ -0,0 +1,249 @@
import { useEffect } from 'react';
import {
useTable,
useSortBy,
useFilters,
useGlobalFilter,
usePagination,
Row,
} from 'react-table';
import { useRowSelectColumn } from '@lineup-lite/hooks';
import { PaginationControls } from '@/portainer/components/pagination-controls';
import {
QuickActionsSettings,
buildAction,
} from '@/portainer/components/datatables/components/QuickActionsSettings';
import {
Table,
TableActions,
TableContainer,
TableHeaderRow,
TableRow,
TableSettingsMenu,
TableTitle,
TableTitleActions,
} from '@/portainer/components/datatables/components';
import { multiple } from '@/portainer/components/datatables/components/filter-types';
import { useTableSettings } from '@/portainer/components/datatables/components/useTableSettings';
import { ColumnVisibilityMenu } from '@/portainer/components/datatables/components/ColumnVisibilityMenu';
import { useRepeater } from '@/portainer/components/datatables/components/useRepeater';
import { useDebounce } from '@/portainer/hooks/useDebounce';
import {
useSearchBarContext,
SearchBar,
} from '@/portainer/components/datatables/components/SearchBar';
import type {
ContainersTableSettings,
DockerContainer,
} from '@/docker/containers/types';
import { useEnvironment } from '@/portainer/environments/useEnvironment';
import { useRowSelect } from '@/portainer/components/datatables/components/useRowSelect';
import { Checkbox } from '@/portainer/components/form-components/Checkbox';
import { TableFooter } from '@/portainer/components/datatables/components/TableFooter';
import { SelectedRowsCount } from '@/portainer/components/datatables/components/SelectedRowsCount';
import { ContainersDatatableActions } from './ContainersDatatableActions';
import { ContainersDatatableSettings } from './ContainersDatatableSettings';
import { useColumns } from './columns';
export interface ContainerTableProps {
isAddActionVisible: boolean;
dataset: DockerContainer[];
onRefresh(): Promise<void>;
isHostColumnVisible: boolean;
autoFocusSearch: boolean;
}
export function ContainersDatatable({
isAddActionVisible,
dataset,
onRefresh,
isHostColumnVisible,
autoFocusSearch,
}: ContainerTableProps) {
const { settings, setTableSettings } =
useTableSettings<ContainersTableSettings>();
const [searchBarValue, setSearchBarValue] = useSearchBarContext();
const columns = useColumns();
const endpoint = useEnvironment();
useRepeater(settings.autoRefreshRate, onRefresh);
const {
getTableProps,
getTableBodyProps,
headerGroups,
page,
prepareRow,
selectedFlatRows,
allColumns,
gotoPage,
setPageSize,
setHiddenColumns,
toggleHideColumn,
setGlobalFilter,
state: { pageIndex, pageSize },
} = useTable<DockerContainer>(
{
defaultCanFilter: false,
columns,
data: dataset,
filterTypes: { multiple },
initialState: {
pageSize: settings.pageSize || 10,
hiddenColumns: settings.hiddenColumns,
sortBy: [settings.sortBy],
globalFilter: searchBarValue,
},
isRowSelectable(row: Row<DockerContainer>) {
return !row.original.IsPortainer;
},
selectCheckboxComponent: Checkbox,
},
useFilters,
useGlobalFilter,
useSortBy,
usePagination,
useRowSelect,
useRowSelectColumn
);
const debouncedSearchValue = useDebounce(searchBarValue);
useEffect(() => {
setGlobalFilter(debouncedSearchValue);
}, [debouncedSearchValue, setGlobalFilter]);
useEffect(() => {
toggleHideColumn('host', !isHostColumnVisible);
}, [toggleHideColumn, isHostColumnVisible]);
const columnsToHide = allColumns.filter((colInstance) => {
const columnDef = columns.find((c) => c.id === colInstance.id);
return columnDef?.canHide;
});
const actions = [
buildAction('logs', 'Logs'),
buildAction('inspect', 'Inspect'),
buildAction('stats', 'Stats'),
buildAction('exec', 'Console'),
buildAction('attach', 'Attach'),
];
const tableProps = getTableProps();
const tbodyProps = getTableBodyProps();
return (
<TableContainer>
<TableTitle icon="fa-cubes" label="Containers">
<TableTitleActions>
<ColumnVisibilityMenu
columns={columnsToHide}
onChange={handleChangeColumnsVisibility}
value={settings.hiddenColumns}
/>
<TableSettingsMenu
quickActions={<QuickActionsSettings actions={actions} />}
>
<ContainersDatatableSettings />
</TableSettingsMenu>
</TableTitleActions>
</TableTitle>
<TableActions>
<ContainersDatatableActions
selectedItems={selectedFlatRows.map((row) => row.original)}
isAddActionVisible={isAddActionVisible}
endpointId={endpoint.Id}
/>
</TableActions>
<SearchBar
value={searchBarValue}
onChange={handleSearchBarChange}
autoFocus={autoFocusSearch}
/>
<Table
className={tableProps.className}
role={tableProps.role}
style={tableProps.style}
>
<thead>
{headerGroups.map((headerGroup) => {
const { key, className, role, style } =
headerGroup.getHeaderGroupProps();
return (
<TableHeaderRow<DockerContainer>
key={key}
className={className}
role={role}
style={style}
headers={headerGroup.headers}
onSortChange={handleSortChange}
/>
);
})}
</thead>
<tbody
className={tbodyProps.className}
role={tbodyProps.role}
style={tbodyProps.style}
>
{page.map((row) => {
prepareRow(row);
const { key, className, role, style } = row.getRowProps();
return (
<TableRow<DockerContainer>
cells={row.cells}
key={key}
className={className}
role={role}
style={style}
/>
);
})}
</tbody>
</Table>
<TableFooter>
<SelectedRowsCount value={selectedFlatRows.length} />
<PaginationControls
showAll
pageLimit={pageSize}
page={pageIndex + 1}
onPageChange={(p) => gotoPage(p - 1)}
totalCount={dataset.length}
onPageLimitChange={handlePageSizeChange}
/>
</TableFooter>
</TableContainer>
);
function handlePageSizeChange(pageSize: number) {
setPageSize(pageSize);
setTableSettings((settings) => ({ ...settings, pageSize }));
}
function handleChangeColumnsVisibility(hiddenColumns: string[]) {
setHiddenColumns(hiddenColumns);
setTableSettings((settings) => ({ ...settings, hiddenColumns }));
}
function handleSearchBarChange(value: string) {
setSearchBarValue(value);
}
function handleSortChange(id: string, desc: boolean) {
setTableSettings((settings) => ({
...settings,
sortBy: { id, desc },
}));
}
}

View file

@ -0,0 +1,288 @@
import { useRouter } from '@uirouter/react';
import * as notifications from '@/portainer/services/notifications';
import { useAuthorizations, Authorized } from '@/portainer/hooks/useUser';
import { Link } from '@/portainer/components/Link';
import { confirmContainerDeletion } from '@/portainer/services/modal.service/prompt';
import { setPortainerAgentTargetHeader } from '@/portainer/services/http-request.helper';
import type { ContainerId, DockerContainer } from '@/docker/containers/types';
import {
killContainer,
pauseContainer,
removeContainer,
restartContainer,
resumeContainer,
startContainer,
stopContainer,
} from '@/docker/containers/containers.service';
import type { EnvironmentId } from '@/portainer/environments/types';
import { ButtonGroup, Button } from '@/portainer/components/Button';
type ContainerServiceAction = (
endpointId: EnvironmentId,
containerId: ContainerId
) => Promise<void>;
interface Props {
selectedItems: DockerContainer[];
isAddActionVisible: boolean;
endpointId: EnvironmentId;
}
export function ContainersDatatableActions({
selectedItems,
isAddActionVisible,
endpointId,
}: Props) {
const selectedItemCount = selectedItems.length;
const hasPausedItemsSelected = selectedItems.some(
(item) => item.Status === 'paused'
);
const hasStoppedItemsSelected = selectedItems.some((item) =>
['stopped', 'created'].includes(item.Status)
);
const hasRunningItemsSelected = selectedItems.some((item) =>
['running', 'healthy', 'unhealthy', 'starting'].includes(item.Status)
);
const isAuthorized = useAuthorizations([
'DockerContainerStart',
'DockerContainerStop',
'DockerContainerKill',
'DockerContainerRestart',
'DockerContainerPause',
'DockerContainerUnpause',
'DockerContainerDelete',
'DockerContainerCreate',
]);
const router = useRouter();
if (!isAuthorized) {
return null;
}
return (
<div className="actionBar">
<ButtonGroup>
<Authorized authorizations="DockerContainerStart">
<Button
color="success"
onClick={() => onStartClick(selectedItems)}
disabled={selectedItemCount === 0 || !hasStoppedItemsSelected}
>
<i className="fa fa-play space-right" aria-hidden="true" />
Start
</Button>
</Authorized>
<Authorized authorizations="DockerContainerStop">
<Button
color="danger"
onClick={() => onStopClick(selectedItems)}
disabled={selectedItemCount === 0 || !hasRunningItemsSelected}
>
<i className="fa fa-stop space-right" aria-hidden="true" />
Stop
</Button>
</Authorized>
<Authorized authorizations="DockerContainerKill">
<Button
color="danger"
onClick={() => onKillClick(selectedItems)}
disabled={selectedItemCount === 0}
>
<i className="fa fa-bomb space-right" aria-hidden="true" />
Kill
</Button>
</Authorized>
<Authorized authorizations="DockerContainerRestart">
<Button
onClick={() => onRestartClick(selectedItems)}
disabled={selectedItemCount === 0}
>
<i className="fa fa-sync space-right" aria-hidden="true" />
Restart
</Button>
</Authorized>
<Authorized authorizations="DockerContainerPause">
<Button
onClick={() => onPauseClick(selectedItems)}
disabled={selectedItemCount === 0 || !hasRunningItemsSelected}
>
<i className="fa fa-pause space-right" aria-hidden="true" />
Pause
</Button>
</Authorized>
<Authorized authorizations="DockerContainerUnpause">
<Button
onClick={() => onResumeClick(selectedItems)}
disabled={selectedItemCount === 0 || !hasPausedItemsSelected}
>
<i className="fa fa-play space-right" aria-hidden="true" />
Resume
</Button>
</Authorized>
<Authorized authorizations="DockerContainerDelete">
<Button
color="danger"
onClick={() => onRemoveClick(selectedItems)}
disabled={selectedItemCount === 0}
>
<i className="fa fa-trash-alt space-right" aria-hidden="true" />
Remove
</Button>
</Authorized>
</ButtonGroup>
{isAddActionVisible && (
<Authorized authorizations="DockerContainerCreate">
<Link to="docker.containers.new" className="space-left">
<Button>
<i className="fa fa-plus space-right" aria-hidden="true" />
Add container
</Button>
</Link>
</Authorized>
)}
</div>
);
function onStartClick(selectedItems: DockerContainer[]) {
const successMessage = 'Container successfully started';
const errorMessage = 'Unable to start container';
executeActionOnContainerList(
selectedItems,
startContainer,
successMessage,
errorMessage
);
}
function onStopClick(selectedItems: DockerContainer[]) {
const successMessage = 'Container successfully stopped';
const errorMessage = 'Unable to stop container';
executeActionOnContainerList(
selectedItems,
stopContainer,
successMessage,
errorMessage
);
}
function onRestartClick(selectedItems: DockerContainer[]) {
const successMessage = 'Container successfully restarted';
const errorMessage = 'Unable to restart container';
executeActionOnContainerList(
selectedItems,
restartContainer,
successMessage,
errorMessage
);
}
function onKillClick(selectedItems: DockerContainer[]) {
const successMessage = 'Container successfully killed';
const errorMessage = 'Unable to kill container';
executeActionOnContainerList(
selectedItems,
killContainer,
successMessage,
errorMessage
);
}
function onPauseClick(selectedItems: DockerContainer[]) {
const successMessage = 'Container successfully paused';
const errorMessage = 'Unable to pause container';
executeActionOnContainerList(
selectedItems,
pauseContainer,
successMessage,
errorMessage
);
}
function onResumeClick(selectedItems: DockerContainer[]) {
const successMessage = 'Container successfully resumed';
const errorMessage = 'Unable to resume container';
executeActionOnContainerList(
selectedItems,
resumeContainer,
successMessage,
errorMessage
);
}
function onRemoveClick(selectedItems: DockerContainer[]) {
const isOneContainerRunning = selectedItems.some(
(container) => container.Status === 'running'
);
const runningTitle = isOneContainerRunning ? 'running' : '';
const title = `You are about to remove one or more ${runningTitle} containers.`;
confirmContainerDeletion(title, (result: string[]) => {
if (!result) {
return;
}
const cleanVolumes = !!result[0];
removeSelectedContainers(selectedItems, cleanVolumes);
});
}
async function executeActionOnContainerList(
containers: DockerContainer[],
action: ContainerServiceAction,
successMessage: string,
errorMessage: string
) {
for (let i = 0; i < containers.length; i += 1) {
const container = containers[i];
try {
setPortainerAgentTargetHeader(container.NodeName);
await action(endpointId, container.Id);
notifications.success(successMessage, container.Names[0]);
} catch (err) {
notifications.error(
'Failure',
err as Error,
`${errorMessage}:${container.Names[0]}`
);
}
}
router.stateService.reload();
}
async function removeSelectedContainers(
containers: DockerContainer[],
cleanVolumes: boolean
) {
for (let i = 0; i < containers.length; i += 1) {
const container = containers[i];
try {
setPortainerAgentTargetHeader(container.NodeName);
await removeContainer(endpointId, container, cleanVolumes);
notifications.success(
'Container successfully removed',
container.Names[0]
);
} catch (err) {
notifications.error(
'Failure',
err as Error,
'Unable to remove container'
);
}
}
router.stateService.reload();
}
}

View file

@ -0,0 +1,52 @@
import { react2angular } from '@/react-tools/react2angular';
import { EnvironmentProvider } from '@/portainer/environments/useEnvironment';
import { TableSettingsProvider } from '@/portainer/components/datatables/components/useTableSettings';
import { SearchBarProvider } from '@/portainer/components/datatables/components/SearchBar';
import type { Environment } from '@/portainer/environments/types';
import {
ContainersDatatable,
ContainerTableProps,
} from './ContainersDatatable';
interface Props extends ContainerTableProps {
endpoint: Environment;
}
export function ContainersDatatableContainer({ endpoint, ...props }: Props) {
const defaultSettings = {
autoRefreshRate: 0,
truncateContainerName: 32,
hiddenQuickActions: [],
hiddenColumns: [],
pageSize: 10,
sortBy: { id: 'state', desc: false },
};
return (
<EnvironmentProvider environment={endpoint}>
<TableSettingsProvider defaults={defaultSettings} storageKey="containers">
<SearchBarProvider>
{/* eslint-disable-next-line react/jsx-props-no-spreading */}
<ContainersDatatable {...props} />
</SearchBarProvider>
</TableSettingsProvider>
</EnvironmentProvider>
);
}
export const ContainersDatatableAngular = react2angular(
ContainersDatatableContainer,
[
'endpoint',
'isAddActionVisible',
'containerService',
'httpRequestHelper',
'notifications',
'modalService',
'dataset',
'onRefresh',
'isHostColumnVisible',
'autoFocusSearch',
]
);

View file

@ -0,0 +1,35 @@
import { TableSettingsMenuAutoRefresh } from '@/portainer/components/datatables/components/TableSettingsMenuAutoRefresh';
import { useTableSettings } from '@/portainer/components/datatables/components/useTableSettings';
import { Checkbox } from '@/portainer/components/form-components/Checkbox';
import type { ContainersTableSettings } from '@/docker/containers/types';
export function ContainersDatatableSettings() {
const { settings, setTableSettings } = useTableSettings<
ContainersTableSettings
>();
return (
<>
<Checkbox
id="settings-container-truncate-nae"
label="Truncate container name"
checked={settings.truncateContainerName > 0}
onChange={() =>
setTableSettings((settings) => ({
...settings,
truncateContainerName: settings.truncateContainerName > 0 ? 0 : 32,
}))
}
/>
<TableSettingsMenuAutoRefresh
value={settings.autoRefreshRate}
onChange={handleRefreshRateChange}
/>
</>
);
function handleRefreshRateChange(autoRefreshRate: number) {
setTableSettings({ autoRefreshRate });
}
}

View file

@ -0,0 +1,14 @@
import { Column } from 'react-table';
import { isoDateFromTimestamp } from '@/portainer/filters/filters';
import type { DockerContainer } from '@/docker/containers/types';
export const created: Column<DockerContainer> = {
Header: 'Created',
accessor: 'Created',
id: 'created',
Cell: ({ value }) => isoDateFromTimestamp(value),
disableFilters: true,
canHide: true,
Filter: () => null,
};

View file

@ -0,0 +1,13 @@
import { Column } from 'react-table';
import type { DockerContainer } from '@/docker/containers/types';
export const host: Column<DockerContainer> = {
Header: 'Host',
accessor: (row) => row.NodeName || '-',
id: 'host',
disableFilters: true,
canHide: true,
sortType: 'string',
Filter: () => null,
};

View file

@ -0,0 +1,51 @@
import { Column } from 'react-table';
import { useSref } from '@uirouter/react';
import { useEnvironment } from '@/portainer/environments/useEnvironment';
import { EnvironmentStatus } from '@/portainer/environments/types';
import type { DockerContainer } from '@/docker/containers/types';
export const image: Column<DockerContainer> = {
Header: 'Image',
accessor: 'Image',
id: 'image',
disableFilters: true,
Cell: ImageCell,
canHide: true,
sortType: 'string',
Filter: () => null,
};
interface Props {
value: string;
}
function ImageCell({ value: imageName }: Props) {
const endpoint = useEnvironment();
const offlineMode = endpoint.Status !== EnvironmentStatus.Up;
const shortImageName = trimSHASum(imageName);
const linkProps = useSref('docker.images.image', { id: imageName });
if (offlineMode) {
return shortImageName;
}
return (
<a href={linkProps.href} onClick={linkProps.onClick}>
{shortImageName}
</a>
);
function trimSHASum(imageName: string) {
if (!imageName) {
return '';
}
if (imageName.indexOf('sha256:') === 0) {
return imageName.substring(7, 19);
}
return imageName.split('@sha256')[0];
}
}

View file

@ -0,0 +1,30 @@
import { useMemo } from 'react';
import { created } from './created';
import { host } from './host';
import { image } from './image';
import { ip } from './ip';
import { name } from './name';
import { ownership } from './ownership';
import { ports } from './ports';
import { quickActions } from './quick-actions';
import { stack } from './stack';
import { state } from './state';
export function useColumns() {
return useMemo(
() => [
name,
state,
quickActions,
stack,
image,
created,
ip,
host,
ports,
ownership,
],
[]
);
}

View file

@ -0,0 +1,12 @@
import { Column } from 'react-table';
import type { DockerContainer } from '@/docker/containers/types';
export const ip: Column<DockerContainer> = {
Header: 'IP Address',
accessor: (row) => row.IP || '-',
id: 'ip',
disableFilters: true,
canHide: true,
Filter: () => null,
};

View file

@ -0,0 +1,54 @@
import { CellProps, Column, TableInstance } from 'react-table';
import _ from 'lodash-es';
import { useSref } from '@uirouter/react';
import { useEnvironment } from '@/portainer/environments/useEnvironment';
import { useTableSettings } from '@/portainer/components/datatables/components/useTableSettings';
import type {
ContainersTableSettings,
DockerContainer,
} from '@/docker/containers/types';
export const name: Column<DockerContainer> = {
Header: 'Name',
accessor: (row) => {
const name = row.Names[0];
return name.substring(1, name.length);
},
id: 'name',
Cell: NameCell,
disableFilters: true,
Filter: () => null,
canHide: true,
sortType: 'string',
};
export function NameCell({
value: name,
row: { original: container },
}: CellProps<TableInstance>) {
const { settings } = useTableSettings<ContainersTableSettings>();
const truncate = settings.truncateContainerName;
const endpoint = useEnvironment();
const offlineMode = endpoint.Status !== 1;
const linkProps = useSref('docker.containers.container', {
id: container.Id,
nodeName: container.NodeName,
});
let shortName = name;
if (truncate > 0) {
shortName = _.truncate(name, { length: truncate });
}
if (offlineMode) {
return <span>{shortName}</span>;
}
return (
<a href={linkProps.href} onClick={linkProps.onClick} title={name}>
{shortName}
</a>
);
}

View file

@ -0,0 +1,34 @@
import { Column } from 'react-table';
import clsx from 'clsx';
import { ownershipIcon } from '@/portainer/filters/filters';
import { ResourceControlOwnership } from '@/portainer/models/resourceControl/resourceControlOwnership';
import type { DockerContainer } from '@/docker/containers/types';
export const ownership: Column<DockerContainer> = {
Header: 'Ownership',
id: 'ownership',
accessor: (row) =>
row.ResourceControl?.Ownership || ResourceControlOwnership.ADMINISTRATORS,
Cell: OwnershipCell,
disableFilters: true,
canHide: true,
sortType: 'string',
Filter: () => null,
};
interface Props {
value: 'public' | 'private' | 'restricted' | 'administrators';
}
function OwnershipCell({ value }: Props) {
return (
<>
<i
className={clsx(ownershipIcon(value), 'space-right')}
aria-hidden="true"
/>
{value || ResourceControlOwnership.ADMINISTRATORS}
</>
);
}

View file

@ -0,0 +1,41 @@
import { Column } from 'react-table';
import _ from 'lodash-es';
import { useEnvironment } from '@/portainer/environments/useEnvironment';
import type { DockerContainer, Port } from '@/docker/containers/types';
export const ports: Column<DockerContainer> = {
Header: 'Published Ports',
accessor: 'Ports',
id: 'ports',
Cell: PortsCell,
disableSortBy: true,
disableFilters: true,
canHide: true,
Filter: () => null,
};
interface Props {
value: Port[];
}
function PortsCell({ value: ports }: Props) {
const { PublicURL: publicUrl } = useEnvironment();
if (ports.length === 0) {
return '-';
}
return _.uniqBy(ports, 'public').map((port) => (
<a
key={`${port.host}:${port.public}`}
className="image-tag"
href={`http://${publicUrl || port.host}:${port.public}`}
target="_blank"
rel="noreferrer"
>
<i className="fa fa-external-link-alt" aria-hidden="true" />
{port.public}:{port.private}
</a>
));
}

View file

@ -0,0 +1,70 @@
import { CellProps, Column } from 'react-table';
import { useTableSettings } from '@/portainer/components/datatables/components/useTableSettings';
import { useEnvironment } from '@/portainer/environments/useEnvironment';
import { useAuthorizations } from '@/portainer/hooks/useUser';
import { ContainerQuickActions } from '@/docker/components/container-quick-actions/ContainerQuickActions';
import type {
ContainersTableSettings,
DockerContainer,
} from '@/docker/containers/types';
import { EnvironmentStatus } from '@/portainer/environments/types';
export const quickActions: Column<DockerContainer> = {
Header: 'Quick Actions',
id: 'actions',
Cell: QuickActionsCell,
disableFilters: true,
disableSortBy: true,
canHide: true,
sortType: 'string',
Filter: () => null,
};
function QuickActionsCell({
row: { original: container },
}: CellProps<DockerContainer>) {
const endpoint = useEnvironment();
const offlineMode = endpoint.Status !== EnvironmentStatus.Up;
const { settings } = useTableSettings<ContainersTableSettings>();
const { hiddenQuickActions = [] } = settings;
const wrapperState = {
showQuickActionAttach: !hiddenQuickActions.includes('attach'),
showQuickActionExec: !hiddenQuickActions.includes('exec'),
showQuickActionInspect: !hiddenQuickActions.includes('inspect'),
showQuickActionLogs: !hiddenQuickActions.includes('logs'),
showQuickActionStats: !hiddenQuickActions.includes('stats'),
};
const someOn =
wrapperState.showQuickActionAttach ||
wrapperState.showQuickActionExec ||
wrapperState.showQuickActionInspect ||
wrapperState.showQuickActionLogs ||
wrapperState.showQuickActionStats;
const isAuthorized = useAuthorizations([
'DockerContainerStats',
'DockerContainerLogs',
'DockerExecStart',
'DockerContainerInspect',
'DockerTaskInspect',
'DockerTaskLogs',
]);
if (offlineMode || !someOn || !isAuthorized) {
return null;
}
return (
<ContainerQuickActions
containerId={container.Id}
nodeName={container.NodeName}
status={container.Status}
state={wrapperState}
/>
);
}

View file

@ -0,0 +1,13 @@
import { Column } from 'react-table';
import type { DockerContainer } from '@/docker/containers/types';
export const stack: Column<DockerContainer> = {
Header: 'Stack',
accessor: (row) => row.StackName || '-',
id: 'stack',
sortType: 'string',
disableFilters: true,
canHide: true,
Filter: () => null,
};

View file

@ -0,0 +1,60 @@
import { Column } from 'react-table';
import clsx from 'clsx';
import _ from 'lodash-es';
import { DefaultFilter } from '@/portainer/components/datatables/components/Filter';
import type {
DockerContainer,
DockerContainerStatus,
} from '@/docker/containers/types';
export const state: Column<DockerContainer> = {
Header: 'State',
accessor: 'Status',
id: 'state',
Cell: StatusCell,
sortType: 'string',
filter: 'multiple',
Filter: DefaultFilter,
canHide: true,
};
function StatusCell({ value: status }: { value: DockerContainerStatus }) {
const statusNormalized = _.toLower(status);
const hasHealthCheck = ['starting', 'healthy', 'unhealthy'].includes(
statusNormalized
);
const statusClassName = getClassName();
return (
<span
className={clsx('label', `label-${statusClassName}`, {
interactive: hasHealthCheck,
})}
title={hasHealthCheck ? 'This container has a health check' : ''}
>
{status}
</span>
);
function getClassName() {
if (includeString(['paused', 'starting', 'unhealthy'])) {
return 'warning';
}
if (includeString(['created'])) {
return 'info';
}
if (includeString(['stopped', 'dead', 'exited'])) {
return 'danger';
}
return 'success';
function includeString(values: DockerContainerStatus[]) {
return values.some((val) => statusNormalized.includes(val));
}
}
}