misc_pterodactyl-panel/resources/scripts/components/server/files/FileEditContainer.tsx

187 lines
7.1 KiB
TypeScript
Raw Normal View History

2022-11-25 20:25:03 +00:00
import type { LanguageDescription } from '@codemirror/language';
import { languages } from '@codemirror/language-data';
2022-11-25 20:25:03 +00:00
import { dirname } from 'pathe';
import { useEffect, useState } from 'react';
import { useNavigate, useParams } from 'react-router-dom';
2022-11-25 20:25:03 +00:00
import tw from 'twin.macro';
2019-10-26 20:16:27 +00:00
import { httpErrorToHuman } from '@/api/http';
2022-11-25 20:25:03 +00:00
import getFileContents from '@/api/server/files/getFileContents';
2019-10-26 20:16:27 +00:00
import saveFileContents from '@/api/server/files/saveFileContents';
import FlashMessageRender from '@/components/FlashMessageRender';
import { Button } from '@/components/elements/button';
2022-11-25 20:25:03 +00:00
import Can from '@/components/elements/Can';
import Select from '@/components/elements/Select';
2022-11-25 20:25:03 +00:00
import PageContentBlock from '@/components/elements/PageContentBlock';
import { ServerError } from '@/components/elements/ScreenBlock';
import SpinnerOverlay from '@/components/elements/SpinnerOverlay';
import FileManagerBreadcrumbs from '@/components/server/files/FileManagerBreadcrumbs';
import FileNameModal from '@/components/server/files/FileNameModal';
import ErrorBoundary from '@/components/elements/ErrorBoundary';
import { Editor } from '@/components/elements/editor';
import useFlash from '@/plugins/useFlash';
2020-08-26 04:39:00 +00:00
import { ServerContext } from '@/state/server';
import { encodePathSegments } from '@/helpers';
2019-08-17 18:40:51 +00:00
export default () => {
const [error, setError] = useState('');
const { action, '*': rawFilename } = useParams<{ action: 'edit' | 'new'; '*': string }>();
const [loading, setLoading] = useState(action === 'edit');
const [content, setContent] = useState('');
const [modalVisible, setModalVisible] = useState(false);
2022-11-25 20:25:03 +00:00
const [language, setLanguage] = useState<LanguageDescription>();
2019-10-26 20:16:27 +00:00
const [filename, setFilename] = useState<string>('');
useEffect(() => {
setFilename(decodeURIComponent(rawFilename ?? ''));
}, [rawFilename]);
2022-11-25 20:25:03 +00:00
const navigate = useNavigate();
2022-11-25 20:25:03 +00:00
const id = ServerContext.useStoreState(state => state.server.data!.id);
const uuid = ServerContext.useStoreState(state => state.server.data!.uuid);
const setDirectory = ServerContext.useStoreActions(actions => actions.files.setDirectory);
const { addError, clearFlashes } = useFlash();
2019-10-26 20:16:27 +00:00
let fetchFileContent: null | (() => Promise<string>) = null;
2019-10-12 22:29:45 +00:00
useEffect(() => {
if (action === 'new') {
return;
}
if (filename === '') {
return;
}
setError('');
setLoading(true);
setDirectory(dirname(filename));
getFileContents(uuid, filename)
.then(setContent)
2022-11-25 20:25:03 +00:00
.catch(error => {
console.error(error);
setError(httpErrorToHuman(error));
})
.then(() => setLoading(false));
}, [action, uuid, filename]);
2019-08-17 18:40:51 +00:00
2019-12-22 00:38:40 +00:00
const save = (name?: string) => {
2019-10-26 20:16:27 +00:00
if (!fetchFileContent) {
return;
}
setLoading(true);
clearFlashes('files:view');
fetchFileContent()
.then(content => saveFileContents(uuid, name ?? filename, content))
2019-12-22 00:38:40 +00:00
.then(() => {
if (name) {
navigate(`/server/${id}/files/edit/${encodePathSegments(name)}`);
2019-12-22 00:38:40 +00:00
return;
}
return Promise.resolve();
2019-10-26 20:16:27 +00:00
})
2022-11-25 20:25:03 +00:00
.catch(error => {
2019-10-26 20:16:27 +00:00
console.error(error);
addError({ message: httpErrorToHuman(error), key: 'files:view' });
2019-10-26 20:16:27 +00:00
})
.then(() => setLoading(false));
};
if (error) {
2022-11-25 20:25:03 +00:00
// TODO: onBack
return <ServerError message={error} />;
}
2019-08-17 18:40:51 +00:00
return (
<PageContentBlock>
<FlashMessageRender byKey={'files:view'} css={tw`mb-4`} />
2022-11-25 20:25:03 +00:00
<ErrorBoundary>
<div css={tw`mb-4`}>
<FileManagerBreadcrumbs withinFileEditor isNewFile={action !== 'edit'} />
</div>
</ErrorBoundary>
2022-11-25 20:25:03 +00:00
{filename === '.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 css={tw`font-mono bg-black rounded py-px px-1`}>*</code>).
You can negate a prior rule by prepending an exclamation point (
<code css={tw`font-mono bg-black rounded py-px px-1`}>!</code>).
</p>
</div>
) : null}
2022-11-25 20:25:03 +00:00
2019-12-22 00:38:40 +00:00
<FileNameModal
visible={modalVisible}
onDismissed={() => setModalVisible(false)}
2022-11-25 20:25:03 +00:00
onFileNamed={name => {
2019-12-22 00:38:40 +00:00
setModalVisible(false);
save(name);
}}
/>
2022-11-25 20:25:03 +00:00
2020-07-05 00:57:24 +00:00
<div css={tw`relative`}>
<SpinnerOverlay visible={loading} />
2022-11-25 20:25:03 +00:00
<Editor
2023-01-12 19:08:11 +00:00
style={{ height: 'calc(100vh - 20rem)' }}
childClassName={tw`rounded-md h-full`}
filename={filename}
2019-10-26 20:16:27 +00:00
initialContent={content}
2022-11-25 20:25:03 +00:00
language={language}
onLanguageChanged={l => {
setLanguage(l);
}}
2022-11-25 20:25:03 +00:00
fetchContent={value => {
2019-10-26 20:16:27 +00:00
fetchFileContent = value;
}}
onContentSaved={() => {
if (action !== 'edit') {
setModalVisible(true);
} else {
save();
}
}}
2019-10-26 20:16:27 +00:00
/>
</div>
2022-11-25 20:25:03 +00:00
2020-07-05 00:57:24 +00:00
<div css={tw`flex justify-end mt-4`}>
<div css={tw`flex-1 sm:flex-none rounded bg-neutral-900 mr-4`}>
<Select
value={language?.name ?? ''}
onChange={e => {
setLanguage(languages.find(l => l.name === e.target.value));
}}
>
{languages.map(language => (
<option key={language.name} value={language.name}>
{language.name}
2020-09-16 03:53:23 +00:00
</option>
))}
</Select>
</div>
2022-11-25 20:25:03 +00:00
{action === 'edit' ? (
<Can action={'file.update'}>
<Button css={tw`flex-1 sm:flex-none`} onClick={() => save()}>
Save Content
2020-07-05 00:57:24 +00:00
</Button>
</Can>
) : (
<Can action={'file.create'}>
<Button css={tw`flex-1 sm:flex-none`} onClick={() => setModalVisible(true)}>
Create File
2020-07-05 00:57:24 +00:00
</Button>
</Can>
)}
2019-10-20 00:35:01 +00:00
</div>
</PageContentBlock>
2019-08-17 18:40:51 +00:00
);
};