ui: started migration to GravityUI icons

This commit is contained in:
Naterfute
2025-10-28 05:34:48 -07:00
parent 44be003e54
commit bc044aa868
8 changed files with 412 additions and 375 deletions

View File

@@ -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",

17
pnpm-lock.yaml generated
View File

@@ -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)':

View File

@@ -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<Record<string, {
status: string;
progress: number;
message: string;
canRetry: boolean;
lastUpdated: string;
completed: boolean;
isDeletion: boolean;
backupName?: string;
}>>({});
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 = () => {
<div className='flex flex-col sm:flex-row items-center justify-end gap-4'>
<div className='flex flex-col gap-1 text-center sm:text-right'>
{/* Backup Count Display */}
{backupLimit === null && (
<p className='text-sm text-zinc-300'>
{backupCount} backups
</p>
)}
{backupLimit === null && <p className='text-sm text-zinc-300'>{backupCount} backups</p>}
{backupLimit > 0 && (
<p className='text-sm text-zinc-300'>
{backupCount} of {backupLimit} backups
</p>
)}
{backupLimit === 0 && (
<p className='text-sm text-red-400'>
Backups disabled
</p>
)}
{backupLimit === 0 && <p className='text-sm text-red-400'>Backups disabled</p>}
{/* 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)`}
>
<span className='font-medium'>{formatStorage(storage.used_mb)}</span> storage used
<span className='font-medium'>
{formatStorage(storage.used_mb)}
</span>{' '}
storage used
</p>
{(storage.repository_usage_mb > 0 || storage.legacy_usage_mb > 0) && (storage.repository_usage_mb > 0 && storage.legacy_usage_mb > 0) && (
<p className='text-xs text-zinc-400'>
{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`}
</p>
)}
{(storage.repository_usage_mb > 0 || storage.legacy_usage_mb > 0) &&
storage.repository_usage_mb > 0 &&
storage.legacy_usage_mb > 0 && (
<p className='text-xs text-zinc-400'>
{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`}
</p>
)}
</>
) : (
<>
@@ -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)`}
>
<span className='font-medium'>{formatStorage(storage.used_mb)}</span> {' '}
{backupStorageLimit === null ?
"used" :
(<span className='font-medium'>of {formatStorage(backupStorageLimit)} used</span>)}
<span className='font-medium'>
{formatStorage(storage.used_mb)}
</span>{' '}
{backupStorageLimit === null ? (
'used'
) : (
<span className='font-medium'>
of {formatStorage(backupStorageLimit)} used
</span>
)}
</p>
{(storage.repository_usage_mb > 0 || storage.legacy_usage_mb > 0) && (storage.repository_usage_mb > 0 && storage.legacy_usage_mb > 0) && (
<p className='text-xs text-zinc-400'>
{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`}
</p>
)}
{(storage.repository_usage_mb > 0 || storage.legacy_usage_mb > 0) &&
storage.repository_usage_mb > 0 &&
storage.legacy_usage_mb > 0 && (
<p className='text-xs text-zinc-400'>
{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`}
</p>
)}
</>
)}
</div>
@@ -383,9 +392,18 @@ const BackupContainer = () => {
<div className='flex gap-2'>
{backupCount > 0 && (
<ActionButton variant='danger' onClick={() => setDeleteAllModalVisible(true)}>
<svg className="w-4 h-4 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
<svg
className='w-4 h-4 mr-2'
fill='none'
viewBox='0 0 24 24'
stroke='currentColor'
>
<path
strokeLinecap='round'
strokeLinejoin='round'
strokeWidth={2}
d='M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16'
/>
</svg>
Delete All Backups
</ActionButton>
@@ -431,24 +449,33 @@ const BackupContainer = () => {
}}
title='Delete All Backups'
>
<div className="space-y-4">
<p className="text-sm text-zinc-300">
<div className='space-y-4'>
<p className='text-sm text-zinc-300'>
You are about to permanently delete{' '}
<span className="font-medium text-red-400">
<span className='font-medium text-red-400'>
{backupCount} {backupCount === 1 ? 'backup' : 'backups'}
</span>
{' '}and completely destroy the backup repository for this server.
</span>{' '}
and completely destroy the backup repository for this server.
</p>
<div className="p-4 bg-red-500/10 border border-red-500/20 rounded-lg">
<div className="flex items-start gap-3">
<svg className="w-5 h-5 text-red-400 mt-0.5 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
<div className='p-4 bg-red-500/10 border border-red-500/20 rounded-lg'>
<div className='flex items-start gap-3'>
<svg
className='w-5 h-5 text-red-400 mt-0.5 flex-shrink-0'
fill='none'
viewBox='0 0 24 24'
stroke='currentColor'
>
<path
strokeLinecap='round'
strokeLinejoin='round'
strokeWidth={2}
d='M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z'
/>
</svg>
<div className="text-sm">
<p className="font-medium text-red-300">This action cannot be undone</p>
<ul className="text-red-400 mt-2 space-y-1 list-disc list-inside">
<div className='text-sm'>
<p className='font-medium text-red-300'>This action cannot be undone</p>
<ul className='text-red-400 mt-2 space-y-1 list-disc list-inside'>
<li>All backup data will be permanently deleted</li>
<li>Locked backups will also be deleted</li>
<li>The entire backup repository will be destroyed</li>
@@ -459,16 +486,16 @@ const BackupContainer = () => {
</div>
</div>
<div className="space-y-3">
<div className='space-y-3'>
<div>
<label htmlFor="password" className="block text-sm font-medium text-zinc-300 mb-1">
<label htmlFor='password' className='block text-sm font-medium text-zinc-300 mb-1'>
Password
</label>
<input
id="password"
type="password"
className="w-full px-4 py-2 rounded-lg outline-hidden bg-[#ffffff17] text-sm border border-zinc-700 focus:border-brand"
placeholder="Enter your password"
id='password'
type='password'
className='w-full px-4 py-2 rounded-lg outline-hidden bg-[#ffffff17] text-sm border border-zinc-700 focus:border-brand'
placeholder='Enter your password'
value={deleteAllPassword}
onChange={(e) => setDeleteAllPassword(e.target.value)}
disabled={isDeleting}
@@ -477,14 +504,14 @@ const BackupContainer = () => {
{hasTwoFactor && (
<div>
<label htmlFor="totp_code" className="block text-sm font-medium text-zinc-300 mb-1">
<label htmlFor='totp_code' className='block text-sm font-medium text-zinc-300 mb-1'>
Two-Factor Authentication Code
</label>
<input
id="totp_code"
type="text"
className="w-full px-4 py-2 rounded-lg outline-hidden bg-[#ffffff17] text-sm border border-zinc-700 focus:border-brand"
placeholder="6-digit code"
id='totp_code'
type='text'
className='w-full px-4 py-2 rounded-lg outline-hidden bg-[#ffffff17] text-sm border border-zinc-700 focus:border-brand'
placeholder='6-digit code'
maxLength={6}
value={deleteAllTotpCode}
onChange={(e) => setDeleteAllTotpCode(e.target.value.replace(/[^0-9]/g, ''))}
@@ -494,7 +521,7 @@ const BackupContainer = () => {
)}
</div>
<div className="flex justify-end gap-3 pb-6 pt-2">
<div className='flex justify-end gap-3 pb-6 pt-2'>
<ActionButton
variant='secondary'
onClick={() => {
@@ -506,11 +533,7 @@ const BackupContainer = () => {
>
Cancel
</ActionButton>
<ActionButton
variant='danger'
onClick={handleDeleteAll}
disabled={isDeleting}
>
<ActionButton variant='danger' onClick={handleDeleteAll} disabled={isDeleting}>
{isDeleting && <Spinner size='small' />}
{isDeleting ? 'Deleting...' : 'Delete All Backups'}
</ActionButton>
@@ -531,40 +554,50 @@ const BackupContainer = () => {
title='Delete Selected Backups'
>
<FlashMessageRender byKey={'backups:bulk_delete'} />
<div className="space-y-4">
<p className="text-sm text-zinc-300">
<div className='space-y-4'>
<p className='text-sm text-zinc-300'>
You are about to permanently delete{' '}
<span className="font-medium text-red-400">
<span className='font-medium text-red-400'>
{selectedBackups.size} backup{selectedBackups.size > 1 ? 's' : ''}
</span>
. This action cannot be undone.
</p>
<div className="p-4 bg-red-500/10 border border-red-500/20 rounded-lg">
<div className="flex items-start gap-3">
<svg className="w-5 h-5 text-red-400 mt-0.5 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
<div className='p-4 bg-red-500/10 border border-red-500/20 rounded-lg'>
<div className='flex items-start gap-3'>
<svg
className='w-5 h-5 text-red-400 mt-0.5 flex-shrink-0'
fill='none'
viewBox='0 0 24 24'
stroke='currentColor'
>
<path
strokeLinecap='round'
strokeLinejoin='round'
strokeWidth={2}
d='M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z'
/>
</svg>
<div className="text-sm">
<p className="font-medium text-red-300">Warning</p>
<p className="text-red-400 mt-1">
The selected backup files and their snapshots will be permanently deleted. You will not be able to restore them.
<div className='text-sm'>
<p className='font-medium text-red-300'>Warning</p>
<p className='text-red-400 mt-1'>
The selected backup files and their snapshots will be permanently deleted. You
will not be able to restore them.
</p>
</div>
</div>
</div>
<div className="space-y-3">
<div className='space-y-3'>
<div>
<label htmlFor="bulk-password" className="block text-sm font-medium text-zinc-300 mb-1">
<label htmlFor='bulk-password' className='block text-sm font-medium text-zinc-300 mb-1'>
Password
</label>
<input
id="bulk-password"
type="password"
className="w-full px-4 py-2 rounded-lg outline-hidden bg-[#ffffff17] text-sm border border-zinc-700 focus:border-brand"
placeholder="Enter your password"
id='bulk-password'
type='password'
className='w-full px-4 py-2 rounded-lg outline-hidden bg-[#ffffff17] text-sm border border-zinc-700 focus:border-brand'
placeholder='Enter your password'
value={bulkDeletePassword}
onChange={(e) => setBulkDeletePassword(e.target.value)}
disabled={isBulkDeleting}
@@ -573,14 +606,14 @@ const BackupContainer = () => {
{hasTwoFactor && (
<div>
<label htmlFor="bulk-totp" className="block text-sm font-medium text-zinc-300 mb-1">
<label htmlFor='bulk-totp' className='block text-sm font-medium text-zinc-300 mb-1'>
Two-Factor Authentication Code
</label>
<input
id="bulk-totp"
type="text"
className="w-full px-4 py-2 rounded-lg outline-hidden bg-[#ffffff17] text-sm border border-zinc-700 focus:border-brand"
placeholder="6-digit code"
id='bulk-totp'
type='text'
className='w-full px-4 py-2 rounded-lg outline-hidden bg-[#ffffff17] text-sm border border-zinc-700 focus:border-brand'
placeholder='6-digit code'
maxLength={6}
value={bulkDeleteTotpCode}
onChange={(e) => setBulkDeleteTotpCode(e.target.value.replace(/[^0-9]/g, ''))}
@@ -590,7 +623,7 @@ const BackupContainer = () => {
)}
</div>
<div className="flex justify-end gap-3 pb-6 pt-2">
<div className='flex justify-end gap-3 pb-6 pt-2'>
<ActionButton
variant='secondary'
onClick={() => {
@@ -602,13 +635,11 @@ const BackupContainer = () => {
>
Cancel
</ActionButton>
<ActionButton
variant='danger'
onClick={handleBulkDelete}
disabled={isBulkDeleting}
>
<ActionButton variant='danger' onClick={handleBulkDelete} disabled={isBulkDeleting}>
{isBulkDeleting && <Spinner size='small' />}
{isBulkDeleting ? 'Deleting...' : `Delete ${selectedBackups.size} Backup${selectedBackups.size > 1 ? 's' : ''}`}
{isBulkDeleting
? 'Deleting...'
: `Delete ${selectedBackups.size} Backup${selectedBackups.size > 1 ? 's' : ''}`}
</ActionButton>
</div>
</div>
@@ -619,13 +650,12 @@ const BackupContainer = () => {
<div className='flex flex-col items-center justify-center min-h-[60vh] py-12 px-4'>
<div className='text-center'>
<div className='w-16 h-16 mx-auto mb-4 rounded-full bg-[#ffffff11] flex items-center justify-center'>
<svg className='w-8 h-8 text-zinc-400' fill='currentColor' viewBox='0 0 20 20'>
<path
fillRule='evenodd'
d='M3 17a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zm3.293-7.707a1 1 0 011.414 0L9 10.586V3a1 1 0 112 0v7.586l1.293-1.293a1 1 0 111.414 1.414l-3 3a1 1 0 01-1.414 0l-3-3a1 1 0 010-1.414z'
clipRule='evenodd'
/>
</svg>
<ArrowDownToLine
width={22}
height={22}
className='w-6 h-6 text-zinc-400'
fill=' currentColor'
/>
</div>
<h3 className='text-lg font-medium text-zinc-200 mb-2'>
{backupLimit === 0 ? 'Backups unavailable' : 'No backups found'}
@@ -644,7 +674,10 @@ const BackupContainer = () => {
<div className='mb-8 flex items-center justify-between px-4 py-3.5 rounded-xl bg-[#ffffff08] border border-zinc-700'>
<div className='flex items-center gap-4'>
<Checkbox
checked={selectedBackups.size === selectableBackups.length && selectableBackups.length > 0}
checked={
selectedBackups.size === selectableBackups.length &&
selectableBackups.length > 0
}
onCheckedChange={toggleSelectAll}
/>
<span className='text-sm text-zinc-300'>
@@ -658,18 +691,14 @@ const BackupContainer = () => {
</span>
</div>
<div className={`flex items-center gap-3 transition-opacity ${selectedBackups.size > 0 ? 'opacity-100' : 'opacity-0 pointer-events-none'}`}>
<ActionButton
variant='secondary'
onClick={clearSelection}
>
<div
className={`flex items-center gap-3 transition-opacity ${selectedBackups.size > 0 ? 'opacity-100' : 'opacity-0 pointer-events-none'}`}
>
<ActionButton variant='secondary' onClick={clearSelection}>
Clear
</ActionButton>
<Can action='backup.delete'>
<ActionButton
variant='danger'
onClick={() => setBulkDeleteModalVisible(true)}
>
<ActionButton variant='danger' onClick={() => setBulkDeleteModalVisible(true)}>
Delete Selected ({selectedBackups.size})
</ActionButton>
</Can>
@@ -704,134 +733,139 @@ const BackupContainer = () => {
const BackupContainerWrapper = () => {
const [page, setPage] = useState<number>(1);
const { mutate } = getServerBackups();
const [liveProgress, setLiveProgress] = useState<Record<string, {
status: string;
progress: number;
message: string;
canRetry: boolean;
lastUpdated: string;
completed: boolean;
isDeletion: boolean;
backupName?: string;
}>>({});
const [liveProgress, setLiveProgress] = useState<
Record<
string,
{
status: string;
progress: number;
message: string;
canRetry: boolean;
lastUpdated: string;
completed: boolean;
isDeletion: boolean;
backupName?: string;
}
>
>({});
// Single websocket listener for the entire page
const handleBackupStatus = useCallback((rawData: any) => {
let data;
try {
if (typeof rawData === 'string') {
data = JSON.parse(rawData);
} else {
data = rawData;
}
} catch (error) {
return;
}
const backup_uuid = data?.backup_uuid;
if (!backup_uuid) {
return;
}
const {
status,
progress,
message,
timestamp,
operation,
error: errorMsg,
name,
} = data;
const can_retry = status === 'failed' && operation === 'create';
const last_updated_at = timestamp ? new Date(timestamp * 1000).toISOString() : new Date().toISOString();
const isDeletionOperation = operation === 'delete' || data.deleted === true;
setLiveProgress(prevProgress => {
const currentState = prevProgress[backup_uuid];
const newProgress = progress || 0;
const isCompleted = status === 'completed' && newProgress === 100;
const displayMessage = errorMsg ? `${message || 'Operation failed'}: ${errorMsg}` : (message || '');
if (currentState?.completed && !isCompleted) {
return prevProgress;
}
if (currentState && !isCompleted && currentState.lastUpdated >= last_updated_at && currentState.progress >= newProgress) {
return prevProgress;
}
return {
...prevProgress,
[backup_uuid]: {
status,
progress: newProgress,
message: displayMessage,
canRetry: can_retry || false,
lastUpdated: last_updated_at,
completed: isCompleted,
isDeletion: isDeletionOperation,
backupName: name || currentState?.backupName,
const handleBackupStatus = useCallback(
(rawData: any) => {
let data;
try {
if (typeof rawData === 'string') {
data = JSON.parse(rawData);
} else {
data = rawData;
}
};
});
if (status === 'completed' && progress === 100) {
if (isDeletionOperation) {
// Optimistically remove the deleted backup from SWR cache immediately
// note: this is incredibly buggy sometimes, somebody please refactor how "live" backups work. - ellie
mutate(
(currentData) => {
if (!currentData) return currentData;
return {
...currentData,
items: currentData.items.filter(b => b.uuid !== backup_uuid),
backupCount: Math.max(0, (currentData.backupCount || 0) - 1),
};
},
{ revalidate: true }
);
// Remove from live progress
setTimeout(() => {
setLiveProgress(prev => {
const updated = { ...prev };
delete updated[backup_uuid];
return updated;
});
}, 500);
} else {
// For new backups, wait for them to appear in the API
mutate();
const checkForBackup = async (attempts = 0) => {
if (attempts > 10) {
setLiveProgress(prev => {
const updated = { ...prev };
delete updated[backup_uuid];
return updated;
});
return;
}
// Force fresh data
const currentBackups = await mutate();
const backupExists = currentBackups?.items?.some(b => b.uuid === backup_uuid);
if (backupExists) {
setLiveProgress(prev => {
const updated = { ...prev };
delete updated[backup_uuid];
return updated;
});
} else {
setTimeout(() => checkForBackup(attempts + 1), 1000);
}
};
setTimeout(() => checkForBackup(), 1000);
} catch (error) {
return;
}
}
}, [mutate]);
const backup_uuid = data?.backup_uuid;
if (!backup_uuid) {
return;
}
const { status, progress, message, timestamp, operation, error: errorMsg, name } = data;
const can_retry = status === 'failed' && operation === 'create';
const last_updated_at = timestamp ? new Date(timestamp * 1000).toISOString() : new Date().toISOString();
const isDeletionOperation = operation === 'delete' || data.deleted === true;
setLiveProgress((prevProgress) => {
const currentState = prevProgress[backup_uuid];
const newProgress = progress || 0;
const isCompleted = status === 'completed' && newProgress === 100;
const displayMessage = errorMsg ? `${message || 'Operation failed'}: ${errorMsg}` : message || '';
if (currentState?.completed && !isCompleted) {
return prevProgress;
}
if (
currentState &&
!isCompleted &&
currentState.lastUpdated >= last_updated_at &&
currentState.progress >= newProgress
) {
return prevProgress;
}
return {
...prevProgress,
[backup_uuid]: {
status,
progress: newProgress,
message: displayMessage,
canRetry: can_retry || false,
lastUpdated: last_updated_at,
completed: isCompleted,
isDeletion: isDeletionOperation,
backupName: name || currentState?.backupName,
},
};
});
if (status === 'completed' && progress === 100) {
if (isDeletionOperation) {
// Optimistically remove the deleted backup from SWR cache immediately
// note: this is incredibly buggy sometimes, somebody please refactor how "live" backups work. - ellie
mutate(
(currentData) => {
if (!currentData) return currentData;
return {
...currentData,
items: currentData.items.filter((b) => b.uuid !== backup_uuid),
backupCount: Math.max(0, (currentData.backupCount || 0) - 1),
};
},
{ revalidate: true },
);
// Remove from live progress
setTimeout(() => {
setLiveProgress((prev) => {
const updated = { ...prev };
delete updated[backup_uuid];
return updated;
});
}, 500);
} else {
// For new backups, wait for them to appear in the API
mutate();
const checkForBackup = async (attempts = 0) => {
if (attempts > 10) {
setLiveProgress((prev) => {
const updated = { ...prev };
delete updated[backup_uuid];
return updated;
});
return;
}
// Force fresh data
const currentBackups = await mutate();
const backupExists = currentBackups?.items?.some((b) => b.uuid === backup_uuid);
if (backupExists) {
setLiveProgress((prev) => {
const updated = { ...prev };
delete updated[backup_uuid];
return updated;
});
} else {
setTimeout(() => checkForBackup(attempts + 1), 1000);
}
};
setTimeout(() => checkForBackup(), 1000);
}
}
},
[mutate],
);
useWebsocketEvent(SocketEvent.BACKUP_STATUS, handleBackupStatus);

View File

@@ -1,3 +1,4 @@
import { BarsPlay, Copy, FileArrowDown, FileZipper, PencilToLine, Shield, TrashBin } from '@gravity-ui/icons';
import { join } from 'pathe';
import { memo, useState } from 'react';
import isEqual from 'react-fast-compare';
@@ -6,13 +7,6 @@ import { toast } from 'sonner';
import Can from '@/components/elements/Can';
import { ContextMenuContent, ContextMenuItem } from '@/components/elements/ContextMenu';
import { Dialog } from '@/components/elements/dialog';
import HugeIconsCopy from '@/components/elements/hugeicons/Copy';
import HugeIconsDelete from '@/components/elements/hugeicons/Delete';
import HugeIconsFileDownload from '@/components/elements/hugeicons/FileDownload';
import HugeIconsFileSecurity from '@/components/elements/hugeicons/FileSecurity';
import HugeIconsFileZip from '@/components/elements/hugeicons/FileZip';
import HugeIconsMoveTo from '@/components/elements/hugeicons/MoveTo';
import HugeIconsPencil from '@/components/elements/hugeicons/Pencil';
import ChmodFileModal from '@/components/server/files/ChmodFileModal';
import RenameFileModal from '@/components/server/files/RenameFileModal';
@@ -126,22 +120,22 @@ const FileDropdownMenu = ({ file }: { file: FileObject }) => {
<ContextMenuContent className='flex flex-col gap-1'>
<Can action={'file.update'}>
<ContextMenuItem className='flex gap-2' onSelect={() => setModal('rename')}>
<HugeIconsPencil className='h-4! w-4!' fill='currentColor' />
<PencilToLine className='h-4! w-4!' fill='currentColor' />
<span>Rename</span>
</ContextMenuItem>
<ContextMenuItem className='flex gap-2' onSelect={() => setModal('move')}>
<HugeIconsMoveTo className='h-4! w-4!' fill='currentColor' />
<BarsPlay className='h-4! w-4!' fill='currentColor' />
<span>Move</span>
</ContextMenuItem>
<ContextMenuItem className='flex gap-2' onSelect={() => setModal('chmod')}>
<HugeIconsFileSecurity className='h-4! w-4!' fill='currentColor' />
<Shield className='h-4! w-4!' fill='currentColor' />
<span>Permissions</span>
</ContextMenuItem>
</Can>
{file.isFile && (
<Can action={'file.create'}>
<ContextMenuItem className='flex gap-2' onClick={doCopy}>
<HugeIconsCopy className='h-4! w-4!' fill='currentColor' />
<Copy className='h-4! w-4!' fill='currentColor' />
<span>Duplicate</span>
</ContextMenuItem>
</Can>
@@ -149,27 +143,27 @@ const FileDropdownMenu = ({ file }: { file: FileObject }) => {
{file.isArchiveType() ? (
<Can action={'file.create'}>
<ContextMenuItem className='flex gap-2' onSelect={doUnarchive} title={'Unarchive'}>
<HugeIconsFileZip className='h-4! w-4!' fill='currentColor' />
<FileZipper className='h-4! w-4!' fill='currentColor' />
<span>Unarchive</span>
</ContextMenuItem>
</Can>
) : (
<Can action={'file.archive'}>
<ContextMenuItem className='flex gap-2' onSelect={doArchive}>
<HugeIconsFileZip className='h-4! w-4!' fill='currentColor' />
<FileZipper className='h-4! w-4!' fill='currentColor' />
<span>Archive</span>
</ContextMenuItem>
</Can>
)}
{file.isFile && (
<ContextMenuItem className='flex gap-2' onSelect={doDownload}>
<HugeIconsFileDownload className='h-4! w-4!' fill='currentColor' />
<FileArrowDown className='h-4! w-4!' fill='currentColor' />
<span>Download</span>
</ContextMenuItem>
)}
<Can action={'file.delete'}>
<ContextMenuItem className='flex gap-2' onSelect={() => setShowConfirmation(true)}>
<HugeIconsDelete className='h-4! w-4!' fill='currentColor' />
<TrashBin className='h-4! w-4!' fill='currentColor' />
<span>Delete</span>
</ContextMenuItem>
</Can>

View File

@@ -1,4 +1,5 @@
import { encodePathSegments } from '@/helpers';
import { File, FolderOpenFill } from '@gravity-ui/icons';
import { differenceInHours, format, formatDistanceToNow } from 'date-fns';
import { join } from 'pathe';
import { ReactNode, memo } from 'react';
@@ -48,51 +49,13 @@ const FileObjectRow = ({ file }: { file: FileObject }) => (
<MemoizedClickable file={file}>
<div className={`flex-none text-zinc-400 mr-4 text-lg pl-3 mb-0.5`}>
{file.isFile ? (
// todo handle other types of files. ugh
<svg
width='24'
height='24'
viewBox='0 0 24 24'
fill='none'
xmlns='http://www.w3.org/2000/svg'
>
<path
fillRule='evenodd'
clipRule='evenodd'
d='M16.5635 1.35276C15.5812 1.25 14.3484 1.25001 12.8073 1.25003H11.932C10.039 1.25001 8.52512 1.24999 7.33708 1.40088C6.11256 1.55639 5.08724 1.88708 4.26839 2.66059C3.4412 3.44199 3.07982 4.43383 2.91129 5.61793C2.74994 6.75159 2.74997 8.19141 2.75 9.97015V16.12C2.74999 16.9191 2.74999 17.5667 2.78473 18.0953C2.82052 18.6399 2.89613 19.1256 3.0794 19.5897C3.60821 20.929 4.71664 21.9633 6.09319 22.4483C6.952 22.7509 8.42408 22.7505 9.97909 22.75C12.8187 22.7503 14.5054 22.7505 15.8878 22.2635C18.1078 21.4813 19.8815 19.8185 20.7249 17.6825C21.006 16.9705 21.1306 16.2058 21.1908 15.289C21.25 14.3882 21.25 13.2756 21.25 11.8573V9.27383C21.25 7.82574 21.25 6.65309 21.1402 5.71576C21.026 4.74236 20.7828 3.90448 20.213 3.18541C19.9178 2.81293 19.5692 2.48415 19.1789 2.2081C18.4341 1.68144 17.5729 1.45835 16.5635 1.35276ZM5.60307 4.08392C5.99626 3.7125 6.55233 3.47071 7.58157 3.33999C8.63306 3.20645 10.0233 3.2046 12 3.2046H12.7524C14.361 3.2046 15.4922 3.20585 16.3616 3.2968C17.2155 3.38613 17.6994 3.55289 18.0573 3.80593C18.2987 3.97668 18.5111 4.17777 18.6889 4.40212C18.9445 4.72462 19.1139 5.15741 19.2061 5.9442C19.3011 6.75396 19.3026 7.81129 19.3026 9.33474L19.3027 12.2349C19.3027 12.5019 19.3026 13.1405 19.022 13.6127C18.849 13.9037 18.6276 14.1468 18.4002 14.2706C18.0336 14.4701 17.6135 14.5835 17.1668 14.5835L16.1264 14.547C15.7463 14.5391 15.3028 14.5511 14.8746 14.6658C14.0407 14.8893 13.3893 15.5407 13.1658 16.3747C13.0511 16.8028 13.0391 17.2463 13.047 17.6264L13.0835 18.6668C13.0835 19.1345 12.9591 19.5416 12.7417 19.92C12.615 20.1406 12.3943 20.3425 12.0895 20.5198C11.6274 20.7887 11.074 20.7912 10.7358 20.7927C10.3977 20.7943 10.0409 20.7954 9.74284 20.7954C7.90872 20.7954 7.24159 20.7815 6.73823 20.6041C5.8656 20.2967 5.1999 19.655 4.88981 18.8697C4.81217 18.673 4.75733 18.4146 4.72789 17.9667C4.69788 17.51 4.69739 16.927 4.69739 16.0868V10.0455C4.69739 8.17343 4.69971 6.87375 4.83911 5.89437C4.97359 4.94948 5.21822 4.44747 5.60307 4.08392Z'
fill='white'
/>
<path
fillRule='evenodd'
clipRule='evenodd'
d='M7 7C7 6.44772 7.44772 6 8 6H15C15.5523 6 16 6.44772 16 7C16 7.55228 15.5523 8 15 8H8C7.44772 8 7 7.55228 7 7Z'
fill='white'
/>
<path
fillRule='evenodd'
clipRule='evenodd'
d='M7 11C7 10.4477 7.44772 10 8 10H11C11.5523 10 12 10.4477 12 11C12 11.5523 11.5523 12 11 12H8C7.44772 12 7 11.5523 7 11Z'
fill='white'
/>
</svg>
<div>
<File width={22} height={22} />
</div>
) : (
// Todo componentize this shit
<svg
width='24'
height='24'
viewBox='0 0 24 24'
fill='none'
xmlns='http://www.w3.org/2000/svg'
>
<path
d='M7.785 3.30812C7.6039 3.22376 7.38178 3.20242 6.32595 3.20242C5.55108 3.20242 5.04842 3.20378 4.66633 3.24562C4.30655 3.28502 4.15503 3.35187 4.05577 3.42068C3.81547 3.58728 3.5906 3.8532 3.42965 4.21148C3.33971 4.41171 3.27289 4.69057 3.23698 5.19659C3.20057 5.70964 3.2 6.37092 3.2 7.32086V10.4695C3.2 12.9205 3.20135 14.6795 3.34616 16.0175C3.48985 17.3453 3.76288 18.1104 4.19883 18.652C4.61031 19.1632 5.15176 19.4588 6.1292 19.6221C7.15995 19.7942 8.5304 19.7974 10.5193 19.7974H11.5561C12.0946 19.7974 12.5311 20.2345 12.5311 20.7736C12.5311 21.3127 12.0946 21.7498 11.5561 21.7498H10.4332H10.4331C8.55149 21.7498 7.01783 21.7498 5.80834 21.5478C4.52712 21.3338 3.48373 20.8749 2.68053 19.8771C1.9018 18.9097 1.56749 17.706 1.40751 16.2278C1.24998 14.7723 1.24999 12.9075 1.25 10.53V7.28438V7.28436C1.24999 6.37947 1.24999 5.64868 1.29188 5.05824C1.33484 4.45285 1.42574 3.91267 1.65124 3.41067C1.94002 2.76782 2.3801 2.20767 2.94567 1.81557C3.41168 1.4925 3.91944 1.36341 4.45431 1.30484C4.955 1.25001 5.77487 1.24988 6.485 1.24991C7.29125 1.2487 7.98437 1.24765 8.60763 1.538C9.33078 1.87489 9.79993 2.4365 10.1323 2.99796C10.422 3.48716 10.6449 4.04448 10.8361 4.52255L10.8361 4.52256L11.1776 5.3717L15.4938 5.3717C16.3172 5.37166 17.0246 5.37162 17.6013 5.4445C18.2213 5.52285 18.7983 5.69601 19.3059 6.11735C19.6821 6.42958 19.9927 6.82026 20.2286 7.25887C20.6299 8.00505 20.7194 8.87723 20.7496 9.92617C20.7651 10.4651 20.3413 10.9145 19.8031 10.9301C19.2648 10.9456 18.8159 10.5213 18.8004 9.98243C18.7706 8.94657 18.6758 8.48963 18.5117 8.18454C18.3845 7.94809 18.2286 7.75917 18.0613 7.62033C17.9413 7.52071 17.7677 7.43338 17.3571 7.38149C16.9169 7.32586 16.3337 7.32405 15.435 7.32405H7.10403C6.56555 7.32405 6.12903 6.887 6.12903 6.34788C6.12903 5.80875 6.56555 5.3717 7.10403 5.3717H9.07528C8.87161 4.86433 8.6633 4.34545 8.45492 3.99351C8.23849 3.62796 8.02646 3.4206 7.785 3.30812Z'
fill='white'
/>
<path
d='M17.3112 9.25592C18.4993 9.25589 19.4723 9.25587 20.2219 9.36309C21.0012 9.47456 21.6956 9.72121 22.178 10.3433C22.9349 11.3194 22.8068 12.5251 22.5484 13.4743C22.3712 14.1251 21.8122 15.4777 21.5792 16.0323C21.1387 17.2059 20.7894 18.1365 20.4353 18.8624C20.0717 19.6078 19.6753 20.198 19.109 20.66C18.262 21.3511 17.2532 21.6037 16.304 21.6971C15.6191 21.7644 14.8941 21.7519 14.2529 21.7409L9.87794 21.7333C8.15647 21.7333 6.78588 21.7333 5.73406 21.5893C4.65546 21.4415 3.7619 21.1249 3.12679 20.3687C2.0367 19.0708 2.13865 17.4388 2.49738 16.0483C2.74524 15.0876 3.16552 14.0813 3.51236 13.2508C3.71801 12.7029 4.28236 11.2746 4.4577 10.9114C4.64193 10.5297 4.85595 10.1949 5.17501 9.92133C5.67283 9.49456 6.25423 9.34181 6.76747 9.28454C7.14566 9.24234 7.55281 9.2483 7.88558 9.25316L17.3112 9.25592Z'
fill='white'
/>
</svg>
<div>
<FolderOpenFill width={22} height={22} />
</div>
)}
</div>
<div className='flex-1 truncate font-bold text-sm'>{file.name}</div>

View File

@@ -0,0 +1,46 @@
import { Progress } from '@radix-ui/react-progress';
import { X } from 'lucide-react';
import Code from '@/components/elements/Code';
import { cn } from '@/lib/utils';
import { useStoreActions } from '@/state/hooks';
import { ServerContext } from '@/state/server';
// Assuming you use a utility like this for conditional classnames
interface FileUploadRowProps {
name: string;
loaded: number;
total: number;
}
export default function FileUploadRow({ name, loaded, total }: FileUploadRowProps) {
const cancel = ServerContext.useStoreActions((actions) => actions.files.cancelFileUpload);
const percent = Math.floor((loaded / total) * 100);
return (
<div className='flex items-center px-4 py-3 bg-zinc-800 border-b border-zinc-700 rounded-md space-x-4'>
<div className='flex-1 truncate'>
<Code>{name}</Code>
</div>
<div className='flex flex-col w-1/3'>
<Progress value={percent} className='h-2 rounded bg-zinc-700 overflow-hidden'>
<div className='h-full bg-blue-500 transition-all' style={{ width: `${percent}%` }} />
</Progress>
<div className='text-xs text-zinc-400 mt-1'>{percent}%</div>
</div>
<button
onClick={() => cancelFileUpload(name)}
className={cn('text-red-400 hover:text-red-200 transition-colors', 'p-1')}
title='Cancel upload'
>
<X size={16} />
</button>
</div>
);
}

View File

@@ -1,3 +1,4 @@
import { Ellipsis, Gear, House, Key, Lock } from '@gravity-ui/icons';
import { useStoreState } from 'easy-peasy';
import { Fragment, Suspense, useEffect, useRef, useState } from 'react';
import { NavLink, Route, Routes, useLocation } from 'react-router-dom';
@@ -18,10 +19,6 @@ import { DashboardMobileMenu } from '@/components/elements/MobileFullScreenMenu'
import MobileTopBar from '@/components/elements/MobileTopBar';
import Logo from '@/components/elements/PyroLogo';
import { NotFound } from '@/components/elements/ScreenBlock';
import HugeIconsApi from '@/components/elements/hugeicons/Api';
import HugeIconsDashboardSettings from '@/components/elements/hugeicons/DashboardSettings';
import HugeIconsHome from '@/components/elements/hugeicons/Home';
import HugeIconsSsh from '@/components/elements/hugeicons/Ssh';
import http from '@/api/http';
@@ -129,16 +126,8 @@ const DashboardRouter = () => {
<DropdownMenu>
<DropdownMenuTrigger asChild>
<button className='w-10 h-10 flex items-center justify-center rounded-md text-white hover:bg-white/10 p-2 cursor-pointer'>
<svg
xmlns='http://www.w3.org/2000/svg'
width='16'
height='15'
fill='currentColor'
viewBox='0 0 16 15'
className='flex shrink-0 h-full w-full'
>
<path d='M8.9375 7.3775C8.9375 7.56341 8.88252 7.74515 8.7795 7.89974C8.67649 8.05432 8.53007 8.1748 8.35877 8.24595C8.18746 8.31709 7.99896 8.33571 7.8171 8.29944C7.63525 8.26317 7.4682 8.17364 7.33709 8.04218C7.20598 7.91072 7.11669 7.74323 7.08051 7.56088C7.04434 7.37854 7.06291 7.18954 7.13386 7.01778C7.20482 6.84601 7.32498 6.69921 7.47915 6.59592C7.63332 6.49263 7.81458 6.4375 8 6.4375C8.24864 6.4375 8.4871 6.53654 8.66291 6.71282C8.83873 6.8891 8.9375 7.1282 8.9375 7.3775ZM1.625 6.4375C1.43958 6.4375 1.25832 6.49263 1.10415 6.59592C0.949982 6.69921 0.829821 6.84601 0.758863 7.01778C0.687906 7.18954 0.669341 7.37854 0.705514 7.56088C0.741688 7.74323 0.830976 7.91072 0.962088 8.04218C1.0932 8.17364 1.26025 8.26317 1.4421 8.29944C1.62396 8.33571 1.81246 8.31709 1.98377 8.24595C2.15507 8.1748 2.30149 8.05432 2.4045 7.89974C2.50752 7.74515 2.5625 7.56341 2.5625 7.3775C2.5625 7.1282 2.46373 6.8891 2.28791 6.71282C2.1121 6.53654 1.87364 6.4375 1.625 6.4375ZM14.375 6.4375C14.1896 6.4375 14.0083 6.49263 13.8542 6.59592C13.7 6.69921 13.5798 6.84601 13.5089 7.01778C13.4379 7.18954 13.4193 7.37854 13.4555 7.56088C13.4917 7.74323 13.581 7.91072 13.7121 8.04218C13.8432 8.17364 14.0102 8.26317 14.1921 8.29944C14.374 8.33571 14.5625 8.31709 14.7338 8.24595C14.9051 8.1748 15.0515 8.05432 15.1545 7.89974C15.2575 7.74515 15.3125 7.56341 15.3125 7.3775C15.3125 7.1282 15.2137 6.8891 15.0379 6.71282C14.8621 6.53654 14.6236 6.4375 14.375 6.4375Z' />
</svg>
{' '}
<Ellipsis fill='currentColor' width={26} height={22} />
</button>
</DropdownMenuTrigger>
<DropdownMenuContent className='z-99999' sideOffset={8}>
@@ -158,19 +147,19 @@ const DashboardRouter = () => {
<div aria-hidden className='mt-8 mb-4 bg-[#ffffff33] min-h-[1px] w-6'></div>
<ul data-pyro-subnav-routes-wrapper='' className='pyro-subnav-routes-wrapper'>
<NavLink to={'/'} end className='flex flex-row items-center' ref={NavigationHome}>
<HugeIconsHome fill='currentColor' />
<House width={22} height={22} fill='currentColor' />
<p>Servers</p>
</NavLink>
<NavLink to={'/account/api'} end className='flex flex-row items-center' ref={NavigationApi}>
<HugeIconsApi fill='currentColor' />
<Lock width={22} height={22} fill='currentColor' />
<p>API Keys</p>
</NavLink>
<NavLink to={'/account/ssh'} end className='flex flex-row items-center' ref={NavigationSSH}>
<HugeIconsSsh fill='currentColor' />
<Key width={22} height={22} fill='currentColor' />
<p>SSH Keys</p>
</NavLink>
<NavLink to={'/account'} end className='flex flex-row items-center' ref={NavigationSettings}>
<HugeIconsDashboardSettings fill='currentColor' />
<Gear width={22} height={22} fill='currentColor' />
<p>Settings</p>
</NavLink>
</ul>

View File

@@ -1,5 +1,19 @@
'use client';
import {
Box,
BranchesDown,
ClockArrowRotateLeft,
CloudArrowUpIn,
Database,
Ellipsis,
FolderOpen,
Gear,
House,
PencilToLine,
Persons,
Terminal,
} from '@gravity-ui/icons';
import { useStoreState } from 'easy-peasy';
import React, { Fragment, Suspense, useEffect, useRef, useState } from 'react';
import { NavLink, Route, Routes, useLocation, useParams } from 'react-router-dom';
@@ -19,23 +33,11 @@ import MainSidebar from '@/components/elements/MainSidebar';
import MainWrapper from '@/components/elements/MainWrapper';
import { ServerMobileMenu } from '@/components/elements/MobileFullScreenMenu';
import MobileTopBar from '@/components/elements/MobileTopBar';
// import ModrinthLogo from '@/components/elements/ModrinthLogo';
import ModrinthLogo from '@/components/elements/ModrinthLogo';
import PermissionRoute from '@/components/elements/PermissionRoute';
import Logo from '@/components/elements/PyroLogo';
import { NotFound, ServerError } from '@/components/elements/ScreenBlock';
import CommandMenu from '@/components/elements/commandk/CmdK';
import HugeIconsClock from '@/components/elements/hugeicons/Clock';
import HugeIconsCloudUp from '@/components/elements/hugeicons/CloudUp';
import HugeIconsConnections from '@/components/elements/hugeicons/Connections';
import HugeIconsConsole from '@/components/elements/hugeicons/Console';
import HugeIconsController from '@/components/elements/hugeicons/Controller';
import HugeIconsDashboardSettings from '@/components/elements/hugeicons/DashboardSettings';
import HugeIconsDatabase from '@/components/elements/hugeicons/Database';
import HugeIconsFolder from '@/components/elements/hugeicons/Folder';
import HugeIconsHome from '@/components/elements/hugeicons/Home';
import HugeIconsPencil from '@/components/elements/hugeicons/Pencil';
import HugeIconsPeople from '@/components/elements/hugeicons/People';
import HugeIconsZap from '@/components/elements/hugeicons/Zap';
import ConflictStateRenderer from '@/components/server/ConflictStateRenderer';
import InstallListener from '@/components/server/InstallListener';
import TransferListener from '@/components/server/TransferListener';
@@ -65,7 +67,7 @@ const DatabasesSidebarItem = React.forwardRef<HTMLAnchorElement, { id: string; o
onClick={onClick}
end
>
<HugeIconsDatabase fill='currentColor' />
<Database width={22} height={22} fill='currentColor' />
<p>Databases</p>
</NavLink>
</Can>
@@ -90,7 +92,7 @@ const BackupsSidebarItem = React.forwardRef<HTMLAnchorElement, { id: string; onC
onClick={onClick}
end
>
<HugeIconsCloudUp fill='currentColor' />
<CloudArrowUpIn width={22} height={22} fill='currentColor' />
<p>Backups</p>
</NavLink>
</Can>
@@ -134,7 +136,7 @@ const NetworkingSidebarItem = React.forwardRef<HTMLAnchorElement, { id: string;
onClick={onClick}
end
>
<HugeIconsConnections fill='currentColor' />
<BranchesDown width={22} height={22} fill='currentColor' />
<p>Networking</p>
</NavLink>
</Can>
@@ -365,16 +367,7 @@ const ServerRouter = () => {
<DropdownMenu>
<DropdownMenuTrigger asChild>
<button className='w-10 h-10 flex items-center justify-center rounded-md text-white hover:bg-[#ffffff11] p-2 select-none cursor-pointer'>
<svg
xmlns='http://www.w3.org/2000/svg'
width='16'
height='15'
fill='currentColor'
viewBox='0 0 16 15'
className='flex shrink-0 h-full w-full'
>
<path d='M8.9375 7.3775C8.9375 7.56341 8.88252 7.74515 8.7795 7.89974C8.67649 8.05432 8.53007 8.1748 8.35877 8.24595C8.18746 8.31709 7.99896 8.33571 7.8171 8.29944C7.63525 8.26317 7.4682 8.17364 7.33709 8.04218C7.20598 7.91072 7.11669 7.74323 7.08051 7.56088C7.04434 7.37854 7.06291 7.18954 7.13386 7.01778C7.20482 6.84601 7.32498 6.69921 7.47915 6.59592C7.63332 6.49263 7.81458 6.4375 8 6.4375C8.24864 6.4375 8.4871 6.53654 8.66291 6.71282C8.83873 6.8891 8.9375 7.1282 8.9375 7.3775ZM1.625 6.4375C1.43958 6.4375 1.25832 6.49263 1.10415 6.59592C0.949982 6.69921 0.829821 6.84601 0.758863 7.01778C0.687906 7.18954 0.669341 7.37854 0.705514 7.56088C0.741688 7.74323 0.830976 7.91072 0.962088 8.04218C1.0932 8.17364 1.26025 8.26317 1.4421 8.29944C1.62396 8.33571 1.81246 8.31709 1.98377 8.24595C2.15507 8.1748 2.30149 8.05432 2.4045 7.89974C2.50752 7.74515 2.5625 7.56341 2.5625 7.3775C2.5625 7.1282 2.46373 6.8891 2.28791 6.71282C2.1121 6.53654 1.87364 6.4375 1.625 6.4375ZM14.375 6.4375C14.1896 6.4375 14.0083 6.49263 13.8542 6.59592C13.7 6.69921 13.5798 6.84601 13.5089 7.01778C13.4379 7.18954 13.4193 7.37854 13.4555 7.56088C13.4917 7.74323 13.581 7.91072 13.7121 8.04218C13.8432 8.17364 14.0102 8.26317 14.1921 8.29944C14.374 8.33571 14.5625 8.31709 14.7338 8.24595C14.9051 8.1748 15.0515 8.05432 15.1545 7.89974C15.2575 7.74515 15.3125 7.56341 15.3125 7.3775C15.3125 7.1282 15.2137 6.8891 15.0379 6.71282C14.8621 6.53654 14.6236 6.4375 14.375 6.4375Z' />
</svg>
<Ellipsis fill='currentColor' width={26} height={22} />
</button>
</DropdownMenuTrigger>
<DropdownMenuContent className='z-99999 select-none relative' sideOffset={8}>
@@ -403,7 +396,7 @@ const ServerRouter = () => {
to={`/server/${id}`}
end
>
<HugeIconsHome fill='currentColor' />
<House width={22} height={22} fill='currentColor' />
<p>Home</p>
</NavLink>
<>
@@ -413,7 +406,7 @@ const ServerRouter = () => {
ref={NavigationFiles}
to={`/server/${id}/files`}
>
<HugeIconsFolder fill='currentColor' />
<FolderOpen width={22} height={22} fill='currentColor' />
<p>Files</p>
</NavLink>
</Can>
@@ -427,7 +420,7 @@ const ServerRouter = () => {
to={`/server/${id}/users`}
end
>
<HugeIconsPeople fill='currentColor' />
<Persons width={22} height={22} fill='currentColor' />
<p>Users</p>
</NavLink>
</Can>
@@ -446,7 +439,7 @@ const ServerRouter = () => {
to={`/server/${id}/startup`}
end
>
<HugeIconsConsole fill='currentColor' />
<Terminal width={22} height={22} fill='currentColor' />
<p>Startup</p>
</NavLink>
</Can>
@@ -456,7 +449,7 @@ const ServerRouter = () => {
ref={NavigationSchedules}
to={`/server/${id}/schedules`}
>
<HugeIconsClock fill='currentColor' />
<ClockArrowRotateLeft width={22} height={22} fill='currentColor' />
<p>Schedules</p>
</NavLink>
</Can>
@@ -467,7 +460,7 @@ const ServerRouter = () => {
to={`/server/${id}/settings`}
end
>
<HugeIconsDashboardSettings fill='currentColor' />
<Gear width={22} height={22} fill='currentColor' />
<p>Settings</p>
</NavLink>
</Can>
@@ -478,7 +471,7 @@ const ServerRouter = () => {
to={`/server/${id}/activity`}
end
>
<HugeIconsPencil fill='currentColor' />
<PencilToLine width={22} height={22} fill='currentColor' />
<p>Activity</p>
</NavLink>
</Can>
@@ -502,7 +495,7 @@ const ServerRouter = () => {
to={`/server/${id}/shell`}
end
>
<HugeIconsController fill='currentColor' />
<Box width={22} height={22} fill='currentColor' />
<p>Software</p>
</NavLink>
</Can>