1
0
Fork 0
mirror of https://github.com/portainer/portainer.git synced 2025-07-25 00:09:40 +02:00

refactor(docker/images): convert table to react [EE-4668] (#8910)

This commit is contained in:
Chaim Lev-Ari 2023-07-13 10:47:20 +03:00 committed by GitHub
parent 0e9902fee9
commit ecd54ab929
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
26 changed files with 496 additions and 441 deletions

View file

@ -0,0 +1,213 @@
import {
ChevronDown,
Download,
List,
Plus,
Trash2,
Upload,
} from 'lucide-react';
import { Menu, MenuButton, MenuItem, MenuPopover } from '@reach/menu-button';
import { positionRight } from '@reach/popover';
import { useMemo } from 'react';
import { Environment } from '@/react/portainer/environments/types';
import { Authorized } from '@/react/hooks/useUser';
import { Datatable, TableSettingsMenu } from '@@/datatables';
import {
BasicTableSettings,
createPersistedStore,
refreshableSettings,
RefreshableTableSettings,
} from '@@/datatables/types';
import { useTableState } from '@@/datatables/useTableState';
import { Button, ButtonGroup, LoadingButton } from '@@/buttons';
import { Link } from '@@/Link';
import { ButtonWithRef } from '@@/buttons/Button';
import { useRepeater } from '@@/datatables/useRepeater';
import { TableSettingsMenuAutoRefresh } from '@@/datatables/TableSettingsMenuAutoRefresh';
import { DockerImage } from '../../types';
import { columns as defColumns } from './columns';
import { host as hostColumn } from './columns/host';
import { RowProvider } from './RowContext';
const tableKey = 'images';
export interface TableSettings
extends BasicTableSettings,
RefreshableTableSettings {}
const settingsStore = createPersistedStore<TableSettings>(
tableKey,
'tags',
(set) => ({
...refreshableSettings(set),
})
);
export function ImagesDatatable({
dataset,
environment,
isHostColumnVisible,
isExportInProgress,
onDownload,
onRefresh,
onRemove,
}: {
dataset: Array<DockerImage>;
environment: Environment;
isHostColumnVisible: boolean;
onDownload: (images: Array<DockerImage>) => void;
onRemove: (images: Array<DockerImage>, force: true) => void;
onRefresh: () => Promise<void>;
isExportInProgress: boolean;
}) {
const tableState = useTableState(settingsStore, tableKey);
const columns = useMemo(
() => (isHostColumnVisible ? [...defColumns, hostColumn] : defColumns),
[isHostColumnVisible]
);
useRepeater(tableState.autoRefreshRate, onRefresh);
return (
<RowProvider context={{ environment }}>
<Datatable
title="Images"
titleIcon={List}
renderTableActions={(selectedItems) => (
<div className="flex items-center gap-2">
<RemoveButtonMenu
selectedItems={selectedItems}
onRemove={onRemove}
/>
<ImportExportButtons
isExportInProgress={isExportInProgress}
onExportClick={onDownload}
selectedItems={selectedItems}
/>
<Authorized authorizations="DockerImageBuild">
<Button
as={Link}
props={{ to: 'docker.images.build' }}
data-cy="image-buildImageButton"
icon={Plus}
>
Build a new image
</Button>
</Authorized>
</div>
)}
dataset={dataset}
settingsManager={tableState}
columns={columns}
emptyContentLabel="No images found"
renderTableSettings={() => (
<TableSettingsMenu>
<TableSettingsMenuAutoRefresh
value={tableState.autoRefreshRate}
onChange={(value) => tableState.setAutoRefreshRate(value)}
/>
</TableSettingsMenu>
)}
/>
</RowProvider>
);
}
function RemoveButtonMenu({
onRemove,
selectedItems,
}: {
selectedItems: Array<DockerImage>;
onRemove(selectedItems: Array<DockerImage>, force: boolean): void;
}) {
return (
<Authorized authorizations="DockerImageDelete">
<ButtonGroup>
<Button
size="small"
color="dangerlight"
icon={Trash2}
disabled={selectedItems.length === 0}
data-cy="image-removeImageButton"
onClick={() => {
onRemove(selectedItems, false);
}}
>
Remove
</Button>
<Menu>
<MenuButton
as={ButtonWithRef}
size="small"
color="dangerlight"
disabled={selectedItems.length === 0}
icon={ChevronDown}
>
<span className="sr-only">Toggle Dropdown</span>
</MenuButton>
<MenuPopover position={positionRight}>
<div className="mt-3 bg-white">
<MenuItem
onSelect={() => {
onRemove(selectedItems, true);
}}
>
Force Remove
</MenuItem>
</div>
</MenuPopover>
</Menu>
</ButtonGroup>
</Authorized>
);
}
function ImportExportButtons({
isExportInProgress,
selectedItems,
onExportClick,
}: {
isExportInProgress: boolean;
selectedItems: Array<DockerImage>;
onExportClick(selectedItems: Array<DockerImage>): void;
}) {
return (
<ButtonGroup>
<Authorized authorizations="DockerImageLoad">
<Button
size="small"
color="light"
as={Link}
data-cy="image-importImageButton"
icon={Upload}
disabled={isExportInProgress}
props={{ to: 'docker.images.import' }}
>
Import
</Button>
</Authorized>
<Authorized authorizations="DockerImageGet">
<LoadingButton
size="small"
color="light"
icon={Download}
isLoading={isExportInProgress}
loadingText="Export in progress..."
data-cy="image-exportImageButton"
onClick={() => onExportClick(selectedItems)}
disabled={selectedItems.length === 0}
>
Export
</LoadingButton>
</Authorized>
</ButtonGroup>
);
}

View file

@ -0,0 +1,11 @@
import { Environment } from '@/react/portainer/environments/types';
import { createRowContext } from '@@/datatables/RowContext';
interface RowContextState {
environment: Environment;
}
const { RowProvider, useRowContext } = createRowContext<RowContextState>();
export { RowProvider, useRowContext };

View file

@ -0,0 +1,12 @@
import { isoDateFromTimestamp } from '@/portainer/filters/filters';
import { columnHelper } from './helper';
export const created = columnHelper.accessor('Created', {
id: 'created',
header: 'Created',
cell: ({ getValue }) => {
const value = getValue();
return isoDateFromTimestamp(value);
},
});

View file

@ -0,0 +1,16 @@
import { createColumnHelper } from '@tanstack/react-table';
import { DockerImage } from '@/react/docker/images/types';
export const columnHelper = createColumnHelper<
DockerImage & { NodeName?: string }
>();
/**
* Docker response from proxy (with added portainer metadata)
* images view model
* images snapshot
* snapshots view model
*
*
*/

View file

@ -0,0 +1,9 @@
import { columnHelper } from './helper';
export const host = columnHelper.accessor('NodeName', {
header: 'Host',
cell: ({ getValue }) => {
const value = getValue();
return value || '-';
},
});

View file

@ -0,0 +1,84 @@
import { CellContext, Column } from '@tanstack/react-table';
import { useSref } from '@uirouter/react';
import { DockerImage } from '@/react/docker/images/types';
import { truncate } from '@/portainer/filters/filters';
import { getValueAsArrayOfStrings } from '@/portainer/helpers/array';
import { MultipleSelectionFilter } from '@@/datatables/Filter';
import { columnHelper } from './helper';
export const id = columnHelper.accessor('Id', {
id: 'id',
header: 'Id',
cell: Cell,
enableColumnFilter: true,
filterFn: (
{ original: { Used } },
columnId,
filterValue: Array<'Used' | 'Unused'>
) => {
if (filterValue.length === 0) {
return true;
}
if (filterValue.includes('Used') && Used) {
return true;
}
if (filterValue.includes('Unused') && !Used) {
return true;
}
return false;
},
meta: {
filter: FilterByUsage,
},
});
function FilterByUsage<TData extends { Used: boolean }>({
column: { getFilterValue, setFilterValue, id },
}: {
column: Column<TData>;
}) {
const options = ['Used', 'Unused'];
const value = getFilterValue();
const valueAsArray = getValueAsArrayOfStrings(value);
return (
<MultipleSelectionFilter
options={options}
filterKey={id}
value={valueAsArray}
onChange={setFilterValue}
menuTitle="Filter by usage"
/>
);
}
function Cell({
getValue,
row: { original: image },
}: CellContext<DockerImage, string>) {
const name = getValue();
const linkProps = useSref('.image', {
id: image.Id,
imageId: image.Id,
});
return (
<>
<a href={linkProps.href} onClick={linkProps.onClick} title={name}>
{truncate(name, 40)}
</a>
{!image.Used && (
<span className="label label-warning image-tag ml-2">Unused</span>
)}
</>
);
}

View file

@ -0,0 +1,6 @@
import { created } from './created';
import { id } from './id';
import { size } from './size';
import { tags } from './tags';
export const columns = [id, tags, size, created];

View file

@ -0,0 +1,12 @@
import { humanize } from '@/portainer/filters/filters';
import { columnHelper } from './helper';
export const size = columnHelper.accessor('VirtualSize', {
id: 'size',
header: 'Size',
cell: ({ getValue }) => {
const value = getValue();
return humanize(value);
},
});

View file

@ -0,0 +1,25 @@
import { CellContext } from '@tanstack/react-table';
import { DockerImage } from '@/react/docker/images/types';
import { columnHelper } from './helper';
export const tags = columnHelper.accessor('RepoTags', {
id: 'tags',
header: 'Tags',
cell: Cell,
});
function Cell({ getValue }: CellContext<DockerImage, string[]>) {
const repoTags = getValue();
return (
<>
{repoTags.map((tag, idx) => (
<span key={idx} className="label label-primary image-tag" title={tag}>
{tag}
</span>
))}
</>
);
}

View file

@ -1,3 +1,5 @@
import { PortainerMetadata } from '../../types';
export type DockerImageResponse = {
Containers: number;
Created: number;
@ -9,4 +11,5 @@ export type DockerImageResponse = {
SharedSize: number;
Size: number;
VirtualSize: number;
Portainer?: PortainerMetadata;
};