Massive speed improvements to filemanager

This commit is contained in:
Dane Everitt 2020-07-10 22:10:51 -07:00
parent fdec3cea80
commit 2692e98cd8
No known key found for this signature in database
GPG Key ID: EEA66103B3D71F53
4 changed files with 138 additions and 143 deletions

View File

@ -14,12 +14,12 @@ export interface FileObject {
modifiedAt: Date; modifiedAt: Date;
} }
export default (uuid: string, directory?: string): Promise<FileObject[]> => { export default async (uuid: string, directory?: string): Promise<FileObject[]> => {
return new Promise((resolve, reject) => { const { data } = await http.get(`/api/client/servers/${uuid}/files/list`, {
http.get(`/api/client/servers/${uuid}/files/list`, {
params: { directory }, params: { directory },
}) });
.then(response => resolve((response.data.data || []).map((item: any): FileObject => ({
return (data.data || []).map((item: any): FileObject => ({
uuid: v4(), uuid: v4(),
name: item.attributes.name, name: item.attributes.name,
mode: item.attributes.mode, mode: item.attributes.mode,
@ -30,7 +30,5 @@ export default (uuid: string, directory?: string): Promise<FileObject[]> => {
mimetype: item.attributes.mimetype, mimetype: item.attributes.mimetype,
createdAt: new Date(item.attributes.created_at), createdAt: new Date(item.attributes.created_at),
modifiedAt: new Date(item.attributes.modified_at), modifiedAt: new Date(item.attributes.modified_at),
})))) }));
.catch(reject);
});
}; };

View File

@ -1,21 +1,21 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect } from 'react';
import FlashMessageRender from '@/components/FlashMessageRender';
import { ServerContext } from '@/state/server';
import { Actions, useStoreActions } from 'easy-peasy';
import { ApplicationStore } from '@/state';
import { httpErrorToHuman } from '@/api/http'; import { httpErrorToHuman } from '@/api/http';
import { CSSTransition } from 'react-transition-group'; import { CSSTransition } from 'react-transition-group';
import Spinner from '@/components/elements/Spinner'; import Spinner from '@/components/elements/Spinner';
import FileObjectRow from '@/components/server/files/FileObjectRow'; import FileObjectRow from '@/components/server/files/FileObjectRow';
import FileManagerBreadcrumbs from '@/components/server/files/FileManagerBreadcrumbs'; import FileManagerBreadcrumbs from '@/components/server/files/FileManagerBreadcrumbs';
import { FileObject } from '@/api/server/files/loadDirectory'; import loadDirectory, { FileObject } from '@/api/server/files/loadDirectory';
import NewDirectoryButton from '@/components/server/files/NewDirectoryButton'; import NewDirectoryButton from '@/components/server/files/NewDirectoryButton';
import { Link } from 'react-router-dom'; import { Link, useLocation } from 'react-router-dom';
import Can from '@/components/elements/Can'; import Can from '@/components/elements/Can';
import PageContentBlock from '@/components/elements/PageContentBlock'; import PageContentBlock from '@/components/elements/PageContentBlock';
import ServerError from '@/components/screens/ServerError'; import ServerError from '@/components/screens/ServerError';
import tw from 'twin.macro'; import tw from 'twin.macro';
import Button from '@/components/elements/Button'; import Button from '@/components/elements/Button';
import useSWR from 'swr';
import useServer from '@/plugins/useServer';
import { cleanDirectoryPath } from '@/helpers';
import { ServerContext } from '@/state/server';
const sortFiles = (files: FileObject[]): FileObject[] => { const sortFiles = (files: FileObject[]): FileObject[] => {
return files.sort((a, b) => a.name.localeCompare(b.name)) return files.sort((a, b) => a.name.localeCompare(b.name))
@ -23,48 +23,33 @@ const sortFiles = (files: FileObject[]): FileObject[] => {
}; };
export default () => { export default () => {
const [ error, setError ] = useState(''); const { hash } = useLocation();
const [ loading, setLoading ] = useState(true); const { id, uuid } = useServer();
const { clearFlashes } = useStoreActions((actions: Actions<ApplicationStore>) => actions.flashes); const setDirectory = ServerContext.useStoreActions(actions => actions.files.setDirectory);
const { id } = ServerContext.useStoreState(state => state.server.data!);
const { contents: files } = ServerContext.useStoreState(state => state.files);
const { getDirectoryContents } = ServerContext.useStoreActions(actions => actions.files);
const loadContents = () => { const { data: files, error, mutate } = useSWR(
setError(''); `${uuid}:files:${hash}`,
clearFlashes(); () => loadDirectory(uuid, cleanDirectoryPath(window.location.hash)),
setLoading(true); );
getDirectoryContents(window.location.hash)
.then(() => setLoading(false))
.catch(error => {
console.error(error.message, { error });
setError(httpErrorToHuman(error));
});
};
useEffect(() => { useEffect(() => {
loadContents(); setDirectory(hash.length > 0 ? hash : '/');
}, []); }, [ hash ]);
if (error) { if (error) {
return ( return (
<ServerError <ServerError message={httpErrorToHuman(error)} onRetry={() => mutate()}/>
message={error}
onRetry={() => loadContents()}
/>
); );
} }
return ( return (
<PageContentBlock> <PageContentBlock showFlashKey={'files'}>
<FlashMessageRender byKey={'files'} css={tw`mb-4`}/>
<React.Fragment>
<FileManagerBreadcrumbs/> <FileManagerBreadcrumbs/>
{ {
loading ? !files ?
<Spinner size={'large'} centered/> <Spinner size={'large'} centered/>
: :
<React.Fragment> <>
{!files.length ? {!files.length ?
<p css={tw`text-sm text-neutral-400 text-center`}> <p css={tw`text-sm text-neutral-400 text-center`}>
This directory seems to be empty. This directory seems to be empty.
@ -73,25 +58,19 @@ export default () => {
<CSSTransition classNames={'fade'} timeout={150} appear in> <CSSTransition classNames={'fade'} timeout={150} appear in>
<React.Fragment> <React.Fragment>
<div> <div>
{files.length > 250 ? {files.length > 250 &&
<React.Fragment>
<div css={tw`rounded bg-yellow-400 mb-px p-3`}> <div css={tw`rounded bg-yellow-400 mb-px p-3`}>
<p css={tw`text-yellow-900 text-sm text-center`}> <p css={tw`text-yellow-900 text-sm text-center`}>
This directory is too large to display in the browser, This directory is too large to display in the browser,
limiting the output to the first 250 files. limiting the output to the first 250 files.
</p> </p>
</div> </div>
}
{ {
sortFiles(files.slice(0, 250)).map(file => ( sortFiles(files.slice(0, 250)).map(file => (
<FileObjectRow key={file.uuid} file={file}/> <FileObjectRow key={file.uuid} file={file}/>
)) ))
} }
</React.Fragment>
:
sortFiles(files).map(file => (
<FileObjectRow key={file.uuid} file={file}/>
))
}
</div> </div>
</React.Fragment> </React.Fragment>
</CSSTransition> </CSSTransition>
@ -108,9 +87,8 @@ export default () => {
</Button> </Button>
</div> </div>
</Can> </Can>
</React.Fragment> </>
} }
</React.Fragment>
</PageContentBlock> </PageContentBlock>
); );
}; };

View File

@ -2,29 +2,27 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faFileAlt, faFileImport, faFolder } from '@fortawesome/free-solid-svg-icons'; import { faFileAlt, faFileImport, faFolder } from '@fortawesome/free-solid-svg-icons';
import { bytesToHuman, cleanDirectoryPath } from '@/helpers'; import { bytesToHuman, cleanDirectoryPath } from '@/helpers';
import { differenceInHours, format, formatDistanceToNow } from 'date-fns'; import { differenceInHours, format, formatDistanceToNow } from 'date-fns';
import React from 'react'; import React, { memo } from 'react';
import { FileObject } from '@/api/server/files/loadDirectory'; import { FileObject } from '@/api/server/files/loadDirectory';
import FileDropdownMenu from '@/components/server/files/FileDropdownMenu'; import FileDropdownMenu from '@/components/server/files/FileDropdownMenu';
import { ServerContext } from '@/state/server'; import { ServerContext } from '@/state/server';
import { NavLink, useHistory, useRouteMatch } from 'react-router-dom'; import { NavLink, useHistory, useRouteMatch } from 'react-router-dom';
import tw from 'twin.macro'; import tw from 'twin.macro';
import isEqual from 'react-fast-compare';
import styled from 'styled-components/macro';
export default ({ file }: { file: FileObject }) => { const Row = styled.div`
${tw`flex bg-neutral-700 rounded-sm mb-px text-sm hover:text-neutral-100 cursor-pointer items-center no-underline hover:bg-neutral-600`};
`;
const FileObjectRow = ({ file }: { file: FileObject }) => {
const directory = ServerContext.useStoreState(state => state.files.directory); const directory = ServerContext.useStoreState(state => state.files.directory);
const setDirectory = ServerContext.useStoreActions(actions => actions.files.setDirectory); const setDirectory = ServerContext.useStoreActions(actions => actions.files.setDirectory);
const history = useHistory(); const history = useHistory();
const match = useRouteMatch(); const match = useRouteMatch();
return ( const onRowClick = (e: React.MouseEvent<HTMLAnchorElement, MouseEvent>) => {
<div
key={file.name}
css={tw`flex bg-neutral-700 rounded-sm mb-px text-sm hover:text-neutral-100 cursor-pointer items-center no-underline hover:bg-neutral-600`}
>
<NavLink
to={`${match.url}/${file.isFile ? 'edit/' : ''}#${cleanDirectoryPath(`${directory}/${file.name}`)}`}
css={tw`flex flex-1 text-neutral-300 no-underline p-3`}
onClick={e => {
// Don't rely on the onClick to work with the generated URL. Because of the way this // Don't rely on the onClick to work with the generated URL. Because of the way this
// component re-renders you'll get redirected into a nested directory structure since // component re-renders you'll get redirected into a nested directory structure since
// it'll cause the directory variable to update right away when you click. // it'll cause the directory variable to update right away when you click.
@ -36,7 +34,14 @@ export default ({ file }: { file: FileObject }) => {
history.push(`#${cleanDirectoryPath(`${directory}/${file.name}`)}`); history.push(`#${cleanDirectoryPath(`${directory}/${file.name}`)}`);
setDirectory(`${directory}/${file.name}`); setDirectory(`${directory}/${file.name}`);
} }
}} };
return (
<Row key={file.name}>
<NavLink
to={`${match.url}/${file.isFile ? 'edit/' : ''}#${cleanDirectoryPath(`${directory}/${file.name}`)}`}
css={tw`flex flex-1 text-neutral-300 no-underline p-3`}
onClick={onRowClick}
> >
<div css={tw`flex-none text-neutral-400 mr-4 text-lg pl-3`}> <div css={tw`flex-none text-neutral-400 mr-4 text-lg pl-3`}>
{file.isFile ? {file.isFile ?
@ -65,6 +70,8 @@ export default ({ file }: { file: FileObject }) => {
</div> </div>
</NavLink> </NavLink>
<FileDropdownMenu uuid={file.uuid}/> <FileDropdownMenu uuid={file.uuid}/>
</div> </Row>
); );
}; };
export default memo(FileObjectRow, (prevProps, nextProps) => isEqual(prevProps.file, nextProps.file));

View File

@ -9,6 +9,11 @@ import createDirectory from '@/api/server/files/createDirectory';
import v4 from 'uuid/v4'; import v4 from 'uuid/v4';
import tw from 'twin.macro'; import tw from 'twin.macro';
import Button from '@/components/elements/Button'; 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';
interface Values { interface Values {
directoryName: string; directoryName: string;
@ -18,18 +23,9 @@ const schema = object().shape({
directoryName: string().required('A valid directory name must be provided.'), directoryName: string().required('A valid directory name must be provided.'),
}); });
export default () => { const generateDirectoryData = (name: string): FileObject => ({
const [ visible, setVisible ] = useState(false);
const uuid = ServerContext.useStoreState(state => state.server.data!.uuid);
const directory = ServerContext.useStoreState(state => state.files.directory);
const pushFile = ServerContext.useStoreActions(actions => actions.files.pushFile);
const submit = (values: Values, { setSubmitting }: FormikHelpers<Values>) => {
createDirectory(uuid, directory, values.directoryName)
.then(() => {
pushFile({
uuid: v4(), uuid: v4(),
name: values.directoryName, name: name,
mode: '0644', mode: '0644',
size: 0, size: 0,
isFile: false, isFile: false,
@ -39,16 +35,32 @@ export default () => {
createdAt: new Date(), createdAt: new Date(),
modifiedAt: new Date(), modifiedAt: new Date(),
}); });
export default () => {
const { uuid } = useServer();
const { hash } = useLocation();
const { clearAndAddHttpError } = useFlash();
const [ visible, setVisible ] = useState(false);
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); setVisible(false);
}) })
.catch(error => { .catch(error => {
console.error(error); console.error(error);
setSubmitting(false); setSubmitting(false);
clearAndAddHttpError({ key: 'files', error });
}); });
}; };
return ( return (
<React.Fragment> <>
<Formik <Formik
onSubmit={submit} onSubmit={submit}
validationSchema={schema} validationSchema={schema}
@ -91,6 +103,6 @@ export default () => {
<Button isSecondary css={tw`mr-2`} onClick={() => setVisible(true)}> <Button isSecondary css={tw`mr-2`} onClick={() => setVisible(true)}>
Create Directory Create Directory
</Button> </Button>
</React.Fragment> </>
); );
}; };