diff --git a/package.json b/package.json index 4b0802f6b..884d20d2e 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ "@date-fns/tz": "^1.3.1", "@eslint/compat": "^1.3.1", "@eslint/js": "^9.32.0", + "@gravity-ui/icons": "^2.16.0", "@hcaptcha/react-hcaptcha": "^1.12.0", "@headlessui/react": "^2.2.7", "@lezer/highlight": "^1.2.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b7f51d58d..620f3c365 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -41,6 +41,9 @@ importers: '@eslint/js': specifier: ^9.32.0 version: 9.32.0 + '@gravity-ui/icons': + specifier: ^2.16.0 + version: 2.16.0(react@19.1.1) '@hcaptcha/react-hcaptcha': specifier: ^1.12.0 version: 1.12.0(react-dom@19.1.1(react@19.1.1))(react@19.1.1) @@ -704,6 +707,14 @@ packages: '@floating-ui/utils@0.2.9': resolution: {integrity: sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==} + '@gravity-ui/icons@2.16.0': + resolution: {integrity: sha512-AMjPSO+7yPpvcCa9k+ZFNOZ+8cmIauLyRMswhoyfQ7SeoJOVzsAa2OP/B+Dd+AZRvzTaeJha4m2iEk93JvAklA==} + peerDependencies: + react: '*' + peerDependenciesMeta: + react: + optional: true + '@hcaptcha/loader@2.0.0': resolution: {integrity: sha512-fFQH6ApU/zCCl6Y1bnbsxsp1Er/lKX+qlgljrpWDeFcenpEtoP68hExlKSXECospzKLeSWcr06cbTjlR/x3IJA==} @@ -4234,6 +4245,12 @@ snapshots: '@floating-ui/utils@0.2.9': {} + '@gravity-ui/icons@2.16.0(react@19.1.1)': + dependencies: + tslib: 2.8.1 + optionalDependencies: + react: 19.1.1 + '@hcaptcha/loader@2.0.0': {} '@hcaptcha/react-hcaptcha@1.12.0(react-dom@19.1.1(react@19.1.1))(react@19.1.1)': diff --git a/resources/scripts/components/server/backups/BackupContainer.tsx b/resources/scripts/components/server/backups/BackupContainer.tsx index d72ca8aed..f438b9359 100644 --- a/resources/scripts/components/server/backups/BackupContainer.tsx +++ b/resources/scripts/components/server/backups/BackupContainer.tsx @@ -1,8 +1,9 @@ -import { Form, Formik, Field as FormikField, FormikHelpers, useFormikContext } from 'formik'; -import { useCallback, useContext, useEffect, useState, createContext } from 'react'; -import { boolean, object, string } from 'yup'; +import { ArrowDownToLine } from '@gravity-ui/icons'; import { useStoreState } from 'easy-peasy'; +import { Form, Formik, Field as FormikField, FormikHelpers, useFormikContext } from 'formik'; +import { createContext, useCallback, useContext, useEffect, useState } from 'react'; import { toast } from 'sonner'; +import { boolean, object, string } from 'yup'; import FlashMessageRender from '@/components/FlashMessageRender'; import ActionButton from '@/components/elements/ActionButton'; @@ -18,31 +19,37 @@ import Pagination from '@/components/elements/Pagination'; import ServerContentBlock from '@/components/elements/ServerContentBlock'; import Spinner from '@/components/elements/Spinner'; import { PageListContainer } from '@/components/elements/pages/PageList'; +import { SocketEvent } from '@/components/server/events'; +import { httpErrorToHuman } from '@/api/http'; import { Context as ServerBackupContext } from '@/api/swr/getServerBackups'; import getServerBackups from '@/api/swr/getServerBackups'; -import { SocketEvent } from '@/components/server/events'; -import useWebsocketEvent from '@/plugins/useWebsocketEvent'; import { ApplicationStore } from '@/state'; import { ServerContext } from '@/state/server'; import useFlash from '@/plugins/useFlash'; -import { httpErrorToHuman } from '@/api/http'; -import { useUnifiedBackups } from './useUnifiedBackups'; +import useWebsocketEvent from '@/plugins/useWebsocketEvent'; + import BackupItem from './BackupItem'; +import { useUnifiedBackups } from './useUnifiedBackups'; // Context to share live backup progress across components -export const LiveProgressContext = createContext>({}); +export const LiveProgressContext = createContext< + Record< + string, + { + status: string; + progress: number; + message: string; + canRetry: boolean; + lastUpdated: string; + completed: boolean; + isDeletion: boolean; + backupName?: string; + } + > +>({}); // Helper function to format storage values const formatStorage = (mb: number | undefined | null): string => { @@ -131,17 +138,8 @@ const BackupContainer = () => { const hasTwoFactor = useStoreState((state: ApplicationStore) => state.user.data?.useTotp || false); - const { - backups, - backupCount, - storage, - pagination, - error, - isValidating, - createBackup, - retryBackup, - refresh - } = useUnifiedBackups(); + const { backups, backupCount, storage, pagination, error, isValidating, createBackup, retryBackup, refresh } = + useUnifiedBackups(); const uuid = ServerContext.useStoreState((state) => state.server.data!.uuid); const backupLimit = ServerContext.useStoreState((state) => state.server.data!.featureLimits.backups); @@ -231,9 +229,7 @@ const BackupContainer = () => { }; // Get backups that can be selected (completed and not active) - const selectableBackups = backups.filter( - (b) => b.status === 'completed' && b.isSuccessful && !b.isLiveOnly - ); + const selectableBackups = backups.filter((b) => b.status === 'completed' && b.isSuccessful && !b.isLiveOnly); const handleBulkDelete = async () => { if (!bulkDeletePassword) { @@ -322,21 +318,13 @@ const BackupContainer = () => {
{/* Backup Count Display */} - {backupLimit === null && ( -

- {backupCount} backups -

- )} + {backupLimit === null &&

{backupCount} backups

} {backupLimit > 0 && (

{backupCount} of {backupLimit} backups

)} - {backupLimit === 0 && ( -

- Backups disabled -

- )} + {backupLimit === 0 &&

Backups disabled

} {/* Storage Usage Display */} {storage && ( @@ -347,15 +335,24 @@ const BackupContainer = () => { className='text-sm text-zinc-300 cursor-help' title={`${storage.used_mb?.toFixed(2) || 0}MB total (Repository: ${storage.repository_usage_mb?.toFixed(2) || 0}MB, Legacy: ${storage.legacy_usage_mb?.toFixed(2) || 0}MB)`} > - {formatStorage(storage.used_mb)} storage used + + {formatStorage(storage.used_mb)} + {' '} + storage used

- {(storage.repository_usage_mb > 0 || storage.legacy_usage_mb > 0) && (storage.repository_usage_mb > 0 && storage.legacy_usage_mb > 0) && ( -

- {storage.repository_usage_mb > 0 && `${formatStorage(storage.repository_usage_mb)} deduplicated`} - {storage.repository_usage_mb > 0 && storage.legacy_usage_mb > 0 && ' + '} - {storage.legacy_usage_mb > 0 && `${formatStorage(storage.legacy_usage_mb)} legacy`} -

- )} + {(storage.repository_usage_mb > 0 || storage.legacy_usage_mb > 0) && + storage.repository_usage_mb > 0 && + storage.legacy_usage_mb > 0 && ( +

+ {storage.repository_usage_mb > 0 && + `${formatStorage(storage.repository_usage_mb)} deduplicated`} + {storage.repository_usage_mb > 0 && + storage.legacy_usage_mb > 0 && + ' + '} + {storage.legacy_usage_mb > 0 && + `${formatStorage(storage.legacy_usage_mb)} legacy`} +

+ )} ) : ( <> @@ -363,18 +360,30 @@ const BackupContainer = () => { className='text-sm text-zinc-300 cursor-help' title={`${storage.used_mb?.toFixed(2) || 0}MB used of ${backupStorageLimit}MB (Repository: ${storage.repository_usage_mb?.toFixed(2) || 0}MB, Legacy: ${storage.legacy_usage_mb?.toFixed(2) || 0}MB, ${storage.available_mb?.toFixed(2) || 0}MB Available)`} > - {formatStorage(storage.used_mb)} {' '} - {backupStorageLimit === null ? - "used" : - (of {formatStorage(backupStorageLimit)} used)} + + {formatStorage(storage.used_mb)} + {' '} + {backupStorageLimit === null ? ( + 'used' + ) : ( + + of {formatStorage(backupStorageLimit)} used + + )}

- {(storage.repository_usage_mb > 0 || storage.legacy_usage_mb > 0) && (storage.repository_usage_mb > 0 && storage.legacy_usage_mb > 0) && ( -

- {storage.repository_usage_mb > 0 && `${formatStorage(storage.repository_usage_mb)} deduplicated`} - {storage.repository_usage_mb > 0 && storage.legacy_usage_mb > 0 && ' + '} - {storage.legacy_usage_mb > 0 && `${formatStorage(storage.legacy_usage_mb)} legacy`} -

- )} + {(storage.repository_usage_mb > 0 || storage.legacy_usage_mb > 0) && + storage.repository_usage_mb > 0 && + storage.legacy_usage_mb > 0 && ( +

+ {storage.repository_usage_mb > 0 && + `${formatStorage(storage.repository_usage_mb)} deduplicated`} + {storage.repository_usage_mb > 0 && + storage.legacy_usage_mb > 0 && + ' + '} + {storage.legacy_usage_mb > 0 && + `${formatStorage(storage.legacy_usage_mb)} legacy`} +

+ )} )}
@@ -383,9 +392,18 @@ const BackupContainer = () => {
{backupCount > 0 && ( setDeleteAllModalVisible(true)}> - - + + Delete All Backups @@ -431,24 +449,33 @@ const BackupContainer = () => { }} title='Delete All Backups' > -
-

+

+

You are about to permanently delete{' '} - + {backupCount} {backupCount === 1 ? 'backup' : 'backups'} - - {' '}and completely destroy the backup repository for this server. + {' '} + and completely destroy the backup repository for this server.

-
-
- - +
+
+ + -
-

This action cannot be undone

-
    +
    +

    This action cannot be undone

    +
    • All backup data will be permanently deleted
    • Locked backups will also be deleted
    • The entire backup repository will be destroyed
    • @@ -459,16 +486,16 @@ const BackupContainer = () => {
-
+
-