Skip to content

Commit 1e735cf

Browse files
committed
Support failed backup display on the frontend; use SWR for backup pages
1 parent e3178ba commit 1e735cf

File tree

11 files changed

+140
-150
lines changed

11 files changed

+140
-150
lines changed

resources/scripts/api/server/backups/createServerBackup.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
import { rawDataToServerBackup, ServerBackup } from '@/api/server/backups/getServerBackups';
21
import http from '@/api/http';
2+
import { ServerBackup } from '@/api/server/types';
3+
import { rawDataToServerBackup } from '@/api/server/transformers';
34

45
export default (uuid: string, name?: string, ignored?: string): Promise<ServerBackup> => {
56
return new Promise((resolve, reject) => {

resources/scripts/api/server/backups/getServerBackups.ts

Lines changed: 0 additions & 32 deletions
This file was deleted.
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { FractalResponseData } from '@/api/http';
2+
import { ServerBackup } from '@/api/server/types';
3+
4+
export const rawDataToServerBackup = ({ attributes }: FractalResponseData): ServerBackup => ({
5+
uuid: attributes.uuid,
6+
isSuccessful: attributes.is_successful,
7+
name: attributes.name,
8+
ignoredFiles: attributes.ignored_files,
9+
sha256Hash: attributes.sha256_hash,
10+
bytes: attributes.bytes,
11+
createdAt: new Date(attributes.created_at),
12+
completedAt: attributes.completed_at ? new Date(attributes.completed_at) : null,
13+
});
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
export interface ServerBackup {
2+
uuid: string;
3+
isSuccessful: boolean;
4+
name: string;
5+
ignoredFiles: string;
6+
sha256Hash: string;
7+
bytes: number;
8+
createdAt: Date;
9+
completedAt: Date | null;
10+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import useSWR from 'swr';
2+
import http, { getPaginationSet, PaginatedResult } from '@/api/http';
3+
import { ServerBackup } from '@/api/server/types';
4+
import { rawDataToServerBackup } from '@/api/server/transformers';
5+
import useServer from '@/plugins/useServer';
6+
7+
export default (page?: number | string) => {
8+
const { uuid } = useServer();
9+
10+
return useSWR<PaginatedResult<ServerBackup>>([ 'server:backups', uuid, page ], async () => {
11+
const { data } = await http.get(`/api/client/servers/${uuid}/backups`, { params: { page } });
12+
13+
return ({
14+
items: (data.data || []).map(rawDataToServerBackup),
15+
pagination: getPaginationSet(data.meta.pagination),
16+
});
17+
});
18+
};

resources/scripts/components/server/backups/BackupContainer.tsx

Lines changed: 23 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,72 +1,67 @@
1-
import React, { useEffect, useState } from 'react';
1+
import React, { useEffect } from 'react';
22
import { Helmet } from 'react-helmet';
33
import Spinner from '@/components/elements/Spinner';
4-
import getServerBackups from '@/api/server/backups/getServerBackups';
54
import useServer from '@/plugins/useServer';
65
import useFlash from '@/plugins/useFlash';
7-
import { httpErrorToHuman } from '@/api/http';
86
import Can from '@/components/elements/Can';
97
import CreateBackupButton from '@/components/server/backups/CreateBackupButton';
108
import FlashMessageRender from '@/components/FlashMessageRender';
119
import BackupRow from '@/components/server/backups/BackupRow';
12-
import { ServerContext } from '@/state/server';
1310
import PageContentBlock from '@/components/elements/PageContentBlock';
1411
import tw from 'twin.macro';
12+
import getServerBackups from '@/api/swr/getServerBackups';
1513

1614
export default () => {
17-
const { uuid, featureLimits, name: serverName } = useServer();
18-
const { addError, clearFlashes } = useFlash();
19-
const [ loading, setLoading ] = useState(true);
15+
const { clearFlashes, clearAndAddHttpError } = useFlash();
16+
const { featureLimits, name: serverName } = useServer();
2017

21-
const backups = ServerContext.useStoreState(state => state.backups.data);
22-
const setBackups = ServerContext.useStoreActions(actions => actions.backups.setBackups);
18+
const { data: backups, error, isValidating } = getServerBackups();
2319

2420
useEffect(() => {
25-
clearFlashes('backups');
26-
getServerBackups(uuid)
27-
.then(data => setBackups(data.items))
28-
.catch(error => {
29-
console.error(error);
30-
addError({ key: 'backups', message: httpErrorToHuman(error) });
31-
})
32-
.then(() => setLoading(false));
33-
}, []);
21+
if (!error) {
22+
clearFlashes('backups');
3423

35-
if (backups.length === 0 && loading) {
24+
return;
25+
}
26+
27+
clearAndAddHttpError({ error, key: 'backups' });
28+
}, [ error ]);
29+
30+
if (!backups || (error && isValidating)) {
3631
return <Spinner size={'large'} centered/>;
3732
}
3833

3934
return (
4035
<PageContentBlock>
4136
<Helmet>
42-
<title> {serverName} | Backups</title>
37+
<title>{serverName} | Backups</title>
4338
</Helmet>
4439
<FlashMessageRender byKey={'backups'} css={tw`mb-4`}/>
45-
{!backups.length ?
40+
{!backups.items.length ?
4641
<p css={tw`text-center text-sm text-neutral-400`}>
4742
There are no backups stored for this server.
4843
</p>
4944
:
5045
<div>
51-
{backups.map((backup, index) => <BackupRow
46+
{backups.items.map((backup, index) => <BackupRow
5247
key={backup.uuid}
5348
backup={backup}
5449
css={index > 0 ? tw`mt-2` : undefined}
5550
/>)}
5651
</div>
5752
}
5853
{featureLimits.backups === 0 &&
59-
<p css={tw`text-center text-sm text-neutral-400`}>
60-
Backups cannot be created for this server.
61-
</p>
54+
<p css={tw`text-center text-sm text-neutral-400`}>
55+
Backups cannot be created for this server.
56+
</p>
6257
}
6358
<Can action={'backup.create'}>
64-
{(featureLimits.backups > 0 && backups.length > 0) &&
59+
{(featureLimits.backups > 0 && backups.items.length > 0) &&
6560
<p css={tw`text-center text-xs text-neutral-400 mt-2`}>
66-
{backups.length} of {featureLimits.backups} backups have been created for this server.
61+
{backups.items.length} of {featureLimits.backups} backups have been created for this server.
6762
</p>
6863
}
69-
{featureLimits.backups > 0 && featureLimits.backups !== backups.length &&
64+
{featureLimits.backups > 0 && featureLimits.backups !== backups.items.length &&
7065
<div css={tw`mt-6 flex justify-end`}>
7166
<CreateBackupButton/>
7267
</div>
Lines changed: 49 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,18 @@
11
import React, { useState } from 'react';
2-
import { ServerBackup } from '@/api/server/backups/getServerBackups';
32
import { faCloudDownloadAlt, faEllipsisH, faLock, faTrashAlt } from '@fortawesome/free-solid-svg-icons';
43
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
54
import DropdownMenu, { DropdownButtonRow } from '@/components/elements/DropdownMenu';
65
import getBackupDownloadUrl from '@/api/server/backups/getBackupDownloadUrl';
7-
import { httpErrorToHuman } from '@/api/http';
86
import useFlash from '@/plugins/useFlash';
97
import ChecksumModal from '@/components/server/backups/ChecksumModal';
108
import SpinnerOverlay from '@/components/elements/SpinnerOverlay';
119
import useServer from '@/plugins/useServer';
1210
import deleteBackup from '@/api/server/backups/deleteBackup';
13-
import { ServerContext } from '@/state/server';
1411
import ConfirmationModal from '@/components/elements/ConfirmationModal';
1512
import Can from '@/components/elements/Can';
1613
import tw from 'twin.macro';
14+
import getServerBackups from '@/api/swr/getServerBackups';
15+
import { ServerBackup } from '@/api/server/types';
1716

1817
interface Props {
1918
backup: ServerBackup;
@@ -24,8 +23,8 @@ export default ({ backup }: Props) => {
2423
const [ loading, setLoading ] = useState(false);
2524
const [ visible, setVisible ] = useState(false);
2625
const [ deleteVisible, setDeleteVisible ] = useState(false);
27-
const { addError, clearFlashes } = useFlash();
28-
const removeBackup = ServerContext.useStoreActions(actions => actions.backups.removeBackup);
26+
const { clearFlashes, clearAndAddHttpError } = useFlash();
27+
const { mutate } = getServerBackups();
2928

3029
const doDownload = () => {
3130
setLoading(true);
@@ -37,7 +36,7 @@ export default ({ backup }: Props) => {
3736
})
3837
.catch(error => {
3938
console.error(error);
40-
addError({ key: 'backups', message: httpErrorToHuman(error) });
39+
clearAndAddHttpError({ key: 'backups', error });
4140
})
4241
.then(() => setLoading(false));
4342
};
@@ -46,10 +45,15 @@ export default ({ backup }: Props) => {
4645
setLoading(true);
4746
clearFlashes('backups');
4847
deleteBackup(uuid, backup.uuid)
49-
.then(() => removeBackup(backup.uuid))
48+
.then(() => {
49+
mutate(data => ({
50+
...data,
51+
items: data.items.filter(b => b.uuid !== backup.uuid),
52+
}), false);
53+
})
5054
.catch(error => {
5155
console.error(error);
52-
addError({ key: 'backups', message: httpErrorToHuman(error) });
56+
clearAndAddHttpError({ key: 'backups', error });
5357
setLoading(false);
5458
setDeleteVisible(false);
5559
});
@@ -76,35 +80,44 @@ export default ({ backup }: Props) => {
7680
be recovered once deleted.
7781
</ConfirmationModal>
7882
<SpinnerOverlay visible={loading} fixed/>
79-
<DropdownMenu
80-
renderToggle={onClick => (
81-
<button
82-
onClick={onClick}
83-
css={tw`text-neutral-200 transition-colors duration-150 hover:text-neutral-100 p-2`}
84-
>
85-
<FontAwesomeIcon icon={faEllipsisH}/>
86-
</button>
87-
)}
88-
>
89-
<div css={tw`text-sm`}>
90-
<Can action={'backup.download'}>
91-
<DropdownButtonRow onClick={() => doDownload()}>
92-
<FontAwesomeIcon fixedWidth icon={faCloudDownloadAlt} css={tw`text-xs`}/>
93-
<span css={tw`ml-2`}>Download</span>
83+
{backup.isSuccessful ?
84+
<DropdownMenu
85+
renderToggle={onClick => (
86+
<button
87+
onClick={onClick}
88+
css={tw`text-neutral-200 transition-colors duration-150 hover:text-neutral-100 p-2`}
89+
>
90+
<FontAwesomeIcon icon={faEllipsisH}/>
91+
</button>
92+
)}
93+
>
94+
<div css={tw`text-sm`}>
95+
<Can action={'backup.download'}>
96+
<DropdownButtonRow onClick={() => doDownload()}>
97+
<FontAwesomeIcon fixedWidth icon={faCloudDownloadAlt} css={tw`text-xs`}/>
98+
<span css={tw`ml-2`}>Download</span>
99+
</DropdownButtonRow>
100+
</Can>
101+
<DropdownButtonRow onClick={() => setVisible(true)}>
102+
<FontAwesomeIcon fixedWidth icon={faLock} css={tw`text-xs`}/>
103+
<span css={tw`ml-2`}>Checksum</span>
94104
</DropdownButtonRow>
95-
</Can>
96-
<DropdownButtonRow onClick={() => setVisible(true)}>
97-
<FontAwesomeIcon fixedWidth icon={faLock} css={tw`text-xs`}/>
98-
<span css={tw`ml-2`}>Checksum</span>
99-
</DropdownButtonRow>
100-
<Can action={'backup.delete'}>
101-
<DropdownButtonRow danger onClick={() => setDeleteVisible(true)}>
102-
<FontAwesomeIcon fixedWidth icon={faTrashAlt} css={tw`text-xs`}/>
103-
<span css={tw`ml-2`}>Delete</span>
104-
</DropdownButtonRow>
105-
</Can>
106-
</div>
107-
</DropdownMenu>
105+
<Can action={'backup.delete'}>
106+
<DropdownButtonRow danger onClick={() => setDeleteVisible(true)}>
107+
<FontAwesomeIcon fixedWidth icon={faTrashAlt} css={tw`text-xs`}/>
108+
<span css={tw`ml-2`}>Delete</span>
109+
</DropdownButtonRow>
110+
</Can>
111+
</div>
112+
</DropdownMenu>
113+
:
114+
<button
115+
onClick={() => setDeleteVisible(true)}
116+
css={tw`text-neutral-200 transition-colors duration-150 hover:text-neutral-100 p-2`}
117+
>
118+
<FontAwesomeIcon icon={faTrashAlt}/>
119+
</button>
120+
}
108121
</>
109122
);
110123
};

resources/scripts/components/server/backups/BackupRow.tsx

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,39 @@
11
import React from 'react';
2-
import { ServerBackup } from '@/api/server/backups/getServerBackups';
32
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
43
import { faArchive, faEllipsisH } from '@fortawesome/free-solid-svg-icons';
54
import { format, formatDistanceToNow } from 'date-fns';
65
import Spinner from '@/components/elements/Spinner';
76
import { bytesToHuman } from '@/helpers';
87
import Can from '@/components/elements/Can';
98
import useWebsocketEvent from '@/plugins/useWebsocketEvent';
10-
import { ServerContext } from '@/state/server';
119
import BackupContextMenu from '@/components/server/backups/BackupContextMenu';
1210
import tw from 'twin.macro';
1311
import GreyRowBox from '@/components/elements/GreyRowBox';
12+
import getServerBackups from '@/api/swr/getServerBackups';
13+
import { ServerBackup } from '@/api/server/types';
1414

1515
interface Props {
1616
backup: ServerBackup;
1717
className?: string;
1818
}
1919

2020
export default ({ backup, className }: Props) => {
21-
const appendBackup = ServerContext.useStoreActions(actions => actions.backups.appendBackup);
21+
const { mutate } = getServerBackups();
2222

2323
useWebsocketEvent(`backup completed:${backup.uuid}`, data => {
2424
try {
2525
const parsed = JSON.parse(data);
26-
appendBackup({
27-
...backup,
28-
sha256Hash: parsed.sha256_hash || '',
29-
bytes: parsed.file_size || 0,
30-
completedAt: new Date(),
31-
});
26+
27+
mutate(data => ({
28+
...data,
29+
items: data.items.map(b => b.uuid !== backup.uuid ? b : ({
30+
...b,
31+
isSuccessful: parsed.is_successful || true,
32+
sha256Hash: parsed.sha256_hash || '',
33+
bytes: parsed.file_size || 0,
34+
completedAt: new Date(),
35+
})),
36+
}), false);
3237
} catch (e) {
3338
console.warn(e);
3439
}
@@ -45,8 +50,13 @@ export default ({ backup, className }: Props) => {
4550
</div>
4651
<div css={tw`flex-1`}>
4752
<p css={tw`text-sm mb-1`}>
53+
{!backup.isSuccessful &&
54+
<span css={tw`bg-red-500 py-px px-2 rounded-full text-white text-xs uppercase border border-red-600 mr-2`}>
55+
Failed
56+
</span>
57+
}
4858
{backup.name}
49-
{backup.completedAt &&
59+
{(backup.completedAt && backup.isSuccessful) &&
5060
<span css={tw`ml-3 text-neutral-300 text-xs font-thin`}>{bytesToHuman(backup.bytes)}</span>
5161
}
5262
</p>

0 commit comments

Comments
 (0)