diff --git a/app/agent/components/file-uploader/fileUploader.html b/app/agent/components/file-uploader/fileUploader.html deleted file mode 100644 index 1bd4487ab..000000000 --- a/app/agent/components/file-uploader/fileUploader.html +++ /dev/null @@ -1,4 +0,0 @@ - - diff --git a/app/agent/components/file-uploader/fileUploaderController.js b/app/agent/components/file-uploader/fileUploaderController.js deleted file mode 100644 index d60ca2a0c..000000000 --- a/app/agent/components/file-uploader/fileUploaderController.js +++ /dev/null @@ -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; - } - } -} diff --git a/app/agent/components/file-uploader/index.js b/app/agent/components/file-uploader/index.js deleted file mode 100644 index 6b0b37fc7..000000000 --- a/app/agent/components/file-uploader/index.js +++ /dev/null @@ -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: ' - - -
-
-
- -
- {{ $ctrl.titleText }} -
- - -
-
- - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - - - - Actions
- -
- - - - - - - - - {{ item.Name }} - {{ item.Size | humansize }} - {{ item.ModTime | getisodatefromtimestamp }} - - - Download - - - Rename - - - Delete - -
Loading...
No files found.
-
-
-
- diff --git a/app/agent/components/files-datatable/index.js b/app/agent/components/files-datatable/index.js deleted file mode 100644 index a3ba1bb7e..000000000 --- a/app/agent/components/files-datatable/index.js +++ /dev/null @@ -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: '<', - }, -}); diff --git a/app/agent/components/host-browser/hostBrowser.html b/app/agent/components/host-browser/hostBrowser.html index 9483d5632..53704c6d0 100644 --- a/app/agent/components/host-browser/hostBrowser.html +++ b/app/agent/components/host-browser/hostBrowser.html @@ -1,16 +1,12 @@ - - +> diff --git a/app/agent/components/volume-browser/volumeBrowser.html b/app/agent/components/volume-browser/volumeBrowser.html index 6112f9624..76f20ef92 100644 --- a/app/agent/components/volume-browser/volumeBrowser.html +++ b/app/agent/components/volume-browser/volumeBrowser.html @@ -1,15 +1,13 @@ - + relative-path="$ctrl.state.path" +> diff --git a/app/docker/react/components/index.ts b/app/docker/react/components/index.ts index 84f32e0bf..b20e2d5f4 100644 --- a/app/docker/react/components/index.ts +++ b/app/docker/react/components/index.ts @@ -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; diff --git a/app/docker/views/host/host-browser-view/host-browser-view.html b/app/docker/views/host/host-browser-view/host-browser-view.html index 3cfd30159..2f4958d4b 100644 --- a/app/docker/views/host/host-browser-view/host-browser-view.html +++ b/app/docker/views/host/host-browser-view/host-browser-view.html @@ -1,7 +1,3 @@ -
-
- -
-
+ diff --git a/app/docker/views/nodes/node-browser/node-browser.html b/app/docker/views/nodes/node-browser/node-browser.html index f0f8d4cb6..d73f4d95a 100644 --- a/app/docker/views/nodes/node-browser/node-browser.html +++ b/app/docker/views/nodes/node-browser/node-browser.html @@ -15,8 +15,4 @@ > -
-
- -
-
+ diff --git a/app/docker/views/volumes/browse/browsevolume.html b/app/docker/views/volumes/browse/browsevolume.html index 0f351275a..4f431b19b 100644 --- a/app/docker/views/volumes/browse/browsevolume.html +++ b/app/docker/views/volumes/browse/browsevolume.html @@ -10,8 +10,4 @@ > -
-
- -
-
+ diff --git a/app/react-table-config.d.ts b/app/react-table-config.d.ts index a37055397..80e934d48 100644 --- a/app/react-table-config.d.ts +++ b/app/react-table-config.d.ts @@ -6,4 +6,9 @@ declare module '@tanstack/table-core' { filter?: Filter; width?: number | 'auto' | string; } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + interface TableMeta { + table?: string; + } } diff --git a/app/react/azure/container-instances/ListView/columns/name.tsx b/app/react/azure/container-instances/ListView/columns/name.tsx index 249caa98d..978e177b3 100644 --- a/app/react/azure/container-instances/ListView/columns/name.tsx +++ b/app/react/azure/container-instances/ListView/columns/name.tsx @@ -16,7 +16,6 @@ export function NameCell({ row: { original: container }, }: CellContext) { const name = getValue(); - return ( , - TSettings extends BasicTableSettings = BasicTableSettings + TMeta extends TableMeta = TableMeta > extends AutomationTestingProps { dataset: D[]; columns: TableOptions['columns']; @@ -53,16 +55,17 @@ export interface Props< highlightedItemId?: string; onPageChange?(page: number): void; - settingsManager: TSettings & { - search: string; - setSearch: (value: string) => void; - }; + settingsManager: GlobalTableState; renderRow?(row: Row, highlightedItemId?: string): ReactNode; getRowCanExpand?(row: Row): boolean; noWidget?: boolean; + meta?: TMeta; } -export function Datatable>({ +export function Datatable< + D extends Record, + TMeta extends TableMeta = TableMeta +>({ columns, dataset, renderTableSettings = () => null, @@ -85,7 +88,8 @@ export function Datatable>({ noWidget, getRowCanExpand, 'data-cy': dataCy, -}: Props) { + meta, +}: Props) { const isServerSidePagination = typeof pageCount !== 'undefined'; const enableRowSelection = getIsSelectionEnabled( disableSelect, @@ -127,6 +131,7 @@ export function Datatable>({ getExpandedRowModel: getExpandedRowModel(), getRowCanExpand, ...(isServerSidePagination ? { manualPagination: true, pageCount } : {}), + meta, }); const tableState = tableInstance.getState(); diff --git a/app/react/components/datatables/useTableState.ts b/app/react/components/datatables/useTableState.ts index ce3d8cdf6..0bd74c9a8 100644 --- a/app/react/components/datatables/useTableState.ts +++ b/app/react/components/datatables/useTableState.ts @@ -4,6 +4,11 @@ import { useStore } from 'zustand'; import { useSearchBarState } from './SearchBar'; import { BasicTableSettings, CreatePersistedStoreReturn } from './types'; +export type TableState = TSettings & { + setSearch: (search: string) => void; + search: string; +}; + export function useTableState< TSettings extends BasicTableSettings = BasicTableSettings >(store: CreatePersistedStoreReturn, storageKey: string) { diff --git a/app/react/docker/components/FilesTable/FilesTable.tsx b/app/react/docker/components/FilesTable/FilesTable.tsx new file mode 100644 index 000000000..7b42a7788 --- /dev/null +++ b/app/react/docker/components/FilesTable/FilesTable.tsx @@ -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; + + 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: ( + + ), + 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 ( + + 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 ( + +
+ +
+
+ ); + }} + /> + ); +} diff --git a/app/react/docker/components/FilesTable/columns/ActionsCell.tsx b/app/react/docker/components/FilesTable/columns/ActionsCell.tsx new file mode 100644 index 000000000..74ea258e4 --- /dev/null +++ b/app/react/docker/components/FilesTable/columns/ActionsCell.tsx @@ -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) { + const { meta } = table.options; + if (!isFilesTableMeta(meta)) { + throw new Error('Invalid table meta'); + } + + return ( +
+ {!item.Dir && ( + + + + )} + + + + + + +
+ ); +} diff --git a/app/react/docker/components/FilesTable/columns/NameCell.tsx b/app/react/docker/components/FilesTable/columns/NameCell.tsx new file mode 100644 index 000000000..2e262f7ec --- /dev/null +++ b/app/react/docker/components/FilesTable/columns/NameCell.tsx @@ -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) { + 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 ( + meta.setIsEdit(name, false)} + /> + ); + } + + return ( + <> + {item.Dir ? ( + + ) : ( + + + {name} + + )} + + ); + + 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 ( + onSave(name)} + onReset={onClose} + > + {({ values, setFieldValue }) => ( +
+ setFieldValue('name', e.target.value)} + className="input-sm w-auto" + /> + +