feat: yucca integration

This commit is contained in:
izzy
2026-04-15 15:17:19 +01:00
parent 8454cb2631
commit 77f9e87bd3
102 changed files with 11198 additions and 164 deletions
@@ -1,6 +1,8 @@
<script lang="ts">
import RestoreFlowDetectInstall from '$lib/components/maintenance/restore-flow/RestoreFlowDetectInstall.svelte';
import RestoreFlowIntro from '$lib/components/maintenance/restore-flow/RestoreFlowIntro.svelte';
import RestoreFlowSelectBackup from '$lib/components/maintenance/restore-flow/RestoreFlowSelectBackup.svelte';
import { OnboardingGate } from 'orchestration-ui';
type Props = {
end: () => void;
@@ -9,11 +11,26 @@
const { end, expectedVersion }: Props = $props();
let stage = $state(0);
let stage = $state(localStorage.getItem('restoring-yucca') ? 1 : 0);
$effect(() => {
if (stage === 1) {
localStorage.setItem('restoring-yucca', '1');
} else {
localStorage.removeItem('restoring-yucca');
}
});
const next = () => stage++;
const previous = () => stage--;
</script>
{#if stage === 0}
<RestoreFlowDetectInstall next={() => stage++} {end} />
<RestoreFlowIntro flowToYucca={() => (stage = 1)} flowToDatabase={() => (stage = 2)} {end} />
{:else if stage === 1}
<OnboardingGate flow="immich-restore" onExit={previous} onFinish={() => stage++} />
{:else if stage === 2}
<RestoreFlowDetectInstall {next} previous={() => (stage = 0)} />
{:else}
<RestoreFlowSelectBackup previous={() => stage--} {end} {expectedVersion} />
<RestoreFlowSelectBackup {previous} {end} {expectedVersion} />
{/if}
@@ -7,10 +7,10 @@
type Props = {
next: () => void;
end: () => void;
previous: () => void;
};
const { next, end }: Props = $props();
const { next, previous }: Props = $props();
let detectedInstall: MaintenanceDetectInstallResponseDto | undefined = $state();
@@ -93,6 +93,6 @@
</div>
<Text>{$t('maintenance_restore_library_confirm')}</Text>
<HStack>
<Button onclick={end} variant="ghost">{$t('cancel')}</Button>
<Button onclick={previous} variant="ghost">{$t('back')}</Button>
<Button onclick={next} trailingIcon={mdiArrowRight}>{$t('next')}</Button>
</HStack>
@@ -0,0 +1,20 @@
<script lang="ts">
import { Button, Heading, HStack } from '@immich/ui';
import { mdiArrowRight } from '@mdi/js';
import { t } from 'svelte-i18n';
type Props = {
flowToYucca: () => void;
flowToDatabase: () => void;
end: () => void;
};
const { flowToYucca, flowToDatabase, end }: Props = $props();
</script>
<Heading size="large" color="primary" tag="h1">Where would you like to restore from?</Heading>
<HStack>
<Button onclick={end} variant="ghost">{$t('cancel')}</Button>
<Button onclick={flowToYucca} trailingIcon={mdiArrowRight}>FUTO Backups</Button>
<Button onclick={flowToDatabase} trailingIcon={mdiArrowRight}>Database Backup</Button>
</HStack>
@@ -14,6 +14,7 @@
mdiAccountOutline,
mdiArchiveArrowDown,
mdiArchiveArrowDownOutline,
mdiBackupRestore,
mdiFolderOutline,
mdiHeart,
mdiHeartOutline,
@@ -87,6 +88,8 @@
<NavbarItem title={$t('folders')} href={Route.folders()} icon={{ icon: mdiFolderOutline, flipped: true }} />
{/if}
<NavbarItem title="Backups" href={Route.backups()} icon={mdiBackupRestore} />
<NavbarItem title={$t('utilities')} href={Route.utilities()} icon={mdiToolboxOutline} activeIcon={mdiToolbox} />
<NavbarItem
+6
View File
@@ -147,6 +147,12 @@ export const Route = {
workflows: () => '/utilities/workflows',
viewWorkflow: ({ id }: { id: string }) => `/utilities/workflows/${id}`,
// backups
backups: () => '/backups',
backupsRepositories: () => '/backups/repositories',
backupsSchedules: () => '/backups/schedules',
backupsConfig: () => '/backups/config',
// queues
queues: () => '/admin/queues',
viewQueue: ({ name }: { name: QueueName }) => `/admin/queues/${asQueueSlug(name)}`,
+44
View File
@@ -0,0 +1,44 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { page } from '$app/state';
import NavigationBar from '$lib/components/shared-components/navigation-bar/navigation-bar.svelte';
import { Route } from '$lib/route';
import { sidebarStore } from '$lib/stores/sidebar.svelte';
import { AppShell, AppShellHeader, AppShellSidebar, NavbarItem } from '@immich/ui';
import { mdiBackupRestore, mdiClock, mdiCog, mdiViewDashboard } from '@mdi/js';
import { OnboardingGate, orchestrationApiProvider, sdk, setProvider, YuccaContext } from 'orchestration-ui';
import type { Snippet } from 'svelte';
let { children }: { children: Snippet } = $props();
sdk.defaults.baseUrl = window.location.origin;
setProvider(orchestrationApiProvider);
</script>
<AppShell>
<AppShellHeader>
<NavigationBar noBorder />
</AppShellHeader>
<AppShellSidebar bind:open={sidebarStore.isOpen}>
<div class="flex flex-col pt-8 pe-4 gap-1">
<NavbarItem
title="Dashboard"
href={Route.backups()}
icon={mdiViewDashboard}
isActive={() => page.url.pathname === '/backups'}
/>
<NavbarItem title="Repositories" href={Route.backupsRepositories()} icon={mdiBackupRestore} />
<NavbarItem title="Schedules" href={Route.backupsSchedules()} icon={mdiClock} />
<NavbarItem title="Configure" href={Route.backupsConfig()} icon={mdiCog} />
</div>
</AppShellSidebar>
<YuccaContext baseUrl={window.location.origin}>
<div class="p-4 flex flex-col gap-2 max-w-6xl m-auto">
<OnboardingGate flow="immich-setup" onExit={() => goto('/')}>
{@render children()}
</OnboardingGate>
</div>
</YuccaContext>
</AppShell>
+6
View File
@@ -0,0 +1,6 @@
import { authenticate } from '$lib/utils/auth';
import type { LayoutLoad } from './$types';
export const load = (async ({ url }) => {
await authenticate(url);
}) satisfies LayoutLoad;
+13
View File
@@ -0,0 +1,13 @@
<script lang="ts">
import { Dashboard } from 'orchestration-ui';
import { goto } from '$app/navigation';
import { Route } from '$lib/route';
</script>
<Dashboard
onNavigate={(target) => {
if (target === 'backups') goto(Route.backupsRepositories());
else if (target === 'schedules') goto(Route.backupsSchedules());
else if (target === 'config') goto(Route.backupsConfig());
}}
/>
+9
View File
@@ -0,0 +1,9 @@
import type { PageLoad } from './$types';
export const load = (() => {
return {
meta: {
title: 'Backups',
},
};
}) satisfies PageLoad;
@@ -0,0 +1,5 @@
<script lang="ts">
import { BackendsList } from 'orchestration-ui';
</script>
<BackendsList />
@@ -0,0 +1,5 @@
<script lang="ts">
import { BackupsList } from 'orchestration-ui';
</script>
<BackupsList local />
@@ -0,0 +1,5 @@
<script lang="ts">
import { ScheduleList } from 'orchestration-ui';
</script>
<ScheduleList />
+54 -51
View File
@@ -6,6 +6,7 @@
import { maintenanceStore } from '$lib/stores/maintenance.store';
import { MaintenanceAction } from '@immich/sdk';
import { Button, Heading, Link, ProgressBar, Scrollable, Text } from '@immich/ui';
import { YuccaContext } from 'orchestration-ui';
import { t } from 'svelte-i18n';
import type { PageData } from './$types';
@@ -37,58 +38,60 @@
);
</script>
<AuthPageLayout
withHeader={$status?.action === MaintenanceAction.Start || $status?.action === MaintenanceAction.End}
withBackdrop={$status?.action === MaintenanceAction.Start}
>
<div class="flex flex-col place-items-center text-center gap-8">
{#if $status?.action === MaintenanceAction.RestoreDatabase}
<Heading size="large" color="primary" tag="h1">{$t('maintenance_action_restore')}</Heading>
{#if $status.error}
<Scrollable class="max-h-80">
<pre class="text-left text-sm"><code>{error}</code></pre>
</Scrollable>
<Button onclick={end}>{$t('maintenance_end')}</Button>
<YuccaContext baseUrl={window.location.origin}>
<AuthPageLayout
withHeader={$status?.action === MaintenanceAction.Start || $status?.action === MaintenanceAction.End}
withBackdrop={$status?.action === MaintenanceAction.Start}
>
<div class="flex flex-col place-items-center text-center gap-8">
{#if $status?.action === MaintenanceAction.RestoreDatabase}
<Heading size="large" color="primary" tag="h1">{$t('maintenance_action_restore')}</Heading>
{#if $status.error}
<Scrollable class="max-h-80">
<pre class="text-left text-sm"><code>{error}</code></pre>
</Scrollable>
<Button onclick={end}>{$t('maintenance_end')}</Button>
{:else}
<ProgressBar progress={$status.progress || 0} />
{#if $status.task === 'backup'}
<Text>{$t('maintenance_task_backup')}</Text>
{/if}
{#if $status.task === 'restore'}
<Text>{$t('maintenance_task_restore')}</Text>
{/if}
{#if $status.task === 'migrations'}
<Text>{$t('maintenance_task_migrations')}</Text>
{/if}
{#if $status.task === 'rollback'}
<Text>{$t('maintenance_task_rollback')}</Text>
{/if}
{/if}
{:else if $status?.action === MaintenanceAction.SelectDatabaseRestore && $auth}
<MaintenanceRestoreFlow {end} expectedVersion={data.expectedVersion} />
{:else}
<ProgressBar progress={$status.progress || 0} />
{#if $status.task === 'backup'}
<Text>{$t('maintenance_task_backup')}</Text>
{/if}
{#if $status.task === 'restore'}
<Text>{$t('maintenance_task_restore')}</Text>
{/if}
{#if $status.task === 'migrations'}
<Text>{$t('maintenance_task_migrations')}</Text>
{/if}
{#if $status.task === 'rollback'}
<Text>{$t('maintenance_task_rollback')}</Text>
{/if}
{/if}
{:else if $status?.action === MaintenanceAction.SelectDatabaseRestore && $auth}
<MaintenanceRestoreFlow {end} expectedVersion={data.expectedVersion} />
{:else}
<Heading size="large" color="primary" tag="h1">{$t('maintenance_title')}</Heading>
<p>
<FormatMessage key="maintenance_description">
{#snippet children({ tag, message })}
{#if tag === 'link'}
<Link href="https://docs.immich.app/administration/maintenance-mode">
{message}
</Link>
{/if}
{/snippet}
</FormatMessage>
</p>
{#if $auth}
<Heading size="large" color="primary" tag="h1">{$t('maintenance_title')}</Heading>
<p>
{$t('maintenance_logged_in_as', {
values: {
user: $auth.username,
},
})}
<FormatMessage key="maintenance_description">
{#snippet children({ tag, message })}
{#if tag === 'link'}
<Link href="https://docs.immich.app/administration/maintenance-mode">
{message}
</Link>
{/if}
{/snippet}
</FormatMessage>
</p>
<Button onclick={end}>{$t('maintenance_end')}</Button>
{#if $auth}
<p>
{$t('maintenance_logged_in_as', {
values: {
user: $auth.username,
},
})}
</p>
<Button onclick={end}>{$t('maintenance_end')}</Button>
{/if}
{/if}
{/if}
</div>
</AuthPageLayout>
</div>
</AuthPageLayout>
</YuccaContext>