Merge branch 'develop' into feature/file-uploads

This commit is contained in:
Dane Everitt 2020-08-22 18:33:09 -07:00
commit 54f9c5f187
No known key found for this signature in database
GPG key ID: EEA66103B3D71F53
136 changed files with 2178 additions and 971 deletions

View file

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

View file

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

View file

@ -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/>

View file

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

View file

@ -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>

View file

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

View file

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