feat: queue detail page (#24352)

This commit is contained in:
Jason Rasmussen
2025-12-03 13:39:32 -05:00
committed by GitHub
parent 4f93eda8d8
commit 45f68f73a9
28 changed files with 883 additions and 401 deletions
@@ -1,116 +0,0 @@
<script lang="ts">
import { goto } from '$app/navigation';
import JobsPanel from '$lib/components/jobs/JobsPanel.svelte';
import AdminPageLayout from '$lib/components/layouts/AdminPageLayout.svelte';
import { AppRoute } from '$lib/constants';
import JobCreateModal from '$lib/modals/JobCreateModal.svelte';
import { asyncTimeout } from '$lib/utils';
import { handleError } from '$lib/utils/handle-error';
import {
getQueuesLegacy,
QueueCommand,
QueueName,
runQueueCommandLegacy,
type QueuesResponseLegacyDto,
} from '@immich/sdk';
import { Button, CommandPaletteContext, HStack, modalManager, Text, type ActionItem } from '@immich/ui';
import { mdiCog, mdiPlay, mdiPlus } from '@mdi/js';
import { onDestroy, onMount } from 'svelte';
import { t } from 'svelte-i18n';
import type { PageData } from './$types';
interface Props {
data: PageData;
}
let { data }: Props = $props();
let jobs: QueuesResponseLegacyDto | undefined = $state();
let running = true;
const pausedJobs = $derived(
Object.entries(jobs ?? {})
.filter(([_, queue]) => queue.queueStatus?.isPaused)
.map(([name]) => name as QueueName),
);
const handleResumePausedJobs = async () => {
try {
for (const name of pausedJobs) {
await runQueueCommandLegacy({ name, queueCommandDto: { command: QueueCommand.Resume, force: false } });
}
// Refresh jobs status immediately after resuming
jobs = await getQueuesLegacy();
} catch (error) {
handleError(error, $t('admin.failed_job_command', { values: { command: 'resume', job: 'paused jobs' } }));
}
};
const handleCreateJob = () => modalManager.show(JobCreateModal);
const jobConcurrencyLink = `${AppRoute.ADMIN_SETTINGS}?isOpen=job`;
const commands: ActionItem[] = [
{
title: $t('admin.create_job'),
type: $t('command'),
icon: mdiPlus,
onAction: () => void handleCreateJob(),
shortcuts: { shift: true, key: 'n' },
},
{
title: $t('admin.manage_concurrency'),
description: $t('admin.manage_concurrency_description'),
type: $t('page'),
icon: mdiCog,
onAction: () => goto(jobConcurrencyLink),
},
];
onMount(async () => {
while (running) {
jobs = await getQueuesLegacy();
await asyncTimeout(5000);
}
});
onDestroy(() => {
running = false;
});
</script>
<CommandPaletteContext {commands} />
<AdminPageLayout breadcrumbs={[{ title: data.meta.title }]}>
{#snippet buttons()}
<HStack gap={0}>
{#if pausedJobs.length > 0}
<Button
leadingIcon={mdiPlay}
onclick={handleResumePausedJobs}
size="small"
variant="ghost"
title={pausedJobs.join(', ')}
>
<Text class="hidden md:block">
{$t('resume_paused_jobs', { values: { count: pausedJobs.length } })}
</Text>
</Button>
{/if}
<Button leadingIcon={mdiPlus} onclick={handleCreateJob} size="small" variant="ghost" color="secondary">
<Text class="hidden md:block">{$t('admin.create_job')}</Text>
</Button>
<Button leadingIcon={mdiCog} href={jobConcurrencyLink} size="small" variant="ghost" color="secondary">
<Text class="hidden md:block">{$t('admin.manage_concurrency')}</Text>
</Button>
</HStack>
{/snippet}
<section id="setting-content" class="flex place-content-center sm:mx-4">
<section class="w-full pb-28 sm:w-5/6 md:w-212.5">
{#if jobs}
<JobsPanel {jobs} />
{/if}
</section>
</section>
</AdminPageLayout>
+3 -16
View File
@@ -1,18 +1,5 @@
import { authenticate } from '$lib/utils/auth';
import { getFormatter } from '$lib/utils/i18n';
import { getQueuesLegacy } from '@immich/sdk';
import { AppRoute } from '$lib/constants';
import { redirect } from '@sveltejs/kit';
import type { PageLoad } from './$types';
export const load = (async ({ url }) => {
await authenticate(url, { admin: true });
const jobs = await getQueuesLegacy();
const $t = await getFormatter();
return {
jobs,
meta: {
title: $t('admin.job_status'),
},
};
}) satisfies PageLoad;
export const load = (() => redirect(307, AppRoute.ADMIN_QUEUES)) satisfies PageLoad;
+83
View File
@@ -0,0 +1,83 @@
<script lang="ts">
import HeaderButton from '$lib/components/HeaderButton.svelte';
import AdminPageLayout from '$lib/components/layouts/AdminPageLayout.svelte';
import OnEvents from '$lib/components/OnEvents.svelte';
import JobsPanel from '$lib/components/QueuePanel.svelte';
import { queueManager } from '$lib/managers/queue-manager.svelte';
import { getQueuesActions } from '$lib/services/queue.service';
import { handleError } from '$lib/utils/handle-error';
import { QueueCommand, runQueueCommandLegacy, type QueueResponseDto } from '@immich/sdk';
import { Button, CommandPaletteContext, HStack, Text, type ActionItem } from '@immich/ui';
import { mdiPlay } from '@mdi/js';
import { onMount } from 'svelte';
import { t } from 'svelte-i18n';
import type { PageData } from './$types';
type Props = {
data: PageData;
};
const { data }: Props = $props();
onMount(() => queueManager.listen());
let queues = $derived<QueueResponseDto[]>(queueManager.queues);
const pausedQueues = $derived(queues.filter(({ isPaused }) => isPaused).map(({ name }) => name));
const handleResumePausedJobs = async () => {
try {
for (const name of pausedQueues) {
await runQueueCommandLegacy({ name, queueCommandDto: { command: QueueCommand.Resume, force: false } });
}
await queueManager.refresh();
} catch (error) {
handleError(error, $t('admin.failed_job_command', { values: { command: 'resume', job: 'paused jobs' } }));
}
};
const { CreateJob, ManageConcurrency } = $derived(getQueuesActions($t));
const commands: ActionItem[] = $derived([CreateJob, ManageConcurrency]);
const onQueueUpdate = (update: QueueResponseDto) => {
queues = queues.map((queue) => {
if (queue.name === update.name) {
return update;
}
return queue;
});
};
</script>
<CommandPaletteContext {commands} />
<OnEvents {onQueueUpdate} />
<AdminPageLayout breadcrumbs={[{ title: data.meta.title }]}>
{#snippet buttons()}
<HStack gap={0}>
{#if pausedQueues.length > 0}
<Button
leadingIcon={mdiPlay}
onclick={handleResumePausedJobs}
size="small"
variant="ghost"
title={pausedQueues.join(', ')}
>
<Text class="hidden md:block">
{$t('resume_paused_jobs', { values: { count: pausedQueues.length } })}
</Text>
</Button>
{/if}
<HeaderButton action={CreateJob} />
<HeaderButton action={ManageConcurrency} />
</HStack>
{/snippet}
<section id="setting-content" class="flex place-content-center sm:mx-4">
<section class="w-full pb-28 sm:w-5/6 md:w-212.5">
{#if queues}
<JobsPanel {queues} />
{/if}
</section>
</section>
</AdminPageLayout>
+18
View File
@@ -0,0 +1,18 @@
import { authenticate } from '$lib/utils/auth';
import { getFormatter } from '$lib/utils/i18n';
import { getQueues } from '@immich/sdk';
import type { PageLoad } from './$types';
export const load = (async ({ url }) => {
await authenticate(url, { admin: true });
const queues = await getQueues();
const $t = await getFormatter();
return {
queues,
meta: {
title: $t('admin.queues'),
},
};
}) satisfies PageLoad;
@@ -0,0 +1,82 @@
<script lang="ts">
import HeaderButton from '$lib/components/HeaderButton.svelte';
import AdminPageLayout from '$lib/components/layouts/AdminPageLayout.svelte';
import OnEvents from '$lib/components/OnEvents.svelte';
import QueueGraph from '$lib/components/QueueGraph.svelte';
import { AppRoute } from '$lib/constants';
import { queueManager } from '$lib/managers/queue-manager.svelte';
import { asQueueItem, getQueueActions } from '$lib/services/queue.service';
import { type QueueResponseDto } from '@immich/sdk';
import { Badge, Card, CardBody, CardHeader, CardTitle, Container, Heading, HStack, Icon, Text } from '@immich/ui';
import { mdiClockTimeTwoOutline } from '@mdi/js';
import { onMount } from 'svelte';
import { t } from 'svelte-i18n';
import type { PageData } from './$types';
type Props = {
data: PageData;
};
const { data }: Props = $props();
let queue = $derived(data.queue);
const { Pause, Resume, Empty, RemoveFailedJobs } = $derived(getQueueActions($t, queue));
const item = $derived(asQueueItem($t, queue));
onMount(() => queueManager.listen());
const onQueueUpdate = (update: QueueResponseDto) => {
if (update.name === queue.name) {
queue = update;
}
};
</script>
<OnEvents {onQueueUpdate} />
<AdminPageLayout breadcrumbs={[{ title: $t('admin.queues'), href: AppRoute.ADMIN_QUEUES }, { title: item.title }]}>
{#snippet buttons()}
<HStack gap={0}>
<HeaderButton action={Pause} />
<HeaderButton action={Resume} />
<HeaderButton action={Empty} />
<HeaderButton action={RemoveFailedJobs} />
</HStack>
{/snippet}
<div>
<Container size="large" center>
<div class="mb-1 mt-4 flex items-center gap-2">
<Heading tag="h1" size="large">{item.title}</Heading>
{#if queue.isPaused}
<Badge color="warning">
{$t('paused')}
</Badge>
{/if}
</div>
<Text color="muted" class="mb-4">{item.subtitle}</Text>
<div class="flex gap-1 mb-4">
<Badge>{$t('active_count', { values: { count: queue.statistics.active } })}</Badge>
<Badge>{$t('waiting_count', { values: { count: queue.statistics.waiting } })}</Badge>
{#if queue.statistics.failed > 0}
<Badge color="danger">{$t('failed_count', { values: { count: queue.statistics.failed } })}</Badge>
{/if}
</div>
<div class="mt-8">
<Card color="secondary">
<CardHeader>
<div class="flex items-center gap-2 text-primary">
<Icon icon={mdiClockTimeTwoOutline} size="1.5rem" />
<CardTitle>{$t('admin.jobs_over_time')}</CardTitle>
</div>
</CardHeader>
<CardBody>
<QueueGraph {queue} class="h-[300px]" />
</CardBody>
</Card>
</div>
</Container>
</div>
</AdminPageLayout>
@@ -0,0 +1,31 @@
import { AppRoute } from '$lib/constants';
import { fromQueueSlug } from '$lib/services/queue.service';
import { authenticate, requestServerInfo } from '$lib/utils/auth';
import { getFormatter } from '$lib/utils/i18n';
import { getQueue, getQueueJobs, QueueJobStatus } from '@immich/sdk';
import { redirect } from '@sveltejs/kit';
import type { PageLoad } from './$types';
export const load = (async ({ params, url }) => {
await authenticate(url, { admin: true });
await requestServerInfo();
const name = fromQueueSlug(params.name);
if (!name) {
redirect(302, AppRoute.ADMIN_QUEUES);
}
const [queue, failedJobs] = await Promise.all([
getQueue({ name }),
getQueueJobs({ name, status: [QueueJobStatus.Failed, QueueJobStatus.Paused] }),
]);
const $t = await getFormatter();
return {
queue,
failedJobs,
meta: {
title: $t('admin.queue_details'),
},
};
}) satisfies PageLoad;