mirror of
				https://github.com/KevinMidboe/immich.git
				synced 2025-10-29 17:40:28 +00:00 
			
		
		
		
	feat(web) styling server stats page (#866)
This commit is contained in:
		| @@ -48,7 +48,7 @@ html::-webkit-scrollbar-thumb:hover { | |||||||
| body { | body { | ||||||
| 	/* min-height: 100vh; */ | 	/* min-height: 100vh; */ | ||||||
| 	margin: 0; | 	margin: 0; | ||||||
| 	background-color: #f6f8fe; | 	/* background-color: #f6f8fe; */ | ||||||
| 	color: #5f6368; | 	color: #5f6368; | ||||||
| } | } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -11,23 +11,25 @@ | |||||||
| 	const dispatch = createEventDispatcher(); | 	const dispatch = createEventDispatcher(); | ||||||
| </script> | </script> | ||||||
|  |  | ||||||
| <div class="flex border p-6 rounded-2xl bg-white"> | <div class="flex border-b pb-5"> | ||||||
| 	<div class="w-[70%]"> | 	<div class="w-[70%]"> | ||||||
| 		<h1 class="font-medium text-immich-primary">{title}</h1> | 		<h1 class="text-immich-primary text-sm">{title.toUpperCase()}</h1> | ||||||
| 		<p class="text-sm mt-1 font-medium">{subtitle}</p> | 		<p class="text-sm mt-1">{subtitle}</p> | ||||||
| 		<p class="text-sm"> | 		<p class="text-sm"> | ||||||
| 			<slot /> | 			<slot /> | ||||||
| 		</p> | 		</p> | ||||||
| 		<table class="text-left w-full mt-4"> | 		<table class="text-left w-full mt-5"> | ||||||
| 			<!-- table header --> | 			<!-- table header --> | ||||||
| 			<thead class="border rounded-md mb-2 bg-gray-50 flex text-immich-primary w-full h-12"> | 			<thead | ||||||
|  | 				class="border rounded-md mb-2 bg-immich-primary/10 flex text-immich-primary w-full h-12" | ||||||
|  | 			> | ||||||
| 				<tr class="flex w-full place-items-center"> | 				<tr class="flex w-full place-items-center"> | ||||||
| 					<th class="text-center w-1/3 font-medium text-sm">Status</th> | 					<th class="text-center w-1/3 font-medium text-sm">Status</th> | ||||||
| 					<th class="text-center w-1/3 font-medium text-sm">Active</th> | 					<th class="text-center w-1/3 font-medium text-sm">Active</th> | ||||||
| 					<th class="text-center w-1/3 font-medium text-sm">Waiting</th> | 					<th class="text-center w-1/3 font-medium text-sm">Waiting</th> | ||||||
| 				</tr> | 				</tr> | ||||||
| 			</thead> | 			</thead> | ||||||
| 			<tbody class="overflow-y-auto rounded-md w-full max-h-[320px] block border"> | 			<tbody class="overflow-y-auto rounded-md w-full max-h-[320px] block border bg-white"> | ||||||
| 				<tr class="text-center flex place-items-center w-full h-[40px]"> | 				<tr class="text-center flex place-items-center w-full h-[40px]"> | ||||||
| 					<td class="text-sm px-2 w-1/3 text-ellipsis">{jobStatus ? 'Active' : 'Idle'}</td> | 					<td class="text-sm px-2 w-1/3 text-ellipsis">{jobStatus ? 'Active' : 'Idle'}</td> | ||||||
| 					<td class="text-sm px-2 w-1/3 text-ellipsis">{activeJobCount}</td> | 					<td class="text-sm px-2 w-1/3 text-ellipsis">{activeJobCount}</td> | ||||||
| @@ -39,7 +41,7 @@ | |||||||
| 	<div class="w-[30%] flex place-items-center place-content-end"> | 	<div class="w-[30%] flex place-items-center place-content-end"> | ||||||
| 		<button | 		<button | ||||||
| 			on:click={() => dispatch('click')} | 			on:click={() => dispatch('click')} | ||||||
| 			class="border px-6 py-3 text-sm bg-gray-50 font-medium rounded-2xl hover:bg-immich-primary/10 transition-all hover:cursor-pointer disabled:cursor-not-allowed" | 			class="px-6 py-3 text-sm bg-immich-primary font-medium rounded-2xl hover:bg-immich-primary/50 transition-all hover:cursor-pointer disabled:cursor-not-allowed shadow-sm text-immich-bg" | ||||||
| 			disabled={jobStatus} | 			disabled={jobStatus} | ||||||
| 		> | 		> | ||||||
| 			{#if jobStatus} | 			{#if jobStatus} | ||||||
|   | |||||||
| @@ -106,7 +106,7 @@ | |||||||
| 	}; | 	}; | ||||||
| </script> | </script> | ||||||
|  |  | ||||||
| <div class="flex flex-col gap-6"> | <div class="flex flex-col gap-10"> | ||||||
| 	<JobTile | 	<JobTile | ||||||
| 		title={'Generate thumbnails'} | 		title={'Generate thumbnails'} | ||||||
| 		subtitle={'Regenerate missing thumbnail (JPEG, WEBP)'} | 		subtitle={'Regenerate missing thumbnail (JPEG, WEBP)'} | ||||||
|   | |||||||
| @@ -1,5 +1,10 @@ | |||||||
| <script lang="ts"> | <script lang="ts"> | ||||||
| 	import { ServerStatsResponseDto, UserResponseDto } from '@api'; | 	import { ServerStatsResponseDto, UserResponseDto } from '@api'; | ||||||
|  | 	import CameraIris from 'svelte-material-icons/CameraIris.svelte'; | ||||||
|  | 	import PlayCircle from 'svelte-material-icons/PlayCircle.svelte'; | ||||||
|  | 	import FileImageOutline from 'svelte-material-icons/FileImageOutline.svelte'; | ||||||
|  | 	import Memory from 'svelte-material-icons/Memory.svelte'; | ||||||
|  | 	import StatsCard from './stats-card.svelte'; | ||||||
| 	export let stats: ServerStatsResponseDto; | 	export let stats: ServerStatsResponseDto; | ||||||
| 	export let allUsers: Array<UserResponseDto>; | 	export let allUsers: Array<UserResponseDto>; | ||||||
| 
 | 
 | ||||||
| @@ -10,24 +15,27 @@ | |||||||
| 		}); | 		}); | ||||||
| 		return name; | 		return name; | ||||||
| 	}; | 	}; | ||||||
|  | 
 | ||||||
|  | 	$: spaceUnit = stats.usage.slice(stats.usage.length - 2, stats.usage.length); | ||||||
|  | 	$: spaceUsage = stats.usage.slice(0, stats.usage.length - 2); | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
| <div class="flex flex-col gap-6"> | <div class="flex flex-col gap-5"> | ||||||
| 	<div class="border p-6 rounded-2xl bg-white text-center"> | 	<div> | ||||||
| 		<h1 class="font-medium text-immich-primary">Server Usage</h1> | 		<p class="text-sm">TOTAL USAGE</p> | ||||||
| 		<div class="flex flex-row gap-6 mt-4 font-medium"> | 
 | ||||||
| 			<p class="grow">Photos: {stats.photos}</p> | 		<div class="flex mt-5 justify-between"> | ||||||
| 			<p class="grow">Videos: {stats.videos}</p> | 			<StatsCard logo={CameraIris} title={'PHOTOS'} value={stats.photos.toString()} /> | ||||||
| 			<p class="grow">Objects: {stats.objects}</p> | 			<StatsCard logo={PlayCircle} title={'VIDEOS'} value={stats.videos.toString()} /> | ||||||
| 			<p class="grow">Size: {stats.usage}</p> | 			<StatsCard logo={FileImageOutline} title={'OBJECTS'} value={stats.objects.toString()} /> | ||||||
|  | 			<StatsCard logo={Memory} title={'STORAGE'} value={spaceUsage} unit={spaceUnit} /> | ||||||
| 		</div> | 		</div> | ||||||
| 	</div> | 	</div> | ||||||
| 
 | 
 | ||||||
| 	<div class="border p-6 rounded-2xl bg-white"> | 	<div> | ||||||
| 		<h1 class="font-medium text-immich-primary">Usage by User</h1> | 		<p class="text-sm">USER USAGE DETAIL</p> | ||||||
| 		<table class="text-left w-full mt-4"> | 		<table class="text-left w-full mt-5"> | ||||||
| 			<!-- table header --> | 			<thead class="border rounded-md mb-4 bg-gray-50 flex text-immich-primary w-full h-12"> | ||||||
| 			<thead class="border rounded-md mb-2 bg-gray-50 flex text-immich-primary w-full h-12"> |  | ||||||
| 				<tr class="flex w-full place-items-center"> | 				<tr class="flex w-full place-items-center"> | ||||||
| 					<th class="text-center w-1/5 font-medium text-sm">User</th> | 					<th class="text-center w-1/5 font-medium text-sm">User</th> | ||||||
| 					<th class="text-center w-1/5 font-medium text-sm">Photos</th> | 					<th class="text-center w-1/5 font-medium text-sm">Photos</th> | ||||||
| @@ -37,8 +45,12 @@ | |||||||
| 				</tr> | 				</tr> | ||||||
| 			</thead> | 			</thead> | ||||||
| 			<tbody class="overflow-y-auto rounded-md w-full max-h-[320px] block border"> | 			<tbody class="overflow-y-auto rounded-md w-full max-h-[320px] block border"> | ||||||
| 				{#each stats.usageByUser as user} | 				{#each stats.usageByUser as user, i} | ||||||
| 					<tr class="text-center flex place-items-center w-full h-[40px]"> | 					<tr | ||||||
|  | 						class={`text-center flex place-items-center w-full h-[50px] ${ | ||||||
|  | 							i % 2 == 0 ? 'bg-immich-gray' : 'bg-immich-bg' | ||||||
|  | 						}`} | ||||||
|  | 					> | ||||||
| 						<td class="text-sm px-2 w-1/5 text-ellipsis">{getFullName(user.userId)}</td> | 						<td class="text-sm px-2 w-1/5 text-ellipsis">{getFullName(user.userId)}</td> | ||||||
| 						<td class="text-sm px-2 w-1/5 text-ellipsis">{user.photos}</td> | 						<td class="text-sm px-2 w-1/5 text-ellipsis">{user.photos}</td> | ||||||
| 						<td class="text-sm px-2 w-1/5 text-ellipsis">{user.videos}</td> | 						<td class="text-sm px-2 w-1/5 text-ellipsis">{user.videos}</td> | ||||||
| @@ -0,0 +1,33 @@ | |||||||
|  | <script lang="ts"> | ||||||
|  | 	export let logo: any; | ||||||
|  | 	export let title: string; | ||||||
|  | 	export let value: string; | ||||||
|  | 	export let unit: string | undefined = undefined; | ||||||
|  |  | ||||||
|  | 	$: zeros = () => { | ||||||
|  | 		let result = ''; | ||||||
|  | 		const maxLength = 9; | ||||||
|  | 		const valueLength = parseInt(value).toString().length; | ||||||
|  | 		const zeroLength = maxLength - valueLength; | ||||||
|  | 		for (let i = 0; i < zeroLength; i++) { | ||||||
|  | 			result += '0'; | ||||||
|  | 		} | ||||||
|  | 		return result; | ||||||
|  | 	}; | ||||||
|  | </script> | ||||||
|  |  | ||||||
|  | <div class="w-[180px] h-[140px] bg-immich-gray rounded-3xl p-5 flex flex-col justify-between"> | ||||||
|  | 	<div class="flex place-items-center gap-4"> | ||||||
|  | 		<svelte:component this={logo} size="40" color={'#4250af'} /> | ||||||
|  | 		<p class="text-immich-primary">{title}</p> | ||||||
|  | 	</div> | ||||||
|  |  | ||||||
|  | 	<div class="relative text-center font-mono font-semibold text-2xl"> | ||||||
|  | 		<span class="text-[#DCDADA]">{zeros()}</span><span class="text-immich-primary" | ||||||
|  | 			>{parseInt(value)}</span | ||||||
|  | 		> | ||||||
|  | 		{#if unit} | ||||||
|  | 			<span class="absolute -top-5 right-2 text-base font-light text-gray-400">{unit}</span> | ||||||
|  | 		{/if} | ||||||
|  | 	</div> | ||||||
|  | </div> | ||||||
| @@ -8,8 +8,8 @@ | |||||||
| 	const dispatch = createEventDispatcher(); | 	const dispatch = createEventDispatcher(); | ||||||
| </script> | </script> | ||||||
|  |  | ||||||
| <table class="text-left w-full my-4"> | <table class="text-left w-full my-5"> | ||||||
| 	<thead class="border rounded-md mb-2 bg-gray-50 flex text-immich-primary w-full h-12 "> | 	<thead class="border rounded-md mb-4 bg-gray-50 flex text-immich-primary w-full h-12 "> | ||||||
| 		<tr class="flex w-full place-items-center"> | 		<tr class="flex w-full place-items-center"> | ||||||
| 			<th class="text-center w-1/4 font-medium text-sm">Email</th> | 			<th class="text-center w-1/4 font-medium text-sm">Email</th> | ||||||
| 			<th class="text-center w-1/4 font-medium text-sm">First name</th> | 			<th class="text-center w-1/4 font-medium text-sm">First name</th> | ||||||
| @@ -20,7 +20,7 @@ | |||||||
| 	<tbody class="overflow-y-auto rounded-md w-full max-h-[320px] block border"> | 	<tbody class="overflow-y-auto rounded-md w-full max-h-[320px] block border"> | ||||||
| 		{#each allUsers as user, i} | 		{#each allUsers as user, i} | ||||||
| 			<tr | 			<tr | ||||||
| 				class={`text-center flex place-items-center w-full border-b h-[80px] ${ | 				class={`text-center flex place-items-center w-full h-[80px] ${ | ||||||
| 					i % 2 == 0 ? 'bg-gray-100' : 'bg-immich-bg' | 					i % 2 == 0 ? 'bg-gray-100' : 'bg-immich-bg' | ||||||
| 				}`} | 				}`} | ||||||
| 			> | 			> | ||||||
|   | |||||||
| @@ -15,7 +15,7 @@ | |||||||
| 	import type { PageData } from './$types'; | 	import type { PageData } from './$types'; | ||||||
| 	import { api, ServerStatsResponseDto, UserResponseDto } from '@api'; | 	import { api, ServerStatsResponseDto, UserResponseDto } from '@api'; | ||||||
| 	import JobsPanel from '$lib/components/admin-page/jobs/jobs-panel.svelte'; | 	import JobsPanel from '$lib/components/admin-page/jobs/jobs-panel.svelte'; | ||||||
| 	import ServerStats from '$lib/components/admin-page/server-stats.svelte'; | 	import ServerStatsPanel from '$lib/components/admin-page/server-stats/server-stats-panel.svelte'; | ||||||
|  |  | ||||||
| 	let selectedAction: AdminSideBarSelection = AdminSideBarSelection.USER_MANAGEMENT; | 	let selectedAction: AdminSideBarSelection = AdminSideBarSelection.USER_MANAGEMENT; | ||||||
|  |  | ||||||
| @@ -33,7 +33,7 @@ | |||||||
| 	}; | 	}; | ||||||
|  |  | ||||||
| 	onMount(() => { | 	onMount(() => { | ||||||
| 		selectedAction = AdminSideBarSelection.USER_MANAGEMENT; | 		selectedAction = AdminSideBarSelection.JOBS; | ||||||
| 		getServerStats(); | 		getServerStats(); | ||||||
| 	}); | 	}); | ||||||
|  |  | ||||||
| @@ -146,13 +146,13 @@ | |||||||
| 		</div> | 		</div> | ||||||
| 	</section> | 	</section> | ||||||
| 	<section class="overflow-y-auto relative"> | 	<section class="overflow-y-auto relative"> | ||||||
| 		<div id="setting-title" class="pt-10 fixed w-full z-50 bg-immich-bg"> | 		<div id="setting-title" class="pt-10 fixed w-full z-50"> | ||||||
| 			<h1 class="text-lg ml-8 mb-4 text-immich-primary font-medium">{selectedAction}</h1> | 			<h1 class="text-lg ml-8 mb-4 text-immich-primary font-medium">{selectedAction}</h1> | ||||||
| 			<hr /> | 			<hr /> | ||||||
| 		</div> | 		</div> | ||||||
|  |  | ||||||
| 		<section id="setting-content" class="relative pt-[85px] flex place-content-center"> | 		<section id="setting-content" class="relative pt-[85px] flex place-content-center"> | ||||||
| 			<section class="w-[800px] pt-4"> | 			<section class="w-[800px] pt-5"> | ||||||
| 				{#if selectedAction === AdminSideBarSelection.USER_MANAGEMENT} | 				{#if selectedAction === AdminSideBarSelection.USER_MANAGEMENT} | ||||||
| 					<UserManagement | 					<UserManagement | ||||||
| 						allUsers={data.allUsers} | 						allUsers={data.allUsers} | ||||||
| @@ -164,7 +164,7 @@ | |||||||
| 					<JobsPanel /> | 					<JobsPanel /> | ||||||
| 				{/if} | 				{/if} | ||||||
| 				{#if selectedAction === AdminSideBarSelection.STATS && serverStat} | 				{#if selectedAction === AdminSideBarSelection.STATS && serverStat} | ||||||
| 					<ServerStats stats={serverStat} allUsers={data.allUsers} /> | 					<ServerStatsPanel stats={serverStat} allUsers={data.allUsers} /> | ||||||
| 				{/if} | 				{/if} | ||||||
| 			</section> | 			</section> | ||||||
| 		</section> | 		</section> | ||||||
|   | |||||||
| @@ -4,9 +4,9 @@ module.exports = { | |||||||
| 		extend: { | 		extend: { | ||||||
| 			colors: { | 			colors: { | ||||||
| 				'immich-primary': '#4250af', | 				'immich-primary': '#4250af', | ||||||
| 				'immich-bg': '#f6f8fe', | 				'immich-bg': 'white', | ||||||
| 				'immich-fg': 'black' | 				'immich-fg': 'black', | ||||||
|  | 				'immich-gray': '#F6F6F4' | ||||||
| 				// 'immich-bg': '#121212', | 				// 'immich-bg': '#121212', | ||||||
| 				// 'immich-fg': '#D0D0D0', | 				// 'immich-fg': '#D0D0D0', | ||||||
| 			}, | 			}, | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user