mirror of
https://github.com/portainer/portainer.git
synced 2025-07-23 15:29:42 +02:00
refactor(containers): migrate view to react [EE-2212] (#6577)
Co-authored-by: LP B <xAt0mZ@users.noreply.github.com>
This commit is contained in:
parent
5ee570e075
commit
bed4257194
71 changed files with 1616 additions and 875 deletions
|
@ -1,256 +1,105 @@
|
|||
import { useEffect } from 'react';
|
||||
import {
|
||||
useTable,
|
||||
useSortBy,
|
||||
useFilters,
|
||||
useGlobalFilter,
|
||||
usePagination,
|
||||
Row,
|
||||
} from 'react-table';
|
||||
import { useRowSelectColumn } from '@lineup-lite/hooks';
|
||||
import _ from 'lodash';
|
||||
|
||||
import { useDebounce } from '@/portainer/hooks/useDebounce';
|
||||
import type {
|
||||
ContainersTableSettings,
|
||||
DockerContainer,
|
||||
} from '@/react/docker/containers/types';
|
||||
import { useEnvironment } from '@/portainer/environments/useEnvironment';
|
||||
import { Environment } from '@/portainer/environments/types';
|
||||
import type { DockerContainer } from '@/react/docker/containers/types';
|
||||
|
||||
import { PaginationControls } from '@@/PaginationControls';
|
||||
import { TableSettingsMenu, Datatable } from '@@/datatables';
|
||||
import {
|
||||
QuickActionsSettings,
|
||||
buildAction,
|
||||
QuickActionsSettings,
|
||||
} from '@@/datatables/QuickActionsSettings';
|
||||
import {
|
||||
Table,
|
||||
TableActions,
|
||||
TableContainer,
|
||||
TableHeaderRow,
|
||||
TableRow,
|
||||
TableSettingsMenu,
|
||||
TableTitle,
|
||||
TableTitleActions,
|
||||
} from '@@/datatables';
|
||||
import { multiple } from '@@/datatables/filter-types';
|
||||
import { useTableSettings } from '@@/datatables/useTableSettings';
|
||||
import { ColumnVisibilityMenu } from '@@/datatables/ColumnVisibilityMenu';
|
||||
import { useRepeater } from '@@/datatables/useRepeater';
|
||||
import { SearchBar, useSearchBarState } from '@@/datatables/SearchBar';
|
||||
import { useRowSelect } from '@@/datatables/useRowSelect';
|
||||
import { Checkbox } from '@@/form-components/Checkbox';
|
||||
import { TableFooter } from '@@/datatables/TableFooter';
|
||||
import { SelectedRowsCount } from '@@/datatables/SelectedRowsCount';
|
||||
|
||||
import { ContainersDatatableActions } from './ContainersDatatableActions';
|
||||
import { useContainers } from '../../queries/containers';
|
||||
|
||||
import { createStore } from './datatable-store';
|
||||
import { ContainersDatatableSettings } from './ContainersDatatableSettings';
|
||||
import { useColumns } from './columns';
|
||||
import { ContainersDatatableActions } from './ContainersDatatableActions';
|
||||
import { RowProvider } from './RowContext';
|
||||
|
||||
export interface ContainerTableProps {
|
||||
isAddActionVisible: boolean;
|
||||
dataset: DockerContainer[];
|
||||
onRefresh?(): Promise<void>;
|
||||
const storageKey = 'containers';
|
||||
const useStore = createStore(storageKey);
|
||||
|
||||
const actions = [
|
||||
buildAction('logs', 'Logs'),
|
||||
buildAction('inspect', 'Inspect'),
|
||||
buildAction('stats', 'Stats'),
|
||||
buildAction('exec', 'Console'),
|
||||
buildAction('attach', 'Attach'),
|
||||
];
|
||||
|
||||
export interface Props {
|
||||
isHostColumnVisible: boolean;
|
||||
tableKey?: string;
|
||||
environment: Environment;
|
||||
}
|
||||
|
||||
export function ContainersDatatable({
|
||||
isAddActionVisible,
|
||||
dataset,
|
||||
onRefresh,
|
||||
isHostColumnVisible,
|
||||
}: ContainerTableProps) {
|
||||
const { settings, setTableSettings } =
|
||||
useTableSettings<ContainersTableSettings>();
|
||||
const [searchBarValue, setSearchBarValue] = useSearchBarState('containers');
|
||||
|
||||
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;
|
||||
},
|
||||
autoResetSelectedRows: false,
|
||||
getRowId(originalRow: DockerContainer) {
|
||||
return originalRow.Id;
|
||||
},
|
||||
selectCheckboxComponent: Checkbox,
|
||||
},
|
||||
useFilters,
|
||||
useGlobalFilter,
|
||||
useSortBy,
|
||||
usePagination,
|
||||
useRowSelect,
|
||||
useRowSelectColumn
|
||||
environment,
|
||||
}: Props) {
|
||||
const settings = useStore();
|
||||
const columns = useColumns(isHostColumnVisible);
|
||||
const hidableColumns = _.compact(
|
||||
columns.filter((col) => col.canHide).map((col) => col.id)
|
||||
);
|
||||
|
||||
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();
|
||||
const containersQuery = useContainers(
|
||||
environment.Id,
|
||||
true,
|
||||
undefined,
|
||||
settings.autoRefreshRate * 1000
|
||||
);
|
||||
|
||||
return (
|
||||
<TableContainer>
|
||||
<TableTitle icon="box" featherIcon label="Containers">
|
||||
<SearchBar
|
||||
value={searchBarValue}
|
||||
onChange={handleSearchBarChange}
|
||||
placeholder="Search for a container..."
|
||||
/>
|
||||
<TableActions>
|
||||
<RowProvider context={{ environment }}>
|
||||
<Datatable
|
||||
titleOptions={{
|
||||
icon: 'fa-cubes',
|
||||
title: 'Containers',
|
||||
}}
|
||||
settingsStore={settings}
|
||||
columns={columns}
|
||||
renderTableActions={(selectedRows) => (
|
||||
<ContainersDatatableActions
|
||||
selectedItems={selectedFlatRows.map((row) => row.original)}
|
||||
isAddActionVisible={isAddActionVisible}
|
||||
endpointId={endpoint.Id}
|
||||
/>
|
||||
</TableActions>
|
||||
<TableTitleActions>
|
||||
<ColumnVisibilityMenu<DockerContainer>
|
||||
columns={columnsToHide}
|
||||
onChange={handleChangeColumnsVisibility}
|
||||
value={settings.hiddenColumns}
|
||||
selectedItems={selectedRows}
|
||||
isAddActionVisible
|
||||
endpointId={environment.Id}
|
||||
/>
|
||||
)}
|
||||
isLoading={containersQuery.isLoading}
|
||||
isRowSelectable={(row) => !row.original.IsPortainer}
|
||||
initialTableState={{ hiddenColumns: settings.hiddenColumns }}
|
||||
renderTableSettings={(tableInstance) => {
|
||||
const columnsToHide = tableInstance.allColumns.filter((colInstance) =>
|
||||
hidableColumns?.includes(colInstance.id)
|
||||
);
|
||||
|
||||
<TableSettingsMenu
|
||||
quickActions={<QuickActionsSettings actions={actions} />}
|
||||
>
|
||||
<ContainersDatatableSettings isRefreshVisible={!!onRefresh} />
|
||||
</TableSettingsMenu>
|
||||
</TableTitleActions>
|
||||
</TableTitle>
|
||||
|
||||
<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}
|
||||
return (
|
||||
<>
|
||||
<ColumnVisibilityMenu<DockerContainer>
|
||||
columns={columnsToHide}
|
||||
onChange={(hiddenColumns) => {
|
||||
settings.setHiddenColumns(hiddenColumns);
|
||||
tableInstance.setHiddenColumns(hiddenColumns);
|
||||
}}
|
||||
value={settings.hiddenColumns}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</thead>
|
||||
<tbody
|
||||
className={tbodyProps.className}
|
||||
role={tbodyProps.role}
|
||||
style={tbodyProps.style}
|
||||
>
|
||||
{page.length > 0 ? (
|
||||
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}
|
||||
<TableSettingsMenu
|
||||
quickActions={<QuickActionsSettings actions={actions} />}
|
||||
>
|
||||
<ContainersDatatableSettings
|
||||
isRefreshVisible
|
||||
settings={settings}
|
||||
/>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<tr>
|
||||
<td colSpan={columns.length} className="text-center text-muted">
|
||||
No container available.
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</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>
|
||||
</TableSettingsMenu>
|
||||
</>
|
||||
);
|
||||
}}
|
||||
storageKey={storageKey}
|
||||
dataset={containersQuery.data || []}
|
||||
emptyContentLabel="No containers found"
|
||||
/>
|
||||
</RowProvider>
|
||||
);
|
||||
|
||||
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 },
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,8 +4,9 @@ import * as notifications from '@/portainer/services/notifications';
|
|||
import { useAuthorizations, Authorized } from '@/portainer/hooks/useUser';
|
||||
import { confirmContainerDeletion } from '@/portainer/services/modal.service/prompt';
|
||||
import { setPortainerAgentTargetHeader } from '@/portainer/services/http-request.helper';
|
||||
import type {
|
||||
import {
|
||||
ContainerId,
|
||||
ContainerStatus,
|
||||
DockerContainer,
|
||||
} from '@/react/docker/containers/types';
|
||||
import {
|
||||
|
@ -40,13 +41,22 @@ export function ContainersDatatableActions({
|
|||
}: Props) {
|
||||
const selectedItemCount = selectedItems.length;
|
||||
const hasPausedItemsSelected = selectedItems.some(
|
||||
(item) => item.Status === 'paused'
|
||||
(item) => item.State === ContainerStatus.Paused
|
||||
);
|
||||
const hasStoppedItemsSelected = selectedItems.some((item) =>
|
||||
['stopped', 'created'].includes(item.Status)
|
||||
[
|
||||
ContainerStatus.Stopped,
|
||||
ContainerStatus.Created,
|
||||
ContainerStatus.Exited,
|
||||
].includes(item.Status)
|
||||
);
|
||||
const hasRunningItemsSelected = selectedItems.some((item) =>
|
||||
['running', 'healthy', 'unhealthy', 'starting'].includes(item.Status)
|
||||
[
|
||||
ContainerStatus.Running,
|
||||
ContainerStatus.Healthy,
|
||||
ContainerStatus.Unhealthy,
|
||||
ContainerStatus.Starting,
|
||||
].includes(item.Status)
|
||||
);
|
||||
|
||||
const isAuthorized = useAuthorizations([
|
||||
|
@ -95,7 +105,7 @@ export function ContainersDatatableActions({
|
|||
<Button
|
||||
color="light"
|
||||
onClick={() => onKillClick(selectedItems)}
|
||||
disabled={selectedItemCount === 0}
|
||||
disabled={selectedItemCount === 0 || hasStoppedItemsSelected}
|
||||
>
|
||||
<i className="fa fa-bomb space-right" aria-hidden="true" />
|
||||
Kill
|
||||
|
@ -228,7 +238,7 @@ export function ContainersDatatableActions({
|
|||
|
||||
function onRemoveClick(selectedItems: DockerContainer[]) {
|
||||
const isOneContainerRunning = selectedItems.some(
|
||||
(container) => container.Status === 'running'
|
||||
(container) => container.State === 'running'
|
||||
);
|
||||
|
||||
const runningTitle = isOneContainerRunning ? 'running' : '';
|
||||
|
|
|
@ -1,37 +0,0 @@
|
|||
import { EnvironmentProvider } from '@/portainer/environments/useEnvironment';
|
||||
import type { Environment } from '@/portainer/environments/types';
|
||||
|
||||
import { TableSettingsProvider } from '@@/datatables/useTableSettings';
|
||||
|
||||
import {
|
||||
ContainersDatatable,
|
||||
ContainerTableProps,
|
||||
} from './ContainersDatatable';
|
||||
|
||||
interface Props extends ContainerTableProps {
|
||||
endpoint: Environment;
|
||||
}
|
||||
|
||||
export function ContainersDatatableContainer({
|
||||
endpoint,
|
||||
tableKey = 'containers',
|
||||
...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={tableKey}>
|
||||
{/* eslint-disable-next-line react/jsx-props-no-spreading */}
|
||||
<ContainersDatatable {...props} />
|
||||
</TableSettingsProvider>
|
||||
</EnvironmentProvider>
|
||||
);
|
||||
}
|
|
@ -1,17 +1,18 @@
|
|||
import type { ContainersTableSettings } from '@/react/docker/containers/types';
|
||||
|
||||
import { TableSettingsMenuAutoRefresh } from '@@/datatables/TableSettingsMenuAutoRefresh';
|
||||
import { useTableSettings } from '@@/datatables/useTableSettings';
|
||||
import { Checkbox } from '@@/form-components/Checkbox';
|
||||
import { TableSettingsMenuAutoRefresh } from '@@/datatables/TableSettingsMenuAutoRefresh';
|
||||
|
||||
import { TableSettings } from './types';
|
||||
import { TRUNCATE_LENGTH } from './datatable-store';
|
||||
|
||||
interface Props {
|
||||
isRefreshVisible: boolean;
|
||||
isRefreshVisible?: boolean;
|
||||
settings: TableSettings;
|
||||
}
|
||||
|
||||
export function ContainersDatatableSettings({ isRefreshVisible }: Props) {
|
||||
const { settings, setTableSettings } =
|
||||
useTableSettings<ContainersTableSettings>();
|
||||
|
||||
export function ContainersDatatableSettings({
|
||||
isRefreshVisible,
|
||||
settings,
|
||||
}: Props) {
|
||||
return (
|
||||
<>
|
||||
<Checkbox
|
||||
|
@ -19,23 +20,18 @@ export function ContainersDatatableSettings({ isRefreshVisible }: Props) {
|
|||
label="Truncate container name"
|
||||
checked={settings.truncateContainerName > 0}
|
||||
onChange={() =>
|
||||
setTableSettings((settings) => ({
|
||||
...settings,
|
||||
truncateContainerName: settings.truncateContainerName > 0 ? 0 : 32,
|
||||
}))
|
||||
settings.setTruncateContainerName(
|
||||
settings.truncateContainerName > 0 ? 0 : TRUNCATE_LENGTH
|
||||
)
|
||||
}
|
||||
/>
|
||||
|
||||
{isRefreshVisible && (
|
||||
<TableSettingsMenuAutoRefresh
|
||||
value={settings.autoRefreshRate}
|
||||
onChange={handleRefreshRateChange}
|
||||
onChange={(value) => settings.setAutoRefreshRate(value)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
function handleRefreshRateChange(autoRefreshRate: number) {
|
||||
setTableSettings({ autoRefreshRate });
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
import { Environment } from '@/portainer/environments/types';
|
||||
|
||||
import { createRowContext } from '@@/datatables/RowContext';
|
||||
|
||||
interface RowContextState {
|
||||
environment: Environment;
|
||||
}
|
||||
|
||||
const { RowProvider, useRowContext } = createRowContext<RowContextState>();
|
||||
|
||||
export { RowProvider, useRowContext };
|
|
@ -1,12 +1,28 @@
|
|||
import { Column } from 'react-table';
|
||||
import { CellProps, Column } from 'react-table';
|
||||
|
||||
import type { DockerContainer } from '@/react/docker/containers/types';
|
||||
import { useEnvironmentId } from '@/portainer/hooks/useEnvironmentId';
|
||||
import { useContainerGpus } from '@/react/docker/containers/queries/gpus';
|
||||
|
||||
export const gpus: Column<DockerContainer> = {
|
||||
Header: 'GPUs',
|
||||
accessor: 'Gpus',
|
||||
id: 'gpus',
|
||||
disableFilters: true,
|
||||
canHide: true,
|
||||
Filter: () => null,
|
||||
Cell: GpusCell,
|
||||
};
|
||||
|
||||
function GpusCell({
|
||||
row: { original: container },
|
||||
}: CellProps<DockerContainer>) {
|
||||
const containerId = container.Id;
|
||||
const environmentId = useEnvironmentId();
|
||||
const gpusQuery = useContainerGpus(environmentId, containerId);
|
||||
|
||||
if (!gpusQuery.data) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <>{gpusQuery.data}</>;
|
||||
}
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
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 '@/react/docker/containers/types';
|
||||
import { isOfflineEndpoint } from '@/portainer/helpers/endpointHelper';
|
||||
import { useCurrentEnvironment } from '@/portainer/hooks/useCurrentEnvironment';
|
||||
|
||||
export const image: Column<DockerContainer> = {
|
||||
Header: 'Image',
|
||||
|
@ -21,14 +21,15 @@ interface Props {
|
|||
}
|
||||
|
||||
function ImageCell({ value: imageName }: Props) {
|
||||
const endpoint = useEnvironment();
|
||||
const offlineMode = endpoint.Status !== EnvironmentStatus.Up;
|
||||
|
||||
const linkProps = useSref('docker.images.image', { id: imageName });
|
||||
const shortImageName = trimSHASum(imageName);
|
||||
|
||||
const linkProps = useSref('docker.images.image', { id: imageName });
|
||||
if (offlineMode) {
|
||||
return shortImageName;
|
||||
const environmentQuery = useCurrentEnvironment();
|
||||
|
||||
const environment = environmentQuery.data;
|
||||
|
||||
if (!environment || isOfflineEndpoint(environment)) {
|
||||
return <span>{shortImageName}</span>;
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import _ from 'lodash';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { created } from './created';
|
||||
|
@ -12,21 +13,22 @@ import { stack } from './stack';
|
|||
import { state } from './state';
|
||||
import { gpus } from './gpus';
|
||||
|
||||
export function useColumns() {
|
||||
export function useColumns(isHostColumnVisible: boolean) {
|
||||
return useMemo(
|
||||
() => [
|
||||
name,
|
||||
state,
|
||||
quickActions,
|
||||
stack,
|
||||
image,
|
||||
created,
|
||||
ip,
|
||||
host,
|
||||
gpus,
|
||||
ports,
|
||||
ownership,
|
||||
],
|
||||
[]
|
||||
() =>
|
||||
_.compact([
|
||||
name,
|
||||
state,
|
||||
quickActions,
|
||||
stack,
|
||||
image,
|
||||
created,
|
||||
ip,
|
||||
isHostColumnVisible && host,
|
||||
gpus,
|
||||
ports,
|
||||
ownership,
|
||||
]),
|
||||
[isHostColumnVisible]
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
import { CellProps, Column, TableInstance } from 'react-table';
|
||||
import { CellProps, Column } from 'react-table';
|
||||
import _ from 'lodash';
|
||||
import { useSref } from '@uirouter/react';
|
||||
|
||||
import { useEnvironment } from '@/portainer/environments/useEnvironment';
|
||||
import type {
|
||||
ContainersTableSettings,
|
||||
DockerContainer,
|
||||
} from '@/react/docker/containers/types';
|
||||
import type { DockerContainer } from '@/react/docker/containers/types';
|
||||
import { isOfflineEndpoint } from '@/portainer/helpers/endpointHelper';
|
||||
import { useCurrentEnvironment } from '@/portainer/hooks/useCurrentEnvironment';
|
||||
|
||||
import { useTableSettings } from '@@/datatables/useTableSettings';
|
||||
import { useTableSettings } from '@@/datatables/useZustandTableSettings';
|
||||
|
||||
import { TableSettings } from '../types';
|
||||
|
||||
export const name: Column<DockerContainer> = {
|
||||
Header: 'Name',
|
||||
|
@ -27,23 +27,24 @@ export const name: Column<DockerContainer> = {
|
|||
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', {
|
||||
}: CellProps<DockerContainer>) {
|
||||
const linkProps = useSref('.container', {
|
||||
id: container.Id,
|
||||
nodeName: container.NodeName,
|
||||
});
|
||||
|
||||
const { settings } = useTableSettings<TableSettings>();
|
||||
const truncate = settings.truncateContainerName;
|
||||
const environmentQuery = useCurrentEnvironment();
|
||||
|
||||
const environment = environmentQuery.data;
|
||||
|
||||
let shortName = name;
|
||||
if (truncate > 0) {
|
||||
shortName = _.truncate(name, { length: truncate });
|
||||
}
|
||||
|
||||
if (offlineMode) {
|
||||
if (!environment || isOfflineEndpoint(environment)) {
|
||||
return <span>{shortName}</span>;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import { Column } from 'react-table';
|
||||
import _ from 'lodash';
|
||||
|
||||
import { useEnvironment } from '@/portainer/environments/useEnvironment';
|
||||
import type { DockerContainer, Port } from '@/react/docker/containers/types';
|
||||
import { useCurrentEnvironment } from '@/portainer/hooks/useCurrentEnvironment';
|
||||
|
||||
export const ports: Column<DockerContainer> = {
|
||||
Header: 'Published Ports',
|
||||
|
@ -20,12 +20,15 @@ interface Props {
|
|||
}
|
||||
|
||||
function PortsCell({ value: ports }: Props) {
|
||||
const { PublicURL: publicUrl } = useEnvironment();
|
||||
const environmentQuery = useCurrentEnvironment();
|
||||
|
||||
if (ports.length === 0) {
|
||||
const environment = environmentQuery.data;
|
||||
if (!environment || ports.length === 0) {
|
||||
return '-';
|
||||
}
|
||||
|
||||
const { PublicURL: publicUrl } = environment;
|
||||
|
||||
return _.uniqBy(ports, 'public').map((port) => (
|
||||
<a
|
||||
key={`${port.host}:${port.public}`}
|
||||
|
|
|
@ -1,15 +1,14 @@
|
|||
import { CellProps, Column } from 'react-table';
|
||||
|
||||
import { useEnvironment } from '@/portainer/environments/useEnvironment';
|
||||
import { useAuthorizations } from '@/portainer/hooks/useUser';
|
||||
import { ContainerQuickActions } from '@/react/docker/containers/components/ContainerQuickActions/ContainerQuickActions';
|
||||
import type {
|
||||
ContainersTableSettings,
|
||||
DockerContainer,
|
||||
} from '@/react/docker/containers/types';
|
||||
import { EnvironmentStatus } from '@/portainer/environments/types';
|
||||
import { isOfflineEndpoint } from '@/portainer/helpers/endpointHelper';
|
||||
import { useCurrentEnvironment } from '@/portainer/hooks/useCurrentEnvironment';
|
||||
import { ContainerQuickActions } from '@/react/docker/containers/components/ContainerQuickActions';
|
||||
import { DockerContainer } from '@/react/docker/containers/types';
|
||||
|
||||
import { useTableSettings } from '@@/datatables/useTableSettings';
|
||||
import { useTableSettings } from '@@/datatables/useZustandTableSettings';
|
||||
|
||||
import { TableSettings } from '../types';
|
||||
|
||||
export const quickActions: Column<DockerContainer> = {
|
||||
Header: 'Quick Actions',
|
||||
|
@ -25,10 +24,12 @@ export const quickActions: Column<DockerContainer> = {
|
|||
function QuickActionsCell({
|
||||
row: { original: container },
|
||||
}: CellProps<DockerContainer>) {
|
||||
const endpoint = useEnvironment();
|
||||
const offlineMode = endpoint.Status !== EnvironmentStatus.Up;
|
||||
const environmentQuery = useCurrentEnvironment();
|
||||
|
||||
const { settings } = useTableSettings<ContainersTableSettings>();
|
||||
const environment = environmentQuery.data;
|
||||
const offlineMode = !environment || isOfflineEndpoint(environment);
|
||||
|
||||
const { settings } = useTableSettings<TableSettings>();
|
||||
|
||||
const { hiddenQuickActions = [] } = settings;
|
||||
|
||||
|
|
|
@ -1,10 +1,9 @@
|
|||
import { Column } from 'react-table';
|
||||
import { CellProps, Column } from 'react-table';
|
||||
import clsx from 'clsx';
|
||||
import _ from 'lodash';
|
||||
|
||||
import type {
|
||||
DockerContainer,
|
||||
DockerContainerStatus,
|
||||
import {
|
||||
type DockerContainer,
|
||||
ContainerStatus,
|
||||
} from '@/react/docker/containers/types';
|
||||
|
||||
import { DefaultFilter } from '@@/datatables/Filter';
|
||||
|
@ -20,11 +19,14 @@ export const state: Column<DockerContainer> = {
|
|||
canHide: true,
|
||||
};
|
||||
|
||||
function StatusCell({ value: status }: { value: DockerContainerStatus }) {
|
||||
const statusNormalized = _.toLower(status);
|
||||
const hasHealthCheck = ['starting', 'healthy', 'unhealthy'].includes(
|
||||
statusNormalized
|
||||
);
|
||||
function StatusCell({
|
||||
value: status,
|
||||
}: CellProps<DockerContainer, ContainerStatus>) {
|
||||
const hasHealthCheck = [
|
||||
ContainerStatus.Starting,
|
||||
ContainerStatus.Healthy,
|
||||
ContainerStatus.Unhealthy,
|
||||
].includes(status);
|
||||
|
||||
const statusClassName = getClassName();
|
||||
|
||||
|
@ -40,22 +42,21 @@ function StatusCell({ value: status }: { value: DockerContainerStatus }) {
|
|||
);
|
||||
|
||||
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));
|
||||
switch (status) {
|
||||
case ContainerStatus.Paused:
|
||||
case ContainerStatus.Starting:
|
||||
case ContainerStatus.Unhealthy:
|
||||
return 'warning';
|
||||
case ContainerStatus.Created:
|
||||
return 'info';
|
||||
case ContainerStatus.Stopped:
|
||||
case ContainerStatus.Dead:
|
||||
case ContainerStatus.Exited:
|
||||
return 'danger';
|
||||
case ContainerStatus.Healthy:
|
||||
case ContainerStatus.Running:
|
||||
default:
|
||||
return 'success';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,40 @@
|
|||
import create from 'zustand';
|
||||
import { persist } from 'zustand/middleware';
|
||||
|
||||
import { keyBuilder } from '@/portainer/hooks/useLocalStorage';
|
||||
import {
|
||||
paginationSettings,
|
||||
sortableSettings,
|
||||
refreshableSettings,
|
||||
hiddenColumnsSettings,
|
||||
} from '@/react/components/datatables/types';
|
||||
|
||||
import { QuickAction, TableSettings } from './types';
|
||||
|
||||
export const TRUNCATE_LENGTH = 32;
|
||||
|
||||
export function createStore(storageKey: string) {
|
||||
return create<TableSettings>()(
|
||||
persist(
|
||||
(set) => ({
|
||||
...sortableSettings(set),
|
||||
...paginationSettings(set),
|
||||
...hiddenColumnsSettings(set),
|
||||
...refreshableSettings(set),
|
||||
truncateContainerName: TRUNCATE_LENGTH,
|
||||
setTruncateContainerName(truncateContainerName: number) {
|
||||
set({
|
||||
truncateContainerName,
|
||||
});
|
||||
},
|
||||
|
||||
hiddenQuickActions: [] as QuickAction[],
|
||||
setHiddenQuickActions: (hiddenQuickActions: QuickAction[]) =>
|
||||
set({ hiddenQuickActions }),
|
||||
}),
|
||||
{
|
||||
name: keyBuilder(storageKey),
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
export { ContainersDatatable } from './ContainersDatatable';
|
|
@ -0,0 +1,23 @@
|
|||
import {
|
||||
PaginationTableSettings,
|
||||
RefreshableTableSettings,
|
||||
SettableColumnsTableSettings,
|
||||
SortableTableSettings,
|
||||
} from '@/react/components/datatables/types';
|
||||
|
||||
export type QuickAction = 'attach' | 'exec' | 'inspect' | 'logs' | 'stats';
|
||||
|
||||
export interface SettableQuickActionsTableSettings<TAction> {
|
||||
hiddenQuickActions: TAction[];
|
||||
setHiddenQuickActions: (hiddenQuickActions: TAction[]) => void;
|
||||
}
|
||||
|
||||
export interface TableSettings
|
||||
extends SortableTableSettings,
|
||||
PaginationTableSettings,
|
||||
SettableColumnsTableSettings,
|
||||
SettableQuickActionsTableSettings<QuickAction>,
|
||||
RefreshableTableSettings {
|
||||
truncateContainerName: number;
|
||||
setTruncateContainerName: (value: number) => void;
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue