Fix up file manager

This commit is contained in:
Dane Everitt 2020-07-04 17:57:24 -07:00
parent 7e8a5f1271
commit 43fbefbdb6
No known key found for this signature in database
GPG key ID: EEA66103B3D71F53
11 changed files with 108 additions and 104 deletions

View file

@ -1,9 +1,8 @@
import React, { useCallback, useEffect, useState, lazy } from 'react';
import useRouter from 'use-react-router';
import { ServerContext } from '@/state/server';
import React, { useCallback, useEffect, useState } from 'react';
import ace, { Editor } from 'brace';
import getFileContents from '@/api/server/files/getFileContents';
import styled from 'styled-components/macro';
import tw from 'twin.macro';
import Select from '@/components/elements/Select';
// @ts-ignore
require('brace/ext/modelist');
@ -11,7 +10,7 @@ require('ayu-ace/mirage');
const EditorContainer = styled.div`
min-height: 16rem;
height: calc(100vh - 16rem);
height: calc(100vh - 20rem);
${tw`relative`};
#editor {
@ -20,9 +19,7 @@ const EditorContainer = styled.div`
`;
const modes: { [k: string]: string } = {
// eslint-disable-next-line @typescript-eslint/camelcase
assembly_x86: 'Assembly (x86)',
// eslint-disable-next-line @typescript-eslint/camelcase
c_cpp: 'C++',
coffee: 'Coffeescript',
css: 'CSS',
@ -40,7 +37,6 @@ const modes: { [k: string]: string } = {
properties: 'Properties',
python: 'Python',
ruby: 'Ruby',
// eslint-disable-next-line @typescript-eslint/camelcase
plain_text: 'Plaintext',
toml: 'TOML',
typescript: 'Typescript',
@ -70,7 +66,7 @@ export default ({ style, initialContent, initialModePath, fetchContent, onConten
useEffect(() => {
editor && editor.session.setMode(mode);
}, [editor, mode]);
}, [ editor, mode ]);
useEffect(() => {
editor && editor.session.setValue(initialContent || '');
@ -113,10 +109,9 @@ export default ({ style, initialContent, initialModePath, fetchContent, onConten
return (
<EditorContainer style={style}>
<div id={'editor'} ref={ref}/>
<div className={'absolute right-0 bottom-0 z-50'}>
<div className={'m-3 rounded bg-neutral-900 border border-black'}>
<select
className={'input-dark'}
<div css={tw`absolute right-0 bottom-0 z-50`}>
<div css={tw`m-3 rounded bg-neutral-900 border border-black`}>
<Select
value={mode.split('/').pop()}
onChange={e => setMode(`ace/mode/${e.currentTarget.value}`)}
>
@ -125,7 +120,7 @@ export default ({ style, initialContent, initialModePath, fetchContent, onConten
<option key={key} value={key}>{modes[key]}</option>
))
}
</select>
</Select>
</div>
</div>
</EditorContainer>

View file

@ -69,8 +69,7 @@ const DropdownMenu = ({ renderToggle, children }: Props) => {
e.stopPropagation();
setVisible(false);
}}
css={tw`absolute bg-white p-2 rounded border border-neutral-700 shadow-lg text-neutral-500`}
style={{ minWidth: '12rem' }}
css={tw`absolute bg-white p-2 rounded border border-neutral-700 shadow-lg text-neutral-500 min-w-48`}
>
{children}
</div>

View file

@ -18,6 +18,8 @@ import Can from '@/components/elements/Can';
import getFileDownloadUrl from '@/api/server/files/getFileDownloadUrl';
import useServer from '@/plugins/useServer';
import useFlash from '@/plugins/useFlash';
import tw from 'twin.macro';
import Fade from '@/components/elements/Fade';
type ModalType = 'rename' | 'move';
@ -113,7 +115,7 @@ export default ({ uuid }: { uuid: string }) => {
<div key={`dropdown:${file.uuid}`}>
<div
ref={menuButton}
className={'p-3 hover:text-white'}
css={tw`p-3 hover:text-white`}
onClick={e => {
e.preventDefault();
if (!menuVisible) {
@ -133,60 +135,60 @@ export default ({ uuid }: { uuid: string }) => {
setMenuVisible(false);
}}
/>
<SpinnerOverlay visible={showSpinner} fixed={true} size={'large'}/>
<SpinnerOverlay visible={showSpinner} fixed size={'large'}/>
</div>
<CSSTransition timeout={250} in={menuVisible} unmountOnExit={true} classNames={'fade'}>
<Fade timeout={250} in={menuVisible} unmountOnExit classNames={'fade'}>
<div
ref={menu}
onClick={e => {
e.stopPropagation();
setMenuVisible(false);
}}
className={'absolute bg-white p-2 rounded border border-neutral-700 shadow-lg text-neutral-500 min-w-48'}
css={tw`absolute bg-white p-2 rounded border border-neutral-700 shadow-lg text-neutral-500 min-w-48`}
>
<Can action={'file.update'}>
<div
onClick={() => setModal('rename')}
className={'hover:text-neutral-700 p-2 flex items-center hover:bg-neutral-100 rounded'}
css={tw`hover:text-neutral-700 p-2 flex items-center hover:bg-neutral-100 rounded`}
>
<FontAwesomeIcon icon={faPencilAlt} className={'text-xs'}/>
<span className={'ml-2'}>Rename</span>
<FontAwesomeIcon icon={faPencilAlt} css={tw`text-xs`}/>
<span css={tw`ml-2`}>Rename</span>
</div>
<div
onClick={() => setModal('move')}
className={'hover:text-neutral-700 p-2 flex items-center hover:bg-neutral-100 rounded'}
css={tw`hover:text-neutral-700 p-2 flex items-center hover:bg-neutral-100 rounded`}
>
<FontAwesomeIcon icon={faLevelUpAlt} className={'text-xs'}/>
<span className={'ml-2'}>Move</span>
<FontAwesomeIcon icon={faLevelUpAlt} css={tw`text-xs`}/>
<span css={tw`ml-2`}>Move</span>
</div>
</Can>
<Can action={'file.create'}>
<div
onClick={() => doCopy()}
className={'hover:text-neutral-700 p-2 flex items-center hover:bg-neutral-100 rounded'}
css={tw`hover:text-neutral-700 p-2 flex items-center hover:bg-neutral-100 rounded`}
>
<FontAwesomeIcon icon={faCopy} className={'text-xs'}/>
<span className={'ml-2'}>Copy</span>
<FontAwesomeIcon icon={faCopy} css={tw`text-xs`}/>
<span css={tw`ml-2`}>Copy</span>
</div>
</Can>
<div
className={'hover:text-neutral-700 p-2 flex items-center hover:bg-neutral-100 rounded'}
css={tw`hover:text-neutral-700 p-2 flex items-center hover:bg-neutral-100 rounded`}
onClick={() => doDownload()}
>
<FontAwesomeIcon icon={faFileDownload} className={'text-xs'}/>
<span className={'ml-2'}>Download</span>
<FontAwesomeIcon icon={faFileDownload} css={tw`text-xs`}/>
<span css={tw`ml-2`}>Download</span>
</div>
<Can action={'file.delete'}>
<div
onClick={() => doDeletion()}
className={'hover:text-red-700 p-2 flex items-center hover:bg-red-100 rounded'}
css={tw`hover:text-red-700 p-2 flex items-center hover:bg-red-100 rounded`}
>
<FontAwesomeIcon icon={faTrashAlt} className={'text-xs'}/>
<span className={'ml-2'}>Delete</span>
<FontAwesomeIcon icon={faTrashAlt} css={tw`text-xs`}/>
<span css={tw`ml-2`}>Delete</span>
</div>
</Can>
</div>
</CSSTransition>
</Fade>
</div>
);
};

View file

@ -14,6 +14,8 @@ import Can from '@/components/elements/Can';
import FlashMessageRender from '@/components/FlashMessageRender';
import PageContentBlock from '@/components/elements/PageContentBlock';
import ServerError from '@/components/screens/ServerError';
import tw from 'twin.macro';
import Button from '@/components/elements/Button';
const LazyAceEditor = lazy(() => import(/* webpackChunkName: "editor" */'@/components/elements/AceEditor'));
@ -81,16 +83,17 @@ export default () => {
return (
<PageContentBlock>
<FlashMessageRender byKey={'files:view'} className={'mb-4'}/>
<FileManagerBreadcrumbs withinFileEditor={true} isNewFile={action !== 'edit'}/>
{(name || hash.replace(/^#/, '')).endsWith('.pteroignore') &&
<div className={'mb-4 p-4 border-l-4 bg-neutral-900 rounded border-cyan-400'}>
<p className={'text-neutral-300 text-sm'}>
You're editing a <code className={'font-mono bg-black rounded py-px px-1'}>.pteroignore</code> file.
<FlashMessageRender byKey={'files:view'} css={tw`mb-4`}/>
<FileManagerBreadcrumbs withinFileEditor isNewFile={action !== 'edit'}/>
{hash.replace(/^#/, '').endsWith('.pteroignore') &&
<div css={tw`mb-4 p-4 border-l-4 bg-neutral-900 rounded border-cyan-400`}>
<p css={tw`text-neutral-300 text-sm`}>
You&apos;re editing
a <code css={tw`font-mono bg-black rounded py-px px-1`}>.pteroignore</code> file.
Any files or directories listed in here will be excluded from backups. Wildcards are supported by
using an asterisk (<code className={'font-mono bg-black rounded py-px px-1'}>*</code>). You can
using an asterisk (<code css={tw`font-mono bg-black rounded py-px px-1`}>*</code>). You can
negate a prior rule by prepending an exclamation point
(<code className={'font-mono bg-black rounded py-px px-1'}>!</code>).
(<code css={tw`font-mono bg-black rounded py-px px-1`}>!</code>).
</p>
</div>
}
@ -102,7 +105,7 @@ export default () => {
save(name);
}}
/>
<div className={'relative'}>
<div css={tw`relative`}>
<SpinnerOverlay visible={loading}/>
<LazyAceEditor
initialModePath={hash.replace(/^#/, '') || 'plain_text'}
@ -113,18 +116,18 @@ export default () => {
onContentSaved={() => save()}
/>
</div>
<div className={'flex justify-end mt-4'}>
<div css={tw`flex justify-end mt-4`}>
{action === 'edit' ?
<Can action={'file.update'}>
<button className={'btn btn-primary btn-sm'} onClick={() => save()}>
<Button onClick={() => save()}>
Save Content
</button>
</Button>
</Can>
:
<Can action={'file.create'}>
<button className={'btn btn-primary btn-sm'} onClick={() => setModalVisible(true)}>
<Button onClick={() => setModalVisible(true)}>
Create File
</button>
</Button>
</Can>
}
</div>

View file

@ -2,6 +2,7 @@ import React, { useEffect, useState } from 'react';
import { ServerContext } from '@/state/server';
import { NavLink } from 'react-router-dom';
import { cleanDirectoryPath } from '@/helpers';
import tw from 'twin.macro';
interface Props {
withinFileEditor?: boolean;
@ -32,11 +33,11 @@ export default ({ withinFileEditor, isNewFile }: Props) => {
});
return (
<div className={'flex items-center text-sm mb-4 text-neutral-500'}>
/<span className={'px-1 text-neutral-300'}>home</span>/
<div css={tw`flex items-center text-sm mb-4 text-neutral-500`}>
/<span css={tw`px-1 text-neutral-300`}>home</span>/
<NavLink
to={`/server/${id}/files`}
className={'px-1 text-neutral-200 no-underline hover:text-neutral-100'}
css={tw`px-1 text-neutral-200 no-underline hover:text-neutral-100`}
>
container
</NavLink>/
@ -46,18 +47,18 @@ export default ({ withinFileEditor, isNewFile }: Props) => {
<React.Fragment key={index}>
<NavLink
to={`/server/${id}/files#${crumb.path}`}
className={'px-1 text-neutral-200 no-underline hover:text-neutral-100'}
css={tw`px-1 text-neutral-200 no-underline hover:text-neutral-100`}
>
{crumb.name}
</NavLink>/
</React.Fragment>
:
<span key={index} className={'px-1 text-neutral-300'}>{crumb.name}</span>
<span key={index} css={tw`px-1 text-neutral-300`}>{crumb.name}</span>
))
}
{file &&
<React.Fragment>
<span className={'px-1 text-neutral-300'}>{decodeURIComponent(file)}</span>
<span css={tw`px-1 text-neutral-300`}>{decodeURIComponent(file)}</span>
</React.Fragment>
}
</div>

View file

@ -14,7 +14,8 @@ import { Link } from 'react-router-dom';
import Can from '@/components/elements/Can';
import PageContentBlock from '@/components/elements/PageContentBlock';
import ServerError from '@/components/screens/ServerError';
import useRouter from 'use-react-router';
import tw from 'twin.macro';
import Button from '@/components/elements/Button';
const sortFiles = (files: FileObject[]): FileObject[] => {
return files.sort((a, b) => a.name.localeCompare(b.name))
@ -24,7 +25,7 @@ const sortFiles = (files: FileObject[]): FileObject[] => {
export default () => {
const [ error, setError ] = useState('');
const [ loading, setLoading ] = useState(true);
const { addError, clearFlashes } = useStoreActions((actions: Actions<ApplicationStore>) => actions.flashes);
const { clearFlashes } = useStoreActions((actions: Actions<ApplicationStore>) => 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);
@ -56,16 +57,16 @@ export default () => {
return (
<PageContentBlock>
<FlashMessageRender byKey={'files'} className={'mb-4'}/>
<FlashMessageRender byKey={'files'} css={tw`mb-4`}/>
<React.Fragment>
<FileManagerBreadcrumbs/>
{
loading ?
<Spinner size={'large'} centered={true}/>
<Spinner size={'large'} centered/>
:
<React.Fragment>
{!files.length ?
<p className={'text-sm text-neutral-400 text-center'}>
<p css={tw`text-sm text-neutral-400 text-center`}>
This directory seems to be empty.
</p>
:
@ -74,8 +75,8 @@ export default () => {
<div>
{files.length > 250 ?
<React.Fragment>
<div className={'rounded bg-yellow-400 mb-px p-3'}>
<p className={'text-yellow-900 text-sm text-center'}>
<div css={tw`rounded bg-yellow-400 mb-px p-3`}>
<p css={tw`text-yellow-900 text-sm text-center`}>
This directory is too large to display in the browser,
limiting the output to the first 250 files.
</p>
@ -96,14 +97,15 @@ export default () => {
</CSSTransition>
}
<Can action={'file.create'}>
<div className={'flex justify-end mt-8'}>
<div css={tw`flex justify-end mt-8`}>
<NewDirectoryButton/>
<Link
<Button
// @ts-ignore
as={Link}
to={`/server/${id}/files/new${window.location.hash}`}
className={'btn btn-sm btn-primary'}
>
New File
</Link>
</Button>
</div>
</Can>
</React.Fragment>

View file

@ -5,6 +5,8 @@ import { object, string } from 'yup';
import Field from '@/components/elements/Field';
import { ServerContext } from '@/state/server';
import { join } from 'path';
import tw from 'twin.macro';
import Button from '@/components/elements/Button';
type Props = RequiredModalProps & {
onFileNamed: (name: string) => void;
@ -44,12 +46,10 @@ export default ({ onFileNamed, onDismissed, ...props }: Props) => {
name={'fileName'}
label={'File Name'}
description={'Enter the name that this file should be saved as.'}
autoFocus={true}
autoFocus
/>
<div className={'mt-6 text-right'}>
<button className={'btn btn-primary btn-sm'}>
Create File
</button>
<div css={tw`mt-6 text-right`}>
<Button>Create File</Button>
</div>
</Form>
</Modal>

View file

@ -10,6 +10,7 @@ import FileDropdownMenu from '@/components/server/files/FileDropdownMenu';
import { ServerContext } from '@/state/server';
import { NavLink } from 'react-router-dom';
import useRouter from 'use-react-router';
import tw from 'twin.macro';
export default ({ file }: { file: FileObject }) => {
const directory = ServerContext.useStoreState(state => state.files.directory);
@ -19,14 +20,11 @@ export default ({ file }: { file: FileObject }) => {
return (
<div
key={file.name}
className={`
flex bg-neutral-700 rounded-sm mb-px text-sm
hover:text-neutral-100 cursor-pointer items-center no-underline hover:bg-neutral-600
`}
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}`)}`}
className={'flex flex-1 text-neutral-300 no-underline p-3'}
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
// component re-renders you'll get redirected into a nested directory structure since
@ -41,27 +39,27 @@ export default ({ file }: { file: FileObject }) => {
}
}}
>
<div className={'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 ?
<FontAwesomeIcon icon={file.isSymlink ? faFileImport : faFileAlt}/>
:
<FontAwesomeIcon icon={faFolder}/>
}
</div>
<div className={'flex-1'}>
<div css={tw`flex-1`}>
{file.name}
</div>
{file.isFile &&
<div className={'w-1/6 text-right mr-4'}>
<div css={tw`w-1/6 text-right mr-4`}>
{bytesToHuman(file.size)}
</div>
}
<div
className={'w-1/5 text-right mr-4'}
css={tw`w-1/5 text-right mr-4`}
title={file.modifiedAt.toString()}
>
{Math.abs(differenceInHours(file.modifiedAt, new Date())) > 48 ?
format(file.modifiedAt, 'MMM Do, YYYY h:mma')
format(file.modifiedAt, 'MMM do, yyyy h:mma')
:
formatDistanceToNow(file.modifiedAt, { addSuffix: true })
}

View file

@ -7,6 +7,8 @@ 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';
interface Values {
directoryName: string;
@ -62,33 +64,33 @@ export default () => {
resetForm();
}}
>
<Form className={'m-0'}>
<Form css={tw`m-0`}>
<Field
id={'directoryName'}
name={'directoryName'}
label={'Directory Name'}
/>
<p className={'text-xs mt-2 text-neutral-400'}>
<span className={'text-neutral-200'}>This directory will be created as</span>
<p css={tw`text-xs mt-2 text-neutral-400`}>
<span css={tw`text-neutral-200`}>This directory will be created as</span>
&nbsp;/home/container/
<span className={'text-cyan-200'}>
<span css={tw`text-cyan-200`}>
{decodeURIComponent(
join(directory, values.directoryName).replace(/^(\.\.\/|\/)+/, ''),
)}
</span>
</p>
<div className={'flex justify-end'}>
<button className={'btn btn-sm btn-primary mt-8'}>
<div css={tw`flex justify-end`}>
<Button css={tw`mt-8`}>
Create Directory
</button>
</Button>
</div>
</Form>
</Modal>
)}
</Formik>
<button className={'btn btn-sm btn-secondary mr-2'} onClick={() => setVisible(true)}>
<Button isSecondary css={tw`mr-2`} onClick={() => setVisible(true)}>
Create Directory
</button>
</Button>
</React.Fragment>
);
};

View file

@ -6,7 +6,8 @@ import { join } from 'path';
import renameFile from '@/api/server/files/renameFile';
import { ServerContext } from '@/state/server';
import { FileObject } from '@/api/server/files/loadDirectory';
import classNames from 'classnames';
import tw from 'twin.macro';
import Button from '@/components/elements/Button';
interface FormikValues {
name: string;
@ -48,14 +49,14 @@ export default ({ file, useMoveTerminology, ...props }: Props) => {
>
{({ isSubmitting, values }) => (
<Modal {...props} dismissable={!isSubmitting} showSpinnerOverlay={isSubmitting}>
<Form className={'m-0'}>
<Form css={tw`m-0`}>
<div
className={classNames('flex', {
'items-center': useMoveTerminology,
'items-end': !useMoveTerminology,
})}
css={[
tw`flex`,
useMoveTerminology ? tw`items-center` : tw`items-end`,
]}
>
<div className={'flex-1 mr-6'}>
<div css={tw`flex-1 mr-6`}>
<Field
type={'string'}
id={'file_name'}
@ -65,18 +66,16 @@ export default ({ file, useMoveTerminology, ...props }: Props) => {
? 'Enter the new name and directory of this file or folder, relative to the current directory.'
: undefined
}
autoFocus={true}
autoFocus
/>
</div>
<div>
<button className={'btn btn-sm btn-primary'}>
{useMoveTerminology ? 'Move' : 'Rename'}
</button>
<Button>{useMoveTerminology ? 'Move' : 'Rename'}</Button>
</div>
</div>
{useMoveTerminology &&
<p className={'text-xs mt-2 text-neutral-400'}>
<strong className={'text-neutral-200'}>New location:</strong>
<p css={tw`text-xs mt-2 text-neutral-400`}>
<strong css={tw`text-neutral-200`}>New location:</strong>
&nbsp;/home/container/{join(directory, values.name).replace(/^(\.\.\/|\/)+/, '')}
</p>
}

View file

@ -119,6 +119,9 @@ module.exports = {
transitionDuration: {
250: '250ms',
},
minWidth: {
'48': '12rem',
},
borderColor: theme => ({
default: theme('colors.neutral.400', 'cuurrentColor'),
}),