Store backups in server state
This commit is contained in:
parent
f9878d842c
commit
2eb6ab4d63
|
@ -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;
|
|
@ -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>
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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 => {
|
||||||
|
|
|
@ -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;
|
|
@ -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();
|
||||||
|
|
Loading…
Reference in New Issue