Merge branch 'develop' into feature/file-uploads
This commit is contained in:
commit
54f9c5f187
136 changed files with 2178 additions and 971 deletions
|
@ -1,6 +1,7 @@
|
|||
import React, { useRef, useState } from 'react';
|
||||
import React, { memo, useRef, useState } from 'react';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import {
|
||||
faBoxOpen,
|
||||
faCopy,
|
||||
faEllipsisH,
|
||||
faFileArchive,
|
||||
|
@ -27,6 +28,8 @@ 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';
|
||||
|
||||
type ModalType = 'rename' | 'move';
|
||||
|
||||
|
@ -43,12 +46,12 @@ interface RowProps extends React.HTMLAttributes<HTMLDivElement> {
|
|||
|
||||
const Row = ({ icon, title, ...props }: RowProps) => (
|
||||
<StyledRow {...props}>
|
||||
<FontAwesomeIcon icon={icon} css={tw`text-xs`}/>
|
||||
<FontAwesomeIcon icon={icon} css={tw`text-xs`} fixedWidth/>
|
||||
<span css={tw`ml-2`}>{title}</span>
|
||||
</StyledRow>
|
||||
);
|
||||
|
||||
export default ({ file }: { file: FileObject }) => {
|
||||
const FileDropdownMenu = ({ file }: { file: FileObject }) => {
|
||||
const onClickRef = useRef<DropdownMenu>(null);
|
||||
const [ showSpinner, setShowSpinner ] = useState(false);
|
||||
const [ modal, setModal ] = useState<ModalType | null>(null);
|
||||
|
@ -58,7 +61,7 @@ export default ({ file }: { file: FileObject }) => {
|
|||
const { clearAndAddHttpError, clearFlashes } = useFlash();
|
||||
const directory = ServerContext.useStoreState(state => state.files.directory);
|
||||
|
||||
useEventListener(`pterodactyl:files:ctx:${file.uuid}`, (e: CustomEvent) => {
|
||||
useEventListener(`pterodactyl:files:ctx:${file.key}`, (e: CustomEvent) => {
|
||||
if (onClickRef.current) {
|
||||
onClickRef.current.triggerMenu(e.detail);
|
||||
}
|
||||
|
@ -69,7 +72,7 @@ export default ({ file }: { file: FileObject }) => {
|
|||
|
||||
// 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.uuid !== file.uuid), false);
|
||||
mutate(files => files.filter(f => f.key !== file.key), false);
|
||||
|
||||
deleteFiles(uuid, directory, [ file.name ]).catch(error => {
|
||||
mutate();
|
||||
|
@ -110,6 +113,16 @@ export default ({ file }: { file: FileObject }) => {
|
|||
.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 (
|
||||
<DropdownMenu
|
||||
ref={onClickRef}
|
||||
|
@ -138,9 +151,15 @@ export default ({ file }: { file: FileObject }) => {
|
|||
<Row onClick={doCopy} icon={faCopy} title={'Copy'}/>
|
||||
</Can>
|
||||
}
|
||||
<Can action={'file.archive'}>
|
||||
<Row onClick={doArchive} icon={faFileArchive} title={'Archive'}/>
|
||||
</Can>
|
||||
{file.isArchiveType() ?
|
||||
<Can action={'file.create'}>
|
||||
<Row onClick={doUnarchive} icon={faBoxOpen} title={'Unarchive'}/>
|
||||
</Can>
|
||||
:
|
||||
<Can action={'file.archive'}>
|
||||
<Row onClick={doArchive} icon={faFileArchive} title={'Archive'}/>
|
||||
</Can>
|
||||
}
|
||||
<Row onClick={doDownload} icon={faFileDownload} title={'Download'}/>
|
||||
<Can action={'file.delete'}>
|
||||
<Row onClick={doDeletion} icon={faTrashAlt} title={'Delete'} $danger/>
|
||||
|
@ -148,3 +167,5 @@ export default ({ file }: { file: FileObject }) => {
|
|||
</DropdownMenu>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(FileDropdownMenu, isEqual);
|
||||
|
|
|
@ -1,8 +1,5 @@
|
|||
import React, { lazy, useEffect, useState } from 'react';
|
||||
import { ServerContext } from '@/state/server';
|
||||
import getFileContents from '@/api/server/files/getFileContents';
|
||||
import { Actions, useStoreActions } from 'easy-peasy';
|
||||
import { ApplicationStore } from '@/state';
|
||||
import { httpErrorToHuman } from '@/api/http';
|
||||
import SpinnerOverlay from '@/components/elements/SpinnerOverlay';
|
||||
import saveFileContents from '@/api/server/files/saveFileContents';
|
||||
|
@ -15,6 +12,10 @@ import PageContentBlock from '@/components/elements/PageContentBlock';
|
|||
import ServerError from '@/components/screens/ServerError';
|
||||
import tw from 'twin.macro';
|
||||
import Button from '@/components/elements/Button';
|
||||
import Select from '@/components/elements/Select';
|
||||
import modes from '@/modes';
|
||||
import useServer from '@/plugins/useServer';
|
||||
import useFlash from '@/plugins/useFlash';
|
||||
|
||||
const LazyAceEditor = lazy(() => import(/* webpackChunkName: "editor" */'@/components/elements/AceEditor'));
|
||||
|
||||
|
@ -24,12 +25,13 @@ export default () => {
|
|||
const [ loading, setLoading ] = useState(action === 'edit');
|
||||
const [ content, setContent ] = useState('');
|
||||
const [ modalVisible, setModalVisible ] = useState(false);
|
||||
const [ mode, setMode ] = useState('plain_text');
|
||||
|
||||
const history = useHistory();
|
||||
const { hash } = useLocation();
|
||||
|
||||
const { id, uuid } = ServerContext.useStoreState(state => state.server.data!);
|
||||
const { addError, clearFlashes } = useStoreActions((actions: Actions<ApplicationStore>) => actions.flashes);
|
||||
const { id, uuid } = useServer();
|
||||
const { addError, clearFlashes } = useFlash();
|
||||
|
||||
let fetchFileContent: null | (() => Promise<string>) = null;
|
||||
|
||||
|
@ -75,10 +77,7 @@ export default () => {
|
|||
|
||||
if (error) {
|
||||
return (
|
||||
<ServerError
|
||||
message={error}
|
||||
onBack={() => history.goBack()}
|
||||
/>
|
||||
<ServerError message={error} onBack={() => history.goBack()}/>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -109,15 +108,24 @@ export default () => {
|
|||
<div css={tw`relative`}>
|
||||
<SpinnerOverlay visible={loading}/>
|
||||
<LazyAceEditor
|
||||
initialModePath={hash.replace(/^#/, '') || 'plain_text'}
|
||||
mode={mode}
|
||||
filename={hash.replace(/^#/, '')}
|
||||
onModeChanged={setMode}
|
||||
initialContent={content}
|
||||
fetchContent={value => {
|
||||
fetchFileContent = value;
|
||||
}}
|
||||
onContentSaved={() => save()}
|
||||
onContentSaved={save}
|
||||
/>
|
||||
</div>
|
||||
<div css={tw`flex justify-end mt-4`}>
|
||||
<div css={tw`rounded bg-neutral-900 mr-4`}>
|
||||
<Select value={mode} onChange={e => setMode(e.currentTarget.value)}>
|
||||
{Object.keys(modes).map(key => (
|
||||
<option key={key} value={key}>{modes[key]}</option>
|
||||
))}
|
||||
</Select>
|
||||
</div>
|
||||
{action === 'edit' ?
|
||||
<Can action={'file.update'}>
|
||||
<Button onClick={() => save()}>
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import React, { useEffect } from 'react';
|
||||
import { Helmet } from 'react-helmet';
|
||||
import { httpErrorToHuman } from '@/api/http';
|
||||
import { CSSTransition } from 'react-transition-group';
|
||||
import Spinner from '@/components/elements/Spinner';
|
||||
|
@ -24,17 +25,14 @@ const sortFiles = (files: FileObject[]): FileObject[] => {
|
|||
};
|
||||
|
||||
export default () => {
|
||||
const { id } = useServer();
|
||||
const { id, name: serverName } = useServer();
|
||||
const { hash } = useLocation();
|
||||
const { data: files, error, mutate } = useFileManagerSwr();
|
||||
|
||||
const setDirectory = ServerContext.useStoreActions(actions => actions.files.setDirectory);
|
||||
const setSelectedFiles = ServerContext.useStoreActions(actions => actions.files.setSelectedFiles);
|
||||
|
||||
useEffect(() => {
|
||||
// We won't automatically mutate the store when the component re-mounts, otherwise because of
|
||||
// my (horrible) programming this fires off way more than we intend it to.
|
||||
mutate();
|
||||
|
||||
setSelectedFiles([]);
|
||||
setDirectory(hash.length > 0 ? hash : '/');
|
||||
}, [ hash ]);
|
||||
|
@ -47,6 +45,9 @@ export default () => {
|
|||
|
||||
return (
|
||||
<PageContentBlock showFlashKey={'files'}>
|
||||
<Helmet>
|
||||
<title> {serverName} | File Manager </title>
|
||||
</Helmet>
|
||||
<FileManagerBreadcrumbs/>
|
||||
{
|
||||
!files ?
|
||||
|
@ -70,7 +71,7 @@ export default () => {
|
|||
}
|
||||
{
|
||||
sortFiles(files.slice(0, 250)).map(file => (
|
||||
<FileObjectRow key={file.uuid} file={file}/>
|
||||
<FileObjectRow key={file.key} file={file}/>
|
||||
))
|
||||
}
|
||||
<MassActionsBar/>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { faFileAlt, faFileImport, faFolder } from '@fortawesome/free-solid-svg-icons';
|
||||
import { faFileAlt, faFileArchive, faFileImport, faFolder } from '@fortawesome/free-solid-svg-icons';
|
||||
import { bytesToHuman, cleanDirectoryPath } from '@/helpers';
|
||||
import { differenceInHours, format, formatDistanceToNow } from 'date-fns';
|
||||
import React, { memo } from 'react';
|
||||
|
@ -18,7 +18,6 @@ const Row = styled.div`
|
|||
|
||||
const FileObjectRow = ({ file }: { file: FileObject }) => {
|
||||
const directory = ServerContext.useStoreState(state => state.files.directory);
|
||||
const setDirectory = ServerContext.useStoreActions(actions => actions.files.setDirectory);
|
||||
|
||||
const history = useHistory();
|
||||
const match = useRouteMatch();
|
||||
|
@ -31,9 +30,7 @@ const FileObjectRow = ({ file }: { file: FileObject }) => {
|
|||
// Just trust me future me, leave this be.
|
||||
if (!file.isFile) {
|
||||
e.preventDefault();
|
||||
|
||||
history.push(`#${cleanDirectoryPath(`${directory}/${file.name}`)}`);
|
||||
setDirectory(`${directory}/${file.name}`);
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -42,7 +39,7 @@ const FileObjectRow = ({ file }: { file: FileObject }) => {
|
|||
key={file.name}
|
||||
onContextMenu={e => {
|
||||
e.preventDefault();
|
||||
window.dispatchEvent(new CustomEvent(`pterodactyl:files:ctx:${file.uuid}`, { detail: e.clientX }));
|
||||
window.dispatchEvent(new CustomEvent(`pterodactyl:files:ctx:${file.key}`, { detail: e.clientX }));
|
||||
}}
|
||||
>
|
||||
<SelectFileCheckbox name={file.name}/>
|
||||
|
@ -53,7 +50,7 @@ const FileObjectRow = ({ file }: { file: FileObject }) => {
|
|||
>
|
||||
<div css={tw`flex-none self-center text-neutral-400 mr-4 text-lg pl-3 ml-6`}>
|
||||
{file.isFile ?
|
||||
<FontAwesomeIcon icon={file.isSymlink ? faFileImport : faFileAlt}/>
|
||||
<FontAwesomeIcon icon={file.isSymlink ? faFileImport : file.isArchiveType() ? faFileArchive : faFileAlt}/>
|
||||
:
|
||||
<FontAwesomeIcon icon={faFolder}/>
|
||||
}
|
||||
|
|
|
@ -72,7 +72,7 @@ const MassActionsBar = () => {
|
|||
title={'Delete these files?'}
|
||||
buttonText={'Yes, Delete Files'}
|
||||
onConfirmed={onClickConfirmDeletion}
|
||||
onDismissed={() => setShowConfirm(false)}
|
||||
onModalDismissed={() => setShowConfirm(false)}
|
||||
>
|
||||
Deleting files is a permanent operation, you cannot undo this action.
|
||||
</ConfirmationModal>
|
||||
|
|
|
@ -6,14 +6,12 @@ import Field from '@/components/elements/Field';
|
|||
import { join } from 'path';
|
||||
import { object, string } from 'yup';
|
||||
import createDirectory from '@/api/server/files/createDirectory';
|
||||
import v4 from 'uuid/v4';
|
||||
import tw from 'twin.macro';
|
||||
import Button from '@/components/elements/Button';
|
||||
import { mutate } from 'swr';
|
||||
import useServer from '@/plugins/useServer';
|
||||
import { FileObject } from '@/api/server/files/loadDirectory';
|
||||
import { useLocation } from 'react-router';
|
||||
import useFlash from '@/plugins/useFlash';
|
||||
import useFileManagerSwr from '@/plugins/useFileManagerSwr';
|
||||
|
||||
interface Values {
|
||||
directoryName: string;
|
||||
|
@ -24,7 +22,7 @@ const schema = object().shape({
|
|||
});
|
||||
|
||||
const generateDirectoryData = (name: string): FileObject => ({
|
||||
uuid: v4(),
|
||||
key: `dir_${name}`,
|
||||
name: name,
|
||||
mode: '0644',
|
||||
size: 0,
|
||||
|
@ -34,24 +32,21 @@ const generateDirectoryData = (name: string): FileObject => ({
|
|||
mimetype: '',
|
||||
createdAt: new Date(),
|
||||
modifiedAt: new Date(),
|
||||
isArchiveType: () => false,
|
||||
});
|
||||
|
||||
export default () => {
|
||||
const { uuid } = useServer();
|
||||
const { hash } = useLocation();
|
||||
const { clearAndAddHttpError } = useFlash();
|
||||
const [ visible, setVisible ] = useState(false);
|
||||
|
||||
const { mutate } = useFileManagerSwr();
|
||||
const directory = ServerContext.useStoreState(state => state.files.directory);
|
||||
|
||||
const submit = ({ directoryName }: Values, { setSubmitting }: FormikHelpers<Values>) => {
|
||||
createDirectory(uuid, directory, directoryName)
|
||||
.then(() => {
|
||||
mutate(
|
||||
`${uuid}:files:${hash}`,
|
||||
(data: FileObject[]) => [ ...data, generateDirectoryData(directoryName) ],
|
||||
);
|
||||
setVisible(false);
|
||||
})
|
||||
.then(() => mutate(data => [ ...data, generateDirectoryData(directoryName) ], false))
|
||||
.then(() => setVisible(false))
|
||||
.catch(error => {
|
||||
console.error(error);
|
||||
setSubmitting(false);
|
||||
|
@ -78,6 +73,7 @@ export default () => {
|
|||
>
|
||||
<Form css={tw`m-0`}>
|
||||
<Field
|
||||
autoFocus
|
||||
id={'directoryName'}
|
||||
name={'directoryName'}
|
||||
label={'Directory Name'}
|
||||
|
|
|
@ -15,9 +15,9 @@ interface FormikValues {
|
|||
name: string;
|
||||
}
|
||||
|
||||
type Props = RequiredModalProps & { files: string[]; useMoveTerminology?: boolean };
|
||||
type OwnProps = RequiredModalProps & { files: string[]; useMoveTerminology?: boolean };
|
||||
|
||||
export default ({ files, useMoveTerminology, ...props }: Props) => {
|
||||
const RenameFileModal = ({ files, useMoveTerminology, ...props }: OwnProps) => {
|
||||
const { uuid } = useServer();
|
||||
const { mutate } = useFileManagerSwr();
|
||||
const { clearFlashes, clearAndAddHttpError } = useFlash();
|
||||
|
@ -96,3 +96,5 @@ export default ({ files, useMoveTerminology, ...props }: Props) => {
|
|||
</Formik>
|
||||
);
|
||||
};
|
||||
|
||||
export default RenameFileModal;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue