Simple User Quota (#367)

* Simple User Quota
* 👷Fix build with OpenSearch
This commit is contained in:
Anton Shepilov
2024-02-22 10:17:25 +01:00
committed by GitHub
parent df0809ec48
commit 63f2c281c0
34 changed files with 10549 additions and 5836 deletions
@@ -7,6 +7,7 @@ import { DriveItemAtom, DriveItemChildrenAtom } from '../state/store';
import { BrowseFilter, DriveItem, DriveItemVersion } from '../types';
import { SharedWithMeFilterState } from '../state/shared-with-me-filter';
import Languages from 'features/global/services/languages-service';
import { useUserQuota } from "features/users/hooks/use-user-quota";
/**
* Returns the children of a drive item
@@ -15,6 +16,7 @@ import Languages from 'features/global/services/languages-service';
export const useDriveActions = () => {
const companyId = useRouterCompany();
const sharedFilter = useRecoilValue(SharedWithMeFilterState);
const { getQuota } = useUserQuota();
const refresh = useRecoilCallback(
({ set, snapshot }) =>
@@ -52,6 +54,7 @@ export const useDriveActions = () => {
try {
driveFile = await DriveApiClient.create(companyId, { item, version });
await refresh(driveFile.parent_id!);
await getQuota();
} catch (e) {
ToasterService.error(Languages.t('hooks.use-drive-actions.unable_create_file'));
}
@@ -89,6 +92,7 @@ export const useDriveActions = () => {
try {
await DriveApiClient.remove(companyId, id);
await refresh(parentId || '');
await getQuota();
} catch (e) {
ToasterService.error(Languages.t('hooks.use-drive-actions.unable_remove_file'));
}
@@ -9,3 +9,15 @@ export const formatBytes = (bytes: number, decimals = 2) => {
return `${parseFloat((bytes / Math.pow(k, i)).toFixed(dm))} ${sizes[i]}`;
};
export const formatBytesToInt = (bytes: number, decimals = 2) => {
if (!+bytes) return '0 KB';
const k = 1024;
const dm = decimals < 0 ? 0 : decimals;
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return `${parseInt((bytes / Math.pow(k, i)).toFixed(dm))} ${sizes[i]}`;
};
@@ -11,6 +11,7 @@ export enum FeatureNames {
COMPANY_SEARCH_USERS = 'company:search_users',
COMPANY_SHARED_DRIVE = 'company:shared_drive',
COMPANY_DISPLAY_EMAIL = 'company:display_email',
COMPANY_USER_QUOTA = 'company:user_quota',
}
export type FeatureValueType = boolean | number;
@@ -29,6 +30,7 @@ availableFeaturesWithDefaults.set(FeatureNames.COMPANY_INVITE_MEMBER, true);
availableFeaturesWithDefaults.set(FeatureNames.COMPANY_SEARCH_USERS, true);
availableFeaturesWithDefaults.set(FeatureNames.COMPANY_SHARED_DRIVE, true);
availableFeaturesWithDefaults.set(FeatureNames.COMPANY_DISPLAY_EMAIL, true);
availableFeaturesWithDefaults.set(FeatureNames.COMPANY_USER_QUOTA, false);
/**
* ChannelServiceImpl that allow you to manage feature flipping in Tdrive using react feature toggles
@@ -70,7 +72,6 @@ class FeatureTogglesService {
}
public isActiveFeatureName(featureName: FeatureNames) {
console.debug(this.activeFeatureNames)
const b = this.activeFeatureNames.includes(featureName);
console.debug(`Feature ${featureName} is ${b}`);
return b;
@@ -7,6 +7,7 @@ import WorkspaceAPIClient from '../../workspaces/api/workspace-api-client';
import CurrentUser from '../../../deprecated/user/CurrentUser';
import { setUserList } from '../hooks/use-user-list';
import Logger from 'features/global/framework/logger-service';
import { UserQuota } from "features/users/types/user-quota";
export type SearchContextType = {
scope: 'company' | 'workspace' | 'all';
@@ -141,6 +142,19 @@ class UserAPIClientService {
});
}
async getQuota(userId: string): Promise<UserQuota> {
return Api.get<UserQuota>(
`/internal/services/users/v1/users/${userId}/quota`,
undefined,
false
).then(result => {
return result;
}).catch(e => {
console.log(`Error getting quota:: ${e.message}`)
return { } as UserQuota;
});
}
async getCompany(companyId: string): Promise<CompanyType> {
return Api.get<{ resource: CompanyType }>(
`/internal/services/users/v1/companies/${companyId}`,
@@ -0,0 +1,41 @@
import { useCallback, useEffect } from "react";
import { UserQuota } from '@features/users/types/user-quota';
import UserAPIClient from '@features/users/api/user-api-client';
import { useCurrentUser } from "features/users/hooks/use-current-user";
import { atom, useRecoilState } from "recoil";
export const QuotaState = atom<UserQuota>({
key: 'QuotaState',
default: {
used: 0,
remaining: 1,
total: 1
},
});
export const useUserQuota = () => {
const nullQuota = {
used: 0,
remaining: 1,
total: 1
}
const {user } = useCurrentUser();
const [quota, setQuota] = useRecoilState(QuotaState);
const getQuota = useCallback(async () => {
let data: UserQuota = nullQuota;
if (user?.id) {
data = await UserAPIClient.getQuota(user.id);
} else {
data = nullQuota;
}
setQuota(data)
}, []);
useEffect(() => {
getQuota();
}, []);
return { quota, getQuota };
};
@@ -0,0 +1,5 @@
export type UserQuota = {
used: number;
remaining: number;
total: number;
}
@@ -1,25 +1,76 @@
import { Base, Title } from '@atoms/text';
import { useDriveItem } from '@features/drive/hooks/use-drive-item';
import { formatBytes } from '@features/drive/utils';
import { useCurrentUser } from 'app/features/users/hooks/use-current-user';
import { Base } from "@atoms/text";
import { formatBytesToInt } from "@features/drive/utils";
import Languages from "features/global/services/languages-service";
import { useUserQuota } from "@features/users/hooks/use-user-quota";
import RouterServices from "features/router/services/router-service";
import { useEffect, useState } from "react";
import FeatureTogglesService, { FeatureNames } from "@features/global/services/feature-toggles-service";
import { useDriveItem } from "features/drive/hooks/use-drive-item";
const DiskUsage = () => {
const { viewId } = RouterServices.getStateFromRoute();
console.log("VIEW-iD::" + viewId);
const [used, setUsed] = useState(0);
const [usedBytes, setUsedBytes] = useState(0);
const [totalBytes, setTotalBytes] = useState(0);
if (FeatureTogglesService.isActiveFeatureName(FeatureNames.COMPANY_USER_QUOTA)) {
const { quota } = useUserQuota()
useEffect(() => {
setUsed(Math.round(quota.used / quota.total * 100))
setUsedBytes(quota.used);
setTotalBytes(quota.total);
}, [quota]);
} else if (viewId) {
const { item } = useDriveItem(viewId);
useEffect(() => {
setUsedBytes(item?.size || 0);
}, [viewId, item])
}
export default () => {
const { access, item } = useDriveItem('root');
const { item: trash } = useDriveItem('trash');
const { user } = useCurrentUser();
return (
<>
{access !== 'read' && (
{FeatureTogglesService.isActiveFeatureName(FeatureNames.COMPANY_USER_QUOTA) && (
<div className="bg-zinc-500 dark:bg-zinc-800 bg-opacity-10 rounded-md p-4 w-auto max-w-md">
<div className="w-full">
<Title>
{formatBytes(item?.size || 0)}
<Base> { Languages.t('components.disk_usage.used')} </Base> <Base>{formatBytes(trash?.size || 0)} {Languages.t('components.disk_usage.in_trash')}</Base>
</Title>
<div className="overflow-hidden h-4 mb-4 text-xs flex rounded bg-emerald-200">
{used > 90 && (
<div style={{ width: used + '%',}} className="shadow-none flex flex-col text-center whitespace-nowrap text-white justify-center bg-red-500"></div>
)}
{used < 80 && (
<div style={{ width: used + '%',}} className="shadow-none flex flex-col text-center whitespace-nowrap text-white justify-center bg-green-500"></div>
)}
{ (used >= 80 && used <= 90 )&& (
<div style={{ width: used + '%',}} className="shadow-none flex flex-col text-center whitespace-nowrap text-white justify-center bg-yellow-500"></div>
)}
<div style={{ width: (100 - used) + '%' }} className="shadow-none flex flex-col text-center whitespace-nowrap text-white justify-center bg-grey-500"></div>
</div>
{/*<div className="bg-blue-600 h-1.5 rounded-full dark:bg-blue-500" style={usedStyle}></div>*/}
<Base>
{formatBytesToInt(usedBytes)}
<Base> { Languages.t('components.disk_usage.of')} </Base>
{formatBytesToInt(totalBytes || 0)}
<Base> { Languages.t('components.disk_usage.used')} </Base>
{/*<Base>{formatBytes(trash?.size || 0)} {Languages.t('components.disk_usage.in_trash')}</Base>*/}
</Base>
</div>
</div>
)}
{!FeatureTogglesService.isActiveFeatureName(FeatureNames.COMPANY_USER_QUOTA) && (
<div className="bg-zinc-500 dark:bg-zinc-800 bg-opacity-10 rounded-md p-4 w-auto max-w-md">
<div className="w-full">
<Base>
{formatBytesToInt(usedBytes)}
<Base> { Languages.t('components.disk_usage.used')} </Base>
</Base>
</div>
</div>
)}
</>
);
};
};
export default DiskUsage;