refactor: use new web service architecture (1/2)

This commit is contained in:
izzy
2026-01-13 13:18:58 +00:00
parent 67cc937bb0
commit 69b2e36a38
6 changed files with 263 additions and 168 deletions

View File

@@ -0,0 +1,41 @@
<script lang="ts">
import OnEvents from '$lib/components/OnEvents.svelte';
import { getIntegrityReportItemActions } from '$lib/services/integrity.service';
import type { IntegrityReportType } from '@immich/sdk';
import { ContextMenuButton, TableCell, TableRow } from '@immich/ui';
import { t } from 'svelte-i18n';
interface Props {
id: string;
path: string;
reportType: IntegrityReportType;
}
let { id, path, reportType }: Props = $props();
let deleting = $state(false);
const { Download, Delete } = $derived(getIntegrityReportItemActions($t, id, reportType));
function onIntegrityReportDelete({
id: reportId,
type,
isDeleting,
}: {
id?: string;
type?: IntegrityReportType;
isDeleting: boolean;
}) {
if (type === reportType || reportId === id) {
deleting = isDeleting;
}
}
</script>
<OnEvents {onIntegrityReportDelete} />
<TableRow>
<TableCell class="w-7/8 text-left px-4">{path}</TableCell>
<TableCell class="w-1/8 flex justify-end">
<ContextMenuButton disabled={deleting} position="top-right" aria-label={$t('open')} items={[Download, Delete]} />
</TableCell>
</TableRow>

View File

@@ -5,6 +5,7 @@ import type {
AlbumResponseDto,
ApiKeyResponseDto,
AssetResponseDto,
IntegrityReportType,
LibraryResponseDto,
LoginResponseDto,
PersonResponseDto,
@@ -64,6 +65,8 @@ export type Events = {
SystemConfigUpdate: [SystemConfigDto];
IntegrityReportDelete: [{ type?: IntegrityReportType; id?: string; isDeleting: boolean; isDeleted: boolean }];
LibraryCreate: [LibraryResponseDto];
LibraryUpdate: [LibraryResponseDto];
LibraryDelete: [{ id: string }];

View File

@@ -0,0 +1,151 @@
import { eventManager } from '$lib/managers/event-manager.svelte';
import { handleError } from '$lib/utils/handle-error';
import { getFormatter } from '$lib/utils/i18n';
import { createJob, deleteIntegrityReport, getBaseUrl, IntegrityReportType, ManualJobName } from '@immich/sdk';
import { modalManager, toastManager, type ActionItem } from '@immich/ui';
import { mdiDownload, mdiTrashCanOutline } from '@mdi/js';
import type { MessageFormatter } from 'svelte-i18n';
export const getIntegrityReportActions = ($t: MessageFormatter, reportType: IntegrityReportType) => {
const Download: ActionItem = {
type: $t('command'),
title: $t('admin.download_csv'),
icon: mdiDownload,
onAction() {
handleDownloadIntegrityReportCsv(reportType);
},
};
const Delete: ActionItem = {
type: $t('command'),
title: $t('trash_page_delete_all'),
icon: mdiTrashCanOutline,
color: 'danger',
onAction() {
void handleRemoveAllIntegrityReportItems(reportType);
},
};
return { Download, Delete };
};
export const getIntegrityReportItemActions = (
$t: MessageFormatter,
reportId: string,
reportType: IntegrityReportType,
) => {
const Download =
reportType === IntegrityReportType.UntrackedFile || reportType === IntegrityReportType.ChecksumMismatch
? {
title: $t('download'),
icon: mdiDownload,
onAction() {
void handleDownloadIntegrityReportFile(reportId);
},
}
: undefined;
const Delete = {
title: $t('delete'),
icon: mdiTrashCanOutline,
color: 'danger',
onAction() {
void handleRemoveIntegrityReportItem(reportId);
},
};
return { Download, Delete };
};
export const handleDownloadIntegrityReportFile = (reportId: string) => {
location.href = `${getBaseUrl()}/admin/integrity/report/${reportId}/file`;
};
export const handleDownloadIntegrityReportCsv = (reportType: IntegrityReportType) => {
location.href = `${getBaseUrl()}/admin/integrity/report/${reportType}/csv`;
};
export const handleRemoveAllIntegrityReportItems = async (reportType: IntegrityReportType) => {
const $t = await getFormatter();
const confirm = await modalManager.showDialog({
confirmText: $t('delete'),
});
if (confirm) {
let name: ManualJobName;
switch (reportType) {
case IntegrityReportType.UntrackedFile: {
name = ManualJobName.IntegrityUntrackedFilesDeleteAll;
break;
}
case IntegrityReportType.MissingFile: {
name = ManualJobName.IntegrityMissingFilesDeleteAll;
break;
}
case IntegrityReportType.ChecksumMismatch: {
name = ManualJobName.IntegrityChecksumMismatchDeleteAll;
break;
}
}
try {
eventManager.emit('IntegrityReportDelete', {
type: reportType,
isDeleting: true,
isDeleted: false,
});
await createJob({ jobCreateDto: { name } });
toastManager.success($t('admin.job_created'));
eventManager.emit('IntegrityReportDelete', {
type: reportType,
isDeleting: false,
isDeleted: true,
});
} catch (error) {
handleError(error, $t('failed_to_delete_file'));
eventManager.emit('IntegrityReportDelete', {
type: reportType,
isDeleting: false,
isDeleted: false,
});
}
}
};
export const handleRemoveIntegrityReportItem = async (reportId: string) => {
const $t = await getFormatter();
const confirm = await modalManager.showDialog({
confirmText: $t('delete'),
});
if (confirm) {
try {
eventManager.emit('IntegrityReportDelete', {
id: reportId,
isDeleting: true,
isDeleted: false,
});
await deleteIntegrityReport({
id: reportId,
});
eventManager.emit('IntegrityReportDelete', {
id: reportId,
isDeleting: false,
isDeleted: true,
});
} catch (error) {
handleError(error, $t('failed_to_delete_file'));
eventManager.emit('IntegrityReportDelete', {
id: reportId,
isDeleting: false,
isDeleted: false,
});
}
}
};

View File

@@ -0,0 +1,31 @@
import { handleError } from '$lib/utils/handle-error';
import { getFormatter } from '$lib/utils/i18n';
import { MaintenanceAction, setMaintenanceMode, type SetMaintenanceModeDto } from '@immich/sdk';
import type { ActionItem } from '@immich/ui';
import { mdiProgressWrench } from '@mdi/js';
import type { MessageFormatter } from 'svelte-i18n';
export const getMaintenanceAdminActions = ($t: MessageFormatter) => {
const StartMaintenance: ActionItem = {
title: $t('admin.maintenance_start'),
onAction: () =>
handleSetMaintenanceMode({
action: MaintenanceAction.Start,
}),
icon: mdiProgressWrench,
};
return { StartMaintenance };
};
export const handleSetMaintenanceMode = async (dto: SetMaintenanceModeDto) => {
const $t = await getFormatter();
try {
await setMaintenanceMode({
setMaintenanceModeDto: dto,
});
} catch (error) {
handleError(error, $t('admin.maintenance_start_error'));
}
};

View File

@@ -2,6 +2,7 @@
import AdminPageLayout from '$lib/components/layouts/AdminPageLayout.svelte';
import ServerStatisticsCard from '$lib/components/server-statistics/ServerStatisticsCard.svelte';
import { AppRoute } from '$lib/constants';
import { getMaintenanceAdminActions } from '$lib/services/maintenance.service';
import { asyncTimeout } from '$lib/utils';
import { handleError } from '$lib/utils/handle-error';
import {
@@ -9,14 +10,11 @@
getIntegrityReportSummary,
getQueuesLegacy,
IntegrityReportType,
MaintenanceAction,
ManualJobName,
setMaintenanceMode,
type IntegrityReportSummaryResponseDto,
type QueuesResponseLegacyDto,
} from '@immich/sdk';
import { Button, HStack, toastManager } from '@immich/ui';
import { mdiProgressWrench } from '@mdi/js';
import { onDestroy, onMount } from 'svelte';
import { t } from 'svelte-i18n';
import type { PageData } from './$types';
@@ -25,7 +23,8 @@
data: PageData;
}
let { data }: Props = $props();
const { data }: Props = $props();
const { StartMaintenance } = $derived(getMaintenanceAdminActions($t));
let integrityReport: IntegrityReportSummaryResponseDto = $state(data.integrityReport);
@@ -35,18 +34,6 @@
IntegrityReportType.ChecksumMismatch,
];
async function switchToMaintenance() {
try {
await setMaintenanceMode({
setMaintenanceModeDto: {
action: MaintenanceAction.Start,
},
});
} catch (error) {
handleError(error, $t('admin.maintenance_start_error'));
}
}
let jobs: QueuesResponseLegacyDto | undefined = $state();
let expectingUpdate: boolean = $state(false);
@@ -106,16 +93,7 @@
});
</script>
<AdminPageLayout
breadcrumbs={[{ title: data.meta.title }]}
actions={[
{
title: $t('admin.maintenance_start'),
onAction: switchToMaintenance,
icon: mdiProgressWrench,
},
]}
>
<AdminPageLayout breadcrumbs={[{ title: data.meta.title }]} actions={[StartMaintenance]}>
<section id="setting-content" class="flex place-content-center sm:mx-4">
<section class="w-full pb-28 sm:w-5/6 md:w-[850px]">
<HStack>

View File

@@ -1,36 +1,14 @@
<script lang="ts">
import AdminPageLayout from '$lib/components/layouts/AdminPageLayout.svelte';
import IntegrityReportTableItem from '$lib/components/maintenance/integrity/IntegrityReportTableItem.svelte';
import OnEvents from '$lib/components/OnEvents.svelte';
import { AppRoute } from '$lib/constants';
import { getIntegrityReportActions } from '$lib/services/integrity.service';
import { asyncTimeout } from '$lib/utils';
import { handleError } from '$lib/utils/handle-error';
import {
createJob,
deleteIntegrityReport,
getBaseUrl,
getIntegrityReport,
getQueuesLegacy,
IntegrityReportType,
ManualJobName,
} from '@immich/sdk';
import {
Button,
IconButton,
menuManager,
modalManager,
Table,
TableBody,
TableCell,
TableHeader,
TableHeading,
TableRow,
toastManager,
type ContextMenuBaseProps,
type MenuItems,
} from '@immich/ui';
import { mdiDotsVertical, mdiDownload, mdiTrashCanOutline } from '@mdi/js';
import { getIntegrityReport, getQueuesLegacy, IntegrityReportType } from '@immich/sdk';
import { Button, Table, TableBody, TableHeader, TableHeading } from '@immich/ui';
import { onDestroy, onMount } from 'svelte';
import { t } from 'svelte-i18n';
import { SvelteSet } from 'svelte/reactivity';
import type { PageData } from './$types';
interface Props {
@@ -39,7 +17,6 @@
let { data }: Props = $props();
let deleting = new SvelteSet();
let integrityReport = $state(data.integrityReport);
async function loadMore() {
@@ -54,92 +31,6 @@
integrityReport.nextCursor = nextCursor;
}
async function removeAll() {
const confirm = await modalManager.showDialog({
confirmText: $t('delete'),
});
if (confirm) {
let name: ManualJobName;
switch (data.type) {
case IntegrityReportType.UntrackedFile: {
name = ManualJobName.IntegrityUntrackedFilesDeleteAll;
break;
}
case IntegrityReportType.MissingFile: {
name = ManualJobName.IntegrityMissingFilesDeleteAll;
break;
}
case IntegrityReportType.ChecksumMismatch: {
name = ManualJobName.IntegrityChecksumMismatchDeleteAll;
break;
}
}
try {
deleting.add('all');
await createJob({ jobCreateDto: { name } });
toastManager.success($t('admin.job_created'));
} catch (error) {
handleError(error, $t('failed_to_delete_file'));
}
}
}
async function remove(id: string) {
const confirm = await modalManager.showDialog({
confirmText: $t('delete'),
});
if (confirm) {
try {
deleting.add(id);
await deleteIntegrityReport({
id,
});
integrityReport.items = integrityReport.items.filter((report) => report.id !== id);
} catch (error) {
handleError(error, $t('failed_to_delete_file'));
} finally {
deleting.delete(id);
}
}
}
function download(reportId: string) {
location.href = `${getBaseUrl()}/admin/integrity/report/${reportId}/file`;
}
const handleOpen = async (event: Event, props: Partial<ContextMenuBaseProps>, reportId: string) => {
const items: MenuItems = [];
if (data.type === IntegrityReportType.UntrackedFile || data.type === IntegrityReportType.ChecksumMismatch) {
items.push({
title: $t('download'),
icon: mdiDownload,
onAction() {
void download(reportId);
},
});
}
await menuManager.show({
...props,
target: event.currentTarget as HTMLElement,
items: [
...items,
{
title: $t('delete'),
icon: mdiTrashCanOutline,
color: 'danger',
onAction() {
void remove(reportId);
},
},
],
});
};
let running = true;
let expectingUpdate = false;
@@ -164,31 +55,43 @@
onDestroy(() => {
running = false;
});
const { Download, Delete } = $derived(getIntegrityReportActions($t, data.type));
function onIntegrityReportDelete({
id,
type,
isDeleted,
}: {
id?: string;
type?: IntegrityReportType;
isDeleted: boolean;
}) {
if (!isDeleted) {
return;
}
if (type === data.type) {
integrityReport.items = [];
integrityReport.nextCursor = undefined;
} else {
integrityReport.items = integrityReport.items.filter((report) => report.id !== id);
}
}
</script>
<OnEvents {onIntegrityReportDelete} />
<AdminPageLayout
breadcrumbs={[
{ title: $t('admin.maintenance_settings'), href: AppRoute.ADMIN_MAINTENANCE_SETTINGS },
{ title: $t('admin.maintenance_integrity_report') },
{ title: data.meta.title },
]}
actions={[
{
title: $t('admin.download_csv'),
icon: mdiDownload,
onAction: () => {
location.href = `${getBaseUrl()}/admin/maintenance/integrity/report/${data.type}/csv`;
},
},
{
title: $t('trash_page_delete_all'),
onAction: removeAll,
icon: mdiTrashCanOutline,
},
]}
actions={[Download, Delete]}
>
<section id="setting-content" class="flex place-content-center sm:mx-4">
<section class="w-full pb-28 sm:w-5/6 md:w-[850px]">
<section class="w-full pb-28 sm:w-5/6 md:w-212.5">
<Table striped spacing="tiny">
<TableHeader>
<TableHeading class="w-7/8 text-left">{$t('filename')}</TableHeading>
@@ -197,19 +100,7 @@
<TableBody>
{#each integrityReport.items as { id, path } (id)}
<TableRow>
<TableCell class="w-7/8 text-left px-4">{path}</TableCell>
<TableCell class="w-1/8 flex justify-end"
><IconButton
color="secondary"
icon={mdiDotsVertical}
variant="ghost"
onclick={(event: Event) => handleOpen(event, { position: 'top-right' }, id)}
aria-label={$t('open')}
disabled={deleting.has(id) || deleting.has('all')}
/></TableCell
>
</TableRow>
<IntegrityReportTableItem {id} {path} reportType={data.type} />
{/each}
</TableBody>