Store backups in server state
This commit is contained in:
parent
f9878d842c
commit
2eb6ab4d63
6 changed files with 75 additions and 23 deletions
|
@ -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 FlashMessageRender from '@/components/FlashMessageRender';
|
||||
import BackupRow from '@/components/server/backups/BackupRow';
|
||||
import { ServerContext } from '@/state/server';
|
||||
import ListRefreshIndicator from '@/components/elements/ListRefreshIndicator';
|
||||
|
||||
export default () => {
|
||||
const { uuid } = useServer();
|
||||
const { addError, clearFlashes } = useFlash();
|
||||
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(() => {
|
||||
clearFlashes('backups');
|
||||
getServerBackups(uuid)
|
||||
.then(data => {
|
||||
setBackups(data.items);
|
||||
})
|
||||
.then(data => setBackups(data.items))
|
||||
.catch(error => {
|
||||
console.error(error);
|
||||
addError({ key: 'backups', message: httpErrorToHuman(error) });
|
||||
|
@ -28,12 +30,13 @@ export default () => {
|
|||
.then(() => setLoading(false));
|
||||
}, []);
|
||||
|
||||
if (loading) {
|
||||
if (backups.length === 0 && loading) {
|
||||
return <Spinner size={'large'} centered={true}/>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={'mt-10 mb-6'}>
|
||||
<ListRefreshIndicator visible={loading}/>
|
||||
<FlashMessageRender byKey={'backups'} className={'mb-4'}/>
|
||||
{!backups.length ?
|
||||
<p className="text-center text-sm text-neutral-400">
|
||||
|
@ -44,18 +47,13 @@ export default () => {
|
|||
{backups.map((backup, index) => <BackupRow
|
||||
key={backup.uuid}
|
||||
backup={backup}
|
||||
onBackupUpdated={data => setBackups(
|
||||
s => ([ ...s.map(b => b.uuid === data.uuid ? data : b) ]),
|
||||
)}
|
||||
className={index !== (backups.length - 1) ? 'mb-2' : undefined}
|
||||
/>)}
|
||||
</div>
|
||||
}
|
||||
<Can action={'backup.create'}>
|
||||
<div className={'mt-6 flex justify-end'}>
|
||||
<CreateBackupButton
|
||||
onBackupGenerated={backup => setBackups(s => [ ...s, backup ])}
|
||||
/>
|
||||
<CreateBackupButton/>
|
||||
</div>
|
||||
</Can>
|
||||
</div>
|
||||
|
|
|
@ -15,10 +15,10 @@ import SpinnerOverlay from '@/components/elements/SpinnerOverlay';
|
|||
import useFlash from '@/plugins/useFlash';
|
||||
import { httpErrorToHuman } from '@/api/http';
|
||||
import useWebsocketEvent from '@/plugins/useWebsocketEvent';
|
||||
import { ServerContext } from '@/state/server';
|
||||
|
||||
interface Props {
|
||||
backup: ServerBackup;
|
||||
onBackupUpdated: (backup: ServerBackup) => void;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
|
@ -34,16 +34,18 @@ const DownloadModal = ({ checksum, ...props }: RequiredModalProps & { checksum:
|
|||
</Modal>
|
||||
);
|
||||
|
||||
export default ({ backup, onBackupUpdated, className }: Props) => {
|
||||
export default ({ backup, className }: Props) => {
|
||||
const { uuid } = useServer();
|
||||
const { addError, clearFlashes } = useFlash();
|
||||
const [ loading, setLoading ] = useState(false);
|
||||
const [ visible, setVisible ] = useState(false);
|
||||
|
||||
const appendBackup = ServerContext.useStoreActions(actions => actions.backups.appendBackup);
|
||||
|
||||
useWebsocketEvent(`backup completed:${backup.uuid}`, data => {
|
||||
try {
|
||||
const parsed = JSON.parse(data);
|
||||
onBackupUpdated({
|
||||
appendBackup({
|
||||
...backup,
|
||||
sha256Hash: parsed.sha256_hash || '',
|
||||
bytes: parsed.file_size || 0,
|
||||
|
|
|
@ -9,17 +9,13 @@ import useServer from '@/plugins/useServer';
|
|||
import createServerBackup from '@/api/server/backups/createServerBackup';
|
||||
import { httpErrorToHuman } from '@/api/http';
|
||||
import FlashMessageRender from '@/components/FlashMessageRender';
|
||||
import { ServerBackup } from '@/api/server/backups/getServerBackups';
|
||||
import { ServerContext } from '@/state/server';
|
||||
|
||||
interface Values {
|
||||
name: string;
|
||||
ignored: string;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
onBackupGenerated: (backup: ServerBackup) => void;
|
||||
}
|
||||
|
||||
const ModalContent = ({ ...props }: RequiredModalProps) => {
|
||||
const { isSubmitting } = useFormikContext<Values>();
|
||||
|
||||
|
@ -66,20 +62,22 @@ const ModalContent = ({ ...props }: RequiredModalProps) => {
|
|||
);
|
||||
};
|
||||
|
||||
export default ({ onBackupGenerated }: Props) => {
|
||||
export default () => {
|
||||
const { uuid } = useServer();
|
||||
const { addError, clearFlashes } = useFlash();
|
||||
const [ visible, setVisible ] = useState(false);
|
||||
|
||||
const appendBackup = ServerContext.useStoreActions(actions => actions.backups.appendBackup);
|
||||
|
||||
useEffect(() => {
|
||||
clearFlashes('backups:create');
|
||||
}, [visible]);
|
||||
}, [ visible ]);
|
||||
|
||||
const submit = ({ name, ignored }: Values, { setSubmitting }: FormikHelpers<Values>) => {
|
||||
clearFlashes('backups:create')
|
||||
clearFlashes('backups:create');
|
||||
createServerBackup(uuid, name, ignored)
|
||||
.then(backup => {
|
||||
onBackupGenerated(backup);
|
||||
appendBackup(backup);
|
||||
setVisible(false);
|
||||
})
|
||||
.catch(error => {
|
||||
|
|
31
resources/scripts/state/server/backups.ts
Normal file
31
resources/scripts/state/server/backups.ts
Normal 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;
|
|
@ -5,6 +5,7 @@ import { ServerDatabase } from '@/api/server/getServerDatabases';
|
|||
import files, { ServerFileStore } from '@/state/server/files';
|
||||
import subusers, { ServerSubuserStore } from '@/state/server/subusers';
|
||||
import { composeWithDevTools } from 'redux-devtools-extension';
|
||||
import backups, { ServerBackupStore } from '@/state/server/backups';
|
||||
|
||||
export type ServerStatus = 'offline' | 'starting' | 'stopping' | 'running';
|
||||
|
||||
|
@ -73,6 +74,7 @@ export interface ServerStore {
|
|||
subusers: ServerSubuserStore;
|
||||
databases: ServerDatabaseStore;
|
||||
files: ServerFileStore;
|
||||
backups: ServerBackupStore;
|
||||
socket: SocketStore;
|
||||
status: ServerStatusStore;
|
||||
clearServerState: Action<ServerStore>;
|
||||
|
@ -85,6 +87,7 @@ export const ServerContext = createContextStore<ServerStore>({
|
|||
databases,
|
||||
files,
|
||||
subusers,
|
||||
backups,
|
||||
clearServerState: action(state => {
|
||||
state.server.data = undefined;
|
||||
state.server.permissions = [];
|
||||
|
@ -92,6 +95,7 @@ export const ServerContext = createContextStore<ServerStore>({
|
|||
state.subusers.data = [];
|
||||
state.files.directory = '/';
|
||||
state.files.contents = [];
|
||||
state.backups.backups = [];
|
||||
|
||||
if (state.socket.instance) {
|
||||
state.socket.instance.removeAllListeners();
|
||||
|
|
Loading…
Reference in a new issue