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

pull/9218/head
Chaim Lev-Ari 1 year ago committed by GitHub
parent 146681e1c7
commit 09f60c3277
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -1,4 +0,0 @@
<button ng-if="!$ctrl.state.uploadInProgress" type="button" ngf-select="$ctrl.onFileSelected($file)" class="btn btn-light ng-scope">
<pr-icon icon="'upload'"></pr-icon>
</button>
<button ng-if="$ctrl.state.uploadInProgress" type="button" class="btn btn-sm btn-light" button-spinner="$ctrl.state.uploadInProgress"></button>

@ -1,30 +0,0 @@
export class FileUploaderController {
/* @ngInject */
constructor($async) {
Object.assign(this, { $async });
this.state = {
uploadInProgress: false,
};
this.onFileSelected = this.onFileSelected.bind(this);
this.onFileSelectedAsync = this.onFileSelectedAsync.bind(this);
}
onFileSelected(file) {
return this.$async(this.onFileSelectedAsync, file);
}
async onFileSelectedAsync(file) {
if (!file) {
return;
}
this.state.uploadInProgress = true;
try {
await this.uploadFile(file);
} finally {
this.state.uploadInProgress = false;
}
}
}

@ -1,10 +0,0 @@
import angular from 'angular';
import { FileUploaderController } from './fileUploaderController';
angular.module('portainer.agent').component('fileUploader', {
templateUrl: './fileUploader.html',
controller: FileUploaderController,
bindings: {
uploadFile: '<onFileSelected',
},
});

@ -1,114 +0,0 @@
<div class="datatable">
<rd-widget>
<rd-widget-body classes="no-padding">
<div class="toolBar">
<div class="toolBarTitle vertical-center">
<div class="widget-icon space-right">
<pr-icon icon="$ctrl.titleIcon"></pr-icon>
</div>
{{ $ctrl.titleText }}
</div>
<div class="searchBar vertical-center">
<pr-icon icon="'search'" class-name="'searchIcon'"></pr-icon>
<input
type="text"
class="searchInput"
ng-model="$ctrl.state.textFilter"
ng-model-options="{ debounce: 300 }"
ng-change="$ctrl.onTextFilterChange()"
placeholder="Search..."
auto-focus
/>
</div>
<file-uploader authorization="DockerAgentBrowsePut" ng-if="$ctrl.isUploadAllowed" on-file-selected="($ctrl.onFileSelectedForUpload)"> </file-uploader>
</div>
<div class="table-responsive">
<table class="table">
<thead>
<tr>
<th>
<table-column-header
col-title="'Name'"
can-sort="true"
is-sorted="$ctrl.state.orderBy === 'Name'"
is-sorted-desc="$ctrl.state.orderBy === 'Name' && $ctrl.state.reverseOrder"
ng-click="$ctrl.changeOrderBy('Name')"
></table-column-header>
</th>
<th>
<table-column-header
col-title="'Size'"
can-sort="true"
is-sorted="$ctrl.state.orderBy === 'Size'"
is-sorted-desc="$ctrl.state.orderBy === 'Size' && $ctrl.state.reverseOrder"
ng-click="$ctrl.changeOrderBy('Size')"
></table-column-header>
</th>
<th>
<table-column-header
col-title="'Last modification'"
can-sort="true"
is-sorted="$ctrl.state.orderBy === 'ModTime'"
is-sorted-desc="$ctrl.state.orderBy === 'ModTime' && $ctrl.state.reverseOrder"
ng-click="$ctrl.changeOrderBy('ModTime')"
></table-column-header>
</th>
<th> Actions </th>
</tr>
</thead>
<tbody>
<tr ng-if="!$ctrl.isRoot">
<td colspan="4">
<button type="button" class="btn btn-link !ml-0 p-0 hover:no-underline" ng-click="$ctrl.goToParent()"
><pr-icon icon="'corner-left-up'"></pr-icon>Go to parent</button
>
</td>
</tr>
<tr ng-repeat="item in ($ctrl.state.filteredDataSet = ($ctrl.dataset | filter:$ctrl.state.textFilter | orderBy:$ctrl.state.orderBy:$ctrl.state.reverseOrder))">
<td>
<span ng-if="item.edit" class="vertical-center">
<input
class="input-sm"
type="text"
ng-model="item.newName"
on-enter-key="$ctrl.rename({ name: item.Name, newName: item.newName }); item.edit = false"
auto-focus
/>
<a class="interactive" ng-click="item.edit = false;"><pr-icon icon="'x'"></pr-icon></a>
<a class="interactive" ng-click="$ctrl.rename({name: item.Name, newName: item.newName}); item.edit = false;"><pr-icon icon="'check'"></pr-icon></a>
</span>
<span ng-if="!item.edit && item.Dir">
<button type="button" class="btn btn-link !ml-0 p-0 hover:no-underline" ng-click="$ctrl.browse({name: item.Name})" class="vertical-center"
><pr-icon icon="'folder'"></pr-icon>{{ item.Name }}</button
>
</span>
<span ng-if="!item.edit && !item.Dir" class="vertical-center"><pr-icon icon="'file'"></pr-icon>{{ item.Name }}</span>
</td>
<td>{{ item.Size | humansize }}</td>
<td>
{{ item.ModTime | getisodatefromtimestamp }}
</td>
<td>
<btn authorization="DockerAgentBrowseGet" class="btn btn-xs btn-secondary space-right" ng-click="$ctrl.download({ name: item.Name })" ng-if="!item.Dir">
<pr-icon icon="'download'"></pr-icon> Download
</btn>
<btn authorization="DockerAgentBrowseRename" class="btn btn-xs btn-secondary space-right" ng-click="item.newName = item.Name; item.edit = true">
<pr-icon icon="'edit'"></pr-icon> Rename
</btn>
<btn authorization="DockerAgentBrowseDelete" class="btn btn-xs btn-dangerlight" ng-click="$ctrl.delete({ name: item.Name })">
<pr-icon icon="'trash-2'"></pr-icon> Delete
</btn>
</td>
</tr>
<tr ng-if="!$ctrl.dataset">
<td colspan="5" class="text-muted text-center">Loading...</td>
</tr>
<tr ng-if="$ctrl.state.filteredDataSet.length === 0">
<td colspan="5" class="text-muted text-center">No files found.</td>
</tr>
</tbody>
</table>
</div>
</rd-widget-body>
</rd-widget>
</div>

@ -1,24 +0,0 @@
import angular from 'angular';
angular.module('portainer.agent').component('filesDatatable', {
templateUrl: './filesDatatable.html',
controller: 'GenericDatatableController',
bindings: {
titleText: '@',
titleIcon: '@',
dataset: '<',
tableKey: '@',
orderBy: '@',
reverseOrder: '<',
isRoot: '<',
goToParent: '&',
browse: '&',
rename: '&',
download: '&',
delete: '&',
isUploadAllowed: '<',
onFileSelectedForUpload: '<',
},
});

@ -1,16 +1,12 @@
<files-datatable
title-text="Host browser - {{ $ctrl.getRelativePath() }}"
title-icon="file"
<agent-host-browser-react
ng-if="$ctrl.files"
dataset="$ctrl.files"
table-key="host_browser"
order-by="Dir"
is-root="$ctrl.isRoot()"
go-to-parent="$ctrl.goToParent()"
browse="$ctrl.browse(name)"
rename="$ctrl.renameFile(name, newName)"
download="$ctrl.downloadFile(name)"
delete="$ctrl.confirmDeleteFile(name)"
is-upload-allowed="true"
on-go-to-parent="($ctrl.goToParent)"
relative-path="$ctrl.getRelativePath()"
on-browse="($ctrl.browse)"
on-rename="($ctrl.renameFile)"
on-download="($ctrl.downloadFile)"
on-delete="($ctrl.confirmDeleteFile)"
on-file-selected-for-upload="($ctrl.onFileSelectedForUpload)"
>
</files-datatable>
></agent-host-browser-react>

@ -1,15 +1,13 @@
<files-datatable
title-text="Volume browser"
title-icon="file"
<agent-volume-browser-react
ng-if="$ctrl.files"
dataset="$ctrl.files"
table-key="volume_browser"
order-by="Dir"
is-root="$ctrl.state.path === '/'"
go-to-parent="$ctrl.up()"
browse="$ctrl.browse(name)"
rename="$ctrl.rename(name, newName)"
download="$ctrl.download(name)"
delete="$ctrl.confirmDelete(name)"
on-go-to-parent="($ctrl.up)"
on-browse="($ctrl.browse)"
on-rename="($ctrl.rename)"
on-download="($ctrl.download)"
on-delete="($ctrl.confirmDelete)"
is-upload-allowed="$ctrl.isUploadEnabled"
on-file-selected-for-upload="($ctrl.onFileSelectedForUpload)"
></files-datatable>
relative-path="$ctrl.state.path"
></agent-volume-browser-react>

@ -19,6 +19,8 @@ import { BetaAlert } from '@/react/portainer/environments/update-schedules/commo
import { ImagesDatatable } from '@/react/docker/images/ListView/ImagesDatatable/ImagesDatatable';
import { EventsDatatable } from '@/react/docker/events/EventsDatatables';
import { ConfigsDatatable } from '@/react/docker/configs/ListView/ConfigsDatatable';
import { AgentHostBrowser } from '@/react/docker/host/BrowseView/AgentHostBrowser';
import { AgentVolumeBrowser } from '@/react/docker/volumes/BrowseView/AgentVolumeBrowser';
const ngModule = angular
.module('portainer.docker.react.components', [])
@ -91,6 +93,35 @@ const ngModule = angular
'onRefresh',
])
)
.component(
'agentHostBrowserReact',
r2a(withUIRouter(withCurrentUser(AgentHostBrowser)), [
'dataset',
'isRoot',
'onBrowse',
'onDelete',
'onDownload',
'onFileSelectedForUpload',
'onGoToParent',
'onRename',
'relativePath',
])
)
.component(
'agentVolumeBrowserReact',
r2a(withUIRouter(withCurrentUser(AgentVolumeBrowser)), [
'dataset',
'isRoot',
'isUploadAllowed',
'onBrowse',
'onDelete',
'onDownload',
'onFileSelectedForUpload',
'onGoToParent',
'onRename',
'relativePath',
])
)
.component('dockerEventsDatatable', r2a(EventsDatatable, ['dataset']));
export const componentsModule = ngModule.name;

@ -1,7 +1,3 @@
<page-header title="'Host Browser'" breadcrumbs="['Host', {label:$ctrl.host.Name, link:'docker.host'}, 'browse']"> </page-header>
<div class="row">
<div class="col-sm-12">
<host-browser ng-if="$ctrl.host" endpoint-id="$ctrl.endpoint.Id"></host-browser>
</div>
</div>
<host-browser ng-if="$ctrl.host" endpoint-id="$ctrl.endpoint.Id"></host-browser>

@ -15,8 +15,4 @@
>
</page-header>
<div class="row">
<div class="col-sm-12">
<host-browser ng-if="$ctrl.node" endpoint-id="$ctrl.endpoint.Id"></host-browser>
</div>
</div>
<host-browser ng-if="$ctrl.node" endpoint-id="$ctrl.endpoint.Id"></host-browser>

@ -10,8 +10,4 @@
>
</page-header>
<div class="row">
<div class="col-sm-12">
<volume-browser volume-id="volumeId" node-name="nodeName" is-upload-enabled="agentApiVersion > 1" endpoint-id="endpointId"></volume-browser>
</div>
</div>
<volume-browser volume-id="volumeId" node-name="nodeName" is-upload-enabled="agentApiVersion > 1" endpoint-id="endpointId"></volume-browser>

@ -6,4 +6,9 @@ declare module '@tanstack/table-core' {
filter?: Filter<TData, TValue>;
width?: number | 'auto' | string;
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
interface TableMeta<TData extends RowData> {
table?: string;
}
}

@ -16,7 +16,6 @@ export function NameCell({
row: { original: container },
}: CellContext<ContainerGroup, string>) {
const name = getValue();
return (
<Link
to="azure.containerinstances.container"

@ -12,6 +12,7 @@ import {
getFacetedMinMaxValues,
getExpandedRowModel,
TableOptions,
TableMeta,
} from '@tanstack/react-table';
import { ReactNode, useMemo } from 'react';
import clsx from 'clsx';
@ -30,10 +31,11 @@ import { BasicTableSettings } from './types';
import { DatatableContent } from './DatatableContent';
import { createSelectColumn } from './select-column';
import { TableRow } from './TableRow';
import { type TableState as GlobalTableState } from './useTableState';
export interface Props<
D extends Record<string, unknown>,
TSettings extends BasicTableSettings = BasicTableSettings
TMeta extends TableMeta<D> = TableMeta<D>
> extends AutomationTestingProps {
dataset: D[];
columns: TableOptions<D>['columns'];
@ -53,16 +55,17 @@ export interface Props<
highlightedItemId?: string;
onPageChange?(page: number): void;
settingsManager: TSettings & {
search: string;
setSearch: (value: string) => void;
};
settingsManager: GlobalTableState<BasicTableSettings>;
renderRow?(row: Row<D>, highlightedItemId?: string): ReactNode;
getRowCanExpand?(row: Row<D>): boolean;
noWidget?: boolean;
meta?: TMeta;
}
export function Datatable<D extends Record<string, unknown>>({
export function Datatable<
D extends Record<string, unknown>,
TMeta extends TableMeta<D> = TableMeta<D>
>({
columns,
dataset,
renderTableSettings = () => null,
@ -85,7 +88,8 @@ export function Datatable<D extends Record<string, unknown>>({
noWidget,
getRowCanExpand,
'data-cy': dataCy,
}: Props<D>) {
meta,
}: Props<D, TMeta>) {
const isServerSidePagination = typeof pageCount !== 'undefined';
const enableRowSelection = getIsSelectionEnabled(
disableSelect,
@ -127,6 +131,7 @@ export function Datatable<D extends Record<string, unknown>>({
getExpandedRowModel: getExpandedRowModel(),
getRowCanExpand,
...(isServerSidePagination ? { manualPagination: true, pageCount } : {}),
meta,
});
const tableState = tableInstance.getState();

@ -4,6 +4,11 @@ import { useStore } from 'zustand';
import { useSearchBarState } from './SearchBar';
import { BasicTableSettings, CreatePersistedStoreReturn } from './types';
export type TableState<TSettings extends BasicTableSettings> = TSettings & {
setSearch: (search: string) => void;
search: string;
};
export function useTableState<
TSettings extends BasicTableSettings = BasicTableSettings
>(store: CreatePersistedStoreReturn<TSettings>, storageKey: string) {

@ -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>
);
}}
/>
);
}

@ -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>
);
}

@ -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>
);
}

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

@ -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);
};
}

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

@ -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';
}

@ -0,0 +1,51 @@
import { ComponentProps } from 'react';
import { FilesTable } from '@/react/docker/components/FilesTable';
import { createPersistedStore } from '@@/datatables/types';
import { useTableState } from '@@/datatables/useTableState';
const tableKey = 'host-browser';
const settingsStore = createPersistedStore(tableKey, {
desc: true,
id: 'Dir',
});
interface Props
extends Omit<
ComponentProps<typeof FilesTable>,
'isUploadAllowed' | 'tableState' | 'title'
> {
relativePath: string;
}
export function AgentHostBrowser({
relativePath,
dataset,
isRoot,
onBrowse,
onDelete,
onDownload,
onFileSelectedForUpload,
onGoToParent,
onRename,
}: Props) {
const tableState = useTableState(settingsStore, tableKey);
return (
<FilesTable
tableState={tableState}
dataset={dataset}
title={`Host browser - ${relativePath}`}
isRoot={isRoot}
onRename={onRename}
onBrowse={onBrowse}
onDownload={onDownload}
onDelete={onDelete}
isUploadAllowed
onFileSelectedForUpload={onFileSelectedForUpload}
onGoToParent={onGoToParent}
/>
);
}

@ -0,0 +1,49 @@
import { ComponentProps } from 'react';
import { FilesTable } from '@/react/docker/components/FilesTable';
import { createPersistedStore } from '@@/datatables/types';
import { useTableState } from '@@/datatables/useTableState';
const tableKey = 'host-browser';
const settingsStore = createPersistedStore(tableKey, 'Name');
interface Props
extends Omit<
ComponentProps<typeof FilesTable>,
'onSearchChange' | 'tableState' | 'title'
> {
relativePath: string;
}
export function AgentVolumeBrowser({
relativePath,
dataset,
isRoot,
onBrowse,
onDelete,
onDownload,
onFileSelectedForUpload,
onGoToParent,
onRename,
isUploadAllowed,
}: Props) {
const tableState = useTableState(settingsStore, tableKey);
return (
<FilesTable
tableState={tableState}
dataset={dataset}
title={`Volume browser - ${relativePath}`}
isRoot={isRoot}
onRename={onRename}
onBrowse={onBrowse}
onDownload={onDownload}
onDelete={onDelete}
isUploadAllowed={isUploadAllowed}
onFileSelectedForUpload={onFileSelectedForUpload}
onGoToParent={onGoToParent}
/>
);
}
Loading…
Cancel
Save