mirror of
https://github.com/immich-app/immich.git
synced 2026-05-18 03:10:24 +03:00
feat: queue detail page (#24352)
This commit is contained in:
@@ -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>
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
Reference in New Issue
Block a user