Jason Rasmussen 1 год назад
Родитель
Сommit
9d225d3d06

+ 0 - 11
web/src/lib/components/admin-page/jobs/jobs-panel.svelte

@@ -3,7 +3,6 @@
     notificationController,
     NotificationType,
   } from '$lib/components/shared-components/notification/notification';
-  import { AppRoute } from '$lib/constants';
   import { featureFlags } from '$lib/stores/server-config.store';
   import { handleError } from '$lib/utils/handle-error';
   import { AllJobStatusResponseDto, api, JobCommand, JobCommandDto, JobName } from '@api';
@@ -14,7 +13,6 @@
   import FileXmlBox from 'svelte-material-icons/FileXmlBox.svelte';
   import LibraryShelves from 'svelte-material-icons/LibraryShelves.svelte';
   import FolderMove from 'svelte-material-icons/FolderMove.svelte';
-  import CogIcon from 'svelte-material-icons/Cog.svelte';
   import Table from 'svelte-material-icons/Table.svelte';
   import TagMultiple from 'svelte-material-icons/TagMultiple.svelte';
   import VectorCircle from 'svelte-material-icons/VectorCircle.svelte';
@@ -22,7 +20,6 @@
   import ConfirmDialogue from '../../shared-components/confirm-dialogue.svelte';
   import JobTile from './job-tile.svelte';
   import StorageMigrationDescription from './storage-migration-description.svelte';
-  import Button from '../../elements/buttons/button.svelte';
 
   export let jobs: AllJobStatusResponseDto;
 
@@ -149,14 +146,6 @@
 {/if}
 
 <div class="flex flex-col gap-7">
-  <div class="flex justify-end">
-    <a href="{AppRoute.ADMIN_SETTINGS}?open=job-settings">
-      <Button size="sm">
-        <CogIcon size="18" />
-        <span class="pl-2">Manage Concurrency</span>
-      </Button>
-    </a>
-  </div>
   {#each jobList as [jobName, { title, subtitle, disabled, allText, missingText, allowForceCommand, icon, component, handleCommand: handleCommandOverride }]}
     {@const { jobCounts, queueStatus } = jobs[jobName]}
     <JobTile

+ 8 - 1
web/src/lib/components/layouts/user-page-layout.svelte

@@ -3,11 +3,14 @@
   import type { UserResponseDto } from '@api';
   import NavigationBar from '../shared-components/navigation-bar/navigation-bar.svelte';
   import SideBar from '../shared-components/side-bar/side-bar.svelte';
+  import AdminSideBar from '../shared-components/side-bar/admin-side-bar.svelte';
+
   export let user: UserResponseDto;
   export let hideNavbar = false;
   export let showUploadButton = false;
   export let title: string | undefined = undefined;
   export let scrollbar = true;
+  export let admin = false;
 
   $: scrollbarClass = scrollbar ? 'immich-scrollbar p-4 pb-8' : 'scrollbar-hidden pl-4';
 </script>
@@ -23,7 +26,11 @@
   class="relative grid h-screen grid-cols-[theme(spacing.18)_auto] overflow-hidden bg-immich-bg pt-[var(--navbar-height)] dark:bg-immich-dark-bg md:grid-cols-[theme(spacing.64)_auto]"
 >
   <slot name="sidebar">
-    <SideBar />
+    {#if admin}
+      <AdminSideBar />
+    {:else}
+      <SideBar />
+    {/if}
   </slot>
   <slot name="content">
     {#if title}

+ 33 - 0
web/src/lib/components/shared-components/side-bar/admin-side-bar.svelte

@@ -0,0 +1,33 @@
+<script lang="ts">
+  import { page } from '$app/stores';
+  import SideBarButton from '$lib/components/shared-components/side-bar/side-bar-button.svelte';
+  import SideBarSection from '$lib/components/shared-components/side-bar/side-bar-section.svelte';
+  import StatusBox from '$lib/components/shared-components/status-box.svelte';
+  import { AppRoute } from '$lib/constants';
+  import AccountMultipleOutline from 'svelte-material-icons/AccountMultipleOutline.svelte';
+  import Cog from 'svelte-material-icons/Cog.svelte';
+  import Server from 'svelte-material-icons/Server.svelte';
+  import Sync from 'svelte-material-icons/Sync.svelte';
+</script>
+
+<SideBarSection>
+  <a data-sveltekit-preload-data="hover" href={AppRoute.ADMIN_USER_MANAGEMENT} draggable="false">
+    <SideBarButton
+      title="Users"
+      logo={AccountMultipleOutline}
+      isSelected={$page.route.id === AppRoute.ADMIN_USER_MANAGEMENT}
+    />
+  </a>
+  <a data-sveltekit-preload-data="hover" href={AppRoute.ADMIN_JOBS} draggable="false">
+    <SideBarButton title="Jobs" logo={Sync} isSelected={$page.route.id === AppRoute.ADMIN_JOBS} />
+  </a>
+  <a data-sveltekit-preload-data="hover" href={AppRoute.ADMIN_SETTINGS} draggable="false">
+    <SideBarButton title="Settings" logo={Cog} isSelected={$page.route.id === AppRoute.ADMIN_SETTINGS} />
+  </a>
+  <a data-sveltekit-preload-data="hover" href={AppRoute.ADMIN_STATS} draggable="false">
+    <SideBarButton title="Server Stats" logo={Server} isSelected={$page.route.id === AppRoute.ADMIN_STATS} />
+  </a>
+  <div class="mb-6 mt-auto">
+    <StatusBox />
+  </div>
+</SideBarSection>

+ 0 - 68
web/src/routes/admin/+layout.svelte

@@ -1,68 +0,0 @@
-<script lang="ts">
-  // DO NOT include `import { page } from '$app/stores'` here, because this can
-  // lead to pages not being unmounted, which then causes weird page nesting
-  // and routing issues.
-  //
-  // This is an issue in SvelteKit caused by using the page store in layouts and
-  // using transitions on pages: https://github.com/sveltejs/kit/issues/7405
-
-  import SideBarButton from '$lib/components/shared-components/side-bar/side-bar-button.svelte';
-  import AccountMultipleOutline from 'svelte-material-icons/AccountMultipleOutline.svelte';
-  import Sync from 'svelte-material-icons/Sync.svelte';
-  import Cog from 'svelte-material-icons/Cog.svelte';
-  import Server from 'svelte-material-icons/Server.svelte';
-  import StatusBox from '$lib/components/shared-components/status-box.svelte';
-  import { AppRoute } from '../../lib/constants';
-  import type { LayoutData } from './$types';
-  import SideBarSection from '$lib/components/shared-components/side-bar/side-bar-section.svelte';
-  import UserPageLayout from '$lib/components/layouts/user-page-layout.svelte';
-
-  export let data: LayoutData;
-
-  // Circumvents the need to import the page store. Should be replaced by
-  // `$page.data.meta.title` once issue #7405 of SvelteKit is resolved.
-  const getPageTitle = (routeId: string | null) => {
-    switch (routeId) {
-      case AppRoute.ADMIN_USER_MANAGEMENT:
-        return 'User Management';
-      case AppRoute.ADMIN_SETTINGS:
-        return 'System Settings';
-      case AppRoute.ADMIN_JOBS:
-        return 'Jobs';
-      case AppRoute.ADMIN_STATS:
-        return 'Server Stats';
-      default:
-        return '';
-    }
-  };
-</script>
-
-<UserPageLayout user={data.user} showUploadButton={false} title={getPageTitle(data.routeId)}>
-  <SideBarSection slot="sidebar">
-    <a data-sveltekit-preload-data="hover" href={AppRoute.ADMIN_USER_MANAGEMENT} draggable="false">
-      <SideBarButton
-        title="Users"
-        logo={AccountMultipleOutline}
-        isSelected={data.routeId === AppRoute.ADMIN_USER_MANAGEMENT}
-      />
-    </a>
-    <a data-sveltekit-preload-data="hover" href={AppRoute.ADMIN_JOBS} draggable="false">
-      <SideBarButton title="Jobs" logo={Sync} isSelected={data.routeId === AppRoute.ADMIN_JOBS} />
-    </a>
-    <a data-sveltekit-preload-data="hover" href={AppRoute.ADMIN_SETTINGS} draggable="false">
-      <SideBarButton title="Settings" logo={Cog} isSelected={data.routeId === AppRoute.ADMIN_SETTINGS} />
-    </a>
-    <a data-sveltekit-preload-data="hover" href={AppRoute.ADMIN_STATS} draggable="false">
-      <SideBarButton title="Server Stats" logo={Server} isSelected={data.routeId === AppRoute.ADMIN_STATS} />
-    </a>
-    <div class="mb-6 mt-auto">
-      <StatusBox />
-    </div>
-  </SideBarSection>
-
-  <section id="setting-content" class="flex place-content-center sm:mx-4">
-    <section class="w-full pb-28 pt-5 sm:w-5/6 md:w-[850px]">
-      <slot />
-    </section>
-  </section>
-</UserPageLayout>

+ 0 - 14
web/src/routes/admin/+layout.ts

@@ -1,14 +0,0 @@
-import { AppRoute } from '$lib/constants';
-import { redirect } from '@sveltejs/kit';
-import type { LayoutLoad } from './$types';
-
-export const load = (async ({ parent, route }) => {
-  const { user } = await parent();
-  if (!user) {
-    throw redirect(302, AppRoute.AUTH_LOGIN);
-  } else if (!user.isAdmin) {
-    throw redirect(302, AppRoute.PHOTOS);
-  }
-
-  return { routeId: route.id, user };
-}) satisfies LayoutLoad;

+ 1 - 0
web/src/routes/admin/jobs-status/+page.server.ts

@@ -13,6 +13,7 @@ export const load = (async ({ locals: { user, api } }) => {
     const { data: jobs } = await api.jobApi.getAllJobsStatus();
 
     return {
+      user,
       jobs,
       meta: {
         title: 'Job Status',

+ 26 - 3
web/src/routes/admin/jobs-status/+page.svelte

@@ -1,7 +1,14 @@
 <script lang="ts">
   import JobsPanel from '$lib/components/admin-page/jobs/jobs-panel.svelte';
+  import LinkButton from '$lib/components/elements/buttons/link-button.svelte';
+  import UserPageLayout from '$lib/components/layouts/user-page-layout.svelte';
+  import { AppRoute } from '$lib/constants';
   import { AllJobStatusResponseDto, api } from '@api';
   import { onDestroy, onMount } from 'svelte';
+  import CogIcon from 'svelte-material-icons/Cog.svelte';
+  import type { PageData } from './$types';
+
+  export let data: PageData;
 
   let timer: ReturnType<typeof setInterval>;
 
@@ -22,6 +29,22 @@
   });
 </script>
 
-{#if jobs}
-  <JobsPanel {jobs} />
-{/if}
+<UserPageLayout user={data.user} title={data.meta.title} admin>
+  <div class="flex justify-end" slot="buttons">
+    <a href="{AppRoute.ADMIN_SETTINGS}?open=job-settings">
+      <LinkButton>
+        <div class="flex place-items-center gap-2 text-sm">
+          <CogIcon size="18" />
+          Manage Concurrency
+        </div>
+      </LinkButton>
+    </a>
+  </div>
+  <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]">
+      {#if jobs}
+        <JobsPanel {jobs} />
+      {/if}
+    </section>
+  </section>
+</UserPageLayout>

+ 1 - 0
web/src/routes/admin/server-status/+page.server.ts

@@ -14,6 +14,7 @@ export const load = (async ({ parent, locals: { api } }) => {
   const { data: stats } = await api.serverInfoApi.getStats();
 
   return {
+    user,
     stats,
     meta: {
       title: 'Server Stats',

+ 11 - 3
web/src/routes/admin/server-status/+page.svelte

@@ -1,10 +1,12 @@
 <script lang="ts">
-  import { onMount, onDestroy } from 'svelte';
-  import { api } from '@api';
   import ServerStatsPanel from '$lib/components/admin-page/server-stats/server-stats-panel.svelte';
+  import UserPageLayout from '$lib/components/layouts/user-page-layout.svelte';
+  import { api } from '@api';
+  import { onDestroy, onMount } from 'svelte';
   import type { PageData } from './$types';
 
   export let data: PageData;
+
   let setIntervalHandler: ReturnType<typeof setInterval>;
 
   onMount(async () => {
@@ -19,4 +21,10 @@
   });
 </script>
 
-<ServerStatsPanel stats={data.stats} />
+<UserPageLayout user={data.user} title={data.meta.title} admin>
+  <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]">
+      <ServerStatsPanel stats={data.stats} />
+    </section>
+  </section>
+</UserPageLayout>

+ 4 - 1
web/src/routes/admin/system-settings/+page.server.ts

@@ -2,7 +2,7 @@ import { AppRoute } from '$lib/constants';
 import { redirect } from '@sveltejs/kit';
 import type { PageServerLoad } from './$types';
 
-export const load: PageServerLoad = async ({ parent }) => {
+export const load: PageServerLoad = async ({ parent, locals: { api } }) => {
   const { user } = await parent();
 
   if (!user) {
@@ -11,8 +11,11 @@ export const load: PageServerLoad = async ({ parent }) => {
     throw redirect(302, AppRoute.PHOTOS);
   }
 
+  const { data: configs } = await api.systemConfigApi.getConfig();
+
   return {
     user,
+    configs,
     meta: {
       title: 'System Settings',
     },

+ 66 - 65
web/src/routes/admin/system-settings/+page.svelte

@@ -9,26 +9,23 @@
   import SettingAccordion from '$lib/components/admin-page/settings/setting-accordion.svelte';
   import StorageTemplateSettings from '$lib/components/admin-page/settings/storage-template/storage-template-settings.svelte';
   import ThumbnailSettings from '$lib/components/admin-page/settings/thumbnail/thumbnail-settings.svelte';
-  import Button from '$lib/components/elements/buttons/button.svelte';
-  import LoadingSpinner from '$lib/components/shared-components/loading-spinner.svelte';
+  import TrashSettings from '$lib/components/admin-page/settings/trash-settings/trash-settings.svelte';
+  import LinkButton from '$lib/components/elements/buttons/link-button.svelte';
+  import UserPageLayout from '$lib/components/layouts/user-page-layout.svelte';
   import { downloadManager } from '$lib/stores/download';
   import { featureFlags } from '$lib/stores/server-config.store';
   import { downloadBlob } from '$lib/utils/asset-utils';
-  import { SystemConfigDto, api, copyToClipboard } from '@api';
+  import { copyToClipboard } from '@api';
   import Alert from 'svelte-material-icons/Alert.svelte';
   import ContentCopy from 'svelte-material-icons/ContentCopy.svelte';
   import Download from 'svelte-material-icons/Download.svelte';
   import type { PageData } from './$types';
-  import TrashSettings from '$lib/components/admin-page/settings/trash-settings/trash-settings.svelte';
 
   export let data: PageData;
 
-  const getConfig = async () => {
-    const { data } = await api.systemConfigApi.getConfig();
-    return data;
-  };
+  const configs = data.configs;
 
-  const downloadConfig = (configs: SystemConfigDto) => {
+  const downloadConfig = () => {
     const blob = new Blob([JSON.stringify(configs, null, 2)], { type: 'application/json' });
     const downloadKey = 'immich-config.json';
     downloadManager.add(downloadKey, blob.size);
@@ -45,70 +42,74 @@
   </div>
 {/if}
 
-<section class="">
-  {#await getConfig()}
-    <LoadingSpinner />
-  {:then configs}
-    <div class="flex justify-end gap-2">
-      <Button size="sm" on:click={() => copyToClipboard(JSON.stringify(configs, null, 2))}>
+<UserPageLayout user={data.user} title={data.meta.title} admin>
+  <div class="flex justify-end gap-2" slot="buttons">
+    <LinkButton on:click={() => copyToClipboard(JSON.stringify(configs, null, 2))}>
+      <div class="flex place-items-center gap-2 text-sm">
         <ContentCopy size="18" />
-        <span class="pl-2">Copy to Clipboard</span>
-      </Button>
-      <Button size="sm" on:click={() => downloadConfig(configs)}>
+        Copy to Clipboard
+      </div>
+    </LinkButton>
+    <LinkButton on:click={() => downloadConfig()}>
+      <div class="flex place-items-center gap-2 text-sm">
         <Download size="18" />
-        <span class="pl-2">Export as JSON</span>
-      </Button>
-    </div>
+        Export as JSON
+      </div>
+    </LinkButton>
+  </div>
 
-    <SettingAccordion
-      title="Job Settings"
-      subtitle="Manage job concurrency"
-      isOpen={$page.url.searchParams.get('open') === 'job-settings'}
-    >
-      <JobSettings disabled={$featureFlags.configFile} jobConfig={configs.job} />
-    </SettingAccordion>
+  <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]">
+      <SettingAccordion
+        title="Job Settings"
+        subtitle="Manage job concurrency"
+        isOpen={$page.url.searchParams.get('open') === 'job-settings'}
+      >
+        <JobSettings disabled={$featureFlags.configFile} jobConfig={configs.job} />
+      </SettingAccordion>
 
-    <SettingAccordion title="Machine Learning Settings" subtitle="Manage machine learning features and settings">
-      <MachineLearningSettings disabled={$featureFlags.configFile} machineLearningConfig={configs.machineLearning} />
-    </SettingAccordion>
+      <SettingAccordion title="Machine Learning Settings" subtitle="Manage machine learning features and settings">
+        <MachineLearningSettings disabled={$featureFlags.configFile} machineLearningConfig={configs.machineLearning} />
+      </SettingAccordion>
 
-    <SettingAccordion title="Map & GPS Settings" subtitle="Manage map related features and setting">
-      <MapSettings disabled={$featureFlags.configFile} config={configs} />
-    </SettingAccordion>
+      <SettingAccordion title="Map & GPS Settings" subtitle="Manage map related features and setting">
+        <MapSettings disabled={$featureFlags.configFile} config={configs} />
+      </SettingAccordion>
 
-    <SettingAccordion title="OAuth Authentication" subtitle="Manage the login with OAuth settings">
-      <OAuthSettings disabled={$featureFlags.configFile} oauthConfig={configs.oauth} />
-    </SettingAccordion>
+      <SettingAccordion title="OAuth Authentication" subtitle="Manage the login with OAuth settings">
+        <OAuthSettings disabled={$featureFlags.configFile} oauthConfig={configs.oauth} />
+      </SettingAccordion>
 
-    <SettingAccordion title="Password Authentication" subtitle="Manage login with password settings">
-      <PasswordLoginSettings disabled={$featureFlags.configFile} passwordLoginConfig={configs.passwordLogin} />
-    </SettingAccordion>
+      <SettingAccordion title="Password Authentication" subtitle="Manage login with password settings">
+        <PasswordLoginSettings disabled={$featureFlags.configFile} passwordLoginConfig={configs.passwordLogin} />
+      </SettingAccordion>
 
-    <SettingAccordion
-      title="Storage Template"
-      subtitle="Manage the folder structure and file name of the upload asset"
-      isOpen={$page.url.searchParams.get('open') === 'storage-template'}
-    >
-      <StorageTemplateSettings
-        disabled={$featureFlags.configFile}
-        storageConfig={configs.storageTemplate}
-        user={data.user}
-      />
-    </SettingAccordion>
+      <SettingAccordion
+        title="Storage Template"
+        subtitle="Manage the folder structure and file name of the upload asset"
+        isOpen={$page.url.searchParams.get('open') === 'storage-template'}
+      >
+        <StorageTemplateSettings
+          disabled={$featureFlags.configFile}
+          storageConfig={configs.storageTemplate}
+          user={data.user}
+        />
+      </SettingAccordion>
 
-    <SettingAccordion title="Thumbnail Settings" subtitle="Manage the resolution of thumbnail sizes">
-      <ThumbnailSettings disabled={$featureFlags.configFile} thumbnailConfig={configs.thumbnail} />
-    </SettingAccordion>
+      <SettingAccordion title="Thumbnail Settings" subtitle="Manage the resolution of thumbnail sizes">
+        <ThumbnailSettings disabled={$featureFlags.configFile} thumbnailConfig={configs.thumbnail} />
+      </SettingAccordion>
 
-    <SettingAccordion title="Trash Settings" subtitle="Manage trash settings">
-      <TrashSettings disabled={$featureFlags.configFile} trashConfig={configs.trash} />
-    </SettingAccordion>
+      <SettingAccordion title="Trash Settings" subtitle="Manage trash settings">
+        <TrashSettings disabled={$featureFlags.configFile} trashConfig={configs.trash} />
+      </SettingAccordion>
 
-    <SettingAccordion
-      title="Video Transcoding Settings"
-      subtitle="Manage the resolution and encoding information of the video files"
-    >
-      <FFmpegSettings disabled={$featureFlags.configFile} ffmpegConfig={configs.ffmpeg} />
-    </SettingAccordion>
-  {/await}
-</section>
+      <SettingAccordion
+        title="Video Transcoding Settings"
+        subtitle="Manage the resolution and encoding information of the video files"
+      >
+        <FFmpegSettings disabled={$featureFlags.configFile} ffmpegConfig={configs.ffmpeg} />
+      </SettingAccordion>
+    </section>
+  </section>
+</UserPageLayout>

+ 176 - 171
web/src/routes/admin/user-management/+page.svelte

@@ -15,6 +15,7 @@
   import { page } from '$app/stores';
   import { locale } from '$lib/stores/preferences.store';
   import Button from '$lib/components/elements/buttons/button.svelte';
+  import UserPageLayout from '$lib/components/layouts/user-page-layout.svelte';
   import type { PageData } from './$types';
 
   export let data: PageData;
@@ -107,185 +108,189 @@
   };
 </script>
 
-<section>
-  {#if shouldShowCreateUserForm}
-    <FullScreenModal on:clickOutside={() => (shouldShowCreateUserForm = false)}>
-      <CreateUserForm on:user-created={onUserCreated} on:cancel={() => (shouldShowCreateUserForm = false)} />
-    </FullScreenModal>
-  {/if}
+<UserPageLayout user={data.user} title={data.meta.title} admin>
+  <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]">
+      {#if shouldShowCreateUserForm}
+        <FullScreenModal on:clickOutside={() => (shouldShowCreateUserForm = false)}>
+          <CreateUserForm on:user-created={onUserCreated} on:cancel={() => (shouldShowCreateUserForm = false)} />
+        </FullScreenModal>
+      {/if}
 
-  {#if shouldShowEditUserForm}
-    <FullScreenModal on:clickOutside={() => (shouldShowEditUserForm = false)}>
-      <EditUserForm
-        user={selectedUser}
-        canResetPassword={selectedUser?.id !== data.user.id}
-        on:edit-success={onEditUserSuccess}
-        on:reset-password-success={onEditPasswordSuccess}
-      />
-    </FullScreenModal>
-  {/if}
+      {#if shouldShowEditUserForm}
+        <FullScreenModal on:clickOutside={() => (shouldShowEditUserForm = false)}>
+          <EditUserForm
+            user={selectedUser}
+            canResetPassword={selectedUser?.id !== data.user.id}
+            on:edit-success={onEditUserSuccess}
+            on:reset-password-success={onEditPasswordSuccess}
+          />
+        </FullScreenModal>
+      {/if}
 
-  {#if shouldShowDeleteConfirmDialog}
-    <DeleteConfirmDialog
-      user={selectedUser}
-      on:user-delete-success={onUserDeleteSuccess}
-      on:user-delete-fail={onUserDeleteFail}
-      on:cancel={() => (shouldShowDeleteConfirmDialog = false)}
-    />
-  {/if}
+      {#if shouldShowDeleteConfirmDialog}
+        <DeleteConfirmDialog
+          user={selectedUser}
+          on:user-delete-success={onUserDeleteSuccess}
+          on:user-delete-fail={onUserDeleteFail}
+          on:cancel={() => (shouldShowDeleteConfirmDialog = false)}
+        />
+      {/if}
 
-  {#if shouldShowRestoreDialog}
-    <RestoreDialogue
-      user={selectedUser}
-      on:user-restore-success={onUserRestoreSuccess}
-      on:user-restore-fail={onUserRestoreFail}
-      on:cancel={() => (shouldShowRestoreDialog = false)}
-    />
-  {/if}
+      {#if shouldShowRestoreDialog}
+        <RestoreDialogue
+          user={selectedUser}
+          on:user-restore-success={onUserRestoreSuccess}
+          on:user-restore-fail={onUserRestoreFail}
+          on:cancel={() => (shouldShowRestoreDialog = false)}
+        />
+      {/if}
 
-  {#if shouldShowInfoPanel}
-    <FullScreenModal on:clickOutside={() => (shouldShowInfoPanel = false)}>
-      <div class="w-[500px] max-w-[95vw] rounded-3xl border bg-white p-8 text-sm shadow-sm">
-        <h1 class="mb-4 text-lg font-medium text-immich-primary">Password reset success</h1>
+      {#if shouldShowInfoPanel}
+        <FullScreenModal on:clickOutside={() => (shouldShowInfoPanel = false)}>
+          <div class="w-[500px] max-w-[95vw] rounded-3xl border bg-white p-8 text-sm shadow-sm">
+            <h1 class="mb-4 text-lg font-medium text-immich-primary">Password reset success</h1>
 
-        <p>
-          The user's password has been reset to the default <code
-            class="rounded-md bg-gray-200 px-2 py-1 font-bold text-immich-primary">password</code
-          >
-          <br />
-          Please inform the user, and they will need to change the password at the next log-on.
-        </p>
+            <p>
+              The user's password has been reset to the default <code
+                class="rounded-md bg-gray-200 px-2 py-1 font-bold text-immich-primary">password</code
+              >
+              <br />
+              Please inform the user, and they will need to change the password at the next log-on.
+            </p>
 
-        <div class="mt-6 flex w-full">
-          <Button fullwidth on:click={() => (shouldShowInfoPanel = false)}>Done</Button>
-        </div>
-      </div>
-    </FullScreenModal>
-  {/if}
+            <div class="mt-6 flex w-full">
+              <Button fullwidth on:click={() => (shouldShowInfoPanel = false)}>Done</Button>
+            </div>
+          </div>
+        </FullScreenModal>
+      {/if}
 
-  <table class="my-5 hidden w-full text-left sm:block">
-    <thead
-      class="mb-4 flex h-12 w-full rounded-md border bg-gray-50 text-immich-primary dark:border-immich-dark-gray dark:bg-immich-dark-gray dark:text-immich-dark-primary"
-    >
-      <tr class="flex w-full place-items-center">
-        <th class="w-4/12 text-center text-sm font-medium">Email</th>
-        <th class="w-2/12 text-center text-sm font-medium">First name</th>
-        <th class="w-2/12 text-center text-sm font-medium">Last name</th>
-        <th class="w-2/12 text-center text-sm font-medium">Can import</th>
-        <th class="w-2/12 text-center text-sm font-medium">Action</th>
-      </tr>
-    </thead>
-    <tbody class="block max-h-[320px] w-full overflow-y-auto rounded-md border dark:border-immich-dark-gray">
-      {#if allUsers}
-        {#each allUsers as user, i}
-          <tr
-            class={`flex h-[80px] w-full place-items-center text-center dark:text-immich-dark-fg ${
-              isDeleted(user)
-                ? 'bg-red-300 dark:bg-red-900'
-                : i % 2 == 0
-                ? 'bg-immich-gray dark:bg-immich-dark-gray/75'
-                : 'bg-immich-bg dark:bg-immich-dark-gray/50'
-            }`}
-          >
-            <td class="w-4/12 text-ellipsis break-all px-2 text-sm">{user.email}</td>
-            <td class="w-2/12 text-ellipsis break-all px-2 text-sm">{user.firstName}</td>
-            <td class="w-2/12 text-ellipsis break-all px-2 text-sm">{user.lastName}</td>
-            <td class="w-2/12 text-ellipsis break-all px-2 text-sm">
-              <div class="container mx-auto flex flex-wrap justify-center">
-                {#if user.externalPath}
-                  <Check size="16" />
-                {:else}
-                  <Close size="16" />
-                {/if}
-              </div>
-            </td>
-            <td class="w-2/12 text-ellipsis break-all px-4 text-sm">
-              {#if !isDeleted(user)}
-                <button
-                  on:click={() => editUserHandler(user)}
-                  class="rounded-full bg-immich-primary p-3 text-gray-100 transition-all duration-150 hover:bg-immich-primary/75 dark:bg-immich-dark-primary dark:text-gray-700"
-                >
-                  <PencilOutline size="16" />
-                </button>
-                {#if user.id !== data.user.id}
-                  <button
-                    on:click={() => deleteUserHandler(user)}
-                    class="rounded-full bg-immich-primary p-3 text-gray-100 transition-all duration-150 hover:bg-immich-primary/75 dark:bg-immich-dark-primary dark:text-gray-700"
-                  >
-                    <TrashCanOutline size="16" />
-                  </button>
-                {/if}
-              {/if}
-              {#if isDeleted(user)}
-                <button
-                  on:click={() => restoreUserHandler(user)}
-                  class="rounded-full bg-immich-primary p-3 text-gray-100 transition-all duration-150 hover:bg-immich-primary/75 dark:bg-immich-dark-primary dark:text-gray-700"
-                  title={`scheduled removal on ${getDeleteDate(user)}`}
-                >
-                  <DeleteRestore size="16" />
-                </button>
-              {/if}
-            </td>
+      <table class="my-5 hidden w-full text-left sm:block">
+        <thead
+          class="mb-4 flex h-12 w-full rounded-md border bg-gray-50 text-immich-primary dark:border-immich-dark-gray dark:bg-immich-dark-gray dark:text-immich-dark-primary"
+        >
+          <tr class="flex w-full place-items-center">
+            <th class="w-4/12 text-center text-sm font-medium">Email</th>
+            <th class="w-2/12 text-center text-sm font-medium">First name</th>
+            <th class="w-2/12 text-center text-sm font-medium">Last name</th>
+            <th class="w-2/12 text-center text-sm font-medium">Can import</th>
+            <th class="w-2/12 text-center text-sm font-medium">Action</th>
           </tr>
-        {/each}
-      {/if}
-    </tbody>
-  </table>
+        </thead>
+        <tbody class="block max-h-[320px] w-full overflow-y-auto rounded-md border dark:border-immich-dark-gray">
+          {#if allUsers}
+            {#each allUsers as user, i}
+              <tr
+                class={`flex h-[80px] w-full place-items-center text-center dark:text-immich-dark-fg ${
+                  isDeleted(user)
+                    ? 'bg-red-300 dark:bg-red-900'
+                    : i % 2 == 0
+                    ? 'bg-immich-gray dark:bg-immich-dark-gray/75'
+                    : 'bg-immich-bg dark:bg-immich-dark-gray/50'
+                }`}
+              >
+                <td class="w-4/12 text-ellipsis break-all px-2 text-sm">{user.email}</td>
+                <td class="w-2/12 text-ellipsis break-all px-2 text-sm">{user.firstName}</td>
+                <td class="w-2/12 text-ellipsis break-all px-2 text-sm">{user.lastName}</td>
+                <td class="w-2/12 text-ellipsis break-all px-2 text-sm">
+                  <div class="container mx-auto flex flex-wrap justify-center">
+                    {#if user.externalPath}
+                      <Check size="16" />
+                    {:else}
+                      <Close size="16" />
+                    {/if}
+                  </div>
+                </td>
+                <td class="w-2/12 text-ellipsis break-all px-4 text-sm">
+                  {#if !isDeleted(user)}
+                    <button
+                      on:click={() => editUserHandler(user)}
+                      class="rounded-full bg-immich-primary p-3 text-gray-100 transition-all duration-150 hover:bg-immich-primary/75 dark:bg-immich-dark-primary dark:text-gray-700"
+                    >
+                      <PencilOutline size="16" />
+                    </button>
+                    {#if user.id !== data.user.id}
+                      <button
+                        on:click={() => deleteUserHandler(user)}
+                        class="rounded-full bg-immich-primary p-3 text-gray-100 transition-all duration-150 hover:bg-immich-primary/75 dark:bg-immich-dark-primary dark:text-gray-700"
+                      >
+                        <TrashCanOutline size="16" />
+                      </button>
+                    {/if}
+                  {/if}
+                  {#if isDeleted(user)}
+                    <button
+                      on:click={() => restoreUserHandler(user)}
+                      class="rounded-full bg-immich-primary p-3 text-gray-100 transition-all duration-150 hover:bg-immich-primary/75 dark:bg-immich-dark-primary dark:text-gray-700"
+                      title={`scheduled removal on ${getDeleteDate(user)}`}
+                    >
+                      <DeleteRestore size="16" />
+                    </button>
+                  {/if}
+                </td>
+              </tr>
+            {/each}
+          {/if}
+        </tbody>
+      </table>
 
-  <table class="my-5 block w-full text-left sm:hidden">
-    <thead
-      class="mb-4 flex h-12 w-full rounded-md border bg-gray-50 text-immich-primary dark:border-immich-dark-gray dark:bg-immich-dark-gray dark:text-immich-dark-primary"
-    >
-      <tr class="flex w-full place-items-center">
-        <th class="w-1/4 text-center text-sm font-medium">Name</th>
-        <th class="w-1/2 text-center text-sm font-medium">Email</th>
-        <th class="w-1/4 text-center text-sm font-medium">Action</th>
-      </tr>
-    </thead>
-    <tbody class="block max-h-[320px] w-full overflow-y-auto rounded-md border dark:border-immich-dark-gray">
-      {#if allUsers}
-        {#each allUsers as user, i}
-          <tr
-            class={`flex h-[80px] w-full place-items-center text-center dark:text-immich-dark-fg ${
-              isDeleted(user)
-                ? 'bg-red-300 dark:bg-red-900'
-                : i % 2 == 0
-                ? 'bg-immich-gray dark:bg-immich-dark-gray/75'
-                : 'bg-immich-bg dark:bg-immich-dark-gray/50'
-            }`}
-          >
-            <td class="w-1/4 text-ellipsis break-words px-2 text-sm">{user.firstName} {user.lastName}</td>
-            <td class="w-1/2 text-ellipsis break-all px-2 text-sm">{user.email}</td>
-            <td class="w-1/4 text-ellipsis px-2 text-sm">
-              {#if !isDeleted(user)}
-                <button
-                  on:click={() => editUserHandler(user)}
-                  class="rounded-full bg-immich-primary p-2 text-gray-100 transition-all duration-150 hover:bg-immich-primary/75 dark:bg-immich-dark-primary dark:text-gray-700 max-sm:mb-1 sm:p-3"
-                >
-                  <PencilOutline size="16" />
-                </button>
-                <button
-                  on:click={() => deleteUserHandler(user)}
-                  class="rounded-full bg-immich-primary p-2 text-gray-100 transition-all duration-150 hover:bg-immich-primary/75 dark:bg-immich-dark-primary dark:text-gray-700 sm:p-3"
-                >
-                  <TrashCanOutline size="16" />
-                </button>
-              {/if}
-              {#if isDeleted(user)}
-                <button
-                  on:click={() => restoreUserHandler(user)}
-                  class="rounded-full bg-immich-primary p-2 text-gray-100 transition-all duration-150 hover:bg-immich-primary/75 dark:bg-immich-dark-primary dark:text-gray-700 sm:p-3"
-                  title={`scheduled removal on ${getDeleteDate(user)}`}
-                >
-                  <DeleteRestore size="16" />
-                </button>
-              {/if}
-            </td>
+      <table class="my-5 block w-full text-left sm:hidden">
+        <thead
+          class="mb-4 flex h-12 w-full rounded-md border bg-gray-50 text-immich-primary dark:border-immich-dark-gray dark:bg-immich-dark-gray dark:text-immich-dark-primary"
+        >
+          <tr class="flex w-full place-items-center">
+            <th class="w-1/4 text-center text-sm font-medium">Name</th>
+            <th class="w-1/2 text-center text-sm font-medium">Email</th>
+            <th class="w-1/4 text-center text-sm font-medium">Action</th>
           </tr>
-        {/each}
-      {/if}
-    </tbody>
-  </table>
+        </thead>
+        <tbody class="block max-h-[320px] w-full overflow-y-auto rounded-md border dark:border-immich-dark-gray">
+          {#if allUsers}
+            {#each allUsers as user, i}
+              <tr
+                class={`flex h-[80px] w-full place-items-center text-center dark:text-immich-dark-fg ${
+                  isDeleted(user)
+                    ? 'bg-red-300 dark:bg-red-900'
+                    : i % 2 == 0
+                    ? 'bg-immich-gray dark:bg-immich-dark-gray/75'
+                    : 'bg-immich-bg dark:bg-immich-dark-gray/50'
+                }`}
+              >
+                <td class="w-1/4 text-ellipsis break-words px-2 text-sm">{user.firstName} {user.lastName}</td>
+                <td class="w-1/2 text-ellipsis break-all px-2 text-sm">{user.email}</td>
+                <td class="w-1/4 text-ellipsis px-2 text-sm">
+                  {#if !isDeleted(user)}
+                    <button
+                      on:click={() => editUserHandler(user)}
+                      class="rounded-full bg-immich-primary p-2 text-gray-100 transition-all duration-150 hover:bg-immich-primary/75 dark:bg-immich-dark-primary dark:text-gray-700 max-sm:mb-1 sm:p-3"
+                    >
+                      <PencilOutline size="16" />
+                    </button>
+                    <button
+                      on:click={() => deleteUserHandler(user)}
+                      class="rounded-full bg-immich-primary p-2 text-gray-100 transition-all duration-150 hover:bg-immich-primary/75 dark:bg-immich-dark-primary dark:text-gray-700 sm:p-3"
+                    >
+                      <TrashCanOutline size="16" />
+                    </button>
+                  {/if}
+                  {#if isDeleted(user)}
+                    <button
+                      on:click={() => restoreUserHandler(user)}
+                      class="rounded-full bg-immich-primary p-2 text-gray-100 transition-all duration-150 hover:bg-immich-primary/75 dark:bg-immich-dark-primary dark:text-gray-700 sm:p-3"
+                      title={`scheduled removal on ${getDeleteDate(user)}`}
+                    >
+                      <DeleteRestore size="16" />
+                    </button>
+                  {/if}
+                </td>
+              </tr>
+            {/each}
+          {/if}
+        </tbody>
+      </table>
 
-  <Button size="sm" on:click={() => (shouldShowCreateUserForm = true)}>Create user</Button>
-</section>
+      <Button size="sm" on:click={() => (shouldShowCreateUserForm = true)}>Create user</Button>
+    </section>
+  </section>
+</UserPageLayout>