1
0
Fork 0
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:
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,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));
}
}
}