feat(web): improved server stats (#1870)

* feat(web): improved server stats

* fix(web): don't log unauthorized errors

* Revert "fix(web): don't log unauthorized errors"

This reverts commit 7fc2987a77.
This commit is contained in:
Michel Heusschen 2023-02-26 20:57:34 +01:00 committed by GitHub
parent 7d45ae68a6
commit 368142e79b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
25 changed files with 199 additions and 260 deletions

View file

@ -1549,19 +1549,7 @@ export interface ServerStatsResponseDto {
* @type {number}
* @memberof ServerStatsResponseDto
*/
'objects': number;
/**
*
* @type {number}
* @memberof ServerStatsResponseDto
*/
'usageRaw': number;
/**
*
* @type {string}
* @memberof ServerStatsResponseDto
*/
'usage': string;
'usage': number;
/**
*
* @type {Array<UsageByUserDto>}
@ -2184,10 +2172,16 @@ export interface UsageByUserDto {
'userId': string;
/**
*
* @type {number}
* @type {string}
* @memberof UsageByUserDto
*/
'videos': number;
'userFirstName': string;
/**
*
* @type {string}
* @memberof UsageByUserDto
*/
'userLastName': string;
/**
*
* @type {number}
@ -2199,13 +2193,13 @@ export interface UsageByUserDto {
* @type {number}
* @memberof UsageByUserDto
*/
'usageRaw': number;
'videos': number;
/**
*
* @type {string}
* @type {number}
* @memberof UsageByUserDto
*/
'usage': string;
'usage': number;
}
/**
*

View file

@ -1,43 +1,20 @@
<script lang="ts">
import { api, ServerStatsResponseDto, UserResponseDto } from '@api';
import { ServerStatsResponseDto } from '@api';
import CameraIris from 'svelte-material-icons/CameraIris.svelte';
import PlayCircle from 'svelte-material-icons/PlayCircle.svelte';
import Memory from 'svelte-material-icons/Memory.svelte';
import StatsCard from './stats-card.svelte';
import { getBytesWithUnit, asByteUnitString } from '../../../utils/byte-units';
import { onMount, onDestroy } from 'svelte';
import LoadingSpinner from '$lib/components/shared-components/loading-spinner.svelte';
import { asByteUnitString, getBytesWithUnit } from '../../../utils/byte-units';
import { locale } from '$lib/stores/preferences.store';
export let allUsers: Array<UserResponseDto>;
let stats: ServerStatsResponseDto;
let setIntervalHandler: NodeJS.Timer;
onMount(async () => {
const { data } = await api.serverInfoApi.getStats();
stats = data;
setIntervalHandler = setInterval(async () => {
const { data } = await api.serverInfoApi.getStats();
stats = data;
}, 5000);
});
onDestroy(() => {
clearInterval(setIntervalHandler);
});
const getFullName = (userId: string) => {
let name = 'Admin'; // since we do not have admin user in allUsers
allUsers.forEach((user) => {
if (user.id === userId) name = `${user.firstName} ${user.lastName}`;
});
return name;
export let stats: ServerStatsResponseDto = {
photos: 0,
videos: 0,
usage: 0,
usageByUser: []
};
// Stats are unavailable if data is not loaded yet
$: [spaceUsage, spaceUnit] = getBytesWithUnit(stats ? stats.usageRaw : 0);
$: [statsUsage, statsUsageUnit] = getBytesWithUnit(stats.usage, 0);
</script>
<div class="flex flex-col gap-5">
@ -45,14 +22,9 @@
<p class="text-sm dark:text-immich-dark-fg">TOTAL USAGE</p>
<div class="flex mt-5 justify-between">
<StatsCard logo={CameraIris} title={'PHOTOS'} value={stats && stats.photos.toString()} />
<StatsCard logo={PlayCircle} title={'VIDEOS'} value={stats && stats.videos.toString()} />
<StatsCard
logo={Memory}
title={'STORAGE'}
value={stats && spaceUsage.toString()}
unit={spaceUnit}
/>
<StatsCard logo={CameraIris} title="PHOTOS" value={stats.photos} />
<StatsCard logo={PlayCircle} title="VIDEOS" value={stats.videos} />
<StatsCard logo={Memory} title="STORAGE" value={statsUsage} unit={statsUsageUnit} />
</div>
</div>
@ -72,32 +44,19 @@
<tbody
class="overflow-y-auto rounded-md w-full max-h-[320px] block border dark:border-immich-dark-gray dark:text-immich-dark-fg"
>
{#if stats}
{#each stats.usageByUser as user, i}
<tr
class={`text-center flex place-items-center w-full h-[50px] ${
i % 2 == 0
? 'bg-immich-gray dark:bg-immich-dark-gray/75'
: 'bg-immich-bg dark:bg-immich-dark-gray/50'
}`}
>
<td class="text-sm px-2 w-1/4 text-ellipsis">{getFullName(user.userId)}</td>
<td class="text-sm px-2 w-1/4 text-ellipsis">{user.photos.toLocaleString($locale)}</td
>
<td class="text-sm px-2 w-1/4 text-ellipsis">{user.videos.toLocaleString($locale)}</td
>
<td class="text-sm px-2 w-1/4 text-ellipsis">{asByteUnitString(user.usageRaw)}</td>
</tr>
{/each}
{:else}
{#each stats.usageByUser as user (user.userId)}
<tr
class="text-center flex place-items-center w-full h-[50px] bg-immich-gray dark:bg-immich-dark-gray/75"
class="text-center flex place-items-center w-full h-[50px] even:bg-immich-bg even:dark:bg-immich-dark-gray/50 odd:bg-immich-gray odd:dark:bg-immich-dark-gray/75"
>
<td class="w-full flex justify-center">
<LoadingSpinner />
</td>
<td class="text-sm px-2 w-1/4 text-ellipsis"
>{user.userFirstName} {user.userLastName}</td
>
<td class="text-sm px-2 w-1/4 text-ellipsis">{user.photos.toLocaleString($locale)}</td>
<td class="text-sm px-2 w-1/4 text-ellipsis">{user.videos.toLocaleString($locale)}</td>
<td class="text-sm px-2 w-1/4 text-ellipsis">{asByteUnitString(user.usage, $locale)}</td
>
</tr>
{/if}
{/each}
</tbody>
</table>
</div>

View file

@ -1,19 +1,14 @@
<script lang="ts">
import type Icon from 'svelte-material-icons/AbTesting.svelte';
import LoadingSpinner from '$lib/components/shared-components/loading-spinner.svelte';
export let logo: typeof Icon;
export let title: string;
export let value: string;
export let value: number;
export let unit: string | undefined = undefined;
$: zeros = () => {
if (!value) {
return '';
}
const maxLength = 13;
const valueLength = parseInt(value).toString().length;
const valueLength = value.toString().length;
const zeroLength = maxLength - valueLength;
return '0'.repeat(zeroLength);
@ -29,15 +24,9 @@
</div>
<div class="relative text-center font-mono font-semibold text-2xl">
{#if value !== undefined}
<span class="text-[#DCDADA] dark:text-[#525252]">{zeros()}</span><span
class="text-immich-primary dark:text-immich-dark-primary">{parseInt(value)}</span
>
{:else}
<div class="flex justify-end pr-2">
<LoadingSpinner />
</div>
{/if}
<span class="text-[#DCDADA] dark:text-[#525252]">{zeros()}</span><span
class="text-immich-primary dark:text-immich-dark-primary">{value}</span
>
{#if unit}
<span class="absolute -top-5 right-2 text-base font-light text-gray-400">{unit}</span>
{/if}

View file

@ -135,7 +135,7 @@
<p>{asset.exifInfo.exifImageHeight} x {asset.exifInfo.exifImageWidth}</p>
{/if}
<p>{asByteUnitString(asset.exifInfo.fileSizeInByte)}</p>
<p>{asByteUnitString(asset.exifInfo.fileSizeInByte, $locale)}</p>
</div>
</div>
</div>

View file

@ -5,6 +5,7 @@
import LoadingSpinner from './loading-spinner.svelte';
import { api, ServerInfoResponseDto } from '@api';
import { asByteUnitString } from '../../utils/byte-units';
import { locale } from '$lib/stores/preferences.store';
let isServerOk = true;
let serverVersion = '';
@ -63,7 +64,8 @@
/>
</div>
<p class="text-xs">
{asByteUnitString(serverInfo?.diskUseRaw)} of {asByteUnitString(serverInfo?.diskSizeRaw)} used
{asByteUnitString(serverInfo?.diskUseRaw, $locale)} of
{asByteUnitString(serverInfo?.diskSizeRaw, $locale)} used
</p>
{:else}
<div class="mt-2">

View file

@ -3,6 +3,7 @@
import { asByteUnitString } from '$lib/utils/byte-units';
import { UploadAsset } from '$lib/models/upload-asset';
import ImmichLogo from './immich-logo.svelte';
import { locale } from '$lib/stores/preferences.store';
export let uploadAsset: UploadAsset;
@ -50,7 +51,7 @@
<input
disabled
class="bg-gray-100 border w-full p-1 rounded-md text-[10px] px-2"
value={`[${asByteUnitString(uploadAsset.file.size)}] ${uploadAsset.file.name}`}
value={`[${asByteUnitString(uploadAsset.file.size, $locale)}] ${uploadAsset.file.name}`}
/>
<div class="w-full bg-gray-300 h-[15px] rounded-md mt-[5px] text-white relative">

View file

@ -38,8 +38,7 @@ export function getBytesWithUnit(bytes: number, maxPrecision = 1): [number, stri
* @param maxPrecision maximum number of decimal places, default is `1`
* @returns localized bytes with unit as string
*/
export function asByteUnitString(bytes: number, maxPrecision = 1): string {
const locale = Array.from(navigator.languages);
export function asByteUnitString(bytes: number, locale?: string, maxPrecision = 1): string {
const [size, unit] = getBytesWithUnit(bytes, maxPrecision);
return `${size.toLocaleString(locale)} ${unit}`;
}

View file

@ -10,12 +10,12 @@ export const load = (async ({ parent, locals: { api } }) => {
throw redirect(302, '/photos');
}
const { data: allUsers } = await api.userApi.getAllUsers(false);
const { data: stats } = await api.serverInfoApi.getStats();
return {
allUsers,
stats,
meta: {
title: 'Server Status'
title: 'Server Stats'
}
};
}) satisfies PageServerLoad;

View file

@ -1,8 +1,22 @@
<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 { page } from '$app/stores';
import type { PageData } from './$types';
export let data: PageData;
let setIntervalHandler: NodeJS.Timer;
onMount(async () => {
setIntervalHandler = setInterval(async () => {
const { data: stats } = await api.serverInfoApi.getStats();
data.stats = stats;
}, 5000);
});
onDestroy(() => {
clearInterval(setIntervalHandler);
});
</script>
{#if $page.data.allUsers}
<ServerStatsPanel allUsers={$page.data.allUsers} />
{/if}
<ServerStatsPanel stats={data.stats} />