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:
parent
65821aaccc
commit
07e7fbd270
80 changed files with 3614 additions and 1084 deletions
|
@ -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 },
|
||||
}));
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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',
|
||||
]
|
||||
);
|
|
@ -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 });
|
||||
}
|
||||
}
|
|
@ -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