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

feat(docker): migrate files table to react [EE-4663] (#8916)

This commit is contained in:
Chaim Lev-Ari 2023-07-16 10:59:58 +03:00 committed by GitHub
parent 146681e1c7
commit 09f60c3277
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
24 changed files with 529 additions and 229 deletions

View file

@ -0,0 +1,123 @@
import { CornerLeftUp, File as FileIcon, Upload } from 'lucide-react';
import { useState } from 'react';
import { Authorized } from '@/react/hooks/useUser';
import { Datatable } from '@@/datatables';
import { BasicTableSettings } from '@@/datatables/types';
import { Button } from '@@/buttons';
import { TableState } from '@@/datatables/useTableState';
import { FileData, FilesTableMeta } from './types';
import { columns } from './columns';
interface Props {
title: string;
dataset: FileData[];
tableState: TableState<BasicTableSettings>;
isRoot: boolean;
onGoToParent: () => void;
onBrowse: (folderName: string) => void;
onRename: (oldName: string, newName: string) => void;
onDownload: (fileName: string) => void;
onDelete: (fileName: string) => void;
isUploadAllowed: boolean;
onFileSelectedForUpload: (file: File) => void;
}
function goToParent(onClick: () => void): FileData {
return {
custom: (
<Button
onClick={onClick}
color="link"
icon={CornerLeftUp}
className="!m-0 !p-0"
>
Go to parent
</Button>
),
Dir: true,
Name: '..',
Size: 0,
ModTime: 0,
};
}
export function FilesTable({
isRoot,
title,
dataset,
tableState,
onGoToParent,
onRename,
onBrowse,
onDelete,
onDownload,
isUploadAllowed,
onFileSelectedForUpload,
}: Props) {
const [isEditState, setIsEditState] = useState(
Object.fromEntries(dataset.map((f) => [f.Name, false]))
);
function isEdit(name: string) {
return isEditState[name];
}
function setIsEdit(name: string, value: boolean) {
setIsEditState((editState) => ({ ...editState, [name]: value }));
}
return (
<Datatable<FileData, FilesTableMeta>
title={title}
titleIcon={FileIcon}
dataset={isRoot ? dataset : [goToParent(onGoToParent), ...dataset]}
settingsManager={tableState}
columns={columns}
getRowId={(row) => row.Name}
meta={{
table: 'files',
isEdit,
setIsEdit,
onRename,
onBrowse,
onDownload,
onDelete,
}}
initialTableState={{
columnVisibility: {
Dir: false,
},
}}
disableSelect
renderTableActions={() => {
if (!isUploadAllowed) {
return null;
}
return (
<Authorized authorizations="DockerAgentBrowsePut">
<div className="flex flex-row items-center">
<Button color="light" icon={Upload} as="label">
<input
type="file"
className="hidden"
onChange={(e) => {
const file = e.target.files?.[0];
if (file) {
onFileSelectedForUpload(file);
}
}}
/>
</Button>
</div>
</Authorized>
);
}}
/>
);
}

View file

@ -0,0 +1,58 @@
import { CellContext } from '@tanstack/react-table';
import { Download, Edit, Trash2 } from 'lucide-react';
import { Authorized } from '@/react/hooks/useUser';
import { Button } from '@@/buttons';
import { FileData, isFilesTableMeta } from '../types';
export function ActionsCell({
row: { original: item },
table,
}: CellContext<FileData, unknown>) {
const { meta } = table.options;
if (!isFilesTableMeta(meta)) {
throw new Error('Invalid table meta');
}
return (
<div className="flex gap-2">
{!item.Dir && (
<Authorized authorizations="DockerAgentBrowseGet">
<Button
color="secondary"
size="xsmall"
onClick={() => meta.onDownload(item.Name)}
icon={Download}
className="!m-0"
>
Download
</Button>
</Authorized>
)}
<Authorized authorizations="DockerAgentBrowseRename">
<Button
color="secondary"
size="xsmall"
icon={Edit}
onClick={() => meta.setIsEdit(item.Name, true)}
className="!m-0"
>
Rename
</Button>
</Authorized>
<Authorized authorizations="DockerAgentBrowseDelete">
<Button
color="dangerlight"
size="xsmall"
icon={Trash2}
onClick={() => meta.onDelete(item.Name)}
className="!m-0"
>
Delete
</Button>
</Authorized>
</div>
);
}

View file

@ -0,0 +1,98 @@
import { CellContext } from '@tanstack/react-table';
import { Check, File as FileIcon, Folder, X } from 'lucide-react';
import { Form, Formik } from 'formik';
import { Icon } from '@@/Icon';
import { Button } from '@@/buttons';
import { Input } from '@@/form-components/Input';
import { FileData, isFilesTableMeta } from '../types';
export function NameCell({
getValue,
row: { original: item },
table,
}: CellContext<FileData, string>) {
const name = getValue();
const { meta } = table.options;
if (!isFilesTableMeta(meta)) {
throw new Error('Invalid table meta');
}
const isEdit = meta.isEdit(name);
if (item.custom) {
return item.custom;
}
if (isEdit) {
return (
<EditForm
originalName={name}
onSave={handleRename}
onClose={() => meta.setIsEdit(name, false)}
/>
);
}
return (
<>
{item.Dir ? (
<Button
color="link"
className="!ml-0 p-0"
onClick={() => meta.onBrowse(name)}
icon={Folder}
>
{name}
</Button>
) : (
<span className="vertical-center">
<Icon icon={FileIcon} />
{name}
</span>
)}
</>
);
function handleRename(name: string) {
if (!isFilesTableMeta(meta)) {
throw new Error('Invalid table meta');
}
meta.onRename(item.Name, name);
meta.setIsEdit(name, false);
}
}
function EditForm({
originalName,
onSave,
onClose,
}: {
originalName: string;
onSave: (name: string) => void;
onClose: () => void;
}) {
return (
<Formik
initialValues={{ name: originalName }}
onSubmit={({ name }) => onSave(name)}
onReset={onClose}
>
{({ values, setFieldValue }) => (
<Form className="flex items-center">
<Input
name="name"
value={values.name}
onChange={(e) => setFieldValue('name', e.target.value)}
className="input-sm w-auto"
/>
<Button color="none" type="reset" icon={X} />
<Button color="none" type="submit" icon={Check} />
</Form>
)}
</Formik>
);
}

View file

@ -0,0 +1,5 @@
import { createColumnHelper } from '@tanstack/react-table';
import { FileData } from '../types';
export const columnHelper = createColumnHelper<FileData>();

View file

@ -0,0 +1,44 @@
import {
CellContext,
ColumnDef,
ColumnDefTemplate,
} from '@tanstack/react-table';
import { humanize, isoDateFromTimestamp } from '@/portainer/filters/filters';
import { FileData } from '../types';
import { columnHelper } from './helper';
import { NameCell } from './NameCell';
import { ActionsCell } from './ActionsCell';
export const columns = [
columnHelper.accessor('Name', {
header: 'Name',
cell: NameCell,
}),
columnHelper.accessor('Size', {
header: 'Size',
cell: hideIfCustom(({ getValue }) => humanize(getValue())),
}),
columnHelper.accessor('ModTime', {
header: 'Last modification',
cell: hideIfCustom(({ getValue }) => isoDateFromTimestamp(getValue())),
}),
columnHelper.display({
header: 'Actions',
cell: hideIfCustom(ActionsCell),
}),
columnHelper.accessor('Dir', {}), // workaround, to enable sorting by Dir (put directory first)
] as ColumnDef<FileData>[];
function hideIfCustom<TValue>(
template: ColumnDefTemplate<CellContext<FileData, TValue>>
): ColumnDefTemplate<CellContext<FileData, TValue>> {
return (props) => {
if (props.row.original.custom) {
return null;
}
return typeof template === 'string' ? template : template(props);
};
}

View file

@ -0,0 +1 @@
export { FilesTable } from './FilesTable';

View file

@ -0,0 +1,26 @@
import { TableMeta } from '@tanstack/react-table';
import { ReactNode } from 'react';
export type FileData = {
Name: string;
Dir: boolean;
Size: number;
ModTime: number;
custom: ReactNode;
};
export type FilesTableMeta = TableMeta<FileData> & {
table: 'files';
isEdit(rowId: string): boolean;
setIsEdit(rowId: string, isEdit: boolean): void;
onRename: (oldName: string, newName: string) => void;
onBrowse: (name: string) => void;
onDownload: (fileName: string) => void;
onDelete: (fileName: string) => void;
};
export function isFilesTableMeta(
meta?: TableMeta<FileData>
): meta is FilesTableMeta {
return !!meta && meta.table === 'files';
}