diff --git a/resources/scripts/api/server/backups/createServerBackup.ts b/resources/scripts/api/server/backups/createServerBackup.ts index ade809bb1..f86088994 100644 --- a/resources/scripts/api/server/backups/createServerBackup.ts +++ b/resources/scripts/api/server/backups/createServerBackup.ts @@ -1,5 +1,6 @@ -import { rawDataToServerBackup, ServerBackup } from '@/api/server/backups/getServerBackups'; import http from '@/api/http'; +import { ServerBackup } from '@/api/server/types'; +import { rawDataToServerBackup } from '@/api/server/transformers'; export default (uuid: string, name?: string, ignored?: string): Promise => { return new Promise((resolve, reject) => { diff --git a/resources/scripts/api/server/backups/getServerBackups.ts b/resources/scripts/api/server/backups/getServerBackups.ts deleted file mode 100644 index 49f3aa24c..000000000 --- a/resources/scripts/api/server/backups/getServerBackups.ts +++ /dev/null @@ -1,32 +0,0 @@ -import http, { FractalResponseData, getPaginationSet, PaginatedResult } from '@/api/http'; - -export interface ServerBackup { - uuid: string; - name: string; - ignoredFiles: string; - sha256Hash: string; - bytes: number; - createdAt: Date; - completedAt: Date | null; -} - -export const rawDataToServerBackup = ({ attributes }: FractalResponseData): ServerBackup => ({ - uuid: attributes.uuid, - name: attributes.name, - ignoredFiles: attributes.ignored_files, - sha256Hash: attributes.sha256_hash, - bytes: attributes.bytes, - createdAt: new Date(attributes.created_at), - completedAt: attributes.completed_at ? new Date(attributes.completed_at) : null, -}); - -export default (uuid: string, page?: number | string): Promise> => { - return new Promise((resolve, reject) => { - http.get(`/api/client/servers/${uuid}/backups`, { params: { page } }) - .then(({ data }) => resolve({ - items: (data.data || []).map(rawDataToServerBackup), - pagination: getPaginationSet(data.meta.pagination), - })) - .catch(reject); - }); -}; diff --git a/resources/scripts/api/server/transformers.ts b/resources/scripts/api/server/transformers.ts new file mode 100644 index 000000000..f6f98e054 --- /dev/null +++ b/resources/scripts/api/server/transformers.ts @@ -0,0 +1,13 @@ +import { FractalResponseData } from '@/api/http'; +import { ServerBackup } from '@/api/server/types'; + +export const rawDataToServerBackup = ({ attributes }: FractalResponseData): ServerBackup => ({ + uuid: attributes.uuid, + isSuccessful: attributes.is_successful, + name: attributes.name, + ignoredFiles: attributes.ignored_files, + sha256Hash: attributes.sha256_hash, + bytes: attributes.bytes, + createdAt: new Date(attributes.created_at), + completedAt: attributes.completed_at ? new Date(attributes.completed_at) : null, +}); diff --git a/resources/scripts/api/server/types.d.ts b/resources/scripts/api/server/types.d.ts new file mode 100644 index 000000000..bcdd7416d --- /dev/null +++ b/resources/scripts/api/server/types.d.ts @@ -0,0 +1,10 @@ +export interface ServerBackup { + uuid: string; + isSuccessful: boolean; + name: string; + ignoredFiles: string; + sha256Hash: string; + bytes: number; + createdAt: Date; + completedAt: Date | null; +} diff --git a/resources/scripts/api/swr/getServerBackups.ts b/resources/scripts/api/swr/getServerBackups.ts new file mode 100644 index 000000000..b07a5bea3 --- /dev/null +++ b/resources/scripts/api/swr/getServerBackups.ts @@ -0,0 +1,18 @@ +import useSWR from 'swr'; +import http, { getPaginationSet, PaginatedResult } from '@/api/http'; +import { ServerBackup } from '@/api/server/types'; +import { rawDataToServerBackup } from '@/api/server/transformers'; +import useServer from '@/plugins/useServer'; + +export default (page?: number | string) => { + const { uuid } = useServer(); + + return useSWR>([ 'server:backups', uuid, page ], async () => { + const { data } = await http.get(`/api/client/servers/${uuid}/backups`, { params: { page } }); + + return ({ + items: (data.data || []).map(rawDataToServerBackup), + pagination: getPaginationSet(data.meta.pagination), + }); + }); +}; diff --git a/resources/scripts/components/server/backups/BackupContainer.tsx b/resources/scripts/components/server/backups/BackupContainer.tsx index bcead7abb..c7504b164 100644 --- a/resources/scripts/components/server/backups/BackupContainer.tsx +++ b/resources/scripts/components/server/backups/BackupContainer.tsx @@ -1,54 +1,49 @@ -import React, { useEffect, useState } from 'react'; +import React, { useEffect } from 'react'; import { Helmet } from 'react-helmet'; import Spinner from '@/components/elements/Spinner'; -import getServerBackups from '@/api/server/backups/getServerBackups'; import useServer from '@/plugins/useServer'; import useFlash from '@/plugins/useFlash'; -import { httpErrorToHuman } from '@/api/http'; 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 PageContentBlock from '@/components/elements/PageContentBlock'; import tw from 'twin.macro'; +import getServerBackups from '@/api/swr/getServerBackups'; export default () => { - const { uuid, featureLimits, name: serverName } = useServer(); - const { addError, clearFlashes } = useFlash(); - const [ loading, setLoading ] = useState(true); + const { clearFlashes, clearAndAddHttpError } = useFlash(); + const { featureLimits, name: serverName } = useServer(); - const backups = ServerContext.useStoreState(state => state.backups.data); - const setBackups = ServerContext.useStoreActions(actions => actions.backups.setBackups); + const { data: backups, error, isValidating } = getServerBackups(); useEffect(() => { - clearFlashes('backups'); - getServerBackups(uuid) - .then(data => setBackups(data.items)) - .catch(error => { - console.error(error); - addError({ key: 'backups', message: httpErrorToHuman(error) }); - }) - .then(() => setLoading(false)); - }, []); + if (!error) { + clearFlashes('backups'); - if (backups.length === 0 && loading) { + return; + } + + clearAndAddHttpError({ error, key: 'backups' }); + }, [ error ]); + + if (!backups || (error && isValidating)) { return ; } return ( - {serverName} | Backups + {serverName} | Backups - {!backups.length ? + {!backups.items.length ?

There are no backups stored for this server.

:
- {backups.map((backup, index) => 0 ? tw`mt-2` : undefined} @@ -56,17 +51,17 @@ export default () => {
} {featureLimits.backups === 0 && -

- Backups cannot be created for this server. -

+

+ Backups cannot be created for this server. +

} - {(featureLimits.backups > 0 && backups.length > 0) && + {(featureLimits.backups > 0 && backups.items.length > 0) &&

- {backups.length} of {featureLimits.backups} backups have been created for this server. + {backups.items.length} of {featureLimits.backups} backups have been created for this server.

} - {featureLimits.backups > 0 && featureLimits.backups !== backups.length && + {featureLimits.backups > 0 && featureLimits.backups !== backups.items.length &&
diff --git a/resources/scripts/components/server/backups/BackupContextMenu.tsx b/resources/scripts/components/server/backups/BackupContextMenu.tsx index d9c74150d..9542389cc 100644 --- a/resources/scripts/components/server/backups/BackupContextMenu.tsx +++ b/resources/scripts/components/server/backups/BackupContextMenu.tsx @@ -1,19 +1,18 @@ import React, { useState } from 'react'; -import { ServerBackup } from '@/api/server/backups/getServerBackups'; import { faCloudDownloadAlt, faEllipsisH, faLock, faTrashAlt } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import DropdownMenu, { DropdownButtonRow } from '@/components/elements/DropdownMenu'; import getBackupDownloadUrl from '@/api/server/backups/getBackupDownloadUrl'; -import { httpErrorToHuman } from '@/api/http'; import useFlash from '@/plugins/useFlash'; import ChecksumModal from '@/components/server/backups/ChecksumModal'; import SpinnerOverlay from '@/components/elements/SpinnerOverlay'; import useServer from '@/plugins/useServer'; import deleteBackup from '@/api/server/backups/deleteBackup'; -import { ServerContext } from '@/state/server'; import ConfirmationModal from '@/components/elements/ConfirmationModal'; import Can from '@/components/elements/Can'; import tw from 'twin.macro'; +import getServerBackups from '@/api/swr/getServerBackups'; +import { ServerBackup } from '@/api/server/types'; interface Props { backup: ServerBackup; @@ -24,8 +23,8 @@ export default ({ backup }: Props) => { const [ loading, setLoading ] = useState(false); const [ visible, setVisible ] = useState(false); const [ deleteVisible, setDeleteVisible ] = useState(false); - const { addError, clearFlashes } = useFlash(); - const removeBackup = ServerContext.useStoreActions(actions => actions.backups.removeBackup); + const { clearFlashes, clearAndAddHttpError } = useFlash(); + const { mutate } = getServerBackups(); const doDownload = () => { setLoading(true); @@ -37,7 +36,7 @@ export default ({ backup }: Props) => { }) .catch(error => { console.error(error); - addError({ key: 'backups', message: httpErrorToHuman(error) }); + clearAndAddHttpError({ key: 'backups', error }); }) .then(() => setLoading(false)); }; @@ -46,10 +45,15 @@ export default ({ backup }: Props) => { setLoading(true); clearFlashes('backups'); deleteBackup(uuid, backup.uuid) - .then(() => removeBackup(backup.uuid)) + .then(() => { + mutate(data => ({ + ...data, + items: data.items.filter(b => b.uuid !== backup.uuid), + }), false); + }) .catch(error => { console.error(error); - addError({ key: 'backups', message: httpErrorToHuman(error) }); + clearAndAddHttpError({ key: 'backups', error }); setLoading(false); setDeleteVisible(false); }); @@ -76,35 +80,44 @@ export default ({ backup }: Props) => { be recovered once deleted. - ( - - )} - > -
- - doDownload()}> - - Download + {backup.isSuccessful ? + ( + + )} + > +
+ + doDownload()}> + + Download + + + setVisible(true)}> + + Checksum - - setVisible(true)}> - - Checksum - - - setDeleteVisible(true)}> - - Delete - - -
-
+ + setDeleteVisible(true)}> + + Delete + + +
+
+ : + + } ); }; diff --git a/resources/scripts/components/server/backups/BackupRow.tsx b/resources/scripts/components/server/backups/BackupRow.tsx index 2a7b625bc..e6a16a2f1 100644 --- a/resources/scripts/components/server/backups/BackupRow.tsx +++ b/resources/scripts/components/server/backups/BackupRow.tsx @@ -1,5 +1,4 @@ import React from 'react'; -import { ServerBackup } from '@/api/server/backups/getServerBackups'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { faArchive, faEllipsisH } from '@fortawesome/free-solid-svg-icons'; import { format, formatDistanceToNow } from 'date-fns'; @@ -7,10 +6,11 @@ import Spinner from '@/components/elements/Spinner'; import { bytesToHuman } from '@/helpers'; import Can from '@/components/elements/Can'; import useWebsocketEvent from '@/plugins/useWebsocketEvent'; -import { ServerContext } from '@/state/server'; import BackupContextMenu from '@/components/server/backups/BackupContextMenu'; import tw from 'twin.macro'; import GreyRowBox from '@/components/elements/GreyRowBox'; +import getServerBackups from '@/api/swr/getServerBackups'; +import { ServerBackup } from '@/api/server/types'; interface Props { backup: ServerBackup; @@ -18,17 +18,22 @@ interface Props { } export default ({ backup, className }: Props) => { - const appendBackup = ServerContext.useStoreActions(actions => actions.backups.appendBackup); + const { mutate } = getServerBackups(); useWebsocketEvent(`backup completed:${backup.uuid}`, data => { try { const parsed = JSON.parse(data); - appendBackup({ - ...backup, - sha256Hash: parsed.sha256_hash || '', - bytes: parsed.file_size || 0, - completedAt: new Date(), - }); + + mutate(data => ({ + ...data, + items: data.items.map(b => b.uuid !== backup.uuid ? b : ({ + ...b, + isSuccessful: parsed.is_successful || true, + sha256Hash: parsed.sha256_hash || '', + bytes: parsed.file_size || 0, + completedAt: new Date(), + })), + }), false); } catch (e) { console.warn(e); } @@ -45,8 +50,13 @@ export default ({ backup, className }: Props) => {

+ {!backup.isSuccessful && + + Failed + + } {backup.name} - {backup.completedAt && + {(backup.completedAt && backup.isSuccessful) && {bytesToHuman(backup.bytes)} }

diff --git a/resources/scripts/components/server/backups/CreateBackupButton.tsx b/resources/scripts/components/server/backups/CreateBackupButton.tsx index 3d7834fa9..3fd53403a 100644 --- a/resources/scripts/components/server/backups/CreateBackupButton.tsx +++ b/resources/scripts/components/server/backups/CreateBackupButton.tsx @@ -7,12 +7,11 @@ import FormikFieldWrapper from '@/components/elements/FormikFieldWrapper'; import useFlash from '@/plugins/useFlash'; import useServer from '@/plugins/useServer'; import createServerBackup from '@/api/server/backups/createServerBackup'; -import { httpErrorToHuman } from '@/api/http'; import FlashMessageRender from '@/components/FlashMessageRender'; -import { ServerContext } from '@/state/server'; import Button from '@/components/elements/Button'; import tw from 'twin.macro'; import { Textarea } from '@/components/elements/Input'; +import getServerBackups from '@/api/swr/getServerBackups'; interface Values { name: string; @@ -60,10 +59,9 @@ const ModalContent = ({ ...props }: RequiredModalProps) => { export default () => { const { uuid } = useServer(); - const { addError, clearFlashes } = useFlash(); + const { clearFlashes, clearAndAddHttpError } = useFlash(); const [ visible, setVisible ] = useState(false); - - const appendBackup = ServerContext.useStoreActions(actions => actions.backups.appendBackup); + const { mutate } = getServerBackups(); useEffect(() => { clearFlashes('backups:create'); @@ -73,12 +71,11 @@ export default () => { clearFlashes('backups:create'); createServerBackup(uuid, name, ignored) .then(backup => { - appendBackup(backup); + mutate(data => ({ ...data, items: data.items.concat(backup) }), false); setVisible(false); }) .catch(error => { - console.error(error); - addError({ key: 'backups:create', message: httpErrorToHuman(error) }); + clearAndAddHttpError({ key: 'backups:create', error }); setSubmitting(false); }); }; diff --git a/resources/scripts/state/server/backups.ts b/resources/scripts/state/server/backups.ts deleted file mode 100644 index aa24bdf7f..000000000 --- a/resources/scripts/state/server/backups.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { ServerBackup } from '@/api/server/backups/getServerBackups'; -import { action, Action } from 'easy-peasy'; - -export interface ServerBackupStore { - data: ServerBackup[]; - setBackups: Action; - appendBackup: Action; - removeBackup: Action; -} - -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; diff --git a/resources/scripts/state/server/index.ts b/resources/scripts/state/server/index.ts index febf0951a..87023a020 100644 --- a/resources/scripts/state/server/index.ts +++ b/resources/scripts/state/server/index.ts @@ -4,7 +4,6 @@ import socket, { SocketStore } from './socket'; 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'; import schedules, { ServerScheduleStore } from '@/state/server/schedules'; import databases, { ServerDatabaseStore } from '@/state/server/databases'; @@ -56,7 +55,6 @@ export interface ServerStore { databases: ServerDatabaseStore; files: ServerFileStore; schedules: ServerScheduleStore; - backups: ServerBackupStore; socket: SocketStore; status: ServerStatusStore; clearServerState: Action; @@ -69,7 +67,6 @@ export const ServerContext = createContextStore({ databases, files, subusers, - backups, schedules, clearServerState: action(state => { state.server.data = undefined; @@ -78,7 +75,6 @@ export const ServerContext = createContextStore({ state.subusers.data = []; state.files.directory = '/'; state.files.selectedFiles = []; - state.backups.data = []; state.schedules.data = []; if (state.socket.instance) {