feat: new actions page + finish impl v4 export

This commit is contained in:
diced
2025-12-08 23:30:04 -08:00
parent ca09b1319d
commit 589f06b460
23 changed files with 146 additions and 62 deletions

View File

@@ -0,0 +1,10 @@
import DashboardServerActions from '@/components/pages/serverActions';
import { useTitle } from '@/lib/hooks/useTitle';
export function Component() {
useTitle('Server Actions');
return <DashboardServerActions />;
}
Component.displayName = 'Dashboard/Admin/Actions';

View File

@@ -82,6 +82,7 @@ export const router = createBrowserRouter([
children: [ children: [
{ path: 'invites', lazy: () => import('./pages/dashboard/admin/invites') }, { path: 'invites', lazy: () => import('./pages/dashboard/admin/invites') },
{ path: 'settings', lazy: () => import('./pages/dashboard/admin/settings') }, { path: 'settings', lazy: () => import('./pages/dashboard/admin/settings') },
{ path: 'actions', lazy: () => import('./pages/dashboard/admin/actions') },
{ {
path: 'users', path: 'users',
children: [ children: [

View File

@@ -41,6 +41,7 @@ import {
IconRefreshDot, IconRefreshDot,
IconSettingsFilled, IconSettingsFilled,
IconShieldLockFilled, IconShieldLockFilled,
IconStopwatch,
IconTags, IconTags,
IconUpload, IconUpload,
IconUsersGroup, IconUsersGroup,
@@ -126,6 +127,12 @@ const navLinks: NavLinks[] = [
if: (user) => user?.role === 'SUPERADMIN', if: (user) => user?.role === 'SUPERADMIN',
href: '/dashboard/admin/settings', href: '/dashboard/admin/settings',
}, },
{
label: 'Actions',
icon: <IconStopwatch size='1rem' />,
active: (path: string) => path === '/dashboard/admin/actions',
href: '/dashboard/admin/actions',
},
{ {
label: 'Users', label: 'Users',
icon: <IconUsersGroup size='1rem' />, icon: <IconUsersGroup size='1rem' />,

View File

@@ -0,0 +1,19 @@
import { ActionIcon } from '@mantine/core';
import { IconPlayerPlayFilled } from '@tabler/icons-react';
const ICON_SIZE = '1.75rem';
export default function ActionButton({ onClick, Icon }: { onClick: () => void; Icon?: React.FC<any> }) {
return (
<ActionIcon
onClick={onClick}
variant='filled'
color='blue'
radius='md'
size='xl'
className='zip-click-action-button'
>
{Icon ? <Icon size={ICON_SIZE} /> : <IconPlayerPlayFilled size={ICON_SIZE} />}
</ActionIcon>
);
}

View File

@@ -1,9 +1,9 @@
import { Response } from '@/lib/api/response'; import { Response } from '@/lib/api/response';
import { fetchApi } from '@/lib/fetchApi'; import { fetchApi } from '@/lib/fetchApi';
import { Button } from '@mantine/core';
import { modals } from '@mantine/modals'; import { modals } from '@mantine/modals';
import { showNotification } from '@mantine/notifications'; import { showNotification } from '@mantine/notifications';
import { IconTrashFilled } from '@tabler/icons-react'; import { IconTrashFilled } from '@tabler/icons-react';
import ActionButton from '../ActionButton';
export default function ClearTempButton() { export default function ClearTempButton() {
const openModal = () => const openModal = () =>
@@ -30,11 +30,5 @@ export default function ClearTempButton() {
}, },
}); });
return ( return <ActionButton onClick={openModal} Icon={IconTrashFilled} />;
<>
<Button size='sm' leftSection={<IconTrashFilled size='1rem' />} onClick={openModal}>
Clear Temp Files
</Button>
</>
);
} }

View File

@@ -1,10 +1,10 @@
import { Response } from '@/lib/api/response'; import { Response } from '@/lib/api/response';
import { fetchApi } from '@/lib/fetchApi'; import { fetchApi } from '@/lib/fetchApi';
import { Button } from '@mantine/core';
import { modals } from '@mantine/modals'; import { modals } from '@mantine/modals';
import { showNotification } from '@mantine/notifications'; import { showNotification } from '@mantine/notifications';
import { IconTrashFilled } from '@tabler/icons-react'; import { IconTrashFilled } from '@tabler/icons-react';
import useSWR from 'swr'; import useSWR from 'swr';
import ActionButton from '../ActionButton';
export default function ClearZerosButton() { export default function ClearZerosButton() {
const { data } = useSWR<Response['/api/server/clear_zeros']>('/api/server/clear_zeros'); const { data } = useSWR<Response['/api/server/clear_zeros']>('/api/server/clear_zeros');
@@ -32,11 +32,5 @@ export default function ClearZerosButton() {
}, },
}); });
return ( return <ActionButton onClick={openModal} Icon={IconTrashFilled} />;
<>
<Button size='sm' leftSection={<IconTrashFilled size='1rem' />} onClick={openModal}>
Clear Zero Byte Files
</Button>
</>
);
} }

View File

@@ -2,8 +2,9 @@ import { Response } from '@/lib/api/response';
import { fetchApi } from '@/lib/fetchApi'; import { fetchApi } from '@/lib/fetchApi';
import { Button, Group, Modal, Stack, Switch } from '@mantine/core'; import { Button, Group, Modal, Stack, Switch } from '@mantine/core';
import { showNotification } from '@mantine/notifications'; import { showNotification } from '@mantine/notifications';
import { IconVideo, IconVideoOff } from '@tabler/icons-react'; import { IconVideoOff, IconVideoPlusFilled } from '@tabler/icons-react';
import { useState } from 'react'; import { useState } from 'react';
import ActionButton from '../ActionButton';
export default function GenThumbsButton() { export default function GenThumbsButton() {
const [rerun, setRerun] = useState(false); const [rerun, setRerun] = useState(false);
@@ -53,9 +54,8 @@ export default function GenThumbsButton() {
</Button> </Button>
</Group> </Group>
</Modal> </Modal>
<Button size='sm' leftSection={<IconVideo size='1rem' />} onClick={() => setOpen(true)}>
Generate Thumbnails <ActionButton onClick={() => setOpen(true)} Icon={IconVideoPlusFilled} />
</Button>
</> </>
); );
} }

View File

@@ -93,8 +93,6 @@ export default function ImportV3Button() {
color: 'green', color: 'green',
icon: <IconDeviceFloppy size='1rem' />, icon: <IconDeviceFloppy size='1rem' />,
}); });
await fetch('/reload');
} }
}; };

View File

@@ -11,6 +11,7 @@ import { useUserStore } from '@/lib/store/user';
import { modals } from '@mantine/modals'; import { modals } from '@mantine/modals';
import { fetchApi } from '@/lib/fetchApi'; import { fetchApi } from '@/lib/fetchApi';
import { Response } from '@/lib/api/response'; import { Response } from '@/lib/api/response';
import { mutate } from 'swr';
export default function ImportV4Button() { export default function ImportV4Button() {
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
@@ -52,6 +53,36 @@ export default function ImportV4Button() {
setExport4(validated.data); setExport4(validated.data);
}; };
const handleImportSettings = async () => {
if (!export4) return;
const { error } = await fetchApi<Response['/api/server/settings']>(
'/api/server/settings',
'PATCH',
export4.data.settings,
);
if (error) {
showNotification({
title: 'Failed to import settings',
message: error.issues
? error.issues.map((x: { message: string }) => x.message).join('\n')
: error.error,
color: 'red',
});
} else {
showNotification({
title: 'Settings imported',
message: 'To ensure that all settings take effect, it is recommended to restart Zipline.',
color: 'green',
});
mutate('/api/server/settings');
mutate('/api/server/settings/web');
mutate('/api/server/public');
}
};
const handleImport = async () => { const handleImport = async () => {
if (!export4) return; if (!export4) return;
@@ -88,6 +119,8 @@ export default function ImportV4Button() {
setOpen(false); setOpen(false);
await handleImportSettings();
const { error, data } = await fetchApi<Response['/api/server/import/v4']>( const { error, data } = await fetchApi<Response['/api/server/import/v4']>(
'/api/server/import/v4', '/api/server/import/v4',
'POST', 'POST',

View File

@@ -1,9 +1,10 @@
import { Button, Divider, Group, Modal } from '@mantine/core'; import { Divider, Group, Modal } from '@mantine/core';
import { IconDatabaseExport } from '@tabler/icons-react';
import { useState } from 'react'; import { useState } from 'react';
import ImportV3Button from './ImportV3Button'; import ImportV3Button from './ImportV3Button';
import ImportV4Button from './ImportV4Button'; import ImportV4Button from './ImportV4Button';
import ExportButton from './ExportButton'; import ExportButton from './ExportButton';
import ActionButton from '../../ActionButton';
import { IconDatabasePlus } from '@tabler/icons-react';
export default function ImportExport() { export default function ImportExport() {
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
@@ -21,9 +22,7 @@ export default function ImportExport() {
<ExportButton /> <ExportButton />
</Modal> </Modal>
<Button size='sm' leftSection={<IconDatabaseExport size='1rem' />} onClick={() => setOpen(true)}> <ActionButton onClick={() => setOpen(true)} Icon={IconDatabasePlus} />
Import / Export Data
</Button>
</> </>
); );
} }

View File

@@ -4,6 +4,7 @@ import { Button, Group, Modal, Stack, Switch } from '@mantine/core';
import { showNotification } from '@mantine/notifications'; import { showNotification } from '@mantine/notifications';
import { IconFileSearch } from '@tabler/icons-react'; import { IconFileSearch } from '@tabler/icons-react';
import { useState } from 'react'; import { useState } from 'react';
import ActionButton from '../ActionButton';
export default function RequerySizeButton() { export default function RequerySizeButton() {
const [forceUpdate, setForceUpdate] = useState(false); const [forceUpdate, setForceUpdate] = useState(false);
@@ -65,9 +66,8 @@ export default function RequerySizeButton() {
</Button> </Button>
</Group> </Group>
</Modal> </Modal>
<Button size='sm' leftSection={<IconFileSearch size='1rem' />} onClick={() => setOpen(true)}>
Requery Size of Files <ActionButton onClick={() => setOpen(true)} />
</Button>
</> </>
); );
} }

View File

@@ -0,0 +1,61 @@
import { Group, Paper, Stack, Text, Title } from '@mantine/core';
import ClearTempButton from './actions/ClearTempButton';
import ClearZerosButton from './actions/ClearZerosButton';
import GenThumbsButton from './actions/GenThumbsButton';
import ImportExport from './actions/ImportExportButton';
import RequerySizeButton from './actions/RequerySizeButton';
const ACTIONS = [
{
name: 'Import/Export Data',
desc: 'Allows you to import or export server data and configurations.',
Component: ImportExport,
},
{
name: 'Clear Temporary Files',
desc: 'Removes all temporary files from the temporary directory.',
Component: ClearTempButton,
},
{
name: 'Clear Zero Byte Files',
desc: 'Deletes all files with zero bytes from the database and/or storage.',
Component: ClearZerosButton,
},
{
name: 'Requery File Sizes',
desc: 'Recalculates and updates the sizes of all files in the database.',
Component: RequerySizeButton,
},
{
name: 'Generate Thumbnails',
desc: 'Creates thumbnails for all image and video files that lack them.',
Component: GenThumbsButton,
},
];
export default function DashboardServerActions() {
return (
<>
<Group gap='sm'>
<Title order={1}>Server Actions</Title>
</Group>
<Text c='dimmed' mb='xs'>
Useful tools and scripts for server management.
</Text>
<Stack gap='xs' my='sm'>
{ACTIONS.map(({ name, desc, Component }) => (
<Paper withBorder p='sm' key={name}>
<Group gap='md'>
<Component />
<div>
<Title order={4}>{name}</Title>
<Text c='dimmed'>{desc}</Text>
</div>
</Group>
</Paper>
))}
</Stack>
</>
);
}

View File

@@ -39,7 +39,6 @@ export function settingsOnSubmit(navigate: NavigateFunction, form: ReturnType<ty
icon: <IconDeviceFloppy size='1rem' />, icon: <IconDeviceFloppy size='1rem' />,
}); });
await fetch('/reload');
mutate('/api/server/settings', data); mutate('/api/server/settings', data);
mutate('/api/server/settings/web'); mutate('/api/server/settings/web');
mutate('/api/server/public'); mutate('/api/server/public');

View File

@@ -1,7 +1,5 @@
import { useConfig } from '@/components/ConfigProvider'; import { useConfig } from '@/components/ConfigProvider';
import { eitherTrue } from '@/lib/primitive'; import { eitherTrue } from '@/lib/primitive';
import { isAdministrator } from '@/lib/role';
import { useUserStore } from '@/lib/store/user';
import { Group, SimpleGrid, Stack, Title } from '@mantine/core'; import { Group, SimpleGrid, Stack, Title } from '@mantine/core';
import { lazy } from 'react'; import { lazy } from 'react';
@@ -10,7 +8,6 @@ const SettingsDashboard = lazy(() => import('./parts/SettingsDashboard'));
const SettingsFileView = lazy(() => import('./parts/SettingsFileView')); const SettingsFileView = lazy(() => import('./parts/SettingsFileView'));
const SettingsGenerators = lazy(() => import('./parts/SettingsGenerators')); const SettingsGenerators = lazy(() => import('./parts/SettingsGenerators'));
const SettingsMfa = lazy(() => import('./parts/SettingsMfa')); const SettingsMfa = lazy(() => import('./parts/SettingsMfa'));
const SettingsServerActions = lazy(() => import('./parts/SettingsServerUtil'));
const SettingsUser = lazy(() => import('./parts/SettingsUser')); const SettingsUser = lazy(() => import('./parts/SettingsUser'));
const SettingsExports = lazy(() => import('./parts/SettingsExports')); const SettingsExports = lazy(() => import('./parts/SettingsExports'));
const SettingsSessions = lazy(() => import('./parts/SettingsSessions')); const SettingsSessions = lazy(() => import('./parts/SettingsSessions'));
@@ -18,7 +15,6 @@ const SettingsOAuth = lazy(() => import('./parts/SettingsOAuth'));
export default function DashboardSettings() { export default function DashboardSettings() {
const config = useConfig(); const config = useConfig();
const user = useUserStore((state) => state.user);
return ( return (
<> <>
@@ -49,8 +45,6 @@ export default function DashboardSettings() {
<SettingsExports /> <SettingsExports />
<SettingsGenerators /> <SettingsGenerators />
{isAdministrator(user?.role) && <SettingsServerActions />}
</SimpleGrid> </SimpleGrid>
</> </>
); );

View File

@@ -1,25 +0,0 @@
import { Group, Paper, Text, Title } from '@mantine/core';
import ClearTempButton from './ClearTempButton';
import ClearZerosButton from './ClearZerosButton';
import GenThumbsButton from './GenThumbsButton';
import RequerySizeButton from './RequerySizeButton';
import ImportExportButton from './ImportExportButton';
export default function SettingsServerActions() {
return (
<Paper withBorder p='sm'>
<Title order={2}>Server Actions</Title>
<Text size='sm' c='dimmed' mt={3}>
Helpful scripts and tools for server management.
</Text>
<Group mt='xs'>
<ClearZerosButton />
<ClearTempButton />
<RequerySizeButton />
<GenThumbsButton />
<ImportExportButton />
</Group>
</Paper>
);
}