Store backups in server state

This commit is contained in:
Dane Everitt 2020-04-06 22:25:54 -07:00
parent f9878d842c
commit 2eb6ab4d63
No known key found for this signature in database
GPG Key ID: EEA66103B3D71F53
6 changed files with 75 additions and 23 deletions

View File

@ -0,0 +1,19 @@
import React from 'react';
import Spinner from '@/components/elements/Spinner';
import { CSSTransition } from 'react-transition-group';
interface Props {
visible: boolean;
children?: React.ReactChild;
}
const ListRefreshIndicator = ({ visible, children }: Props) => (
<CSSTransition timeout={250} in={visible} appear={true} unmountOnExit={true} classNames={'fade'}>
<div className={'flex items-center mb-2'}>
<Spinner size={'tiny'}/>
<p className={'ml-2 text-sm text-neutral-400'}>{children || 'Refreshing listing...'}</p>
</div>
</CSSTransition>
);
export default ListRefreshIndicator;

View File

@ -8,19 +8,21 @@ import Can from '@/components/elements/Can';
import CreateBackupButton from '@/components/server/backups/CreateBackupButton'; import CreateBackupButton from '@/components/server/backups/CreateBackupButton';
import FlashMessageRender from '@/components/FlashMessageRender'; import FlashMessageRender from '@/components/FlashMessageRender';
import BackupRow from '@/components/server/backups/BackupRow'; import BackupRow from '@/components/server/backups/BackupRow';
import { ServerContext } from '@/state/server';
import ListRefreshIndicator from '@/components/elements/ListRefreshIndicator';
export default () => { export default () => {
const { uuid } = useServer(); const { uuid } = useServer();
const { addError, clearFlashes } = useFlash(); const { addError, clearFlashes } = useFlash();
const [ loading, setLoading ] = useState(true); const [ loading, setLoading ] = useState(true);
const [ backups, setBackups ] = useState<ServerBackup[]>([]);
const backups = ServerContext.useStoreState(state => state.backups.data);
const setBackups = ServerContext.useStoreActions(actions => actions.backups.setBackups);
useEffect(() => { useEffect(() => {
clearFlashes('backups'); clearFlashes('backups');
getServerBackups(uuid) getServerBackups(uuid)
.then(data => { .then(data => setBackups(data.items))
setBackups(data.items);
})
.catch(error => { .catch(error => {
console.error(error); console.error(error);
addError({ key: 'backups', message: httpErrorToHuman(error) }); addError({ key: 'backups', message: httpErrorToHuman(error) });
@ -28,12 +30,13 @@ export default () => {
.then(() => setLoading(false)); .then(() => setLoading(false));
}, []); }, []);
if (loading) { if (backups.length === 0 && loading) {
return <Spinner size={'large'} centered={true}/>; return <Spinner size={'large'} centered={true}/>;
} }
return ( return (
<div className={'mt-10 mb-6'}> <div className={'mt-10 mb-6'}>
<ListRefreshIndicator visible={loading}/>
<FlashMessageRender byKey={'backups'} className={'mb-4'}/> <FlashMessageRender byKey={'backups'} className={'mb-4'}/>
{!backups.length ? {!backups.length ?
<p className="text-center text-sm text-neutral-400"> <p className="text-center text-sm text-neutral-400">
@ -44,18 +47,13 @@ export default () => {
{backups.map((backup, index) => <BackupRow {backups.map((backup, index) => <BackupRow
key={backup.uuid} key={backup.uuid}
backup={backup} backup={backup}
onBackupUpdated={data => setBackups(
s => ([ ...s.map(b => b.uuid === data.uuid ? data : b) ]),
)}
className={index !== (backups.length - 1) ? 'mb-2' : undefined} className={index !== (backups.length - 1) ? 'mb-2' : undefined}
/>)} />)}
</div> </div>
} }
<Can action={'backup.create'}> <Can action={'backup.create'}>
<div className={'mt-6 flex justify-end'}> <div className={'mt-6 flex justify-end'}>
<CreateBackupButton <CreateBackupButton/>
onBackupGenerated={backup => setBackups(s => [ ...s, backup ])}
/>
</div> </div>
</Can> </Can>
</div> </div>

View File

@ -15,10 +15,10 @@ import SpinnerOverlay from '@/components/elements/SpinnerOverlay';
import useFlash from '@/plugins/useFlash'; import useFlash from '@/plugins/useFlash';
import { httpErrorToHuman } from '@/api/http'; import { httpErrorToHuman } from '@/api/http';
import useWebsocketEvent from '@/plugins/useWebsocketEvent'; import useWebsocketEvent from '@/plugins/useWebsocketEvent';
import { ServerContext } from '@/state/server';
interface Props { interface Props {
backup: ServerBackup; backup: ServerBackup;
onBackupUpdated: (backup: ServerBackup) => void;
className?: string; className?: string;
} }
@ -34,16 +34,18 @@ const DownloadModal = ({ checksum, ...props }: RequiredModalProps & { checksum:
</Modal> </Modal>
); );
export default ({ backup, onBackupUpdated, className }: Props) => { export default ({ backup, className }: Props) => {
const { uuid } = useServer(); const { uuid } = useServer();
const { addError, clearFlashes } = useFlash(); const { addError, clearFlashes } = useFlash();
const [ loading, setLoading ] = useState(false); const [ loading, setLoading ] = useState(false);
const [ visible, setVisible ] = useState(false); const [ visible, setVisible ] = useState(false);
const appendBackup = ServerContext.useStoreActions(actions => actions.backups.appendBackup);
useWebsocketEvent(`backup completed:${backup.uuid}`, data => { useWebsocketEvent(`backup completed:${backup.uuid}`, data => {
try { try {
const parsed = JSON.parse(data); const parsed = JSON.parse(data);
onBackupUpdated({ appendBackup({
...backup, ...backup,
sha256Hash: parsed.sha256_hash || '', sha256Hash: parsed.sha256_hash || '',
bytes: parsed.file_size || 0, bytes: parsed.file_size || 0,

View File

@ -9,17 +9,13 @@ import useServer from '@/plugins/useServer';
import createServerBackup from '@/api/server/backups/createServerBackup'; import createServerBackup from '@/api/server/backups/createServerBackup';
import { httpErrorToHuman } from '@/api/http'; import { httpErrorToHuman } from '@/api/http';
import FlashMessageRender from '@/components/FlashMessageRender'; import FlashMessageRender from '@/components/FlashMessageRender';
import { ServerBackup } from '@/api/server/backups/getServerBackups'; import { ServerContext } from '@/state/server';
interface Values { interface Values {
name: string; name: string;
ignored: string; ignored: string;
} }
interface Props {
onBackupGenerated: (backup: ServerBackup) => void;
}
const ModalContent = ({ ...props }: RequiredModalProps) => { const ModalContent = ({ ...props }: RequiredModalProps) => {
const { isSubmitting } = useFormikContext<Values>(); const { isSubmitting } = useFormikContext<Values>();
@ -66,20 +62,22 @@ const ModalContent = ({ ...props }: RequiredModalProps) => {
); );
}; };
export default ({ onBackupGenerated }: Props) => { export default () => {
const { uuid } = useServer(); const { uuid } = useServer();
const { addError, clearFlashes } = useFlash(); const { addError, clearFlashes } = useFlash();
const [ visible, setVisible ] = useState(false); const [ visible, setVisible ] = useState(false);
const appendBackup = ServerContext.useStoreActions(actions => actions.backups.appendBackup);
useEffect(() => { useEffect(() => {
clearFlashes('backups:create'); clearFlashes('backups:create');
}, [visible]); }, [ visible ]);
const submit = ({ name, ignored }: Values, { setSubmitting }: FormikHelpers<Values>) => { const submit = ({ name, ignored }: Values, { setSubmitting }: FormikHelpers<Values>) => {
clearFlashes('backups:create') clearFlashes('backups:create');
createServerBackup(uuid, name, ignored) createServerBackup(uuid, name, ignored)
.then(backup => { .then(backup => {
onBackupGenerated(backup); appendBackup(backup);
setVisible(false); setVisible(false);
}) })
.catch(error => { .catch(error => {

View File

@ -0,0 +1,31 @@
import { ServerBackup } from '@/api/server/backups/getServerBackups';
import { action, Action } from 'easy-peasy';
export interface ServerBackupStore {
data: ServerBackup[];
setBackups: Action<ServerBackupStore, ServerBackup[]>;
appendBackup: Action<ServerBackupStore, ServerBackup>;
removeBackup: Action<ServerBackupStore, string>;
}
const backups: ServerBackupStore = {
data: [],
setBackups: action((state, payload) => {
state.data = payload;
}),
appendBackup: action((state, payload) => {
if (state.data.find(backup => backup.uuid === payload.uuid)) {
state.data = state.data.map(backup => backup.uuid === payload.uuid ? payload : backup);
} else {
state.data = [ ...state.data, payload ];
}
}),
removeBackup: action((state, payload) => {
state.data = [ ...state.data.filter(backup => backup.uuid !== payload) ];
}),
};
export default backups;

View File

@ -5,6 +5,7 @@ import { ServerDatabase } from '@/api/server/getServerDatabases';
import files, { ServerFileStore } from '@/state/server/files'; import files, { ServerFileStore } from '@/state/server/files';
import subusers, { ServerSubuserStore } from '@/state/server/subusers'; import subusers, { ServerSubuserStore } from '@/state/server/subusers';
import { composeWithDevTools } from 'redux-devtools-extension'; import { composeWithDevTools } from 'redux-devtools-extension';
import backups, { ServerBackupStore } from '@/state/server/backups';
export type ServerStatus = 'offline' | 'starting' | 'stopping' | 'running'; export type ServerStatus = 'offline' | 'starting' | 'stopping' | 'running';
@ -73,6 +74,7 @@ export interface ServerStore {
subusers: ServerSubuserStore; subusers: ServerSubuserStore;
databases: ServerDatabaseStore; databases: ServerDatabaseStore;
files: ServerFileStore; files: ServerFileStore;
backups: ServerBackupStore;
socket: SocketStore; socket: SocketStore;
status: ServerStatusStore; status: ServerStatusStore;
clearServerState: Action<ServerStore>; clearServerState: Action<ServerStore>;
@ -85,6 +87,7 @@ export const ServerContext = createContextStore<ServerStore>({
databases, databases,
files, files,
subusers, subusers,
backups,
clearServerState: action(state => { clearServerState: action(state => {
state.server.data = undefined; state.server.data = undefined;
state.server.permissions = []; state.server.permissions = [];
@ -92,6 +95,7 @@ export const ServerContext = createContextStore<ServerStore>({
state.subusers.data = []; state.subusers.data = [];
state.files.directory = '/'; state.files.directory = '/';
state.files.contents = []; state.files.contents = [];
state.backups.backups = [];
if (state.socket.instance) { if (state.socket.instance) {
state.socket.instance.removeAllListeners(); state.socket.instance.removeAllListeners();