From b55b4c9198d0344450cc85fbf971600988c54419 Mon Sep 17 00:00:00 2001 From: prplwtf Date: Sat, 6 Jan 2024 21:40:40 +0100 Subject: [PATCH] feat `core` `react`: Add DropdownItems to components --- blueprint.sh | 4 + .../Server/Backups/DropdownItems.tsx | 10 + .../Server/Files/Browse/DropdownItems.tsx | 10 + .../server/backups/BackupContextMenu.tsx | 222 ++++++++++++++++++ .../server/files/FileDropdownMenu.tsx | 200 ++++++++++++++++ 5 files changed, 446 insertions(+) create mode 100644 resources/scripts/blueprint/components/Server/Backups/DropdownItems.tsx create mode 100644 resources/scripts/blueprint/components/Server/Files/Browse/DropdownItems.tsx create mode 100644 resources/scripts/components/server/backups/BackupContextMenu.tsx create mode 100644 resources/scripts/components/server/files/FileDropdownMenu.tsx diff --git a/blueprint.sh b/blueprint.sh index 8ebdf03..e7d59fe 100644 --- a/blueprint.sh +++ b/blueprint.sh @@ -656,6 +656,7 @@ if [[ ( $2 == "-i" ) || ( $2 == "-install" ) ]]; then VCMD="y" PLACE_REACT "$Components_Server_Files_Browse_BeforeContent" "Server/Files/Browse/BeforeContent.tsx" "$OldComponents_Server_Files_Browse_BeforeContent" PLACE_REACT "$Components_Server_Files_Browse_FileButtons" "Server/Files/Browse/FileButtons.tsx" "$OldComponents_Server_Files_Browse_FileButtons" + PLACE_REACT "$Components_Server_Files_Browse_DropdownItems" "Server/Files/Browse/DropdownItems.tsx" "$OldComponents_Server_Files_Browse_DropdownItems" PLACE_REACT "$Components_Server_Files_Browse_AfterContent" "Server/Files/Browse/AfterContent.tsx" "$OldComponents_Server_Files_Browse_AfterContent" PLACE_REACT "$Components_Server_Files_Edit_BeforeEdit" "Server/Files/Edit/BeforeEdit.tsx" "$OldComponents_Server_Files_Edit_BeforeEdit" PLACE_REACT "$Components_Server_Files_Edit_AfterEdit" "Server/Files/Edit/AfterEdit.tsx" "$OldComponents_Server_Files_Edit_AfterEdit" @@ -672,6 +673,7 @@ if [[ ( $2 == "-i" ) || ( $2 == "-install" ) ]]; then VCMD="y" PLACE_REACT "$Components_Server_Users_AfterContent" "Server/Users/AfterContent.tsx" "$OldComponents_Server_Users_AfterContent" PLACE_REACT "$Components_Server_Backups_BeforeContent" "Server/Backups/BeforeContent.tsx" "$OldComponents_Server_Backups_BeforeContent" + PLACE_REACT "$Components_Server_Backups_DropdownItems" "Server/Backups/DropdownItems.tsx" "$OldComponents_Server_Backups_DropdownItems" PLACE_REACT "$Components_Server_Backups_AfterContent" "Server/Backups/AfterContent.tsx" "$OldComponents_Server_Backups_AfterContent" PLACE_REACT "$Components_Server_Network_BeforeContent" "Server/Network/BeforeContent.tsx" "$OldComponents_Server_Network_BeforeContent" @@ -1126,6 +1128,7 @@ if [[ ( $2 == "-r" ) || ( $2 == "-remove" ) ]]; then VCMD="y" REMOVE_REACT "$Components_Server_Files_Browse_BeforeContent" "Server/Files/Browse/BeforeContent.tsx" REMOVE_REACT "$Components_Server_Files_Browse_FileButtons" "Server/Files/Browse/FileButtons.tsx" + REMOVE_REACT "$Components_Server_Files_Browse_DropdownItems" "Server/Files/Browse/DropdownItems.tsx" REMOVE_REACT "$Components_Server_Files_Browse_AfterContent" "Server/Files/Browse/AfterContent.tsx" REMOVE_REACT "$Components_Server_Files_Edit_BeforeEdit" "Server/Files/Edit/BeforeEdit.tsx" REMOVE_REACT "$Components_Server_Files_Edit_AfterEdit" "Server/Files/Edit/AfterEdit.tsx" @@ -1142,6 +1145,7 @@ if [[ ( $2 == "-r" ) || ( $2 == "-remove" ) ]]; then VCMD="y" REMOVE_REACT "$Components_Server_Users_AfterContent" "Server/Users/AfterContent.tsx" REMOVE_REACT "$Components_Server_Backups_BeforeContent" "Server/Backups/BeforeContent.tsx" + REMOVE_REACT "$Components_Server_Backups_DropdownItems" "Server/Backups/DropdownItems.tsx" REMOVE_REACT "$Components_Server_Backups_AfterContent" "Server/Backups/AfterContent.tsx" REMOVE_REACT "$Components_Server_Network_BeforeContent" "Server/Network/BeforeContent.tsx" diff --git a/resources/scripts/blueprint/components/Server/Backups/DropdownItems.tsx b/resources/scripts/blueprint/components/Server/Backups/DropdownItems.tsx new file mode 100644 index 0000000..5c85b6c --- /dev/null +++ b/resources/scripts/blueprint/components/Server/Backups/DropdownItems.tsx @@ -0,0 +1,10 @@ +import React from 'react'; +/* blueprint/import */ + +export default () => { + return ( + <> + {/* blueprint/react */} + + ); +}; diff --git a/resources/scripts/blueprint/components/Server/Files/Browse/DropdownItems.tsx b/resources/scripts/blueprint/components/Server/Files/Browse/DropdownItems.tsx new file mode 100644 index 0000000..5c85b6c --- /dev/null +++ b/resources/scripts/blueprint/components/Server/Files/Browse/DropdownItems.tsx @@ -0,0 +1,10 @@ +import React from 'react'; +/* blueprint/import */ + +export default () => { + return ( + <> + {/* blueprint/react */} + + ); +}; diff --git a/resources/scripts/components/server/backups/BackupContextMenu.tsx b/resources/scripts/components/server/backups/BackupContextMenu.tsx new file mode 100644 index 0000000..b5bf320 --- /dev/null +++ b/resources/scripts/components/server/backups/BackupContextMenu.tsx @@ -0,0 +1,222 @@ +import React, { useState } from 'react'; +import { + faBoxOpen, + faCloudDownloadAlt, + faEllipsisH, + faLock, + faTrashAlt, + faUnlock, +} from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import DropdownMenu, { DropdownButtonRow } from '@/components/elements/DropdownMenu'; +import getBackupDownloadUrl from '@/api/server/backups/getBackupDownloadUrl'; +import useFlash from '@/plugins/useFlash'; +import SpinnerOverlay from '@/components/elements/SpinnerOverlay'; +import deleteBackup from '@/api/server/backups/deleteBackup'; +import Can from '@/components/elements/Can'; +import tw from 'twin.macro'; +import getServerBackups from '@/api/swr/getServerBackups'; +import { ServerBackup } from '@/api/server/types'; +import { ServerContext } from '@/state/server'; +import Input from '@/components/elements/Input'; +import { restoreServerBackup } from '@/api/server/backups'; +import http, { httpErrorToHuman } from '@/api/http'; +import { Dialog } from '@/components/elements/dialog'; + +import DropdownItems from '@/blueprint/components/Server/Backups/DropdownItems'; + +interface Props { + backup: ServerBackup; +} + +export default ({ backup }: Props) => { + const uuid = ServerContext.useStoreState((state) => state.server.data!.uuid); + const setServerFromState = ServerContext.useStoreActions((actions) => actions.server.setServerFromState); + const [modal, setModal] = useState(''); + const [loading, setLoading] = useState(false); + const [truncate, setTruncate] = useState(false); + const { clearFlashes, clearAndAddHttpError } = useFlash(); + const { mutate } = getServerBackups(); + + const doDownload = () => { + setLoading(true); + clearFlashes('backups'); + getBackupDownloadUrl(uuid, backup.uuid) + .then((url) => { + // @ts-expect-error this is valid + window.location = url; + }) + .catch((error) => { + console.error(error); + clearAndAddHttpError({ key: 'backups', error }); + }) + .then(() => setLoading(false)); + }; + + const doDeletion = () => { + setLoading(true); + clearFlashes('backups'); + deleteBackup(uuid, backup.uuid) + .then(() => + mutate( + (data) => ({ + ...data, + items: data.items.filter((b) => b.uuid !== backup.uuid), + backupCount: data.backupCount - 1, + }), + false + ) + ) + .catch((error) => { + console.error(error); + clearAndAddHttpError({ key: 'backups', error }); + setLoading(false); + setModal(''); + }); + }; + + const doRestorationAction = () => { + setLoading(true); + clearFlashes('backups'); + restoreServerBackup(uuid, backup.uuid, truncate) + .then(() => + setServerFromState((s) => ({ + ...s, + status: 'restoring_backup', + })) + ) + .catch((error) => { + console.error(error); + clearAndAddHttpError({ key: 'backups', error }); + }) + .then(() => setLoading(false)) + .then(() => setModal('')); + }; + + const onLockToggle = () => { + if (backup.isLocked && modal !== 'unlock') { + return setModal('unlock'); + } + + http.post(`/api/client/servers/${uuid}/backups/${backup.uuid}/lock`) + .then(() => + mutate( + (data) => ({ + ...data, + items: data.items.map((b) => + b.uuid !== backup.uuid + ? b + : { + ...b, + isLocked: !b.isLocked, + } + ), + }), + false + ) + ) + .catch((error) => alert(httpErrorToHuman(error))) + .then(() => setModal('')); + }; + + return ( + <> + setModal('')} + title={`Unlock "${backup.name}"`} + onConfirmed={onLockToggle} + > + This backup will no longer be protected from automated or accidental deletions. + + setModal('')} + confirm={'Restore'} + title={`Restore "${backup.name}"`} + onConfirmed={() => doRestorationAction()} + > +

+ Your server will be stopped. You will not be able to control the power state, access the file + manager, or create additional backups until completed. +

+

+ +

+
+ setModal('')} + onConfirmed={doDeletion} + > + This is a permanent operation. The backup cannot be recovered once deleted. + + + {backup.isSuccessful ? ( + ( + + )} + > +
+ + + + Download + + + + setModal('restore')}> + + Restore + + + + <> + + + {backup.isLocked ? 'Unlock' : 'Lock'} + + {!backup.isLocked && ( + setModal('delete')}> + + Delete + + )} + + +
+ +
+ ) : ( + + )} + + ); +}; diff --git a/resources/scripts/components/server/files/FileDropdownMenu.tsx b/resources/scripts/components/server/files/FileDropdownMenu.tsx new file mode 100644 index 0000000..09ec996 --- /dev/null +++ b/resources/scripts/components/server/files/FileDropdownMenu.tsx @@ -0,0 +1,200 @@ +import React, { memo, useRef, useState } from 'react'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { + faBoxOpen, + faCopy, + faEllipsisH, + faFileArchive, + faFileCode, + faFileDownload, + faLevelUpAlt, + faPencilAlt, + faTrashAlt, + IconDefinition, +} from '@fortawesome/free-solid-svg-icons'; +import RenameFileModal from '@/components/server/files/RenameFileModal'; +import { ServerContext } from '@/state/server'; +import { join } from 'path'; +import deleteFiles from '@/api/server/files/deleteFiles'; +import SpinnerOverlay from '@/components/elements/SpinnerOverlay'; +import copyFile from '@/api/server/files/copyFile'; +import Can from '@/components/elements/Can'; +import getFileDownloadUrl from '@/api/server/files/getFileDownloadUrl'; +import useFlash from '@/plugins/useFlash'; +import tw from 'twin.macro'; +import { FileObject } from '@/api/server/files/loadDirectory'; +import useFileManagerSwr from '@/plugins/useFileManagerSwr'; +import DropdownMenu from '@/components/elements/DropdownMenu'; +import styled from 'styled-components/macro'; +import useEventListener from '@/plugins/useEventListener'; +import compressFiles from '@/api/server/files/compressFiles'; +import decompressFiles from '@/api/server/files/decompressFiles'; +import isEqual from 'react-fast-compare'; +import ChmodFileModal from '@/components/server/files/ChmodFileModal'; +import { Dialog } from '@/components/elements/dialog'; + +import DropdownItems from '@/blueprint/components/Server/Files/Browse/DropdownItems'; + +type ModalType = 'rename' | 'move' | 'chmod'; + +const StyledRow = styled.div<{ $danger?: boolean }>` + ${tw`p-2 flex items-center rounded`}; + ${(props) => + props.$danger ? tw`hover:bg-red-100 hover:text-red-700` : tw`hover:bg-neutral-100 hover:text-neutral-700`}; +`; + +interface RowProps extends React.HTMLAttributes { + icon: IconDefinition; + title: string; + $danger?: boolean; +} + +const Row = ({ icon, title, ...props }: RowProps) => ( + + + {title} + +); + +const FileDropdownMenu = ({ file }: { file: FileObject }) => { + const onClickRef = useRef(null); + const [showSpinner, setShowSpinner] = useState(false); + const [modal, setModal] = useState(null); + const [showConfirmation, setShowConfirmation] = useState(false); + + const uuid = ServerContext.useStoreState((state) => state.server.data!.uuid); + const { mutate } = useFileManagerSwr(); + const { clearAndAddHttpError, clearFlashes } = useFlash(); + const directory = ServerContext.useStoreState((state) => state.files.directory); + + useEventListener(`pterodactyl:files:ctx:${file.key}`, (e: CustomEvent) => { + if (onClickRef.current) { + onClickRef.current.triggerMenu(e.detail); + } + }); + + const doDeletion = () => { + clearFlashes('files'); + + // For UI speed, immediately remove the file from the listing before calling the deletion function. + // If the delete actually fails, we'll fetch the current directory contents again automatically. + mutate((files) => files.filter((f) => f.key !== file.key), false); + + deleteFiles(uuid, directory, [file.name]).catch((error) => { + mutate(); + clearAndAddHttpError({ key: 'files', error }); + }); + }; + + const doCopy = () => { + setShowSpinner(true); + clearFlashes('files'); + + copyFile(uuid, join(directory, file.name)) + .then(() => mutate()) + .catch((error) => clearAndAddHttpError({ key: 'files', error })) + .then(() => setShowSpinner(false)); + }; + + const doDownload = () => { + setShowSpinner(true); + clearFlashes('files'); + + getFileDownloadUrl(uuid, join(directory, file.name)) + .then((url) => { + // @ts-expect-error this is valid + window.location = url; + }) + .catch((error) => clearAndAddHttpError({ key: 'files', error })) + .then(() => setShowSpinner(false)); + }; + + const doArchive = () => { + setShowSpinner(true); + clearFlashes('files'); + + compressFiles(uuid, directory, [file.name]) + .then(() => mutate()) + .catch((error) => clearAndAddHttpError({ key: 'files', error })) + .then(() => setShowSpinner(false)); + }; + + const doUnarchive = () => { + setShowSpinner(true); + clearFlashes('files'); + + decompressFiles(uuid, directory, file.name) + .then(() => mutate()) + .catch((error) => clearAndAddHttpError({ key: 'files', error })) + .then(() => setShowSpinner(false)); + }; + + return ( + <> + setShowConfirmation(false)} + title={`Delete ${file.isFile ? 'File' : 'Directory'}`} + confirm={'Delete'} + onConfirmed={doDeletion} + > + You will not be able to recover the contents of  + {file.name} once deleted. + + ( +
+ + {modal ? ( + modal === 'chmod' ? ( + setModal(null)} + /> + ) : ( + setModal(null)} + /> + ) + ) : null} + +
+ )} + > + + setModal('rename')} icon={faPencilAlt} title={'Rename'} /> + setModal('move')} icon={faLevelUpAlt} title={'Move'} /> + setModal('chmod')} icon={faFileCode} title={'Permissions'} /> + + {file.isFile && ( + + + + )} + {file.isArchiveType() ? ( + + + + ) : ( + + + + )} + {file.isFile && } + + setShowConfirmation(true)} icon={faTrashAlt} title={'Delete'} $danger /> + + +
+ + ); +}; + +export default memo(FileDropdownMenu, isEqual);