mirror of
				https://github.com/immich-app/immich.git
				synced 2025-11-03 19:17:11 -05:00 
			
		
		
		
	feat(web): add warning when setting a quota superior to the disk size (#6737)
* refactor: inline warning * fix: do not use onmount * chore: remove outdated comment * wording --------- Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
This commit is contained in:
		
							parent
							
								
									4290a29107
								
							
						
					
					
						commit
						7a1f25b515
					
				@ -5,6 +5,7 @@
 | 
				
			|||||||
  import { notificationController, NotificationType } from '../shared-components/notification/notification';
 | 
					  import { notificationController, NotificationType } from '../shared-components/notification/notification';
 | 
				
			||||||
  import Button from '../elements/buttons/button.svelte';
 | 
					  import Button from '../elements/buttons/button.svelte';
 | 
				
			||||||
  import { convertToBytes } from '$lib/utils/byte-converter';
 | 
					  import { convertToBytes } from '$lib/utils/byte-converter';
 | 
				
			||||||
 | 
					  import { serverInfo } from '$lib/stores/server-info.store';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  let error: string;
 | 
					  let error: string;
 | 
				
			||||||
  let success: string;
 | 
					  let success: string;
 | 
				
			||||||
@ -13,9 +14,11 @@
 | 
				
			|||||||
  let confirmPassowrd = '';
 | 
					  let confirmPassowrd = '';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  let canCreateUser = false;
 | 
					  let canCreateUser = false;
 | 
				
			||||||
 | 
					  let quotaSize: number | undefined = undefined;
 | 
				
			||||||
  let isCreatingUser = false;
 | 
					  let isCreatingUser = false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  $: quotaSizeWarning = quotaSize && convertToBytes(Number(quotaSize), 'GiB') > $serverInfo.diskSizeRaw;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  $: {
 | 
					  $: {
 | 
				
			||||||
    if (password !== confirmPassowrd && confirmPassowrd.length > 0) {
 | 
					    if (password !== confirmPassowrd && confirmPassowrd.length > 0) {
 | 
				
			||||||
      error = 'Password does not match';
 | 
					      error = 'Password does not match';
 | 
				
			||||||
@ -121,8 +124,12 @@
 | 
				
			|||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <div class="m-4 flex flex-col gap-2">
 | 
					    <div class="m-4 flex flex-col gap-2">
 | 
				
			||||||
      <label class="immich-form-label" for="quotaSize">Quota Size (GiB)</label>
 | 
					      <label class="flex items-center gap-2 immich-form-label" for="quotaSize"
 | 
				
			||||||
      <input class="immich-form-input" id="quotaSize" name="quotaSize" type="number" min="0" />
 | 
					        >Quota Size (GiB) {#if quotaSizeWarning}
 | 
				
			||||||
 | 
					          <p class="text-red-400 text-sm">You set a quota higher than the disk size</p>
 | 
				
			||||||
 | 
					        {/if}</label
 | 
				
			||||||
 | 
					      >
 | 
				
			||||||
 | 
					      <input class="immich-form-input" id="quotaSize" name="quotaSize" type="number" min="0" bind:value={quotaSize} />
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    {#if error}
 | 
					    {#if error}
 | 
				
			||||||
 | 
				
			|||||||
@ -10,14 +10,22 @@
 | 
				
			|||||||
  import CircleIconButton from '../elements/buttons/circle-icon-button.svelte';
 | 
					  import CircleIconButton from '../elements/buttons/circle-icon-button.svelte';
 | 
				
			||||||
  import { handleError } from '$lib/utils/handle-error';
 | 
					  import { handleError } from '$lib/utils/handle-error';
 | 
				
			||||||
  import { convertFromBytes, convertToBytes } from '$lib/utils/byte-converter';
 | 
					  import { convertFromBytes, convertToBytes } from '$lib/utils/byte-converter';
 | 
				
			||||||
 | 
					  import { serverInfo } from '$lib/stores/server-info.store';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  export let user: UserResponseDto;
 | 
					  export let user: UserResponseDto;
 | 
				
			||||||
  export let canResetPassword = true;
 | 
					  export let canResetPassword = true;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  let error: string;
 | 
					  let error: string;
 | 
				
			||||||
  let success: string;
 | 
					  let success: string;
 | 
				
			||||||
 | 
					 | 
				
			||||||
  let isShowResetPasswordConfirmation = false;
 | 
					  let isShowResetPasswordConfirmation = false;
 | 
				
			||||||
 | 
					  let quotaSize = user.quotaSizeInBytes ? convertFromBytes(user.quotaSizeInBytes, 'GiB') : null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const previousQutoa = user.quotaSizeInBytes;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  $: quotaSizeWarning =
 | 
				
			||||||
 | 
					    previousQutoa !== convertToBytes(Number(quotaSize), 'GiB') &&
 | 
				
			||||||
 | 
					    !!quotaSize &&
 | 
				
			||||||
 | 
					    convertToBytes(Number(quotaSize), 'GiB') > $serverInfo.diskSizeRaw;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const dispatch = createEventDispatcher<{
 | 
					  const dispatch = createEventDispatcher<{
 | 
				
			||||||
    close: void;
 | 
					    close: void;
 | 
				
			||||||
@ -25,8 +33,6 @@
 | 
				
			|||||||
    editSuccess: void;
 | 
					    editSuccess: void;
 | 
				
			||||||
  }>();
 | 
					  }>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  let quotaSize = user.quotaSizeInBytes ? convertFromBytes(user.quotaSizeInBytes, 'GiB') : null;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const editUser = async () => {
 | 
					  const editUser = async () => {
 | 
				
			||||||
    try {
 | 
					    try {
 | 
				
			||||||
      const { id, email, name, storageLabel, externalPath } = user;
 | 
					      const { id, email, name, storageLabel, externalPath } = user;
 | 
				
			||||||
@ -102,7 +108,11 @@
 | 
				
			|||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <div class="m-4 flex flex-col gap-2">
 | 
					    <div class="m-4 flex flex-col gap-2">
 | 
				
			||||||
      <label class="immich-form-label" for="quotaSize">Quota Size (GiB)</label>
 | 
					      <label class="flex items-center gap-2 immich-form-label" for="quotaSize"
 | 
				
			||||||
 | 
					        >Quota Size (GiB) {#if quotaSizeWarning}
 | 
				
			||||||
 | 
					          <p class="text-red-400 text-sm">You set a quota higher than the disk size</p>
 | 
				
			||||||
 | 
					        {/if}</label
 | 
				
			||||||
 | 
					      >
 | 
				
			||||||
      <input class="immich-form-input" id="quotaSize" name="quotaSize" type="number" min="0" bind:value={quotaSize} />
 | 
					      <input class="immich-form-input" id="quotaSize" name="quotaSize" type="number" min="0" bind:value={quotaSize} />
 | 
				
			||||||
      <p>Note: Enter 0 for unlimited quota</p>
 | 
					      <p>Note: Enter 0 for unlimited quota</p>
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
 | 
				
			|||||||
@ -2,13 +2,13 @@
 | 
				
			|||||||
  import Icon from '$lib/components/elements/icon.svelte';
 | 
					  import Icon from '$lib/components/elements/icon.svelte';
 | 
				
			||||||
  import { locale } from '$lib/stores/preferences.store';
 | 
					  import { locale } from '$lib/stores/preferences.store';
 | 
				
			||||||
  import { websocketStore } from '$lib/stores/websocket';
 | 
					  import { websocketStore } from '$lib/stores/websocket';
 | 
				
			||||||
  import { api } from '@api';
 | 
					 | 
				
			||||||
  import { onMount } from 'svelte';
 | 
					  import { onMount } from 'svelte';
 | 
				
			||||||
  import { asByteUnitString } from '../../utils/byte-units';
 | 
					  import { asByteUnitString } from '../../utils/byte-units';
 | 
				
			||||||
  import LoadingSpinner from './loading-spinner.svelte';
 | 
					  import LoadingSpinner from './loading-spinner.svelte';
 | 
				
			||||||
  import { mdiChartPie, mdiDns } from '@mdi/js';
 | 
					  import { mdiChartPie, mdiDns } from '@mdi/js';
 | 
				
			||||||
  import { serverInfoStore } from '$lib/stores/server-info.store';
 | 
					  import { serverInfo } from '$lib/stores/server-info.store';
 | 
				
			||||||
  import { user } from '$lib/stores/user.store';
 | 
					  import { user } from '$lib/stores/user.store';
 | 
				
			||||||
 | 
					  import { requestServerInfo } from '$lib/utils/auth';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const { serverVersion, connected } = websocketStore;
 | 
					  const { serverVersion, connected } = websocketStore;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -16,8 +16,8 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  $: version = $serverVersion ? `v${$serverVersion.major}.${$serverVersion.minor}.${$serverVersion.patch}` : null;
 | 
					  $: version = $serverVersion ? `v${$serverVersion.major}.${$serverVersion.minor}.${$serverVersion.patch}` : null;
 | 
				
			||||||
  $: hasQuota = $user?.quotaSizeInBytes !== null;
 | 
					  $: hasQuota = $user?.quotaSizeInBytes !== null;
 | 
				
			||||||
  $: availableBytes = (hasQuota ? $user?.quotaSizeInBytes : $serverInfoStore?.diskSizeRaw) || 0;
 | 
					  $: availableBytes = (hasQuota ? $user?.quotaSizeInBytes : $serverInfo?.diskSizeRaw) || 0;
 | 
				
			||||||
  $: usedBytes = (hasQuota ? $user?.quotaUsageInBytes : $serverInfoStore?.diskUseRaw) || 0;
 | 
					  $: usedBytes = (hasQuota ? $user?.quotaUsageInBytes : $serverInfo?.diskUseRaw) || 0;
 | 
				
			||||||
  $: usedPercentage = Math.round((usedBytes / availableBytes) * 100);
 | 
					  $: usedPercentage = Math.round((usedBytes / availableBytes) * 100);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const onUpdate = () => {
 | 
					  const onUpdate = () => {
 | 
				
			||||||
@ -39,19 +39,8 @@
 | 
				
			|||||||
  $: $user && onUpdate();
 | 
					  $: $user && onUpdate();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  onMount(async () => {
 | 
					  onMount(async () => {
 | 
				
			||||||
    await refresh();
 | 
					    await requestServerInfo();
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					 | 
				
			||||||
  const refresh = async () => {
 | 
					 | 
				
			||||||
    try {
 | 
					 | 
				
			||||||
      if (!$serverInfoStore) {
 | 
					 | 
				
			||||||
        const { data } = await api.serverInfoApi.getServerInfo();
 | 
					 | 
				
			||||||
        $serverInfoStore = data;
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    } catch (e) {
 | 
					 | 
				
			||||||
      console.log('Error [StatusBox] [onMount]');
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  };
 | 
					 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<div class="dark:text-immich-dark-fg">
 | 
					<div class="dark:text-immich-dark-fg">
 | 
				
			||||||
@ -64,7 +53,7 @@
 | 
				
			|||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
    <div class="hidden group-hover:sm:block md:block">
 | 
					    <div class="hidden group-hover:sm:block md:block">
 | 
				
			||||||
      <p class="text-sm font-medium text-immich-primary dark:text-immich-dark-primary">Storage</p>
 | 
					      <p class="text-sm font-medium text-immich-primary dark:text-immich-dark-primary">Storage</p>
 | 
				
			||||||
      {#if $serverInfoStore}
 | 
					      {#if $serverInfo}
 | 
				
			||||||
        <div class="my-2 h-[7px] w-full rounded-full bg-gray-200 dark:bg-gray-700">
 | 
					        <div class="my-2 h-[7px] w-full rounded-full bg-gray-200 dark:bg-gray-700">
 | 
				
			||||||
          <div class="h-[7px] rounded-full {usageClasses}" style="width: {usedPercentage}%" />
 | 
					          <div class="h-[7px] rounded-full {usageClasses}" style="width: {usedPercentage}%" />
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
 | 
				
			|||||||
@ -1,4 +1,4 @@
 | 
				
			|||||||
import { writable } from 'svelte/store';
 | 
					import { writable } from 'svelte/store';
 | 
				
			||||||
import type { ServerInfoResponseDto } from '@api';
 | 
					import type { ServerInfoResponseDto } from '@api';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const serverInfoStore = writable<ServerInfoResponseDto>();
 | 
					export const serverInfo = writable<ServerInfoResponseDto>();
 | 
				
			||||||
 | 
				
			|||||||
@ -2,6 +2,7 @@ import { api } from '@api';
 | 
				
			|||||||
import { redirect } from '@sveltejs/kit';
 | 
					import { redirect } from '@sveltejs/kit';
 | 
				
			||||||
import { AppRoute } from '../constants';
 | 
					import { AppRoute } from '../constants';
 | 
				
			||||||
import { getSavedUser, setUser } from '$lib/stores/user.store';
 | 
					import { getSavedUser, setUser } from '$lib/stores/user.store';
 | 
				
			||||||
 | 
					import { serverInfo } from '$lib/stores/server-info.store';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export interface AuthOptions {
 | 
					export interface AuthOptions {
 | 
				
			||||||
  admin?: true;
 | 
					  admin?: true;
 | 
				
			||||||
@ -16,7 +17,6 @@ export const getAuthUser = async () => {
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// TODO: re-use already loaded user (once) instead of fetching on each page navigation
 | 
					 | 
				
			||||||
export const authenticate = async (options?: AuthOptions) => {
 | 
					export const authenticate = async (options?: AuthOptions) => {
 | 
				
			||||||
  options = options || {};
 | 
					  options = options || {};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -36,6 +36,13 @@ export const authenticate = async (options?: AuthOptions) => {
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const requestServerInfo = async () => {
 | 
				
			||||||
 | 
					  if (getSavedUser()) {
 | 
				
			||||||
 | 
					    const { data } = await api.serverInfoApi.getServerInfo();
 | 
				
			||||||
 | 
					    serverInfo.set(data);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const isLoggedIn = async () => {
 | 
					export const isLoggedIn = async () => {
 | 
				
			||||||
  const savedUser = getSavedUser();
 | 
					  const savedUser = getSavedUser();
 | 
				
			||||||
  const user = savedUser || (await getAuthUser());
 | 
					  const user = savedUser || (await getAuthUser());
 | 
				
			||||||
 | 
				
			|||||||
@ -1,9 +1,10 @@
 | 
				
			|||||||
import { authenticate } from '$lib/utils/auth';
 | 
					import { authenticate, requestServerInfo } from '$lib/utils/auth';
 | 
				
			||||||
import { api } from '@api';
 | 
					import { api } from '@api';
 | 
				
			||||||
import type { PageLoad } from './$types';
 | 
					import type { PageLoad } from './$types';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const load = (async () => {
 | 
					export const load = (async () => {
 | 
				
			||||||
  await authenticate({ admin: true });
 | 
					  await authenticate({ admin: true });
 | 
				
			||||||
 | 
					  await requestServerInfo();
 | 
				
			||||||
  const { data: allUsers } = await api.userApi.getAllUsers({ isAll: false });
 | 
					  const { data: allUsers } = await api.userApi.getAllUsers({ isAll: false });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return {
 | 
					  return {
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user