mirror of
https://github.com/immich-app/immich.git
synced 2026-01-24 12:47:25 -05:00
refactor: tables
This commit is contained in:
parent
94ea83c415
commit
87fbe95f58
10
pnpm-lock.yaml
generated
10
pnpm-lock.yaml
generated
@ -738,8 +738,8 @@ importers:
|
||||
specifier: file:../open-api/typescript-sdk
|
||||
version: link:../open-api/typescript-sdk
|
||||
'@immich/ui':
|
||||
specifier: ^0.56.1
|
||||
version: 0.56.1(@sveltejs/kit@2.49.2(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.46.1)(vite@7.3.0(@types/node@25.0.3)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.46.1)(vite@7.3.0(@types/node@25.0.3)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.46.1)
|
||||
specifier: ^0.57.3
|
||||
version: 0.57.3(@sveltejs/kit@2.49.2(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.46.1)(vite@7.3.0(@types/node@25.0.3)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.46.1)(vite@7.3.0(@types/node@25.0.3)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.46.1)
|
||||
'@mapbox/mapbox-gl-rtl-text':
|
||||
specifier: 0.2.3
|
||||
version: 0.2.3(mapbox-gl@1.13.3)
|
||||
@ -3087,8 +3087,8 @@ packages:
|
||||
peerDependencies:
|
||||
svelte: ^5.0.0
|
||||
|
||||
'@immich/ui@0.56.1':
|
||||
resolution: {integrity: sha512-W4uEQn9pxVKRvIV7sl9p6dU2r7xlVsMFxBeClxtXzSsiJEoE10uZwBIm0L9q17c4TQ/+lk9e/w1e4jNSvFqFwQ==}
|
||||
'@immich/ui@0.57.3':
|
||||
resolution: {integrity: sha512-5Y0KmyHRojem1gvX4hbr01GZ35oq22AkYE3CImvg3+jmZQhP0newTiqyVYJsfnEupLZKu5bFIlWykIe8uwMqDQ==}
|
||||
peerDependencies:
|
||||
svelte: ^5.0.0
|
||||
|
||||
@ -15104,7 +15104,7 @@ snapshots:
|
||||
dependencies:
|
||||
svelte: 5.46.1
|
||||
|
||||
'@immich/ui@0.56.1(@sveltejs/kit@2.49.2(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.46.1)(vite@7.3.0(@types/node@25.0.3)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.46.1)(vite@7.3.0(@types/node@25.0.3)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.46.1)':
|
||||
'@immich/ui@0.57.3(@sveltejs/kit@2.49.2(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.46.1)(vite@7.3.0(@types/node@25.0.3)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.46.1)(vite@7.3.0(@types/node@25.0.3)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.46.1)':
|
||||
dependencies:
|
||||
'@immich/svelte-markdown-preprocess': 0.1.0(svelte@5.46.1)
|
||||
'@internationalized/date': 3.10.0
|
||||
|
||||
@ -27,7 +27,7 @@
|
||||
"@formatjs/icu-messageformat-parser": "^3.0.0",
|
||||
"@immich/justified-layout-wasm": "^0.4.3",
|
||||
"@immich/sdk": "file:../open-api/typescript-sdk",
|
||||
"@immich/ui": "^0.56.1",
|
||||
"@immich/ui": "^0.57.3",
|
||||
"@mapbox/mapbox-gl-rtl-text": "0.2.3",
|
||||
"@mdi/js": "^7.4.47",
|
||||
"@photo-sphere-viewer/core": "^5.14.0",
|
||||
|
||||
@ -60,7 +60,7 @@
|
||||
{disabled}
|
||||
onCheckedChange={() => handleCheckboxChange(option.value)}
|
||||
/>
|
||||
<Label label={option.text} for="{option.value}-checkbox" />
|
||||
<Label label={option.text} for="{option.value}-checkbox" size="small" />
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
@ -31,7 +31,7 @@
|
||||
|
||||
<div>
|
||||
<div class="flex h-6.5 place-items-center gap-1">
|
||||
<Label>{title}</Label>
|
||||
<Label size="small">{title}</Label>
|
||||
{#if isEdited}
|
||||
<div
|
||||
transition:fly={{ x: 10, duration: 200, easing: quintOut }}
|
||||
|
||||
@ -39,7 +39,7 @@
|
||||
{#if showSettingDescription}
|
||||
<div>
|
||||
<div class="flex h-6.5 place-items-center gap-1">
|
||||
<Label>{$t('language')}</Label>
|
||||
<Label size="small">{$t('language')}</Label>
|
||||
</div>
|
||||
|
||||
<Text size="small" color="muted">{$t('language_setting_description')}</Text>
|
||||
|
||||
@ -5,7 +5,7 @@
|
||||
import { getApiKeyActions, getApiKeysActions } from '$lib/services/api-key.service';
|
||||
import { locale } from '$lib/stores/preferences.store';
|
||||
import { getApiKeys, type ApiKeyResponseDto } from '@immich/sdk';
|
||||
import { Button } from '@immich/ui';
|
||||
import { Button, Table, TableBody, TableCell, TableHeader, TableHeading, TableRow, Text } from '@immich/ui';
|
||||
import { t } from 'svelte-i18n';
|
||||
import { fade } from 'svelte/transition';
|
||||
|
||||
@ -20,15 +20,11 @@
|
||||
};
|
||||
|
||||
const onApiKeyUpdate = (update: ApiKeyResponseDto) => {
|
||||
for (const key of keys) {
|
||||
if (key.id === update.id) {
|
||||
Object.assign(key, update);
|
||||
}
|
||||
}
|
||||
keys = keys.map((key) => (key.id === update.id ? update : key));
|
||||
};
|
||||
|
||||
const onApiKeyDelete = ({ id }: ApiKeyResponseDto) => {
|
||||
keys = keys.filter((apiKey) => apiKey.id !== id);
|
||||
keys = keys.filter((key) => key.id !== id);
|
||||
};
|
||||
|
||||
const { Create } = $derived(getApiKeysActions($t));
|
||||
@ -39,45 +35,39 @@
|
||||
<section class="my-4">
|
||||
<div class="flex flex-col gap-2" in:fade={{ duration: 500 }}>
|
||||
<div class="mb-2 flex justify-end">
|
||||
<Button leadingIcon={Create.icon} shape="round" size="small" onclick={() => Create.onAction(Create)}
|
||||
>{Create.title}</Button
|
||||
>
|
||||
<Button leadingIcon={Create.icon} shape="round" size="small" onclick={() => Create.onAction(Create)}>
|
||||
{Create.title}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{#if keys.length > 0}
|
||||
<table class="w-full text-start">
|
||||
<thead
|
||||
class="mb-4 flex h-12 w-full rounded-md border bg-gray-50 text-primary dark:border-immich-dark-gray dark:bg-immich-dark-gray"
|
||||
>
|
||||
<tr class="flex w-full place-items-center">
|
||||
<th class="w-1/4 text-center text-sm font-medium">{$t('name')}</th>
|
||||
<th class="w-1/4 text-center text-sm font-medium">{$t('permission')}</th>
|
||||
<th class="w-1/4 text-center text-sm font-medium">{$t('created')}</th>
|
||||
<th class="w-1/4 text-center text-sm font-medium">{$t('action')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="block w-full overflow-y-auto rounded-md border dark:border-immich-dark-gray">
|
||||
<Table class="mt-4" striped spacing="small">
|
||||
<TableHeader>
|
||||
<TableHeading>{$t('name')}</TableHeading>
|
||||
<TableHeading>{$t('permission')}</TableHeading>
|
||||
<TableHeading>{$t('created')}</TableHeading>
|
||||
<TableHeading>{$t('action')}</TableHeading>
|
||||
</TableHeader>
|
||||
|
||||
<TableBody>
|
||||
{#each keys as key (key.id)}
|
||||
{@const { Update, Delete } = getApiKeyActions($t, key)}
|
||||
<tr
|
||||
class="flex h-20 w-full place-items-center text-center dark:text-immich-dark-fg even:bg-subtle/20 odd:bg-subtle/80"
|
||||
>
|
||||
<td class="w-1/4 text-ellipsis px-4 text-sm overflow-hidden">{key.name}</td>
|
||||
<td
|
||||
class="w-1/4 text-ellipsis px-4 text-xs overflow-hidden line-clamp-3 break-all font-mono"
|
||||
title={JSON.stringify(key.permissions, undefined, 2)}>{key.permissions}</td
|
||||
>
|
||||
<td class="w-1/4 text-ellipsis px-4 text-sm overflow-hidden"
|
||||
>{new Date(key.createdAt).toLocaleDateString($locale, dateFormats.settings)}
|
||||
</td>
|
||||
<td class="flex flex-row flex-wrap justify-center gap-x-2 gap-y-1 w-1/4">
|
||||
<TableRow>
|
||||
<TableCell>{key.name}</TableCell>
|
||||
<TableCell class="font-mono">
|
||||
<Text class="font-mono" size="small" title={JSON.stringify(key.permissions, null, 2)}
|
||||
>{key.permissions}</Text
|
||||
>
|
||||
</TableCell>
|
||||
<TableCell>{new Date(key.createdAt).toLocaleDateString($locale, dateFormats.settings)}</TableCell>
|
||||
<TableCell class="flex flex-row flex-wrap justify-center gap-x-2 gap-y-1">
|
||||
<TableButton action={Update} size="small" />
|
||||
<TableButton action={Delete} size="small" />
|
||||
</td>
|
||||
</tr>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
</TableBody>
|
||||
</Table>
|
||||
{/if}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@ -7,6 +7,7 @@
|
||||
type AlbumStatisticsResponseDto,
|
||||
type AssetStatsResponseDto,
|
||||
} from '@immich/sdk';
|
||||
import { Heading, Stack, Table, TableBody, TableCell, TableHeader, TableHeading, TableRow } from '@immich/ui';
|
||||
import { onMount } from 'svelte';
|
||||
import { t } from 'svelte-i18n';
|
||||
|
||||
@ -56,56 +57,73 @@
|
||||
</script>
|
||||
|
||||
{#snippet row(viewName: string, stats: AssetStatsResponseDto)}
|
||||
<tr
|
||||
class="flex h-14 w-full place-items-center text-center dark:text-immich-dark-fg even:bg-subtle/20 odd:bg-subtle/80"
|
||||
>
|
||||
<td class="w-1/4 px-4 text-sm">{viewName}</td>
|
||||
<td class="w-1/4 px-4 text-sm">{stats.images.toLocaleString($locale)}</td>
|
||||
<td class="w-1/4 px-4 text-sm">{stats.videos.toLocaleString($locale)}</td>
|
||||
<td class="w-1/4 px-4">{stats.total.toLocaleString($locale)}</td>
|
||||
</tr>
|
||||
<TableRow>
|
||||
<TableCell class="w-1/4">{viewName}</TableCell>
|
||||
<TableCell class="w-1/4">{stats.images.toLocaleString($locale)}</TableCell>
|
||||
<TableCell class="w-1/4">{stats.videos.toLocaleString($locale)}</TableCell>
|
||||
<TableCell class="w-1/4">{stats.total.toLocaleString($locale)}</TableCell>
|
||||
</TableRow>
|
||||
{/snippet}
|
||||
|
||||
<section class="my-6">
|
||||
<p class="text-xs dark:text-white uppercase">{$t('photos_and_videos')}</p>
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full text-start mt-4">
|
||||
<thead
|
||||
class="mb-4 flex h-12 w-full rounded-md border bg-gray-50 text-primary dark:border-immich-dark-gray dark:bg-immich-dark-gray"
|
||||
>
|
||||
<tr class="flex w-full place-items-center text-sm font-medium text-center">
|
||||
<th class="w-1/4">{$t('view_name')}</th>
|
||||
<th class="w-1/4">{$t('photos')}</th>
|
||||
<th class="w-1/4">{$t('videos')}</th>
|
||||
<th class="w-1/4">{$t('total')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="block w-full overflow-y-auto rounded-md border dark:border-immich-dark-gray">
|
||||
{@render row($t('timeline'), timelineStats)}
|
||||
{@render row($t('favorites'), favoriteStats)}
|
||||
{@render row($t('archive'), archiveStats)}
|
||||
{@render row($t('trash'), trashStats)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{#snippet card(viewName: string, stats: AssetStatsResponseDto)}
|
||||
<Table striped spacing="small" size="small" class="border-0 mt-6">
|
||||
<TableHeader>
|
||||
<TableHeading>{viewName}</TableHeading>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
<TableRow>
|
||||
<TableCell class="w-1/2">{$t('photos')}</TableCell>
|
||||
<TableCell class="w-1/2">{stats.images.toLocaleString($locale)}</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell class="w-1/2">{$t('videos')}</TableCell>
|
||||
<TableCell class="w-1/2">{stats.videos.toLocaleString($locale)}</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell class="w-1/2">{$t('total')}</TableCell>
|
||||
<TableCell class="w-1/2">{stats.total.toLocaleString($locale)}</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
{/snippet}
|
||||
|
||||
<div class="mt-6">
|
||||
<p class="text-xs dark:text-white uppercase">{$t('albums')}</p>
|
||||
</div>
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full text-start mt-4">
|
||||
<thead class="mb-4 flex h-12 w-full rounded-md border text-primary dark:border-immich-dark-gray bg-subtle">
|
||||
<tr class="flex w-full place-items-center text-sm font-medium text-center">
|
||||
<th class="w-1/2">{$t('owned')}</th>
|
||||
<th class="w-1/2">{$t('shared')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="block w-full overflow-y-auto rounded-md border dark:border-immich-dark-gray">
|
||||
<tr class="flex h-14 w-full place-items-center text-center dark:text-immich-dark-fg bg-subtle/20">
|
||||
<td class="w-1/2 px-4 text-sm">{albumStats.owned.toLocaleString($locale)}</td>
|
||||
<td class="w-1/2 px-4 text-sm">{albumStats.shared.toLocaleString($locale)}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<section class="my-6 block sm:hidden">
|
||||
<Stack gap={4}>
|
||||
{@render card($t('timeline'), timelineStats)}
|
||||
{@render card($t('favorites'), favoriteStats)}
|
||||
{@render card($t('archive'), archiveStats)}
|
||||
{@render card($t('trash'), trashStats)}
|
||||
</Stack>
|
||||
</section>
|
||||
|
||||
<section class="my-6 w-full hidden sm:block">
|
||||
<Heading size="tiny">{$t('photos_and_videos')}</Heading>
|
||||
<Table striped spacing="medium" class="mt-4">
|
||||
<TableHeader>
|
||||
<TableHeading class="w-1/4">{$t('view_name')}</TableHeading>
|
||||
<TableHeading class="w-1/4">{$t('photos')}</TableHeading>
|
||||
<TableHeading class="w-1/4">{$t('videos')}</TableHeading>
|
||||
<TableHeading class="w-1/4">{$t('total')}</TableHeading>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{@render row($t('timeline'), timelineStats)}
|
||||
{@render row($t('favorites'), favoriteStats)}
|
||||
{@render row($t('archive'), archiveStats)}
|
||||
{@render row($t('trash'), trashStats)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
|
||||
<Heading size="tiny" class="mt-8">{$t('albums')}</Heading>
|
||||
<Table striped spacing="medium" class="mt-4">
|
||||
<TableHeader>
|
||||
<TableHeading class="w-1/2">{$t('owned')}</TableHeading>
|
||||
<TableHeading class="w-1/2">{$t('shared')}</TableHeading>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
<TableRow>
|
||||
<TableCell class="w-1/2">{albumStats.owned.toLocaleString($locale)}</TableCell>
|
||||
<TableCell class="w-1/2">{albumStats.shared.toLocaleString($locale)}</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</section>
|
||||
|
||||
@ -8,7 +8,20 @@
|
||||
import { locale } from '$lib/stores/preferences.store';
|
||||
import { getBytesWithUnit } from '$lib/utils/byte-units';
|
||||
import { getLibrary, getLibraryStatistics, type LibraryResponseDto } from '@immich/sdk';
|
||||
import { Button, CommandPaletteDefaultProvider } from '@immich/ui';
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
CardHeader,
|
||||
CommandPaletteDefaultProvider,
|
||||
Container,
|
||||
Heading,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHeader,
|
||||
TableHeading,
|
||||
TableRow,
|
||||
} from '@immich/ui';
|
||||
import type { Snippet } from 'svelte';
|
||||
import { t } from 'svelte-i18n';
|
||||
import { fade } from 'svelte/transition';
|
||||
@ -47,6 +60,15 @@
|
||||
};
|
||||
|
||||
const { Create, ScanAll } = $derived(getLibrariesActions($t, libraries));
|
||||
|
||||
const classes = {
|
||||
column1: 'w-4/12',
|
||||
column2: 'w-4/12',
|
||||
column3: 'w-2/12',
|
||||
column4: 'w-2/12',
|
||||
column5: 'w-2/12',
|
||||
column6: 'w-2/12',
|
||||
};
|
||||
</script>
|
||||
|
||||
<OnEvents {onLibraryCreate} {onLibraryUpdate} {onLibraryDelete} />
|
||||
@ -54,51 +76,71 @@
|
||||
<CommandPaletteDefaultProvider name={$t('library')} actions={[Create, ScanAll]} />
|
||||
|
||||
<AdminPageLayout breadcrumbs={[{ title: data.meta.title }]} actions={[ScanAll, Create]}>
|
||||
<section class="my-4">
|
||||
<Container size="large" center class="my-4">
|
||||
<div class="flex flex-col items-center gap-2" in:fade={{ duration: 500 }}>
|
||||
{#if libraries.length > 0}
|
||||
<table class="text-start">
|
||||
<thead
|
||||
class="mb-4 flex h-12 w-full rounded-md border bg-gray-50 text-primary dark:border-immich-dark-gray dark:bg-immich-dark-gray"
|
||||
>
|
||||
<tr class="grid grid-cols-6 w-full place-items-center">
|
||||
<th class="text-center text-sm font-medium">{$t('name')}</th>
|
||||
<th class="text-center text-sm font-medium">{$t('owner')}</th>
|
||||
<th class="text-center text-sm font-medium">{$t('photos')}</th>
|
||||
<th class="text-center text-sm font-medium">{$t('videos')}</th>
|
||||
<th class="text-center text-sm font-medium">{$t('size')}</th>
|
||||
<th class="text-center text-sm font-medium"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="block overflow-y-auto rounded-md border dark:border-immich-dark-gray">
|
||||
<Table striped class="hidden sm:block">
|
||||
<TableHeader>
|
||||
<TableHeading class={classes.column1}>{$t('name')}</TableHeading>
|
||||
<TableHeading class={classes.column2}>{$t('owner')}</TableHeading>
|
||||
<TableHeading class={classes.column3}>{$t('photos')}</TableHeading>
|
||||
<TableHeading class={classes.column4}>{$t('videos')}</TableHeading>
|
||||
<TableHeading class={classes.column5}>{$t('size')}</TableHeading>
|
||||
<TableHeading class={classes.column6}></TableHeading>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{#each libraries as library (library.id + library.name)}
|
||||
{@const { photos, usage, videos } = statistics[library.id]}
|
||||
{@const [diskUsage, diskUsageUnit] = getBytesWithUnit(usage, 0)}
|
||||
<tr
|
||||
class="grid grid-cols-6 h-20 w-full place-items-center text-center dark:text-immich-dark-fg even:bg-subtle/20 odd:bg-subtle/80"
|
||||
>
|
||||
<td class="text-ellipsis px-4 text-sm">{library.name}</td>
|
||||
<td class="text-ellipsis px-4 text-sm">
|
||||
{owners[library.id].name}
|
||||
</td>
|
||||
<td class="text-ellipsis px-4 text-sm">
|
||||
{photos.toLocaleString($locale)}
|
||||
</td>
|
||||
<td class="text-ellipsis px-4 text-sm">
|
||||
{videos.toLocaleString($locale)}
|
||||
</td>
|
||||
<td class="text-ellipsis px-4 text-sm">
|
||||
{diskUsage}
|
||||
{diskUsageUnit}
|
||||
</td>
|
||||
|
||||
<td class="flex gap-2 text-ellipsis px-4 text-sm">
|
||||
<TableRow>
|
||||
<TableCell class={classes.column1}>{library.name}</TableCell>
|
||||
<TableCell class={classes.column2}>{owners[library.id].name}</TableCell>
|
||||
<TableCell class={classes.column3}>{photos.toLocaleString($locale)}</TableCell>
|
||||
<TableCell class={classes.column4}>{videos.toLocaleString($locale)}</TableCell>
|
||||
<TableCell class={classes.column5}>{diskUsage} {diskUsageUnit}</TableCell>
|
||||
<TableCell class={classes.column6}>
|
||||
<Button size="small" onclick={() => handleViewLibrary(library)}>{$t('view')}</Button>
|
||||
</td>
|
||||
</tr>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
</TableBody>
|
||||
</Table>
|
||||
|
||||
<div class="flex sm:hidden flex-col gap-4 w-full">
|
||||
{#each libraries as library (library.id + library.name)}
|
||||
{@const { photos, usage, videos } = statistics[library.id]}
|
||||
{@const [diskUsage, diskUsageUnit] = getBytesWithUnit(usage, 0)}
|
||||
|
||||
<Card color="secondary">
|
||||
<CardHeader>
|
||||
<div class="flex justify-between items-center">
|
||||
<Heading>{library.name}</Heading>
|
||||
<Button size="small" onclick={() => handleViewLibrary(library)}>{$t('view')}</Button>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<Table shape="rectangle" spacing="small" size="medium" border={false}>
|
||||
<TableBody>
|
||||
<TableRow>
|
||||
<TableCell class="w-1/2">{$t('owner')}</TableCell>
|
||||
<TableCell class="w-1/2">{owners[library.id].name}</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell class="w-1/2">{$t('photos')}</TableCell>
|
||||
<TableCell class="w-1/2">{photos.toLocaleString($locale)}</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell class="w-1/2">{$t('videos')}</TableCell>
|
||||
<TableCell class="w-1/2">{videos.toLocaleString($locale)}</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell class="w-1/2">{$t('total')}</TableCell>
|
||||
<TableCell class="w-1/2">{diskUsage} {diskUsageUnit}</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</Card>
|
||||
{/each}
|
||||
</div>
|
||||
{:else}
|
||||
<EmptyPlaceholder
|
||||
text={$t('no_libraries_message')}
|
||||
@ -109,5 +151,5 @@
|
||||
|
||||
{@render children?.()}
|
||||
</div>
|
||||
</section>
|
||||
</Container>
|
||||
</AdminPageLayout>
|
||||
|
||||
@ -5,7 +5,18 @@
|
||||
import { locale } from '$lib/stores/preferences.store';
|
||||
import { getByteUnitString } from '$lib/utils/byte-units';
|
||||
import { searchUsersAdmin, type UserAdminResponseDto } from '@immich/sdk';
|
||||
import { Button, CommandPaletteDefaultProvider, Container, Icon } from '@immich/ui';
|
||||
import {
|
||||
Button,
|
||||
CommandPaletteDefaultProvider,
|
||||
Container,
|
||||
Icon,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHeader,
|
||||
TableHeading,
|
||||
TableRow,
|
||||
} from '@immich/ui';
|
||||
import { mdiInfinity } from '@mdi/js';
|
||||
import type { Snippet } from 'svelte';
|
||||
import { t } from 'svelte-i18n';
|
||||
@ -34,6 +45,13 @@
|
||||
};
|
||||
|
||||
const { Create } = $derived(getUserAdminsActions($t));
|
||||
|
||||
const classes = {
|
||||
column1: 'w-8/12 sm:w-5/12 lg:w-6/12 xl:w-4/12 2xl:w-5/12',
|
||||
column2: 'hidden sm:block w-3/12',
|
||||
column3: 'hidden xl:block w-3/12 2xl:w-2/12',
|
||||
column4: 'w-4/12 lg:w-3/12 xl:w-2/12',
|
||||
};
|
||||
</script>
|
||||
|
||||
<OnEvents
|
||||
@ -48,28 +66,19 @@
|
||||
|
||||
<AdminPageLayout breadcrumbs={[{ title: data.meta.title }]} actions={[Create]}>
|
||||
<Container center size="large">
|
||||
<table class="my-5 w-full text-start">
|
||||
<thead
|
||||
class="mb-4 flex h-12 w-full rounded-md border bg-gray-50 text-primary dark:border-immich-dark-gray dark:bg-immich-dark-gray"
|
||||
>
|
||||
<tr class="flex w-full place-items-center">
|
||||
<th class="w-8/12 sm:w-5/12 lg:w-6/12 xl:w-4/12 2xl:w-5/12 text-center text-sm font-medium">{$t('email')}</th>
|
||||
<th class="hidden sm:block w-3/12 text-center text-sm font-medium">{$t('name')}</th>
|
||||
<th class="hidden xl:block w-3/12 2xl:w-2/12 text-center text-sm font-medium">{$t('has_quota')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="block w-full overflow-y-auto rounded-md border dark:border-immich-dark-gray">
|
||||
<Table class="mt-4" striped spacing="large">
|
||||
<TableHeader>
|
||||
<TableHeading class={classes.column1}>{$t('email')}</TableHeading>
|
||||
<TableHeading class={classes.column2}>{$t('name')}</TableHeading>
|
||||
<TableHeading class={classes.column3}>{$t('has_quota')}</TableHeading>
|
||||
</TableHeader>
|
||||
|
||||
<TableBody>
|
||||
{#each users as user (user.id)}
|
||||
<tr
|
||||
class="flex h-20 overflow-hidden w-full place-items-center text-center dark:text-immich-dark-fg {user.deletedAt
|
||||
? 'bg-red-300 dark:bg-red-900'
|
||||
: 'even:bg-subtle/20 odd:bg-subtle/80'}"
|
||||
>
|
||||
<td class="w-8/12 sm:w-5/12 lg:w-6/12 xl:w-4/12 2xl:w-5/12 text-ellipsis break-all px-2 text-sm">
|
||||
{user.email}
|
||||
</td>
|
||||
<td class="hidden sm:block w-3/12 text-ellipsis break-all px-2 text-sm">{user.name}</td>
|
||||
<td class="hidden xl:block w-3/12 2xl:w-2/12 text-ellipsis break-all px-2 text-sm">
|
||||
<TableRow color={user.deletedAt ? 'danger' : undefined}>
|
||||
<TableCell class={classes.column1}>{user.email}</TableCell>
|
||||
<TableCell class={classes.column2}>{user.name}</TableCell>
|
||||
<TableCell class={classes.column3}>
|
||||
<div class="container mx-auto flex flex-wrap justify-center">
|
||||
{#if user.quotaSizeInBytes !== null && user.quotaSizeInBytes >= 0}
|
||||
{getByteUnitString(user.quotaSizeInBytes, $locale)}
|
||||
@ -77,16 +86,14 @@
|
||||
<Icon icon={mdiInfinity} size="16" />
|
||||
{/if}
|
||||
</div>
|
||||
</td>
|
||||
<td
|
||||
class="flex flex-row flex-wrap justify-center gap-x-2 gap-y-1 w-4/12 lg:w-3/12 xl:w-2/12 text-ellipsis break-all text-sm"
|
||||
>
|
||||
</TableCell>
|
||||
<TableCell class="{classes.column4} flex flex-row flex-wrap justify-center gap-x-2 gap-y-1">
|
||||
<Button onclick={() => handleNavigateUserAdmin(user)}>{$t('view')}</Button>
|
||||
</td>
|
||||
</tr>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
</TableBody>
|
||||
</Table>
|
||||
|
||||
{@render children?.()}
|
||||
</Container>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user