From da5dd0215b7f2501fba72a29840e1992db8fced8 Mon Sep 17 00:00:00 2001 From: SungChul Hong Date: Tue, 9 Jul 2024 12:42:42 +0900 Subject: [PATCH] feat: introduce regacy folder explorer for calling explorer at anywhere --- react/src/components/BAIModal.tsx | 9 +- react/src/components/FolderExplorerOpener.tsx | 32 +- react/src/components/LegacyFolderExplorer.tsx | 206 ++ resources/i18n/de.json | 7 +- resources/i18n/el.json | 7 +- resources/i18n/en.json | 7 +- resources/i18n/es.json | 7 +- resources/i18n/fi.json | 7 +- resources/i18n/fr.json | 7 +- resources/i18n/id.json | 7 +- resources/i18n/it.json | 7 +- resources/i18n/ja.json | 7 +- resources/i18n/ko.json | 7 +- resources/i18n/mn.json | 7 +- resources/i18n/ms.json | 7 +- resources/i18n/pl.json | 7 +- resources/i18n/pt-BR.json | 7 +- resources/i18n/pt.json | 7 +- resources/i18n/ru.json | 7 +- resources/i18n/tr.json | 7 +- resources/i18n/vi.json | 7 +- resources/i18n/zh-CN.json | 7 +- resources/icons/filebrowser.svg | 190 +- src/backend-ai-app.ts | 1 + src/components/backend-ai-app-launcher.ts | 2 +- src/components/backend-ai-folder-explorer.ts | 2008 ++++++++++++++++ src/components/backend-ai-storage-list.ts | 2100 +---------------- src/lib/backend.ai-client-es6.js | 4 +- src/lib/backend.ai-client-esm.ts | 4 +- src/lib/backend.ai-client-node.js | 4 +- src/lib/backend.ai-client-node.ts | 4 +- 31 files changed, 2487 insertions(+), 2210 deletions(-) create mode 100644 react/src/components/LegacyFolderExplorer.tsx create mode 100644 src/components/backend-ai-folder-explorer.ts diff --git a/react/src/components/BAIModal.tsx b/react/src/components/BAIModal.tsx index 10e8132acf..7e715b7e4b 100644 --- a/react/src/components/BAIModal.tsx +++ b/react/src/components/BAIModal.tsx @@ -11,8 +11,13 @@ export const DEFAULT_BAI_MODAL_Z_INDEX = 1001; export interface BAIModalProps extends ModalProps { okText?: string; // customize text of ok button with adequate content draggable?: boolean; // modal can be draggle + className?: string; } -const BAIModal: React.FC = ({ styles, ...modalProps }) => { +const BAIModal: React.FC = ({ + className, + styles, + ...modalProps +}) => { const { token } = theme.useToken(); const [disabled, setDisabled] = useState(true); const [bounds, setBounds] = useState({ @@ -46,7 +51,7 @@ const BAIModal: React.FC = ({ styles, ...modalProps }) => { keyboard={false} {...modalProps} centered={modalProps.centered ?? true} - className="bai-modal" + className={`bai-modal ${className ?? ''}`} wrapClassName={modalProps.draggable ? 'draggable' : ''} styles={{ ...styles, diff --git a/react/src/components/FolderExplorerOpener.tsx b/react/src/components/FolderExplorerOpener.tsx index fe1ad21225..819c4d1a1c 100644 --- a/react/src/components/FolderExplorerOpener.tsx +++ b/react/src/components/FolderExplorerOpener.tsx @@ -1,9 +1,10 @@ import { useBaiSignedRequestWithPromise } from '../helper'; import { useCurrentProjectValue } from '../hooks/useCurrentProject'; import { jotaiStore } from './DefaultProviders'; +import LegacyFolderExplorer from './LegacyFolderExplorer'; import { VFolder } from './VFolderSelect'; import { atom, useAtomValue } from 'jotai'; -import { useEffect } from 'react'; +import { useEffect, useState } from 'react'; import { StringParam, useQueryParam } from 'use-query-params'; // TODO: Separate Folder Explorer from `backend-ai-data-view` and make it opened directly on all pages. @@ -18,6 +19,8 @@ document.addEventListener('backend-ai-data-view:disconnected', () => { const FolderExplorerOpener = () => { const [folderId] = useQueryParam('folder', StringParam) || ''; + const [vfolderName, setVFolderName] = useState(''); + const [open, setOpen] = useState(false); const normalizedFolderId = folderId?.replaceAll('-', ''); const currentProject = useCurrentProjectValue(); const baiRequestWithPromise = useBaiSignedRequestWithPromise(); @@ -38,22 +41,33 @@ const FolderExplorerOpener = () => { // `id` of `/folders` API is not UUID, but UUID without `-` (vFolder) => vFolder.id === normalizedFolderId, ); - document.dispatchEvent( - new CustomEvent('folderExplorer:open', { - detail: { - vFolder, - }, - }), - ); + // document.dispatchEvent( + // new CustomEvent('folderExplorer:open', { + // detail: { + // vFolder, + // }, + // }), + // ); + setVFolderName(vFolder?.name || ''); }) .catch(() => { // do nothing }); + setOpen(true); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [isDataViewReady, folderId]); // don't need to watch `folderId` because this used only once right after the backend-ai-data-view is ready - return null; + // return null; + return ( + setOpen(false)} + destroyOnClose + /> + ); }; export default FolderExplorerOpener; diff --git a/react/src/components/LegacyFolderExplorer.tsx b/react/src/components/LegacyFolderExplorer.tsx new file mode 100644 index 0000000000..bac86e8275 --- /dev/null +++ b/react/src/components/LegacyFolderExplorer.tsx @@ -0,0 +1,206 @@ +import BAIModal, { BAIModalProps } from './BAIModal'; +import Flex from './Flex'; +import { + DeleteOutlined, + FileAddOutlined, + FolderAddOutlined, + UploadOutlined, +} from '@ant-design/icons'; +import { + Button, + Dropdown, + Grid, + Image, + Tooltip, + Typography, + theme, +} from 'antd'; +import { createStyles } from 'antd-style'; +import { useEffect, useRef, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useNavigate } from 'react-router-dom'; + +const useStyles = createStyles(({ token, css }) => ({ + baiModalHeader: css` + .ant-modal-title { + width: 100%; + margin-right: ${token.marginXXL}px; + } + `, +})); + +interface LegacyFolderExplorerProps extends BAIModalProps { + vfolderName: string; + vfolderID: string; + onRequestClose: () => void; +} + +const LegacyFolderExplorer: React.FC = ({ + vfolderName, + vfolderID, + onRequestClose, + ...modalProps +}) => { + const { t } = useTranslation(); + const { token } = theme.useToken(); + const { styles } = useStyles(); + const { lg } = Grid.useBreakpoint(); + const [isWritable, setIsWritable] = useState(false); + const [isSelected, setIsSelected] = useState(false); + // TODO: Events are sent and received as normal, + // but the Lit Element is not rendered and the values inside are not available but ref is available. + const folderExplorerRef = useRef(null); + const navigate = useNavigate(); + + useEffect(() => { + const handleConnected = (e: any) => { + setIsWritable(e.detail || false); + }; + + const handleColumnSelected = (e: any) => { + setIsSelected(e.detail || false); + }; + + document.addEventListener('folderExplorer:connected', handleConnected); + document.addEventListener( + 'folderExplorer:columnSelected', + handleColumnSelected, + ); + return () => { + document.removeEventListener('folderExplorer:connected', handleConnected); + document.removeEventListener( + 'folderExplorer:columnSelected', + handleColumnSelected, + ); + }; + }, []); + + return ( + + + + + {vfolderName} + + + + + + + , + onClick: () => { + // @ts-ignore + folderExplorerRef.current?.handleUpload('file'); + }, + }, + { + key: 'upload folder', + label: t('data.explorer.UploadFolder'), + icon: , + onClick: () => { + // @ts-ignore + folderExplorerRef.current?.handleUpload('folder'); + }, + }, + ], + }} + > + + + + + + + } + onCancel={() => { + onRequestClose(); + const queryParams = new URLSearchParams(window.location.search).get( + 'tab', + ); + if (queryParams) { + navigate(`?tab=${queryParams}`); + } else { + navigate('/data'); + } + }} + {...modalProps} + > + {/* @ts-ignore */} + + + ); +}; + +export default LegacyFolderExplorer; diff --git a/resources/i18n/de.json b/resources/i18n/de.json index 667d9e1091..1aefd2fb87 100644 --- a/resources/i18n/de.json +++ b/resources/i18n/de.json @@ -630,7 +630,6 @@ "Delete": "Löschen", "FolderOptionUpdate": "Ordneroption aktualisieren", "Leave": "Verlassen", - "RenameAFolder": "Einen Ordner umbenennen", "TypeNewFolderName": "Geben Sie einen neuen Ordnernamen ein", "FolderCreated": "Ordner erstellt", "FolderCloned": "Ordner geklont", @@ -709,7 +708,6 @@ "FileExtensionChanged": "Möchten Sie die Dateierweiterung ändern?", "KeepFileExtension": "Behalten", "UseNewFileExtension": "Benutzen", - "RemoveFileExtension": "Dateierweiterung entfernen", "ExecutingFileBrowser": "Dateibrowser wird ausgeführt...", "ExecuteFileBrowser": "Dateibrowser ausführen", "NotEnoughResourceForFileBrowserSession": "Nicht genügend Ressourcen (CPU: 1 Core, Speicher: 0,5 GB), um die Sitzung für den Dateibrowser zu erstellen. Bitte überprüfen Sie die verfügbaren Ressourcen.", @@ -725,7 +723,10 @@ "StartingSSH/SFTPSession": "SFTP-Sitzung starten...", "NumberOfSFTPSessionsExceededTitle": "Limit der laufenden Upload-Session erreicht", "NumberOfSFTPSessionsExceededBody": "Sie führen alle verfügbaren Upload-Sitzungen aus, die Sie erstellen können. Bitte beenden Sie ungenutzte Upload-Sitzungen, bevor Sie eine neue Sitzung starten.", - "DownloadNotAllowed": "Das Herunterladen von Dateien/Ordnern ist in diesem Ordner nicht gestattet." + "DownloadNotAllowed": "Das Herunterladen von Dateien/Ordnern ist in diesem Ordner nicht gestattet.", + "RenameAFolder": "Ordner umbenennen", + "Filename": "Dateiname", + "RemoveFileExtension": "Entfernen " }, "invitation": { "NoValidEmails": "Es wurden keine gültigen E-Mails eingegeben", diff --git a/resources/i18n/el.json b/resources/i18n/el.json index c2d92a5233..e15496221a 100644 --- a/resources/i18n/el.json +++ b/resources/i18n/el.json @@ -630,7 +630,6 @@ "Delete": "Διαγράφω", "FolderOptionUpdate": "Ενημέρωση επιλογής φακέλου", "Leave": "Αδεια", - "RenameAFolder": "Μετονομάστε ένα φάκελο", "TypeNewFolderName": "Πληκτρολογήστε νέο όνομα φακέλου", "FolderCreated": "Δημιουργήθηκε φάκελος", "FolderCloned": "Ο φάκελος κλωνοποιήθηκε", @@ -709,7 +708,6 @@ "FileExtensionChanged": "Θέλετε να αλλάξετε την επέκταση αρχείου;", "KeepFileExtension": "Διατήρηση", "UseNewFileExtension": "Χρήση", - "RemoveFileExtension": "αφαιρέστε την επέκταση αρχείου", "ExecutingFileBrowser": "Εκτέλεση προγράμματος περιήγησης ...", "ExecuteFileBrowser": "Εκτελέστε πρόγραμμα περιήγησης αρχείων", "NotEnoughResourceForFileBrowserSession": "Δεν υπάρχουν αρκετοί πόροι (cpu: 1 Core, mem: 0,5 GB) για τη δημιουργία της περιόδου λειτουργίας για το πρόγραμμα περιήγησης αρχείων παρακαλούμε ελέγξτε τους διαθέσιμους πόρους.", @@ -725,7 +723,10 @@ "StartingSSH/SFTPSession": "Έναρξη συνεδρίας SFTP...", "NumberOfSFTPSessionsExceededTitle": "Έφθασε το όριο του αριθμού των τρεχουσών συνόδων μεταφόρτωσης", "NumberOfSFTPSessionsExceededBody": "Εκτελείτε όλες τις διαθέσιμες συνεδρίες μεταφόρτωσης που επιτρέπεται να δημιουργήσετε. Παρακαλούμε τερματίστε τις αχρησιμοποίητες συνεδρίες μεταφόρτωσης πριν ξεκινήσετε μια νέα συνεδρία.", - "DownloadNotAllowed": "Η λήψη αρχείου/φακέλου δεν επιτρέπεται σε αυτόν τον φάκελο." + "DownloadNotAllowed": "Η λήψη αρχείου/φακέλου δεν επιτρέπεται σε αυτόν τον φάκελο.", + "RenameAFolder": "Μετονομασία φακέλου", + "Filename": "Ονομα αρχείου", + "RemoveFileExtension": "Αφαιρώ " }, "invitation": { "NoValidEmails": "Δεν καταχωρήθηκαν έγκυρα μηνύματα ηλεκτρονικού ταχυδρομείου", diff --git a/resources/i18n/en.json b/resources/i18n/en.json index 387ee32c71..75cd77f606 100644 --- a/resources/i18n/en.json +++ b/resources/i18n/en.json @@ -766,7 +766,6 @@ "Delete": "Delete", "FolderOptionUpdate": "Update folder option", "Leave": "Leave", - "RenameAFolder": "Rename a folder", "TypeNewFolderName": "Type new folder name", "FolderCreated": "Folder created", "FolderCloned": "Folder cloned", @@ -844,7 +843,6 @@ "FileExtensionChanged": "Would you like to change the file extension?", "KeepFileExtension": "Keep ", "UseNewFileExtension": "Use ", - "RemoveFileExtension": "remove file extension", "ExecutingFileBrowser": "Executing filebrowser...", "ExecuteFileBrowser": "Execute filebrowser", "NotEnoughResourceForFileBrowserSession": "No enough resources(cpu: 1 Core, mem: 0.5GB) to create the session for filebrowser. please check the available resources.", @@ -858,7 +856,10 @@ "EmptyFilesAndFoldersAreNotUploaded": "Empty files and empty folders are not uploaded", "NumberOfSFTPSessionsExceededTitle": "Reached limit of running upload session count", "NumberOfSFTPSessionsExceededBody": "You are running all available upload sessions you are allowed to create. Please terminated unused upload sessions before starting a new session.", - "DownloadNotAllowed": "Downloading file/folder is not allowed in this folder." + "DownloadNotAllowed": "Downloading file/folder is not allowed in this folder.", + "RenameAFolder": "Rename Folder", + "Filename": "File name", + "RemoveFileExtension": "Remove " }, "invitation": { "NoValidEmails": "No valid emails were entered", diff --git a/resources/i18n/es.json b/resources/i18n/es.json index e13993b474..37aef24a50 100644 --- a/resources/i18n/es.json +++ b/resources/i18n/es.json @@ -297,7 +297,6 @@ "Permissions": "Permisos", "ReadOnlyFolderOnFileBrowser": "Abrir carpeta de sólo lectura en filebrowser, sólo se permite descargar archivo(s)/carpeta(s).", "ReadonlyFolder": "Carpeta de sólo lectura", - "RemoveFileExtension": "eliminar extensión de archivo", "RenameAFile": "Cambiar nombre de archivo", "RunSSH/SFTPserver": "Ejecutar servidor SFTP", "SFTPSessionNotAvailable": "SFTP Session no está disponible ahora", @@ -312,7 +311,10 @@ "ValueRequired": "Se requiere valor", "ValueShouldBeStarted": "La ruta debe empezar por .(punto) o letras, sólo números.", "WritePermissionRequiredInUploadFiles": "Se requiere permiso de escritura para cargar archivos.", - "DownloadNotAllowed": "No se permite descargar archivos/carpetas en esta carpeta." + "DownloadNotAllowed": "No se permite descargar archivos/carpetas en esta carpeta.", + "RenameAFolder": "Renombrar carpeta", + "Filename": "Nombre del archivo", + "RemoveFileExtension": "Eliminar " }, "folders": { "CannotDeleteFolder": "No se puede eliminar la carpeta montada en una o más sesiones. Por favor, termine la sesión primero.", @@ -355,7 +357,6 @@ "Ownership": "Propiedad", "Permission": "Permiso", "Rename": "Cambie el nombre de", - "RenameAFolder": "Cambiar el nombre de una carpeta", "SameFileName": "El valor de entrada es el mismo que el nombre del archivo que se va a actualizar. Por favor, cámbielo por otro nombre.", "SelectPermission": "Seleccionar permiso", "Serve": "Modelo de servicio", diff --git a/resources/i18n/fi.json b/resources/i18n/fi.json index 07c53734f0..4dadc0f700 100644 --- a/resources/i18n/fi.json +++ b/resources/i18n/fi.json @@ -297,7 +297,6 @@ "Permissions": "Luvat", "ReadOnlyFolderOnFileBrowser": "Lukukansion avaaminen tiedostoselaimessa, vain tiedostojen/kansioiden lataaminen on sallittua.", "ReadonlyFolder": "Vain luku Kansio", - "RemoveFileExtension": "poista tiedostopääte", "RenameAFile": "Nimeä tiedosto uudelleen", "RunSSH/SFTPserver": "Käynnistä SFTP-palvelin", "SFTPSessionNotAvailable": "SFTP-istunto ei ole nyt käytettävissä", @@ -312,7 +311,10 @@ "ValueRequired": "Arvo vaaditaan", "ValueShouldBeStarted": "Polun tulee alkaa kirjaimella .(piste) tai vain kirjaimilla ja numeroilla.", "WritePermissionRequiredInUploadFiles": "Tiedoston (tiedostojen) lataaminen edellyttää kirjoitusoikeutta.", - "DownloadNotAllowed": "Tiedoston/kansion lataaminen ei ole sallittua tässä kansiossa." + "DownloadNotAllowed": "Tiedoston/kansion lataaminen ei ole sallittua tässä kansiossa.", + "RenameAFolder": "Nimeä kansio uudelleen", + "Filename": "Tiedoston nimi", + "RemoveFileExtension": "Poista " }, "folders": { "CannotDeleteFolder": "Yhdessä tai useammassa istunnossa asennettua kansiota ei voi poistaa. Lopeta istunto ensin.", @@ -355,7 +357,6 @@ "Ownership": "Omistus", "Permission": "Lupa", "Rename": "Nimeä uudelleen", - "RenameAFolder": "Nimeä kansio uudelleen", "SameFileName": "Syöttöarvo on sama kuin päivitettävän tiedoston nimi. Vaihda se toiseen nimeen.", "SelectPermission": "Valitse lupa", "Serve": "Malli, joka palvelee", diff --git a/resources/i18n/fr.json b/resources/i18n/fr.json index 0cab51ecbb..e9aacdee91 100644 --- a/resources/i18n/fr.json +++ b/resources/i18n/fr.json @@ -630,7 +630,6 @@ "Delete": "Effacer", "FolderOptionUpdate": "Option de mise à jour du dossier", "Leave": "Quitter", - "RenameAFolder": "Renommer un dossier", "TypeNewFolderName": "Tapez un nouveau nom de dossier", "FolderCreated": "Dossier créé", "FolderCloned": "Dossier cloné", @@ -709,7 +708,6 @@ "FileExtensionChanged": "Souhaitez-vous modifier l'extension de fichier ?", "KeepFileExtension": "Garder", "UseNewFileExtension": "Utiliser", - "RemoveFileExtension": "supprimer l'extension de fichier", "ExecutingFileBrowser": "Exécution du navigateur de fichiers...", "ExecuteFileBrowser": "Exécuter le navigateur de fichiers", "NotEnoughResourceForFileBrowserSession": "Pas assez de ressources (cpu : 1 Core, mem : 0,5 Go) pour créer la session pour le navigateur de fichiers. veuillez vérifier les ressources disponibles.", @@ -725,7 +723,10 @@ "FolderAlreadyExists": "Le dossier sur le point d'être téléchargé existe déjà.", "NumberOfSFTPSessionsExceededTitle": "Limite atteinte pour le nombre de sessions de téléchargement en cours", "NumberOfSFTPSessionsExceededBody": "Vous exécutez toutes les sessions de téléchargement que vous êtes autorisé à créer. Veuillez mettre fin aux sessions de téléchargement inutilisées avant de commencer une nouvelle session.", - "DownloadNotAllowed": "Le téléchargement de fichiers/dossiers n'est pas autorisé dans ce dossier." + "DownloadNotAllowed": "Le téléchargement de fichiers/dossiers n'est pas autorisé dans ce dossier.", + "RenameAFolder": "Renommer le dossier", + "Filename": "Nom de fichier", + "RemoveFileExtension": "Retirer " }, "invitation": { "NoValidEmails": "Aucun e-mail valide n'a été saisi", diff --git a/resources/i18n/id.json b/resources/i18n/id.json index 46965f3dd7..78f4aad385 100644 --- a/resources/i18n/id.json +++ b/resources/i18n/id.json @@ -631,7 +631,6 @@ "Delete": "Hapus", "FolderOptionUpdate": "Opsi perbaruan folder", "Leave": "Tinggalkan", - "RenameAFolder": "Ganti nama folder", "TypeNewFolderName": "Ketik nama folder baru", "FolderCreated": "Folder dibuat", "FolderCloned": "Folder diklon", @@ -710,7 +709,6 @@ "FileExtensionChanged": "Apakah Anda ingin mengubah ekstensi file?", "KeepFileExtension": "Jaga", "UseNewFileExtension": "Gunakan", - "RemoveFileExtension": "hapus ekstensi file", "ExecutingFileBrowser": "Menjalankan file browser...", "ExecuteFileBrowser": "Jalankan file browser", "NotEnoughResourceForFileBrowserSession": "Tidak ada sumber daya yang cukup (cpu: 1 Core, mem: 0.5GB) untuk membuat sesi untuk filebrowser. Mohon periksa sumber daya yang tersedia.", @@ -726,7 +724,10 @@ "FolderAlreadyExists": "Folder yang akan diunggah sudah ada.", "NumberOfSFTPSessionsExceededTitle": "Mencapai batas jumlah sesi unggahan yang sedang berjalan", "NumberOfSFTPSessionsExceededBody": "Anda menjalankan semua sesi unggahan yang tersedia yang diizinkan untuk Anda buat. Hentikan sesi unggahan yang tidak terpakai sebelum memulai sesi baru.", - "DownloadNotAllowed": "Mengunduh file/folder tidak diperbolehkan di folder ini." + "DownloadNotAllowed": "Mengunduh file/folder tidak diperbolehkan di folder ini.", + "RenameAFolder": "Mengganti nama folder", + "Filename": "Nama file", + "RemoveFileExtension": "Menghapus " }, "invitation": { "NoValidEmails": "Tidak ada email valid yang dimasukkan", diff --git a/resources/i18n/it.json b/resources/i18n/it.json index cdd93a090d..f942142daf 100644 --- a/resources/i18n/it.json +++ b/resources/i18n/it.json @@ -631,7 +631,6 @@ "Delete": "Elimina", "FolderOptionUpdate": "Aggiorna opzione cartella", "Leave": "Partire", - "RenameAFolder": "Rinomina una cartella", "TypeNewFolderName": "Digita il nuovo nome della cartella", "FolderCreated": "Cartella creata", "FolderCloned": "Cartella clonata", @@ -710,7 +709,6 @@ "FileExtensionChanged": "Vuoi cambiare l'estensione del file?", "KeepFileExtension": "Mantenere", "UseNewFileExtension": "Uso", - "RemoveFileExtension": "rimuovere l'estensione del file", "ExecutingFileBrowser": "Esecuzione del browser di file in corso...", "ExecuteFileBrowser": "Esegui filebrowser", "NotEnoughResourceForFileBrowserSession": "Risorse insufficienti (cpu: 1 Core, mem: 0,5 GB) per creare la sessione per filebrowser. si prega di controllare le risorse disponibili.", @@ -726,7 +724,10 @@ "StartingSSH/SFTPSession": "Avvio della sessione SFTP...", "NumberOfSFTPSessionsExceededTitle": "Raggiunto il limite del conteggio della sessione di caricamento in corso", "NumberOfSFTPSessionsExceededBody": "Si stanno eseguendo tutte le sessioni di caricamento disponibili che si possono creare. Chiudere le sessioni di caricamento inutilizzate prima di avviare una nuova sessione.", - "DownloadNotAllowed": "Il download di file/cartelle non è consentito in questa cartella." + "DownloadNotAllowed": "Il download di file/cartelle non è consentito in questa cartella.", + "RenameAFolder": "Rinomina cartella", + "Filename": "Nome del file", + "RemoveFileExtension": "Rimuovere " }, "invitation": { "NoValidEmails": "Non sono state inserite email valide", diff --git a/resources/i18n/ja.json b/resources/i18n/ja.json index e04ba72610..7d3310c746 100644 --- a/resources/i18n/ja.json +++ b/resources/i18n/ja.json @@ -630,7 +630,6 @@ "Delete": "削除", "FolderOptionUpdate": "フォルダの更新オプション", "Leave": "去る", - "RenameAFolder": "フォルダの名前を変更する", "TypeNewFolderName": "新しいフォルダ名を入力します", "FolderCreated": "作成されたフォルダ", "FolderCloned": "クローンされたフォルダー", @@ -709,7 +708,6 @@ "FileExtensionChanged": "ファイル拡張子を変更しますか?", "KeepFileExtension": "保つ", "UseNewFileExtension": "使用する", - "RemoveFileExtension": "ファイル拡張子を削除します", "ExecutingFileBrowser": "filebrowserを実行しています...", "ExecuteFileBrowser": "ファイルブラウザを実行する", "NotEnoughResourceForFileBrowserSession": "ファイルブラウザのセッションを作成するのに十分なリソース(CPU:1コア、メモリ:0.5GB)がありません。利用可能なリソースを確認してください。", @@ -725,7 +723,10 @@ "StartingSSH/SFTPSession": "SFTPサーバー起動", "NumberOfSFTPSessionsExceededTitle": "生成可能なアップロードセッション数の制限に達しました。", "NumberOfSFTPSessionsExceededBody": "設定されたアップロードセッション数を超えてセッションを作成しようとしています。続行する前に未使用のアップロードセッションを停止してください。", - "DownloadNotAllowed": "このフォルダーではファイル/フォルダーのダウンロードは許可されていません。" + "DownloadNotAllowed": "このフォルダーではファイル/フォルダーのダウンロードは許可されていません。", + "RenameAFolder": "フォルダー名の変更", + "Filename": "ファイル名", + "RemoveFileExtension": "取り除く " }, "invitation": { "NoValidEmails": "有効なメールが入力されていません", diff --git a/resources/i18n/ko.json b/resources/i18n/ko.json index 337e7a5c6d..21f9e9b907 100644 --- a/resources/i18n/ko.json +++ b/resources/i18n/ko.json @@ -754,7 +754,6 @@ "Delete": "삭제", "FolderOptionUpdate": "폴더 옵션 변경", "Leave": "연결 해제", - "RenameAFolder": "폴더 이름 변경", "TypeNewFolderName": "새 폴더 이름을 입력하세요", "FolderCreated": "폴더가 생성되었습니다", "FolderCloned": "폴더가 복제되었습니다.", @@ -832,7 +831,6 @@ "FileExtensionChanged": "파일 확장자를 변경하시겠습니까?", "KeepFileExtension": " 유지", "UseNewFileExtension": " 사용", - "RemoveFileExtension": "확장자 삭제", "ExecutingFileBrowser": "파일브라우저를 실행합니다...", "ExecuteFileBrowser": "파일브라우저 실행", "NotEnoughResourceForFileBrowserSession": "파일브라우저용 세션 생성을 위한 충분한 자원(cpu: 1 Core, mem: 0.5GB)이 없습니다. 자원 확인 후 다시 시도하십시오.", @@ -846,7 +844,10 @@ "EmptyFilesAndFoldersAreNotUploaded": "빈 파일과 빈 폴더들은 업로드되지 않습니다.", "NumberOfSFTPSessionsExceededTitle": "생성 가능한 업로드 세션 갯수 한도에 도달했습니다.", "NumberOfSFTPSessionsExceededBody": "설정된 업로드 세션 갯수를 초과하여 세션 생성을 시도하고 있습니다. 계속하기 전에 사용하지 않는 업로드 세션을 중지시키세요.", - "DownloadNotAllowed": "이 폴더에서는 파일/폴더 다운로드가 허용되지 않습니다. " + "DownloadNotAllowed": "이 폴더에서는 파일/폴더 다운로드가 허용되지 않습니다. ", + "RenameAFolder": "폴더 이름 변경", + "Filename": "파일 이름", + "RemoveFileExtension": " 제거" }, "invitation": { "NoValidEmails": "정상적인 이메일을 입력해주세요", diff --git a/resources/i18n/mn.json b/resources/i18n/mn.json index 895f5fcf13..561e510bcc 100644 --- a/resources/i18n/mn.json +++ b/resources/i18n/mn.json @@ -632,7 +632,6 @@ "Delete": "Устгах", "FolderOptionUpdate": "Фолдерын сонголтыг шинэчлэх", "Leave": "Орхи", - "RenameAFolder": "Фолдерын нэрийг өөрчлөх", "TypeNewFolderName": "Шинэ хавтасны нэрийг оруулна уу", "FolderCreated": "Фолдер үүсгэсэн", "FolderCloned": "Фолдерыг хуулсан", @@ -711,7 +710,6 @@ "FileExtensionChanged": "Файлын өргөтгөлийг өөрчлөх үү?", "KeepFileExtension": "Хадгалах", "UseNewFileExtension": "Ашиглах", - "RemoveFileExtension": "файлын өргөтгөлийг устгах", "ExecutingFileBrowser": "Файл хөтөчийг гүйцэтгэж байна ...", "ExecuteFileBrowser": "Файл хөтөчийг ажиллуулна уу", "NotEnoughResourceForFileBrowserSession": "Filebrowser-д зориулж Session үүсгэх хангалттай нөөц байхгүй (cpu: 1 Core, mem: 0.5GB). боломжтой нөөцийг шалгана уу.", @@ -727,7 +725,10 @@ "FolderAlreadyExists": "Байршуулах гэж буй фолдер аль хэдийн байна.", "NumberOfSFTPSessionsExceededTitle": "Байршуулах сеанс тоолох хязгаарт хүрсэн", "NumberOfSFTPSessionsExceededBody": "Та үүсгэх боломжтой бүх байршуулах сешнүүдийг ажиллуулж байна. \nШинэ сесс эхлэхээс өмнө ашиглагдаагүй байршуулах сешнүүдийг дуусгана уу.", - "DownloadNotAllowed": "Энэ фолдерт файл/хавтас татаж авахыг хориглоно." + "DownloadNotAllowed": "Энэ фолдерт файл/хавтас татаж авахыг хориглоно.", + "RenameAFolder": "Хавтасны нэрийг өөрчлөх", + "Filename": "Файлын нэр", + "RemoveFileExtension": "Устгах " }, "invitation": { "NoValidEmails": "Хүчинтэй имэйл оруулаагүй болно", diff --git a/resources/i18n/ms.json b/resources/i18n/ms.json index 22fb6beff1..bec395002d 100644 --- a/resources/i18n/ms.json +++ b/resources/i18n/ms.json @@ -630,7 +630,6 @@ "Delete": "Padam", "FolderOptionUpdate": "Kemas kini pilihan folder", "Leave": "Tinggalkan", - "RenameAFolder": "Namakan semula folder", "TypeNewFolderName": "Taipkan nama folder baru", "FolderCreated": "Folder dibuat", "FolderCloned": "Folder diklon", @@ -709,7 +708,6 @@ "FileExtensionChanged": "Adakah anda ingin menukar peluasan fail?", "KeepFileExtension": "Jaga", "UseNewFileExtension": "Gunakan", - "RemoveFileExtension": "buang peluasan fail", "ExecutingFileBrowser": "Melaksanakan penyemak imbas ...", "ExecuteFileBrowser": "Laksanakan penyaring data", "NotEnoughResourceForFileBrowserSession": "Tidak ada sumber yang mencukupi (cpu: 1 Core, mem: 0.5GB) untuk membuat sesi untuk penyemak imbas fail. sila periksa sumber yang ada.", @@ -725,7 +723,10 @@ "StartingSSH/SFTPSession": "Memulakan sesi SFTP...", "NumberOfSFTPSessionsExceededTitle": "Had capaian untuk menjalankan kiraan sesi muat naik", "NumberOfSFTPSessionsExceededBody": "Anda menjalankan semua sesi muat naik tersedia yang anda dibenarkan buat. \nSila tamatkan sesi muat naik yang tidak digunakan sebelum memulakan sesi baharu.", - "DownloadNotAllowed": "Memuat turun fail/folder tidak dibenarkan dalam folder ini." + "DownloadNotAllowed": "Memuat turun fail/folder tidak dibenarkan dalam folder ini.", + "RenameAFolder": "Namakan semula Folder", + "Filename": "Nama fail", + "RemoveFileExtension": "Alih keluar " }, "invitation": { "NoValidEmails": "Tidak ada e-mel yang sah dimasukkan", diff --git a/resources/i18n/pl.json b/resources/i18n/pl.json index 305d651271..832c8a24e5 100644 --- a/resources/i18n/pl.json +++ b/resources/i18n/pl.json @@ -630,7 +630,6 @@ "Delete": "Kasować", "FolderOptionUpdate": "Opcja folderu aktualizacji", "Leave": "Wychodzić", - "RenameAFolder": "Zmień nazwę folderu", "TypeNewFolderName": "Wpisz nową nazwę folderu", "FolderCreated": "Utworzono folder", "FolderCloned": "Sklonowany folder", @@ -709,7 +708,6 @@ "FileExtensionChanged": "Czy chcesz zmienić rozszerzenie pliku?", "KeepFileExtension": "Trzymać", "UseNewFileExtension": "Posługiwać się", - "RemoveFileExtension": "usuń rozszerzenie pliku", "ExecutingFileBrowser": "Uruchamiam przeglądarkę plików...", "ExecuteFileBrowser": "Uruchom przeglądarkę plików", "NotEnoughResourceForFileBrowserSession": "Brak wystarczających zasobów (cpu: 1 rdzeń, mem: 0,5 GB), aby utworzyć sesję dla przeglądarki plików. sprawdź dostępne zasoby.", @@ -725,7 +723,10 @@ "StartingSSH/SFTPSession": "Rozpoczęcie sesji SFTP...", "NumberOfSFTPSessionsExceededTitle": "Osiągnięto limit liczby uruchomionych sesji przesyłania", "NumberOfSFTPSessionsExceededBody": "Uruchomiono wszystkie dostępne sesje przesyłania, które można utworzyć. Przed rozpoczęciem nowej sesji zakończ nieużywane sesje przesyłania.", - "DownloadNotAllowed": "Pobieranie pliku/folderu w tym folderze jest niedozwolone." + "DownloadNotAllowed": "Pobieranie pliku/folderu w tym folderze jest niedozwolone.", + "RenameAFolder": "Zmień nazwę folderu", + "Filename": "Nazwa pliku", + "RemoveFileExtension": "Usunąć " }, "invitation": { "NoValidEmails": "Nie wprowadzono poprawnych adresów e-mail", diff --git a/resources/i18n/pt-BR.json b/resources/i18n/pt-BR.json index ed87f0808b..c90dcc60b3 100644 --- a/resources/i18n/pt-BR.json +++ b/resources/i18n/pt-BR.json @@ -630,7 +630,6 @@ "Delete": "Excluir", "FolderOptionUpdate": "Opção de atualização de pasta", "Leave": "Sair", - "RenameAFolder": "Renomear uma pasta", "TypeNewFolderName": "Digite o nome da nova pasta", "FolderCreated": "Pasta criada", "FolderCloned": "Pasta clonada", @@ -709,7 +708,6 @@ "FileExtensionChanged": "Você gostaria de mudar a extensão do arquivo?", "KeepFileExtension": "Manter", "UseNewFileExtension": "Usar", - "RemoveFileExtension": "remover extensão de arquivo", "ExecutingFileBrowser": "Executando o navegador de arquivos ...", "ExecuteFileBrowser": "Executar o navegador de arquivos", "NotEnoughResourceForFileBrowserSession": "Sem recursos suficientes (cpu: 1 Core, mem: 0,5 GB) para criar a sessão para o navegador de arquivos. verifique os recursos disponíveis.", @@ -725,7 +723,10 @@ "StartingSSH/SFTPSession": "Iniciando a sessão SFTP...", "NumberOfSFTPSessionsExceededTitle": "Atingiu o limite da contagem de sessões de carregamento em execução", "NumberOfSFTPSessionsExceededBody": "Está a executar todas as sessões de carregamento disponíveis que tem permissão para criar. Termine as sessões de carregamento não utilizadas antes de iniciar uma nova sessão.", - "DownloadNotAllowed": "O download de arquivo/pasta não é permitido nesta pasta." + "DownloadNotAllowed": "O download de arquivo/pasta não é permitido nesta pasta.", + "RenameAFolder": "Renomear pasta", + "Filename": "Nome do arquivo", + "RemoveFileExtension": "Remover " }, "invitation": { "NoValidEmails": "Nenhum e-mail válido foi inserido", diff --git a/resources/i18n/pt.json b/resources/i18n/pt.json index 9219762ca7..9fd34135c8 100644 --- a/resources/i18n/pt.json +++ b/resources/i18n/pt.json @@ -630,7 +630,6 @@ "Delete": "Excluir", "FolderOptionUpdate": "Opção de atualização de pasta", "Leave": "Sair", - "RenameAFolder": "Renomear uma pasta", "TypeNewFolderName": "Digite o nome da nova pasta", "FolderCreated": "Pasta criada", "FolderCloned": "Pasta clonada", @@ -709,7 +708,6 @@ "FileExtensionChanged": "Você gostaria de mudar a extensão do arquivo?", "KeepFileExtension": "Manter", "UseNewFileExtension": "Usar", - "RemoveFileExtension": "remover extensão de arquivo", "ExecutingFileBrowser": "Executando o navegador de arquivos ...", "ExecuteFileBrowser": "Executar o navegador de arquivos", "NotEnoughResourceForFileBrowserSession": "Sem recursos suficientes (cpu: 1 Core, mem: 0,5 GB) para criar a sessão para o navegador de arquivos. verifique os recursos disponíveis.", @@ -725,7 +723,10 @@ "StartingSSH/SFTPSession": "Iniciando a sessão SFTP...", "NumberOfSFTPSessionsExceededTitle": "Atingiu o limite da contagem de sessões de carregamento em execução", "NumberOfSFTPSessionsExceededBody": "Está a executar todas as sessões de carregamento disponíveis que tem permissão para criar. Termine as sessões de carregamento não utilizadas antes de iniciar uma nova sessão.", - "DownloadNotAllowed": "O download de arquivo/pasta não é permitido nesta pasta." + "DownloadNotAllowed": "O download de arquivo/pasta não é permitido nesta pasta.", + "RenameAFolder": "Renomear pasta", + "Filename": "Nome do arquivo", + "RemoveFileExtension": "Remover " }, "invitation": { "NoValidEmails": "Nenhum e-mail válido foi inserido", diff --git a/resources/i18n/ru.json b/resources/i18n/ru.json index d965b167b9..6d5b83aff8 100644 --- a/resources/i18n/ru.json +++ b/resources/i18n/ru.json @@ -630,7 +630,6 @@ "Delete": "Удалить", "FolderOptionUpdate": "Опция обновления папки", "Leave": "Покинуть", - "RenameAFolder": "Переименовать папку", "TypeNewFolderName": "Введите новое имя папки", "FolderCreated": "Папка создана", "FolderCloned": "Папка клонирована", @@ -709,7 +708,6 @@ "FileExtensionChanged": "Вы хотите изменить расширение файла?", "KeepFileExtension": "Хранить", "UseNewFileExtension": "Использовать", - "RemoveFileExtension": "удалить расширение файла", "ExecutingFileBrowser": "Запуск файлового браузера ...", "ExecuteFileBrowser": "Запустить файловый браузер", "NotEnoughResourceForFileBrowserSession": "Недостаточно ресурсов (cpu: 1 Core, mem: 0,5GB) для создания сеанса для просмотра файлов. пожалуйста, проверьте доступные ресурсы.", @@ -725,7 +723,10 @@ "FolderAlreadyExists": "Папка, которую планируется загрузить, уже существует.", "NumberOfSFTPSessionsExceededTitle": "Достигнут предел количества запущенных сеансов выгрузки", "NumberOfSFTPSessionsExceededBody": "У вас запущены все доступные сессии выгрузки, которые вы можете создать. Перед началом нового сеанса завершите неиспользованные сеансы выгрузки.", - "DownloadNotAllowed": "Загрузка файла/папки в этой папке запрещена." + "DownloadNotAllowed": "Загрузка файла/папки в этой папке запрещена.", + "RenameAFolder": "Переименовать папку", + "Filename": "Имя файла", + "RemoveFileExtension": "Удалять " }, "invitation": { "NoValidEmails": "Не введены действительные адреса электронной почты", diff --git a/resources/i18n/tr.json b/resources/i18n/tr.json index f6dcc26536..5ed0de56d8 100644 --- a/resources/i18n/tr.json +++ b/resources/i18n/tr.json @@ -630,7 +630,6 @@ "Delete": "Sil", "FolderOptionUpdate": "Klasörü güncelle seçeneği", "Leave": "Ayrılmak", - "RenameAFolder": "Bir klasörü yeniden adlandırın", "TypeNewFolderName": "Yeni klasör adını yazın", "FolderCreated": "Klasör oluşturuldu", "FolderCloned": "Klasör klonlandı", @@ -709,7 +708,6 @@ "FileExtensionChanged": "Dosya uzantısını değiştirmek ister misiniz?", "KeepFileExtension": "Tut", "UseNewFileExtension": "kullanın", - "RemoveFileExtension": "dosya uzantısını kaldır", "ExecutingFileBrowser": "Dosya tarayıcısı yürütülüyor...", "ExecuteFileBrowser": "Dosya tarayıcısını çalıştır", "NotEnoughResourceForFileBrowserSession": "Dosya tarayıcısı için oturum oluşturmak için yeterli kaynak (işlemci: 1 Çekirdek, mem: 0,5 GB) yok. lütfen mevcut kaynakları kontrol edin.", @@ -725,7 +723,10 @@ "StartingSSH/SFTPSession": "SFTP oturumu başlatılıyor...", "NumberOfSFTPSessionsExceededTitle": "Çalışan yükleme oturumu sayısı sınırına ulaşıldı", "NumberOfSFTPSessionsExceededBody": "Oluşturmanıza izin verilen tüm mevcut yükleme oturumlarını çalıştırıyorsunuz. Lütfen yeni bir oturum başlatmadan önce kullanılmayan yükleme oturumlarını sonlandırın.", - "DownloadNotAllowed": "Bu klasörde dosya/klasör indirmeye izin verilmiyor." + "DownloadNotAllowed": "Bu klasörde dosya/klasör indirmeye izin verilmiyor.", + "RenameAFolder": "Dosyayı yeniden adlandır", + "Filename": "Dosya adı", + "RemoveFileExtension": "Kaldırmak " }, "invitation": { "NoValidEmails": "Geçerli e-posta girilmedi", diff --git a/resources/i18n/vi.json b/resources/i18n/vi.json index dbd09dc090..7aa943fc44 100644 --- a/resources/i18n/vi.json +++ b/resources/i18n/vi.json @@ -630,7 +630,6 @@ "Delete": "Xóa bỏ", "FolderOptionUpdate": "Cập nhật tùy chọn thư mục", "Leave": "Rời khỏi", - "RenameAFolder": "Đổi tên một thư mục", "TypeNewFolderName": "Nhập tên thư mục mới", "FolderCreated": "Thư mục đã được tạo", "FolderCloned": "Thư mục được nhân bản", @@ -709,7 +708,6 @@ "FileExtensionChanged": "Bạn có muốn thay đổi phần mở rộng tệp không?", "KeepFileExtension": "Giữ", "UseNewFileExtension": "Sử dụng", - "RemoveFileExtension": "xóa phần mở rộng tệp", "ExecutingFileBrowser": "Đang thực thi trình duyệt tệp ...", "ExecuteFileBrowser": "Thực thi trình duyệt tệp", "NotEnoughResourceForFileBrowserSession": "Không có đủ tài nguyên (cpu: 1 Core, mem: 0,5GB) để tạo phiên cho trình duyệt tệp. xin vui lòng kiểm tra các tài nguyên có sẵn.", @@ -725,7 +723,10 @@ "StartingSSH/SFTPSession": "Đang bắt đầu phiên SFTP...", "NumberOfSFTPSessionsExceededTitle": "Đã đạt đến giới hạn số phiên tải lên đang chạy", "NumberOfSFTPSessionsExceededBody": "Bạn đang chạy tất cả các phiên tải lên có sẵn mà bạn được phép tạo. \nVui lòng chấm dứt các phiên tải lên không sử dụng trước khi bắt đầu phiên mới.", - "DownloadNotAllowed": "Không được phép tải xuống tập tin/thư mục trong thư mục này." + "DownloadNotAllowed": "Không được phép tải xuống tập tin/thư mục trong thư mục này.", + "RenameAFolder": "Đổi tên thư mục", + "Filename": "Tên tập tin", + "RemoveFileExtension": "Di dời " }, "invitation": { "NoValidEmails": "Không có email hợp lệ nào được nhập", diff --git a/resources/i18n/zh-CN.json b/resources/i18n/zh-CN.json index 6e9f1ba645..c10eb1dcbe 100644 --- a/resources/i18n/zh-CN.json +++ b/resources/i18n/zh-CN.json @@ -630,7 +630,6 @@ "Delete": "删除", "FolderOptionUpdate": "更新文件夹选项", "Leave": "离开", - "RenameAFolder": "重命名文件夹", "TypeNewFolderName": "输入新文件夹名称", "FolderCreated": "文件夹已创建", "FolderCloned": "文件夹已克隆", @@ -709,7 +708,6 @@ "FileExtensionChanged": "您想更改文件扩展名吗?", "KeepFileExtension": "保持", "UseNewFileExtension": "用", - "RemoveFileExtension": "删除文件扩展名", "ExecutingFileBrowser": "正在执行文件浏览器...", "ExecuteFileBrowser": "执行文件浏览器", "NotEnoughResourceForFileBrowserSession": "没有足够的资源(cpu: 1 Core, mem: 0.5GB)来为文件浏览器创建会话。请检查可用资源。", @@ -725,7 +723,10 @@ "StartingSSH/SFTPSession": "开始 SFTP 会话...", "NumberOfSFTPSessionsExceededTitle": "运行中的上传会话次数已达上限", "NumberOfSFTPSessionsExceededBody": "您正在运行允许创建的所有可用上传会话。请在启动新会话前终止未使用的上传会话。", - "DownloadNotAllowed": "不允许在此文件夹中下载文件/文件夹。" + "DownloadNotAllowed": "不允许在此文件夹中下载文件/文件夹。", + "RenameAFolder": "重命名文件夹", + "Filename": "文件名", + "RemoveFileExtension": "消除 " }, "invitation": { "NoValidEmails": "没有输入有效的电子邮件", diff --git a/resources/icons/filebrowser.svg b/resources/icons/filebrowser.svg index 5b3abdacf8..5e78eccff1 100644 --- a/resources/icons/filebrowser.svg +++ b/resources/icons/filebrowser.svg @@ -1,43 +1,147 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +image/svg+xml + + + + + \ No newline at end of file diff --git a/src/backend-ai-app.ts b/src/backend-ai-app.ts index 61152b6346..db21aaa025 100644 --- a/src/backend-ai-app.ts +++ b/src/backend-ai-app.ts @@ -114,6 +114,7 @@ const loadPage = break; case 'data': import('./components/backend-ai-data-view.js'); + import('./components/backend-ai-folder-explorer.js'); break; case 'agent': case 'resource': diff --git a/src/components/backend-ai-app-launcher.ts b/src/components/backend-ai-app-launcher.ts index 8e0e80a23d..258ff57292 100644 --- a/src/components/backend-ai-app-launcher.ts +++ b/src/components/backend-ai-app-launcher.ts @@ -1807,7 +1807,7 @@ export default class BackendAiAppLauncher extends BackendAIPage { - + SSH / SFTP connection
diff --git a/src/components/backend-ai-folder-explorer.ts b/src/components/backend-ai-folder-explorer.ts new file mode 100644 index 0000000000..29c65a1af4 --- /dev/null +++ b/src/components/backend-ai-folder-explorer.ts @@ -0,0 +1,2008 @@ +/** + @license + Copyright (c) 2015-2024 Lablup Inc. All rights reserved. + */ +import tus from '../lib/tus'; +import { + IronFlex, + IronFlexAlignment, + IronPositioning, +} from '../plastics/layout/iron-flex-layout-classes'; +import BackendAIDialog from './backend-ai-dialog'; +import { BackendAiStyles } from './backend-ai-general-styles'; +import { BackendAIPage } from './backend-ai-page'; +import { default as PainKiller } from './backend-ai-painkiller'; +import { Button } from '@material/mwc-button'; +import { TextField } from '@material/mwc-textfield'; +import { css, CSSResultGroup, html, render } from 'lit'; +import { get as _text, translate as _t } from 'lit-translate'; +import { customElement, property, query, state } from 'lit/decorators.js'; + +/* FIXME: + * This type definition is a workaround for resolving both Type error and Importing error. + */ +type VaadinGrid = HTMLElementTagNameMap['vaadin-grid']; + +interface fileData { + id: string; + progress: number; + caption: string; + error: boolean; + complete: boolean; +} + +/** + Backend AI Session View + + Example: + + + ... + + + @group Backend.AI Web UI + @element backend-ai-folder-explorer +*/ + +@customElement('backend-ai-folder-explorer') +export default class BackendAIFolderExplorer extends BackendAIPage { + // [target vfolder information] + @property({ type: String }) vfolderID = ''; + @property({ type: String }) vfolderName = ''; + @property({ type: Array }) vfolderFiles = []; + @property({ type: String }) vhost = ''; + @property({ type: Boolean }) isWritable = false; + @property({ type: Array }) breadcrumb = ['.']; + @property({ type: Number }) _APIMajorVersion = 5; + @property({ type: Array }) uploadFiles: fileData[] = []; + @property({ type: Boolean }) _isDirectorySizeVisible = true; + @property({ type: Object }) notification = Object(); + // [rename folder or file] + @property({ type: String }) newName = ''; + @property({ type: String }) oldFileExtension = ''; + @property({ type: String }) newFileExtension = ''; + @property({ type: Boolean }) is_dir = false; + // [download file] + @property({ type: String }) downloadURL = ''; + // [upload file] + @property({ type: Boolean }) _uploadFlag = true; + @property({ type: Boolean }) uploadFilesExist = false; + @property({ type: Object }) currentUploadFile = Object(); + @property({ type: Number }) _maxFileUploadSize = -1; + @property({ type: Array }) fileUploadQueue = []; + @property({ type: Number }) fileUploadCount = 0; + @property({ type: Number }) concurrentFileUploadLimit = 2; + @property({ type: Object }) _boundUploadListRenderer = Object(); + @property({ type: Object }) _boundUploadProgressRenderer = Object(); + // [filebrowser] + @property({ type: Number }) minimumResource = { + cpu: 1, + mem: 0.5, + }; + @property({ type: Array }) filebrowserSupportedImages = []; + @property({ type: Array }) systemRoleSupportedImages = []; + @property({ type: Object }) indicator = Object(); + @query('#filebrowser-notification-dialog') + fileBrowserNotificationDialog!: BackendAIDialog; + // [SSH / SFTP] + @property({ type: Object }) volumeInfo = Object(); + // [renderers] + @property({ type: Object }) _boundIndexRenderer = Object(); + @property({ type: Object }) _boundFileNameRenderer = Object(); + @property({ type: Object }) _boundCreatedTimeRenderer = Object(); + @property({ type: Object }) _boundSizeRenderer = Object(); + @property({ type: Object }) _boundControlFileListRenderer = Object(); + @state() private _unionedAllowedPermissionByVolume = Object(); + + @query('#rename-file-dialog') renameDialog!: BackendAIDialog; + @query('#new-file-name') newNameInput!: TextField; + @query('#file-extension-change-dialog') + fileExtensionChangeDialog!: BackendAIDialog; + @query('#delete-file-dialog') deleteFileDialog!: BackendAIDialog; + @query('#mkdir-dialog') mkdirDialog!: BackendAIDialog; + @query('#mkdir-name') mkdirNameInput!: TextField; + @query('#file-list-grid') fileListGrid!: VaadinGrid; + + static get styles(): CSSResultGroup { + return [ + BackendAiStyles, + IronFlex, + IronFlexAlignment, + IronPositioning, + // language=CSS + css` + div#container { + } + + div#dropzone { + display: none; + position: absolute; + top: var(--general-modal-header-height, 69px); + height: calc(100% - 89px); + width: calc(100% - 48px); + z-index: 10; + } + + div#dropzone { + background: rgba(211, 211, 211, 0.5); + text-align: center; + } + + span.title { + margin: auto 10px; + min-width: 35px; + } + + img#filebrowser-img, + img#ssh-img { + width: 18px; + margin: 15px 10px; + } + + div.breadcrumb { + color: #637282; + font-size: 1em; + margin-bottom: 10px; + } + + div.breadcrumb span:first-child { + display: none; + } + + .breadcrumb li:before { + padding: 3px; + transform: rotate(-45deg) translateY(-2px); + transition: color ease-in 0.2s; + border: solid; + border-width: 0 2px 2px 0; + border-color: var(--token-colorBorder, #242424); + margin-right: 10px; + content: ''; + display: inline-block; + } + + .breadcrumb li { + display: inline-block; + font-size: 16px; + } + + .breadcrumb ul { + padding: 0; + margin: 0; + list-style-type: none; + } + + .breadcrumb mwc-icon-button { + --mdc-icon-size: 20px; + --mdc-icon-button-size: 22px; + } + + backend-ai-dialog mwc-textfield, + backend-ai-dialog mwc-select { + --mdc-typography-label-font-size: var(--token-fontSizeSM, 12px); + } + + backend-ai-dialog { + --component-min-width: 350px; + } + + vaadin-item { + font-size: 13px; + font-weight: 100; + } + + vaadin-text-field { + --vaadin-text-field-default-width: auto; + } + + vaadin-grid-cell-content { + font-size: 13px; + } + + mwc-icon-button { + --mdc-icon-size: 24px; + --mdc-icon-button-size: 28px; + padding: 4px; + } + + mwc-button { + --mdc-typography-button-font-size: 12px; + } + + mwc-textfield { + width: 100%; + } + `, + ]; + } + + constructor() { + super(); + this._boundIndexRenderer = this.indexRenderer.bind(this); + this._boundFileNameRenderer = this.fileNameRenderer.bind(this); + this._boundCreatedTimeRenderer = this.createdTimeRenderer.bind(this); + this._boundSizeRenderer = this.sizeRenderer.bind(this); + this._boundControlFileListRenderer = + this.controlFileListRenderer.bind(this); + this._boundUploadListRenderer = this.uploadListRenderer.bind(this); + this._boundUploadProgressRenderer = this.uploadProgressRenderer.bind(this); + } + + async firstUpdated() { + this.indicator = globalThis.lablupIndicator; + this.notification = globalThis.lablupNotification; + this._maxFileUploadSize = + globalThis.backendaiclient._config.maxFileUploadSize; + this._APIMajorVersion = globalThis.backendaiclient.APIMajorVersion; + this.fileListGrid.addEventListener('selected-items-changed', () => { + this._toggleFileListCheckbox(); + }); + this._addEventListenerDropZone(); + await this._fetchVFolder(); + this.fileListGrid.clearCache(); + } + + _toggleFileListCheckbox() { + const buttons = this.shadowRoot?.querySelectorAll
- { - this.triggerCloseFilebrowserToReact(); - }} - > - ${this.explorer.id} -
-
- ${this.isWritable - ? html` - -
- - ${_t('data.explorer.UploadFiles')} - -
-
- - ${_t('data.explorer.UploadFolder')} - -
-
- - ${_t('data.explorer.NewFolder')} - -
- ` - : html` - - ${_t('data.explorer.ReadonlyFolder')} - - `} -
- - File Browser - ${_t('data.explorer.ExecuteFileBrowser')} - -
-
- - SSH / SFTP - ${_t('data.explorer.RunSSH/SFTPserver')} - -
-
-
- -

drag

- - - ${this.uploadFilesExist - ? html` -
- - ${_t('data.explorer.StopUploading')} - -
-
- ${this.currentUploadFile?.complete - ? html` - check - ` - : html``} -
- ${this.currentUploadFile?.name} - - ${this.currentUploadFile?.caption} -
-
- - ` - : html``} - - - - - - - - -
-
- - ${_t('data.explorer.CreateANewFolder')} -
- -
-
-
- - ${_t('button.Create')} - -
-
${_t('data.explorer.ShareFolder')}
@@ -1456,148 +1013,6 @@ export default class BackendAiStorageList extends BackendAIPage {
- - ${_t('data.explorer.RenameAFile')} -
- -
-
-
- - ${_t('data.explorer.RenameAFile')} - -
-
- - ${_t('dialog.title.LetsDouble-Check')} -
-

- ${_t('dialog.warning.CannotBeUndone')} - ${_t('dialog.ask.DoYouWantToProceed')} -

-
-
- - ${_t('button.Cancel')} - - - ${_t('button.Okay')} - -
-
- - ${_t('data.explorer.DownloadFile')} - -
- - ${_t('button.Close')} - -
-
- - ${_t('dialog.title.LetsDouble-Check')} -
-

${_t('data.explorer.FileExtensionChanged')}

-
-
- - ${globalThis.backendaioptions.get( - 'language', - 'default', - 'general', - ) !== 'ko' - ? html` - ${_text('data.explorer.KeepFileExtension') + - this.oldFileExtension} - ` - : html` - ${this.oldFileExtension + - _text('data.explorer.KeepFileExtension')} - `} - - - ${globalThis.backendaioptions.get( - 'language', - 'default', - 'general', - ) !== 'ko' - ? html` - ${this.newFileExtension - ? _text('data.explorer.UseNewFileExtension') + - this.newFileExtension - : _text('data.explorer.RemoveFileExtension')} - ` - : html` - ${this.newFileExtension - ? this.newFileExtension + - _text('data.explorer.UseNewFileExtension') - : _text('data.explorer.RemoveFileExtension')} - `} - -
-
- - ${_t('dialog.title.Notice')} -
- ${_t('data.explorer.ReadOnlyFolderOnFileBrowser')} -
-
-
- - - ${_text('dialog.hide.DonotShowThisAgain')} - -
- - ${_t('button.Confirm')} - -
-
${_t('dialog.title.DeleteForever')}
@@ -1631,11 +1046,6 @@ export default class BackendAiStorageList extends BackendAIPage { } firstUpdated() { - this._addEventListenerDropZone(); - this._mkdir = this._mkdir.bind(this); - this.fileListGrid.addEventListener('selected-items-changed', () => { - this._toggleFileListCheckbox(); - }); this.indicator = globalThis.lablupIndicator; this.notification = globalThis.lablupNotification; const textfields = this.shadowRoot?.querySelectorAll( @@ -1656,10 +1066,6 @@ export default class BackendAiStorageList extends BackendAIPage { document.addEventListener('backend-ai-group-changed', (e) => this._refreshFolderList(true, 'group-changed'), ); - document.addEventListener('backend-ai-ui-changed', (e) => - this._refreshFolderUI(e), - ); - this._refreshFolderUI({ detail: { 'mini-ui': globalThis.mini_ui } }); } _modifySharedFolderPermissions() { @@ -1838,48 +1244,6 @@ export default class BackendAiStorageList extends BackendAIPage { ); } - uploadListRenderer(root, column?, rowData?) { - render( - // language=HTML - html` - -
- ${rowData.item.complete - ? html` - check - ` - : html``} -
-
- `, - root, - ); - } - - uploadProgressRenderer(root, column?, rowData?) { - render( - // language=HTML - html` - - ${rowData.item.name} - ${rowData.item.complete - ? html`` - : html` -
- -
-
- ${rowData.item.caption} -
- `} -
- `, - root, - ); - } - inviteeInfoRenderer(root, column?, rowData?) { render( // language=HTML @@ -2219,99 +1583,6 @@ export default class BackendAiStorageList extends BackendAIPage { ); } - /** - * Render control file options - downloadFile, openRenameFileDialog, openDeleteFileDialog, etc. - * - * @param {Element} root - the row details content DOM element - * @param {Element} column - the column element that controls the state of the host element - * @param {Object} rowData - the object with the properties related with the rendered item - * */ - controlFileListRenderer(root, column?, rowData?) { - render( - // language=HTML - html` -
- - ${!this._isDownloadable(this.vhost) - ? html` - - ` - : html``} - - -
- `, - root, - ); - } - - /** - * Render file name as rowData.item.filename. - * - * @param {Element} root - the row details content DOM element - * @param {Element} column - the column element that controls the state of the host element - * @param {Object} rowData - the object with the properties related with the rendered item - * */ - fileNameRenderer(root, column?, rowData?) { - render( - html` - ${this._isDir(rowData.item) - ? html` -
- - ${rowData.item.filename} -
- ` - : html` -
- - ${rowData.item.filename} -
- `} - `, - root, - ); - } - /** * Render permission view - r, w, d. * @@ -2413,128 +1684,30 @@ export default class BackendAiStorageList extends BackendAIPage { } /** - * Render created time. + * Render type of user - person, group. * * @param {Element} root - the row details content DOM element * @param {Element} column - the column element that controls the state of the host element * @param {Object} rowData - the object with the properties related with the rendered item * */ - createdTimeRenderer(root, column?, rowData?) { + typeRenderer(root, column?, rowData?) { render( // language=HTML html` -
- ${this._humanReadableTime(rowData.item.ctime)} +
+ ${rowData.item.type == 'user' + ? html` + person + ` + : html` + group + `}
`, root, ); } - /** - * Render size by condition - * - * @param {Element} root - the row details content DOM element - * @param {Element} column - the column element that controls the state of the host element - * @param {Object} rowData - the object with the properties related with the rendered item - * */ - sizeRenderer(root, column?, rowData?) { - render( - // language=HTML - html` -
- ${(rowData.item.type as string).toUpperCase() === 'DIRECTORY' && - !this._isDirectorySizeVisible - ? html` - - - ` - : html` - ${rowData.item.size} - `} -
- `, - root, - ); - } - - /** - * Render type of user - person, group. - * - * @param {Element} root - the row details content DOM element - * @param {Element} column - the column element that controls the state of the host element - * @param {Object} rowData - the object with the properties related with the rendered item - * */ - typeRenderer(root, column?, rowData?) { - render( - // language=HTML - html` -
- ${rowData.item.type == 'user' - ? html` - person - ` - : html` - group - `} -
- `, - root, - ); - } - - private async _getCurrentKeypairResourcePolicy() { - const accessKey = globalThis.backendaiclient._config.accessKey; - const res = await globalThis.backendaiclient.keypair.info(accessKey, [ - 'resource_policy', - ]); - return res.keypair.resource_policy; - } - - async _getVolumeInformation() { - const vhostInfo = await globalThis.backendaiclient.vfolder.list_hosts(); - this.volumeInfo = vhostInfo.volume_info || {}; - } - - async _getAllowedVFolderHostsByCurrentUserInfo() { - const [vhostInfo, currentKeypairResourcePolicy] = await Promise.all([ - globalThis.backendaiclient.vfolder.list_hosts(), - this._getCurrentKeypairResourcePolicy(), - ]); - const currentDomain = globalThis.backendaiclient._config.domainName; - const currentGroupId = globalThis.backendaiclient.current_group_id(); - const mergedData = - await globalThis.backendaiclient.storageproxy.getAllowedVFolderHostsByCurrentUserInfo( - currentDomain, - currentGroupId, - currentKeypairResourcePolicy, - ); - - const allowedPermissionForDomainsByVolume = JSON.parse( - mergedData?.domain?.allowed_vfolder_hosts || '{}', - ); - const allowedPermissionForGroupsByVolume = JSON.parse( - mergedData?.group?.allowed_vfolder_hosts || '{}', - ); - const allowedPermissionForResourcePolicyByVolume = JSON.parse( - mergedData?.keypair_resource_policy.allowed_vfolder_hosts || '{}', - ); - - const _mergeDedupe = (arr) => [...new Set([].concat(...arr))]; - this._unionedAllowedPermissionByVolume = Object.assign( - {}, - ...vhostInfo.allowed.map((volume) => { - return { - [volume]: _mergeDedupe([ - allowedPermissionForDomainsByVolume[volume], - allowedPermissionForGroupsByVolume[volume], - allowedPermissionForResourcePolicyByVolume[volume], - ]), - }; - }), - ); - this.folderListGrid.clearCache(); - } - _checkFolderSupportDirectoryBasedUsage(host: string) { if ( !host || @@ -2649,55 +1822,6 @@ export default class BackendAiStorageList extends BackendAIPage { }); } - _refreshFolderUI(e) { - if ( - Object.prototype.hasOwnProperty.call(e.detail, 'mini-ui') && - e.detail['mini-ui'] === true - ) { - this.folderExplorerDialog.classList.add('mini_ui'); - } else { - this.folderExplorerDialog.classList.remove('mini_ui'); - } - } - - /** - * Check the images that supports filebrowser application - * - */ - async _checkImageSupported() { - const fields = [ - 'name', - 'tag', - 'registry', - 'digest', - 'installed', - 'labels { key value }', - 'resource_limits { key min max }', - ]; - const response = await globalThis.backendaiclient.image.list( - fields, - true, - true, - ); - const images = response.images; - // Filter filebrowser supported images. - this.filebrowserSupportedImages = images.filter((image) => - image.labels.find( - (label) => - label.key === 'ai.backend.service-ports' && - label.value.toLowerCase().includes('filebrowser'), - ), - ); - // Filter service supported images. - this.systemRoleSupportedImages = images.filter((image) => - image.labels.find( - (label) => - label.key === 'ai.backend.role' && - label.value.toLowerCase().includes('system'), - ), - ); - } - async _viewStateChanged(active) { await this.updateComplete; if (active === false) { @@ -2719,19 +1843,11 @@ export default class BackendAiStorageList extends BackendAIPage { this.enableVfolderTrashBin = globalThis.backendaiclient.supports('vfolder-trash-bin'); this.authenticated = true; - this._APIMajorVersion = globalThis.backendaiclient.APIMajorVersion; - this._maxFileUploadSize = - globalThis.backendaiclient._config.maxFileUploadSize; this.directoryBasedUsage = globalThis.backendaiclient._config.directoryBasedUsage && !globalThis.backendaiclient.supports( 'deprecated-max-quota-scope-in-keypair-resource-policy', ); - this._isDirectorySizeVisible = - globalThis.backendaiclient._config.isDirectorySizeVisible; - this._getAllowedVFolderHostsByCurrentUserInfo(); - this._checkImageSupported(); - this._getVolumeInformation(); this._refreshFolderList(false, 'viewStatechanged'); }, true, @@ -2745,32 +1861,15 @@ export default class BackendAiStorageList extends BackendAIPage { this.enableVfolderTrashBin = globalThis.backendaiclient.supports('vfolder-trash-bin'); this.authenticated = true; - this._APIMajorVersion = globalThis.backendaiclient.APIMajorVersion; - this._maxFileUploadSize = - globalThis.backendaiclient._config.maxFileUploadSize; this.directoryBasedUsage = globalThis.backendaiclient._config.directoryBasedUsage && !globalThis.backendaiclient.supports( 'deprecated-max-quota-scope-in-keypair-resource-policy', ); - this._isDirectorySizeVisible = - globalThis.backendaiclient._config.isDirectorySizeVisible; - this._getAllowedVFolderHostsByCurrentUserInfo(); - this._checkImageSupported(); - this._getVolumeInformation(); this._refreshFolderList(false, 'viewStatechanged'); } } - _folderExplorerDialog() { - this.openDialog('folder-explorer-dialog'); - } - - _mkdirDialog() { - this.mkdirNameInput.value = ''; - this.openDialog('mkdir-dialog'); - } - openDialog(id) { // var body = document.querySelector('body'); // body.appendChild(this.$[id]); @@ -3214,67 +2313,6 @@ export default class BackendAiStorageList extends BackendAIPage { document.dispatchEvent(event); } - /** - * Validate file/subfolder name. - */ - _validateExistingFileName() { - this.newFileNameInput.validityTransform = (newValue, nativeValidity) => { - if (!nativeValidity.valid) { - if (nativeValidity.valueMissing) { - this.newFileNameInput.validationMessage = _text( - 'data.FileandFoldernameRequired', - ); - return { - valid: nativeValidity.valid, - customError: !nativeValidity.valid, - }; - } else { - this.newFileNameInput.validationMessage = _text( - 'data.Allowslettersnumbersand-_dot', - ); - return { - valid: nativeValidity.valid, - customError: !nativeValidity.valid, - }; - } - } else { - const regex = /[`~!@#$%^&*()|+=?;:'",<>{}[\]\\/]/gi; - let isValid: boolean; - // compare old name and new name. - if ( - this.newFileNameInput.value === - ( - this.renameFileDialog.querySelector( - '#old-file-name', - ) as HTMLDivElement - ).textContent - ) { - this.newFileNameInput.validationMessage = _text( - 'data.EnterDifferentValue', - ); - isValid = false; - return { - valid: isValid, - customError: !isValid, - }; - } else { - isValid = true; - } - // custom validation for folder name using regex - isValid = !regex.test(this.newFileNameInput.value); - if (!isValid) { - this.newFileNameInput.validationMessage = _text( - 'data.Allowslettersnumbersand-_dot', - ); - } - return { - valid: isValid, - customError: !isValid, - }; - } - }; - } - /** * Validate folder name. * @@ -3333,77 +2371,6 @@ export default class BackendAiStorageList extends BackendAIPage { }; } - /* Folder Explorer*/ - /** - * Clear the folder explorer. - * - * @param {string} path - explorer path - * @param {string} id - explorer id - * @param {boolean} dialog - whether open folder-explorer-dialog or not - * */ - async _clearExplorer( - path = this.explorer.breadcrumb.join('/'), - id = this.explorer.id, - dialog = false, - ) { - const job = await globalThis.backendaiclient.vfolder.list_files(path, id); - this.fileListGrid.selectedItems = []; - if (this._APIMajorVersion < 6) { - this.explorer.files = JSON.parse(job.files); - } else { - // to support dedicated storage vendors such as FlashBlade - const fileInfo = JSON.parse(job.files); - fileInfo.forEach((info, cnt) => { - let ftype = 'FILE'; - if (info.filename === job.items[cnt].name) { - // value.files and value.items have same order - ftype = job.items[cnt].type; - } else { - // In case the order is mixed - for (let i = 0; i < job.items.length; i++) { - if (info.filename === job.items[i].name) { - ftype = job.items[i].type; - break; - } - } - } - info.type = ftype; - }); - this.explorer.files = fileInfo; - } - this.explorerFiles = this.explorer.files; - if (dialog) { - if ( - this.filebrowserSupportedImages.length === 0 || - this.systemRoleSupportedImages.length === 0 - ) { - await this._checkImageSupported(); - } - this._toggleFilebrowserButton(); - this._toggleSSHSessionButton(); - this.openDialog('folder-explorer-dialog'); - } - } - - /** - * toggle filebrowser button in Vfolder explorer dialog - */ - _toggleFilebrowserButton() { - const isfilebrowserSupported = - this.filebrowserSupportedImages.length > 0 && this._isResourceEnough() - ? true - : false; - const filebrowserIcon = this.shadowRoot?.querySelector('#filebrowser-img'); - const filebrowserBtn = this.shadowRoot?.querySelector( - '#filebrowser-btn', - ) as Button; - if (filebrowserIcon && filebrowserBtn) { - filebrowserBtn.disabled = !isfilebrowserSupported; - const filterClass = isfilebrowserSupported ? '' : 'apply-grayscale'; - filebrowserIcon.setAttribute('class', filterClass); - } - } - triggerOpenFilebrowserToReact(rowData) { const queryParams = new URLSearchParams(window.location.search); queryParams.set('folder', rowData.item.id); @@ -3417,1001 +2384,6 @@ export default class BackendAiStorageList extends BackendAIPage { ); } - triggerCloseFilebrowserToReact() { - const queryParams = new URLSearchParams(window.location.search); - queryParams.delete('folder'); - document.dispatchEvent( - new CustomEvent('react-navigate', { - detail: { - pathname: window.location.pathname, - search: queryParams.toString(), - }, - }), - ); - } - - /** - * Set up the explorer of the folder and call the _clearExplorer() function. - * - * @param {Event} e - click the folder_open icon button - * @param {boolean} isWritable - check whether write operation is allowed or not - * */ - _folderExplorer(rowData) { - this.vhost = rowData.item.host; - const folderName = rowData.item.name; - const isWritable = - this._hasPermission(rowData.item, 'w') || - rowData.item.is_owner || - (rowData.item.type === 'group' && this.is_admin); - const explorer = { - id: folderName, - uuid: rowData.item.id, - breadcrumb: ['.'], - }; - - /** - * NOTICE: If it's admin user and the folder type is group, It will have write permission. - */ - this.isWritable = isWritable; - this.explorer = explorer; - this._clearExplorer(explorer.breadcrumb.join('/'), explorer.id, true); - } - - /** - * Enqueue the folder and call the _clearExplorer() function. - * - * @param {Event} e - click the folder_open icon button - * */ - _enqueueFolder(e) { - const button = e.target; - - // disable button to avoid executing extra onclick event - button.setAttribute('disabled', 'true'); - const fn = e.target.getAttribute('name'); - this.explorer.breadcrumb.push(fn); - - // enable button only if the operation is done. - this._clearExplorer().then((res) => { - button.removeAttribute('disabled'); - }); - } - - _gotoFolder(e) { - const dest = e.target.getAttribute('dest'); - let tempBreadcrumb = this.explorer.breadcrumb; - const index = tempBreadcrumb.indexOf(dest); - - if (index === -1) { - return; - } - - tempBreadcrumb = tempBreadcrumb.slice(0, index + 1); - - this.explorer.breadcrumb = tempBreadcrumb; - this._clearExplorer(tempBreadcrumb.join('/'), this.explorer.id, false); - } - - _mkdir(e) { - const newfolder = this.mkdirNameInput.value; - const explorer = this.explorer; - this.mkdirNameInput.reportValidity(); - if (this.mkdirNameInput.checkValidity()) { - const job = globalThis.backendaiclient.vfolder - .mkdir([...explorer.breadcrumb, newfolder].join('/'), explorer.id) - .catch((err) => { - if (err & err.message) { - this.notification.text = PainKiller.relieve(err.title); - this.notification.detail = err.message; - this.notification.show(true, err); - } else if (err && err.title) { - this.notification.text = PainKiller.relieve(err.title); - this.notification.show(true, err); - } - }); - job.then((res) => { - this.closeDialog('mkdir-dialog'); - this._clearExplorer(); - }); - } else { - return; - } - } - - _isDir(file) { - if (this._APIMajorVersion < 6) { - return file.mode.startsWith('d'); - } else { - // For some vendor-specific storage APIs, we cannot discern file and - // directory by just looking at the first letter of file mode. For - // example, FlashBlade API returns file's mode as a pure number. - // So, we have to rely on the explicit file's type property to support - // vendor-specific APIs. - return file.type === 'DIRECTORY'; - } - } - - /* File upload and download */ - /** - * Add eventListener to the dropzone - dragleave, dragover, drop. - * */ - _addEventListenerDropZone() { - const dndZonePlaceholderEl = this.shadowRoot?.querySelector( - '#dropzone', - ) as HTMLDivElement; - dndZonePlaceholderEl.addEventListener('dragleave', () => { - dndZonePlaceholderEl.style.display = 'none'; - }); - - // TODO specify custom event type - this.folderExplorerDialog.addEventListener('dragover', (e: any) => { - e.stopPropagation(); - e.preventDefault(); - if (this.isWritable) { - e.dataTransfer.dropEffect = 'copy'; - dndZonePlaceholderEl.style.display = 'flex'; - return false; - } else { - return true; - } - }); - - // TODO specify custom event type - this.folderExplorerDialog.addEventListener('drop', (e: any) => { - let isNotificationDisplayed = false; - e.stopPropagation(); - e.preventDefault(); - dndZonePlaceholderEl.style.display = 'none'; - if (this.isWritable) { - for (let i = 0; i < e.dataTransfer.files.length; i++) { - if (e.dataTransfer.items[i].webkitGetAsEntry().isFile) { - const file = e.dataTransfer.files[i]; - /* Drag & Drop file upload size limits to configuration */ - if ( - this._maxFileUploadSize > 0 && - file.size > this._maxFileUploadSize - ) { - this.notification.text = - _text('data.explorer.FileUploadSizeLimit') + - ` (${globalThis.backendaiutils._humanReadableFileSize( - this._maxFileUploadSize, - )})`; - this.notification.show(); - return; - } else { - const reUploadFile = this.explorerFiles.find( - (elem: any) => elem.filename === file.name, - ); - if (reUploadFile) { - // plain javascript modal to confirm whether proceed to overwrite operation or not - /* - * TODO: replace confirm operation with customized dialog - */ - const confirmed = window.confirm( - `${_text('data.explorer.FileAlreadyExists')}\n${ - file.name - }\n${_text('data.explorer.DoYouWantToOverwrite')}`, - ); - if (confirmed) { - file.progress = 0; - file.caption = ''; - file.error = false; - file.complete = false; - this.uploadFiles.push(file); - } - } else { - file.progress = 0; - file.caption = ''; - file.error = false; - file.complete = false; - this.uploadFiles.push(file); - } - } - } else { - // let item = e.dataTransfer.items[i].webkitGetAsEntry(); - // console.log(item.webkitRelativePath); - // this._executeFileBrowser(); - // show snackbar to filebrowser only once - if (!isNotificationDisplayed) { - if (this.filebrowserSupportedImages.length > 0) { - this.notification.text = _text( - 'data.explorer.ClickFilebrowserButton', - ); - this.notification.show(); - } else { - this.notification.text = _text( - 'data.explorer.NoImagesSupportingFileBrowser', - ); - this.notification.show(); - } - } - isNotificationDisplayed = true; - } - } - - for (let i = 0; i < this.uploadFiles.length; i++) { - this.fileUpload(this.uploadFiles[i]); - this._clearExplorer(); - } - } else { - this.notification.text = _text( - 'data.explorer.WritePermissionRequiredInUploadFiles', - ); - this.notification.show(); - } - }); - } - - /** - * Create MouseEvents when cloud_upload button is clicked. - * - * @param {Event} e - click the cloud_upload button. - * */ - _uploadBtnClick(e) { - const isFolder = e.target.id === 'add-folder-btn'; - const elem = isFolder - ? (this.shadowRoot?.querySelector('#folderInput') as HTMLInputElement) - : (this.shadowRoot?.querySelector('#fileInput') as HTMLInputElement); - if (elem && document.createEvent) { - // sanity check - const evt = document.createEvent('MouseEvents'); - evt.initEvent('click', true, false); - elem.dispatchEvent(evt); - } - } - - getFolderName(file: File) { - const filePath = file.webkitRelativePath || file.name; - return filePath.split('/')?.[0]; - } - - /** - * If file is added, call the fileUpload() function and initialize fileInput string - * - * @param {Event} e - add file to the input element - * */ - _uploadInputChange(e) { - const length = e.target.files.length; - const isFolderUpload = e.target.id === 'folderInput'; - const inputElement = isFolderUpload - ? (this.shadowRoot?.querySelector('#folderInput') as HTMLInputElement) - : (this.shadowRoot?.querySelector('#fileInput') as HTMLInputElement); - let isEmptyFileIncluded = false; - let reUploadFolderConfirmed = false; - // plain javascript modal to confirm whether proceed to overwrite "folder" operation or not - /* - * TODO: replace confirm operation with customized dialog - */ - if (e.target.files.length > 0 && isFolderUpload) { - const f = e.target.files[0]; - const reUploadFolder = this.explorerFiles.find( - (elem: any) => elem.filename === this.getFolderName(f), - ); - if (reUploadFolder) { - reUploadFolderConfirmed = window.confirm( - `${_text('data.explorer.FolderAlreadyExists')}\n${this.getFolderName( - f, - )}\n${_text('data.explorer.DoYouWantToOverwrite')}`, - ); - if (!reUploadFolderConfirmed) { - inputElement.value = ''; - return; - } - } - } - for (let i = 0; i < length; i++) { - const file = e.target.files[i]; - - let text = ''; - const possible = - 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; - for (let i = 0; i < 5; i++) { - text += possible.charAt(Math.floor(Math.random() * possible.length)); - } - /* File upload size limits to configuration */ - if (this._maxFileUploadSize > 0 && file.size > this._maxFileUploadSize) { - this.notification.text = - _text('data.explorer.FileUploadSizeLimit') + - ` (${globalThis.backendaiutils._humanReadableFileSize( - this._maxFileUploadSize, - )})`; - this.notification.show(); - return; - } else if (file.size === 0) { - // skip the empty file upload - isEmptyFileIncluded = true; - continue; - } else { - const reUploadFile = this.explorerFiles.find( - (elem: any) => elem.filename === file.name, - ); - if (reUploadFile && !reUploadFolderConfirmed) { - // plain javascript modal to confirm whether proceed to overwrite "file" operation or not - // if the user already confirms to overwrite the "folder", the modal doesn't appear. - /* - * TODO: replace confirm operation with customized dialog - */ - const confirmed = window.confirm( - `${_text('data.explorer.FileAlreadyExists')}\n${file.name}\n${_text( - 'data.explorer.DoYouWantToOverwrite', - )}`, - ); - if (confirmed) { - file.id = text; - file.progress = 0; - file.caption = ''; - file.error = false; - file.complete = false; - this.uploadFiles.push(file); - } - } else { - file.id = text; - file.progress = 0; - file.caption = ''; - file.error = false; - file.complete = false; - this.uploadFiles.push(file); - } - } - } - for (let i = 0; i < this.uploadFiles.length; i++) { - this.fileUpload(this.uploadFiles[i]); - } - if (isEmptyFileIncluded || isFolderUpload) { - this.notification.text = _text( - 'data.explorer.EmptyFilesAndFoldersAreNotUploaded', - ); - this.notification.show(); - } - inputElement.value = ''; - } - - /** - * Running file upload queue to upload files. - * - * @param {null | string} session - upload session - * */ - runFileUploadQueue(session = null) { - if (session !== null) { - (this.fileUploadQueue as any).push(session); - } - let queuedSession; - for ( - let i = this.fileUploadCount; - i < this.concurrentFileUploadLimit; - i++ - ) { - if (this.fileUploadQueue.length > 0) { - queuedSession = this.fileUploadQueue.shift(); - this.fileUploadCount = this.fileUploadCount + 1; - queuedSession.start(); - } - } - } - - /** - * Upload the file. - * - * @param {Object} fileObj - file object - * */ - fileUpload(fileObj) { - this._uploadFlag = true; - this.uploadFilesExist = this.uploadFiles.length > 0; - const path = this.explorer.breadcrumb - .concat(fileObj.webkitRelativePath || fileObj.name) - .join('/'); - const job = globalThis.backendaiclient.vfolder.create_upload_session( - path, - fileObj, - this.explorer.id, - ); - job.then((url) => { - const start_date = new Date().getTime(); - const upload = new tus.Upload(fileObj, { - endpoint: url, - retryDelays: [0, 3000, 5000, 10000, 20000], - uploadUrl: url, - chunkSize: 15728640, // 15MB - metadata: { - filename: path, - filetype: fileObj.type, - }, - onError: (error) => { - console.log('Failed because: ' + error); - this.currentUploadFile = - this.uploadFiles[this.uploadFiles.indexOf(fileObj)]; - this.fileUploadCount = this.fileUploadCount - 1; - this.runFileUploadQueue(); - }, - onProgress: (bytesUploaded, bytesTotal) => { - this.currentUploadFile = - this.uploadFiles[this.uploadFiles.indexOf(fileObj)]; - if (!this._uploadFlag) { - upload.abort(); - this.uploadFiles[this.uploadFiles.indexOf(fileObj)].caption = - `Canceling...`; - this.uploadFiles = this.uploadFiles.slice(); - setTimeout(() => { - this.uploadFiles = []; - this.uploadFilesExist = false; - this.fileUploadCount = this.fileUploadCount - 1; - }, 1000); - return; - } - - const now = new Date().getTime(); - const speed: string = - ( - bytesUploaded / - (1024 * 1024) / - ((now - start_date) / 1000) - ).toFixed(1) + 'MB/s'; - const estimated_seconds = Math.floor( - (bytesTotal - bytesUploaded) / - ((bytesUploaded / (now - start_date)) * 1000), - ); - let estimated_time_left = _text('data.explorer.LessThan10Sec'); - if (estimated_seconds >= 86400) { - estimated_time_left = _text('data.explorer.MoreThanADay'); - } else if (estimated_seconds > 10) { - const hour = Math.floor(estimated_seconds / 3600); - const min = Math.floor((estimated_seconds % 3600) / 60); - const sec = estimated_seconds % 60; - estimated_time_left = `${hour}:${min}:${sec}`; - } - const percentage = ((bytesUploaded / bytesTotal) * 100).toFixed(1); - this.uploadFiles[this.uploadFiles.indexOf(fileObj)].progress = - bytesUploaded / bytesTotal; - this.uploadFiles[this.uploadFiles.indexOf(fileObj)].caption = - `${percentage}% / Time left : ${estimated_time_left} / Speed : ${speed}`; - this.uploadFiles = this.uploadFiles.slice(); - }, - onSuccess: () => { - this._clearExplorer(); - this.currentUploadFile = - this.uploadFiles[this.uploadFiles.indexOf(fileObj)]; - this.uploadFiles[this.uploadFiles.indexOf(fileObj)].complete = true; - this.uploadFiles = this.uploadFiles.slice(); - setTimeout(() => { - this.uploadFiles.splice(this.uploadFiles.indexOf(fileObj), 1); - this.uploadFilesExist = this.uploadFiles.length > 0 ? true : false; - this.uploadFiles = this.uploadFiles.slice(); - this.fileUploadCount = this.fileUploadCount - 1; - this.runFileUploadQueue(); - }, 1000); - }, - }); - this.runFileUploadQueue(upload); - }); - } - - /** - * Cancel upload files. - * - * */ - _cancelUpload() { - this._uploadFlag = false; - } - - /** - * Download the file. - * - * @param {Event} e - click the cloud_download icon button - * @param {boolean} archive - whether archive or not - * */ - _downloadFile(e, archive = false) { - if (!this._isDownloadable(this.vhost)) { - this.notification.text = _text('data.explorer.DownloadNotAllowed'); - this.notification.show(); - return; - } - const fn = e.target.getAttribute('filename'); - const path = this.explorer.breadcrumb.concat(fn).join('/'); - const job = globalThis.backendaiclient.vfolder.request_download_token( - path, - this.explorer.id, - archive, - ); - job.then((res) => { - const token = res.token; - let url; - if (this._APIMajorVersion < 6) { - url = - globalThis.backendaiclient.vfolder.get_download_url_with_token(token); - } else { - url = `${res.url}?token=${res.token}&archive=${archive}`; - } - if (globalThis.iOSSafari) { - this.downloadURL = url; - this.downloadFileDialog.show(); - URL.revokeObjectURL(url); - } else { - const a = document.createElement('a'); - a.style.display = 'none'; - a.addEventListener('click', function (e) { - e.stopPropagation(); - }); - a.href = url; - a.download = fn; - document.body.appendChild(a); // we need to append the element to the dom -> otherwise it will not work in firefox - a.click(); - // a.remove(); //afterwards we remove the element again - document.body.removeChild(a); - URL.revokeObjectURL(url); - } - }); - } - - /** - * Select filename without extension - * - */ - _compareFileExtension() { - const newFilename = this.newFileNameInput.value; - const oldFilename = - (this.renameFileDialog.querySelector('#old-file-name') as HTMLDivElement) - .textContent ?? ''; - const regex = /\.([0-9a-z]+)$/i; - const newFileExtension = newFilename.match(regex); - const oldFileExtension = oldFilename.match(regex); - if (newFilename.includes('.') && newFileExtension) { - this.newFileExtension = newFileExtension[1].toLowerCase(); - } else { - this.newFileExtension = ''; - } - if (oldFilename.includes('.') && oldFileExtension) { - this.oldFileExtension = oldFileExtension[1].toLowerCase(); - } else { - this.oldFileExtension = ''; - } - - if (newFilename) { - if (this.newFileExtension !== this.oldFileExtension) { - this.fileExtensionChangeDialog.show(); - } else if (this.oldFileExtension) { - this._keepFileExtension(); - } else { - this._renameFile(); - } - } else { - this._renameFile(); - } - } - - /** - * Keep the file extension whether the file extension is explicit or not. - * - */ - _keepFileExtension() { - let newFilename = this.newFileNameInput.value; - if (this.newFileExtension) { - newFilename = newFilename.replace( - new RegExp(this.newFileExtension + '$'), - this.oldFileExtension, - ); - } else { - newFilename = newFilename + '.' + this.oldFileExtension; - } - this.newFileNameInput.value = newFilename; - this._renameFile(); - } - - /* Execute Filebrowser by launching session with mimimum resources - * - */ - _executeFileBrowser() { - if (this._isResourceEnough()) { - if (this.filebrowserSupportedImages.length > 0) { - const isNotificationVisible = localStorage.getItem( - 'backendaiwebui.filebrowserNotification', - ); - if ( - (isNotificationVisible == null || isNotificationVisible === 'true') && - !this.isWritable - ) { - this.fileBrowserNotificationDialog.show(); - } - this._launchFileBrowserSession(); - this._toggleFilebrowserButton(); - } else { - this.notification.text = _text( - 'data.explorer.NoImagesSupportingFileBrowser', - ); - this.notification.show(); - } - } else { - this.notification.text = _text( - 'data.explorer.NotEnoughResourceForFileBrowserSession', - ); - this.notification.show(); - } - } - - /** - * Toggle notification of filebrowser execution on read-only folder - * - * @param {any} e - */ - _toggleShowFilebrowserNotification(e) { - const checkbox = e.target; - if (checkbox) { - const isHidden = (!checkbox.checked).toString(); - localStorage.setItem('backendaiwebui.filebrowserNotification', isHidden); - } - } - - /** - * Open the session launcher dialog to execute filebrowser app. - * - */ - async _launchFileBrowserSession() { - let appOptions; - const imageResource: Record = {}; - // monkeypatch for filebrowser applied environment - // const environment = 'cr.backend.ai/testing/filebrowser:21.01-ubuntu20.04'; - const images = this.filebrowserSupportedImages.filter( - (image: any) => - image['name'].toLowerCase().includes('filebrowser') && - image['installed'], - ); - - // select one image to launch filebrowser supported session - const preferredImage = images[0]; - const environment = - preferredImage['registry'] + - '/' + - preferredImage['name'] + - ':' + - preferredImage['tag']; - - // add current folder - imageResource['mounts'] = [this.explorer.id]; - imageResource['cpu'] = 1; - imageResource['mem'] = this.minimumResource.mem + 'g'; - imageResource['domain'] = globalThis.backendaiclient._config.domainName; - imageResource['group_name'] = globalThis.backendaiclient.current_group; - const indicator = await this.indicator.start('indeterminate'); - - return globalThis.backendaiclient - .get_resource_slots() - .then((response) => { - indicator.set(20, _text('data.explorer.ExecutingFileBrowser')); - return globalThis.backendaiclient.createIfNotExists( - environment, - null, - imageResource, - 10000, - undefined, - ); - }) - .then(async (res) => { - const service_info = res.servicePorts; - appOptions = { - 'session-uuid': res.sessionId, - 'session-name': res.sessionName, - 'access-key': '', - runtime: 'filebrowser', - arguments: { '--root': '/home/work/' + this.explorer.id }, - }; - // only launch filebrowser app when it has valid service ports - if ( - service_info.length > 0 && - service_info.filter((el) => el.name === 'filebrowser').length > 0 - ) { - globalThis.appLauncher.showLauncher(appOptions); - } - if (this.folderExplorerDialog.open) { - this.closeDialog('folder-explorer-dialog'); - } - indicator.end(1000); - }) - .catch((err) => { - this.notification.text = PainKiller.relieve(err.title); - this.notification.detail = err.message; - this.notification.show(true, err); - indicator.end(100); - }); - } - - _executeSSHProxyAgent() { - if (this.volumeInfo[this.vhost]?.sftp_scaling_groups?.length > 0) { - if (this.systemRoleSupportedImages.length > 0) { - this._launchSystemRoleSSHSession(); - this._toggleSSHSessionButton(); - } else { - this.notification.text = _text( - 'data.explorer.NoImagesSupportingSystemSession', - ); - this.notification.show(); - } - } else { - this.notification.text = _text('data.explorer.SFTPSessionNotAvailable'); - this.notification.show(); - } - } - - /** - * Launch system role sftp-uploader image and open the dialog that includes the ssh link. - */ - async _launchSystemRoleSSHSession() { - const imageResource: Record = {}; - const configSSHImage = globalThis.backendaiclient._config.systemSSHImage; - const images = this.systemRoleSupportedImages.filter( - (image: any) => image['installed'], - ); - // TODO: use lablup/openssh-server image - // select one image to launch system role supported session - const preferredImage = images[0]; - const environment = - configSSHImage !== '' - ? configSSHImage - : preferredImage['registry'] + - '/' + - preferredImage['name'] + - ':' + - preferredImage['tag']; - - // add current folder - imageResource['mounts'] = [this.explorer.id]; - imageResource['cpu'] = 1; - imageResource['mem'] = '256m'; - imageResource['domain'] = globalThis.backendaiclient._config.domainName; - imageResource['scaling_group'] = - this.volumeInfo[this.vhost]?.sftp_scaling_groups[0]; - imageResource['group_name'] = globalThis.backendaiclient.current_group; - const indicator = await this.indicator.start('indeterminate'); - return (async () => { - try { - await globalThis.backendaiclient.get_resource_slots(); - indicator.set(50, _text('data.explorer.StartingSSH/SFTPSession')); - const sessionResponse = - await globalThis.backendaiclient.createIfNotExists( - environment, - `sftp-${this.explorer.uuid}`, - imageResource, - 15000, - undefined, - ); - if (sessionResponse.status === 'CANCELLED') { - // Max # of upload sessions exceeded for this used - this.notification.text = PainKiller.relieve( - _text('data.explorer.NumberOfSFTPSessionsExceededTitle'), - ); - this.notification.detail = _text( - 'data.explorer.NumberOfSFTPSessionsExceededBody', - ); - this.notification.show(true, { - title: _text('data.explorer.NumberOfSFTPSessionsExceededTitle'), - message: _text('data.explorer.NumberOfSFTPSessionsExceededBody'), - }); - indicator.end(100); - return; - } - const directAccessInfo = - await globalThis.backendaiclient.get_direct_access_info( - sessionResponse.sessionId, - ); - const host = directAccessInfo.public_host.replace(/^https?:\/\//, ''); - const port = directAccessInfo.sshd_ports; - const event = new CustomEvent('read-ssh-key-and-launch-ssh-dialog', { - detail: { - sessionUuid: sessionResponse.sessionId, - host: host, - port: port, - mounted: this.explorer.id, - }, - }); - document.dispatchEvent(event); - indicator.end(100); - } catch (err) { - this.notification.text = PainKiller.relieve(err.title); - this.notification.detail = err.message; - this.notification.show(true, err); - indicator.end(100); - } - })(); - } - - _toggleSSHSessionButton() { - const isSystemRoleSupported = this.systemRoleSupportedImages.length > 0; - const sshImageIcon = this.shadowRoot?.querySelector('#ssh-img'); - const sshImageBtn = this.shadowRoot?.querySelector('#ssh-btn') as Button; - if (sshImageIcon && sshImageBtn) { - sshImageBtn.disabled = !isSystemRoleSupported; - const filterClass = isSystemRoleSupported ? '' : 'apply-grayscale'; - sshImageIcon.setAttribute('class', filterClass); - } - } - - /** - * Open the renameFileDialog to rename the file. - * - * @param {Event} e - click the edit icon button - * @param {Boolean} is_dir - True when file is directory type - * */ - _openRenameFileDialog(e, is_dir = false) { - const fn = e.target.getAttribute('filename'); - ( - this.renameFileDialog.querySelector('#old-file-name') as HTMLDivElement - ).textContent = fn; - this.newFileNameInput.value = fn; - // TODO define extended type for custom property - this.renameFileDialog.filename = fn; - this.renameFileDialog.show(); - this.is_dir = is_dir; - - this.newFileNameInput.addEventListener('focus', (e) => { - const endOfExtensionLength = fn.replace(/\.([0-9a-z]+)$/i, '').length; - this.newFileNameInput.setSelectionRange(0, endOfExtensionLength); - }); - this.newFileNameInput.focus(); - } - - /** - * Rename the file. - * - * */ - _renameFile() { - // TODO define extended type for custom property - const fn = this.renameFileDialog.filename; - const path = this.explorer.breadcrumb.concat(fn).join('/'); - const newName = this.newFileNameInput.value; - this.fileExtensionChangeDialog.hide(); - this.newFileNameInput.reportValidity(); - if (this.newFileNameInput.checkValidity()) { - if (fn === newName) { - this.newFileNameInput.focus(); - this.notification.text = _text('data.folders.SameFileName'); - this.notification.show(); - return; - } - - const job = globalThis.backendaiclient.vfolder.rename_file( - path, - newName, - this.explorer.id, - this.is_dir, - ); - job - .then((res) => { - this.notification.text = _text('data.folders.FileRenamed'); - this.notification.show(); - this._clearExplorer(); - this.renameFileDialog.hide(); - }) - .catch((err) => { - console.error(err); - if (err && err.message) { - this.notification.text = err.title; - this.notification.detail = err.message; - this.notification.show(true, err); - } - }); - } else { - return; - } - } - - /** - * Open delete file dialog to delete file. - * - * @param {Event} e - click the delete-btn - * */ - _openDeleteFileDialog(e) { - const fn = e.target.getAttribute('filename'); - // TODO define extended type for custom properties - this.deleteFileDialog.filename = fn; - this.deleteFileDialog.files = []; - this.deleteFileDialog.show(); - } - - /** - * Open the deleteFileDialog of selected files. - * - * @param {Event} e - click the delete button - * */ - _openDeleteMultipleFileDialog(e?) { - // TODO define extended type for custom property - this.deleteFileDialog.files = this.fileListGrid.selectedItems; - this.deleteFileDialog.filename = ''; - this.deleteFileDialog.show(); - } - - /** - * If the user presses the Delete File button, and the Okay button on the double check dialogue, delete the file. - * - * @param {Event} e - click the Okay button - * */ - - _deleteFileWithCheck(e) { - // TODO define extended type for custom property - const files = this.deleteFileDialog.files; - if (files.length > 0) { - const filenames: string[] = []; - files.forEach((file) => { - const filename = this.explorer.breadcrumb - .concat(file.filename) - .join('/'); - filenames.push(filename); - }); - const job = globalThis.backendaiclient.vfolder.delete_files( - filenames, - true, - this.explorer.id, - ); - job.then((res) => { - this.notification.text = - files.length == 1 - ? _text('data.folders.FileDeleted') - : _text('data.folders.MultipleFilesDeleted'); - this.notification.show(); - this._clearExplorer(); - this.deleteFileDialog.hide(); - }); - } else { - // TODO define extended type for custom property - if (this.deleteFileDialog.filename != '') { - const path = this.explorer.breadcrumb - .concat(this.deleteFileDialog.filename) - .join('/'); - const job = globalThis.backendaiclient.vfolder.delete_files( - [path], - true, - this.explorer.id, - ); - job.then((res) => { - this.notification.text = _text('data.folders.FileDeleted'); - this.notification.show(); - this._clearExplorer(); - this.deleteFileDialog.hide(); - }); - } - } - } - - /** - * Delete a file. - * - * @param {HTMLElement} e - file list component that contains filename attribute - * */ - _deleteFile(e) { - const fn = e.target.getAttribute('filename'); - const path = this.explorer.breadcrumb.concat(fn).join('/'); - const job = globalThis.backendaiclient.vfolder.delete_files( - [path], - true, - this.explorer.id, - ); - job.then((res) => { - this.notification.text = _text('data.folders.FileDeleted'); - this.notification.show(); - this._clearExplorer(); - }); - } - - /** - * Returns whether resource is enough to launch session for executing filebrowser app or not. - * @return {boolean} - true when resource is enough, false when resource is not enough to create a session. - */ - _isResourceEnough() { - // update current resources statistics. - const event = new CustomEvent('backend-ai-calculate-current-resource'); - document.dispatchEvent(event); - const currentResource = globalThis.backendaioptions.get('current-resource'); - if (currentResource) { - currentResource.cpu = - typeof currentResource.cpu === 'string' - ? parseInt(currentResource['cpu']) - : currentResource['cpu']; - if ( - currentResource.cpu >= this.minimumResource.cpu && - currentResource.mem >= this.minimumResource.mem - ) { - return true; - } - } - return false; - } - /** * Returns the time of the utc type for human reading. * @@ -4426,19 +2398,6 @@ export default class BackendAiStorageList extends BackendAIPage { return date.toUTCString(); } - /** - * Return whether file is downloadable. - * NOTE: For now, It's handled by storage host, not file itself. - * - * @param {Object} host - * @return {boolean} true - * */ - _isDownloadable(host) { - return (this._unionedAllowedPermissionByVolume[host] ?? []).includes( - 'download-file', - ); - } - /** * * Initialize share-folder-dialog to the original layout @@ -4593,45 +2552,6 @@ export default class BackendAiStorageList extends BackendAIPage { }); } - /** - * Validate path name - * */ - _validatePathName() { - this.mkdirNameInput.validityTransform = (newValue, nativeValidity) => { - if (!nativeValidity.valid) { - if (nativeValidity.valueMissing) { - this.mkdirNameInput.validationMessage = _text( - 'data.explorer.ValueRequired', - ); - return { - valid: nativeValidity.valid, - customError: !nativeValidity.valid, - }; - } else { - return { - valid: nativeValidity.valid, - customError: !nativeValidity.valid, - }; - } - } else { - // custom validation for path name using regex - const regex = - /^([^`~!@#$%^&*()|+=?;:'",<>{}[\]\r\n/]{1,})+(\/[^`~!@#$%^&*()|+=?;:'",<>{}[\]\r\n/]{1,})*([/,\\]{0,1})$/gm; - let isValid = regex.test(this.mkdirNameInput.value); - if (!isValid || this.mkdirNameInput.value === './') { - this.mkdirNameInput.validationMessage = _text( - 'data.explorer.ValueShouldBeStarted', - ); - isValid = false; - } - return { - valid: isValid, - customError: !isValid, - }; - } - }; - } - /** * Restore folder. Change the folder status from `delete-pending` to `ready`. * */ diff --git a/src/lib/backend.ai-client-es6.js b/src/lib/backend.ai-client-es6.js index ccc00eb975..0ebf588708 100644 --- a/src/lib/backend.ai-client-es6.js +++ b/src/lib/backend.ai-client-es6.js @@ -31903,8 +31903,8 @@ null == i && (i = this.name), null == e && (e = !1); let r = { files: t, recursive: e }, n = this.client.newSignedRequest( - 'DELETE', - `${this.urlPrefix}/${i}/delete_files`, + 'POST', + `${this.urlPrefix}/${i}/delete-files`, r, ); return this.client._wrapWithPromise(n); diff --git a/src/lib/backend.ai-client-esm.ts b/src/lib/backend.ai-client-esm.ts index f38a139770..7f204287b1 100644 --- a/src/lib/backend.ai-client-esm.ts +++ b/src/lib/backend.ai-client-esm.ts @@ -2434,8 +2434,8 @@ class VFolder { recursive: recursive, }; let rqst = this.client.newSignedRequest( - 'DELETE', - `${this.urlPrefix}/${name}/delete_files`, + this.client._managerVersion >= '24.03.7' ? 'POST' : 'DELETE', + `${this.urlPrefix}/${name}/delete-files`, body, ); return this.client._wrapWithPromise(rqst); diff --git a/src/lib/backend.ai-client-node.js b/src/lib/backend.ai-client-node.js index c5692bc41d..e37863dc3c 100644 --- a/src/lib/backend.ai-client-node.js +++ b/src/lib/backend.ai-client-node.js @@ -1866,8 +1866,8 @@ class VFolder { recursive: recursive, }; let rqst = this.client.newSignedRequest( - 'DELETE', - `${this.urlPrefix}/${name}/delete_files`, + 'POST', + `${this.urlPrefix}/${name}/delete-files`, body, ); return this.client._wrapWithPromise(rqst); diff --git a/src/lib/backend.ai-client-node.ts b/src/lib/backend.ai-client-node.ts index 72ea0c529e..519b390283 100644 --- a/src/lib/backend.ai-client-node.ts +++ b/src/lib/backend.ai-client-node.ts @@ -2062,8 +2062,8 @@ class VFolder { recursive: recursive, }; let rqst = this.client.newSignedRequest( - 'DELETE', - `${this.urlPrefix}/${name}/delete_files`, + 'POST', + `${this.urlPrefix}/${name}/delete-files`, body, ); return this.client._wrapWithPromise(rqst);