mirror of
https://github.com/portainer/portainer.git
synced 2025-07-24 07:49:41 +02:00
refactor(containers): replace containers datatable with react component [EE-1815] (#6059)
This commit is contained in:
parent
65821aaccc
commit
07e7fbd270
80 changed files with 3614 additions and 1084 deletions
|
@ -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,
|
||||
};
|
|
@ -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,
|
||||
};
|
|
@ -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];
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
],
|
||||
[]
|
||||
);
|
||||
}
|
|
@ -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,
|
||||
};
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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}
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -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>
|
||||
));
|
||||
}
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
|
@ -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,
|
||||
};
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue