From 2692e98cd849b86ceffa765634bfc012e1f79441 Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Fri, 10 Jul 2020 22:10:51 -0700 Subject: [PATCH] Massive speed improvements to filemanager --- .../scripts/api/server/files/loadDirectory.ts | 34 ++-- .../server/files/FileManagerContainer.tsx | 146 ++++++++---------- .../components/server/files/FileObjectRow.tsx | 47 +++--- .../server/files/NewDirectoryButton.tsx | 54 ++++--- 4 files changed, 138 insertions(+), 143 deletions(-) diff --git a/resources/scripts/api/server/files/loadDirectory.ts b/resources/scripts/api/server/files/loadDirectory.ts index 7c73c58a9..720486dd3 100644 --- a/resources/scripts/api/server/files/loadDirectory.ts +++ b/resources/scripts/api/server/files/loadDirectory.ts @@ -14,23 +14,21 @@ export interface FileObject { modifiedAt: Date; } -export default (uuid: string, directory?: string): Promise => { - return new Promise((resolve, reject) => { - http.get(`/api/client/servers/${uuid}/files/list`, { - params: { directory }, - }) - .then(response => resolve((response.data.data || []).map((item: any): FileObject => ({ - uuid: v4(), - name: item.attributes.name, - mode: item.attributes.mode, - size: Number(item.attributes.size), - isFile: item.attributes.is_file, - isSymlink: item.attributes.is_symlink, - isEditable: item.attributes.is_editable, - mimetype: item.attributes.mimetype, - createdAt: new Date(item.attributes.created_at), - modifiedAt: new Date(item.attributes.modified_at), - })))) - .catch(reject); +export default async (uuid: string, directory?: string): Promise => { + const { data } = await http.get(`/api/client/servers/${uuid}/files/list`, { + params: { directory }, }); + + return (data.data || []).map((item: any): FileObject => ({ + uuid: v4(), + name: item.attributes.name, + mode: item.attributes.mode, + size: Number(item.attributes.size), + isFile: item.attributes.is_file, + isSymlink: item.attributes.is_symlink, + isEditable: item.attributes.is_editable, + mimetype: item.attributes.mimetype, + createdAt: new Date(item.attributes.created_at), + modifiedAt: new Date(item.attributes.modified_at), + })); }; diff --git a/resources/scripts/components/server/files/FileManagerContainer.tsx b/resources/scripts/components/server/files/FileManagerContainer.tsx index e97bb37f0..35fc034e0 100644 --- a/resources/scripts/components/server/files/FileManagerContainer.tsx +++ b/resources/scripts/components/server/files/FileManagerContainer.tsx @@ -1,21 +1,21 @@ -import React, { useEffect, useState } from 'react'; -import FlashMessageRender from '@/components/FlashMessageRender'; -import { ServerContext } from '@/state/server'; -import { Actions, useStoreActions } from 'easy-peasy'; -import { ApplicationStore } from '@/state'; +import React, { useEffect } from 'react'; import { httpErrorToHuman } from '@/api/http'; import { CSSTransition } from 'react-transition-group'; import Spinner from '@/components/elements/Spinner'; import FileObjectRow from '@/components/server/files/FileObjectRow'; 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 { Link } from 'react-router-dom'; +import { Link, useLocation } from 'react-router-dom'; import Can from '@/components/elements/Can'; import PageContentBlock from '@/components/elements/PageContentBlock'; import ServerError from '@/components/screens/ServerError'; import tw from 'twin.macro'; 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[] => { return files.sort((a, b) => a.name.localeCompare(b.name)) @@ -23,94 +23,72 @@ const sortFiles = (files: FileObject[]): FileObject[] => { }; export default () => { - const [ error, setError ] = useState(''); - const [ loading, setLoading ] = useState(true); - const { clearFlashes } = useStoreActions((actions: Actions) => actions.flashes); - const { id } = ServerContext.useStoreState(state => state.server.data!); - const { contents: files } = ServerContext.useStoreState(state => state.files); - const { getDirectoryContents } = ServerContext.useStoreActions(actions => actions.files); + const { hash } = useLocation(); + const { id, uuid } = useServer(); + const setDirectory = ServerContext.useStoreActions(actions => actions.files.setDirectory); - const loadContents = () => { - setError(''); - clearFlashes(); - setLoading(true); - getDirectoryContents(window.location.hash) - .then(() => setLoading(false)) - .catch(error => { - console.error(error.message, { error }); - setError(httpErrorToHuman(error)); - }); - }; + const { data: files, error, mutate } = useSWR( + `${uuid}:files:${hash}`, + () => loadDirectory(uuid, cleanDirectoryPath(window.location.hash)), + ); useEffect(() => { - loadContents(); - }, []); + setDirectory(hash.length > 0 ? hash : '/'); + }, [ hash ]); if (error) { return ( - loadContents()} - /> + mutate()}/> ); } return ( - - - - - { - loading ? - - : - - {!files.length ? -

- This directory seems to be empty. -

- : - - -
- {files.length > 250 ? - -
-

- This directory is too large to display in the browser, - limiting the output to the first 250 files. -

-
- { - sortFiles(files.slice(0, 250)).map(file => ( - - )) - } -
- : - sortFiles(files).map(file => ( - - )) - } + + + { + !files ? + + : + <> + {!files.length ? +

+ This directory seems to be empty. +

+ : + + +
+ {files.length > 250 && +
+

+ This directory is too large to display in the browser, + limiting the output to the first 250 files. +

- - - } - -
- - -
-
- - } - + } + { + sortFiles(files.slice(0, 250)).map(file => ( + + )) + } +
+
+
+ } + +
+ + +
+
+ + }
); }; diff --git a/resources/scripts/components/server/files/FileObjectRow.tsx b/resources/scripts/components/server/files/FileObjectRow.tsx index d7b323b31..e6f15aed2 100644 --- a/resources/scripts/components/server/files/FileObjectRow.tsx +++ b/resources/scripts/components/server/files/FileObjectRow.tsx @@ -2,41 +2,46 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { faFileAlt, faFileImport, faFolder } from '@fortawesome/free-solid-svg-icons'; import { bytesToHuman, cleanDirectoryPath } from '@/helpers'; import { differenceInHours, format, formatDistanceToNow } from 'date-fns'; -import React from 'react'; +import React, { memo } from 'react'; import { FileObject } from '@/api/server/files/loadDirectory'; import FileDropdownMenu from '@/components/server/files/FileDropdownMenu'; import { ServerContext } from '@/state/server'; import { NavLink, useHistory, useRouteMatch } from 'react-router-dom'; 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 setDirectory = ServerContext.useStoreActions(actions => actions.files.setDirectory); const history = useHistory(); const match = useRouteMatch(); + const onRowClick = (e: React.MouseEvent) => { + // 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 + // it'll cause the directory variable to update right away when you click. + // + // Just trust me future me, leave this be. + if (!file.isFile) { + e.preventDefault(); + + history.push(`#${cleanDirectoryPath(`${directory}/${file.name}`)}`); + setDirectory(`${directory}/${file.name}`); + } + }; + return ( -
+ { - // 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 - // it'll cause the directory variable to update right away when you click. - // - // Just trust me future me, leave this be. - if (!file.isFile) { - e.preventDefault(); - - history.push(`#${cleanDirectoryPath(`${directory}/${file.name}`)}`); - setDirectory(`${directory}/${file.name}`); - } - }} + onClick={onRowClick} >
{file.isFile ? @@ -65,6 +70,8 @@ export default ({ file }: { file: FileObject }) => {
-
+ ); }; + +export default memo(FileObjectRow, (prevProps, nextProps) => isEqual(prevProps.file, nextProps.file)); diff --git a/resources/scripts/components/server/files/NewDirectoryButton.tsx b/resources/scripts/components/server/files/NewDirectoryButton.tsx index 79e55f881..d1f23c022 100644 --- a/resources/scripts/components/server/files/NewDirectoryButton.tsx +++ b/resources/scripts/components/server/files/NewDirectoryButton.tsx @@ -9,6 +9,11 @@ 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'; interface Values { directoryName: string; @@ -18,37 +23,44 @@ const schema = object().shape({ directoryName: string().required('A valid directory name must be provided.'), }); -export default () => { - 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 generateDirectoryData = (name: string): FileObject => ({ + uuid: v4(), + name: name, + mode: '0644', + size: 0, + isFile: false, + isEditable: false, + isSymlink: false, + mimetype: '', + createdAt: new Date(), + modifiedAt: new Date(), +}); - const submit = (values: Values, { setSubmitting }: FormikHelpers) => { - createDirectory(uuid, directory, values.directoryName) +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) => { + createDirectory(uuid, directory, directoryName) .then(() => { - pushFile({ - uuid: v4(), - name: values.directoryName, - mode: '0644', - size: 0, - isFile: false, - isEditable: false, - isSymlink: false, - mimetype: '', - createdAt: new Date(), - modifiedAt: new Date(), - }); + mutate( + `${uuid}:files:${hash}`, + (data: FileObject[]) => [ ...data, generateDirectoryData(directoryName) ], + ); setVisible(false); }) .catch(error => { console.error(error); setSubmitting(false); + clearAndAddHttpError({ key: 'files', error }); }); }; return ( - + <> { - + ); };