✨ Simple User Quota (#367)
* ✨Simple User Quota * 👷Fix build with OpenSearch
This commit is contained in:
@@ -20,13 +20,18 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
- name: e2e-mongo-test
|
- name: e2e-mongo-test
|
||||||
run: cd tdrive && docker-compose -f docker-compose.tests.yml run -e NODE_OPTIONS=--unhandled-rejections=warn -e SEARCH_DRIVER=mongodb -e DB_DRIVER=mongodb -e PUBSUB_TYPE=local node npm run test:all
|
run: |
|
||||||
- name: e2e-mongo-test-clean
|
cd tdrive
|
||||||
run: cd tdrive && docker-compose -f docker-compose.tests.yml stop
|
docker-compose -f docker-compose.tests.yml run --rm -e NODE_OPTIONS=--unhandled-rejections=warn -e SEARCH_DRIVER=mongodb -e DB_DRIVER=mongodb -e PUBSUB_TYPE=local node npm run test:all
|
||||||
|
docker-compose -f docker-compose.tests.yml down
|
||||||
- name: e2e-opensearch-test
|
- name: e2e-opensearch-test
|
||||||
run: cd tdrive && docker-compose -f docker-compose.dev.tests.opensearch.yml run -e NODE_OPTIONS=--unhandled-rejections=warn -e SEARCH_DRIVER=opensearch -e DB_DRIVER=postgres -e PUBSUB_TYPE=local node npm run test:all
|
run: |
|
||||||
- name: e2e-opensearch-test-clean
|
cd tdrive
|
||||||
run: cd tdrive && docker-compose -f docker-compose.dev.tests.opensearch.yml stop
|
docker-compose -f docker-compose.dev.tests.opensearch.yml up -d --force-recreate opensearch-node1 postgres node
|
||||||
|
sleep 60
|
||||||
|
docker-compose -f docker-compose.dev.tests.opensearch.yml logs
|
||||||
|
docker-compose -f docker-compose.dev.tests.opensearch.yml run -e NODE_OPTIONS=--unhandled-rejections=warn -e SEARCH_DRIVER=opensearch -e DB_DRIVER=postgres -e PUBSUB_TYPE=local node npm run test:all
|
||||||
|
docker-compose -f docker-compose.dev.tests.opensearch.yml down
|
||||||
- name: e2e-cassandra-test
|
- name: e2e-cassandra-test
|
||||||
run: cd tdrive && docker-compose -f docker-compose.tests.yml up -d scylladb elasticsearch rabbitmq && sleep 60 && docker-compose -f docker-compose.tests.yml run -e NODE_OPTIONS=--unhandled-rejections=warn -e SEARCH_DRIVER=elasticsearch -e DB_DRIVER=cassandra node npm run test:all
|
run: cd tdrive && docker-compose -f docker-compose.tests.yml up -d scylladb elasticsearch rabbitmq && sleep 60 && docker-compose -f docker-compose.tests.yml run -e NODE_OPTIONS=--unhandled-rejections=warn -e SEARCH_DRIVER=elasticsearch -e DB_DRIVER=cassandra node npm run test:all
|
||||||
- name: coverage
|
- name: coverage
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
<component name="ProjectRunConfigurationManager">
|
<component name="ProjectRunConfigurationManager">
|
||||||
<configuration default="false" name="Run all e2e [OpenSearch]" type="JavaScriptTestRunnerJest">
|
<configuration default="false" name="Run all e2e [OpenSearch]" type="JavaScriptTestRunnerJest">
|
||||||
<node-interpreter value="project" />
|
<node-interpreter value="node" />
|
||||||
|
<node-options value="--experimental-specifier-resolution=node --experimental-vm-modules" />
|
||||||
<jest-package value="$PROJECT_DIR$/tdrive/backend/node/node_modules/jest" />
|
<jest-package value="$PROJECT_DIR$/tdrive/backend/node/node_modules/jest" />
|
||||||
<working-dir value="$PROJECT_DIR$/tdrive/backend/node" />
|
<working-dir value="$PROJECT_DIR$/tdrive/backend/node" />
|
||||||
<jest-options value="--forceExit --coverage --maxWorkers=1 --testTimeout=30000" />
|
<jest-options value="--forceExit --coverage --maxWorkers=1" />
|
||||||
<envs>
|
<envs>
|
||||||
<env name="DB_DRIVER" value="postgres" />
|
<env name="DB_DRIVER" value="postgres" />
|
||||||
<env name="DB_MONGO_URI" value="mongodb://localhost:27017" />
|
<env name="DB_MONGO_URI" value="mongodb://localhost:27017" />
|
||||||
|
|||||||
@@ -138,7 +138,10 @@
|
|||||||
"featureSharedDrive": "ENABLE_FEATURE_SHARED_DRIVE",
|
"featureSharedDrive": "ENABLE_FEATURE_SHARED_DRIVE",
|
||||||
"rootAdmins": "DRIVE_ROOT_ADMINS",
|
"rootAdmins": "DRIVE_ROOT_ADMINS",
|
||||||
"defaultLanguage": "DRIVE_DEFAULT_LANGUAGE",
|
"defaultLanguage": "DRIVE_DEFAULT_LANGUAGE",
|
||||||
|
"defaultCompany": "DRIVE_DEFAULT_COMPANY",
|
||||||
|
"defaultUserQuota": "DRIVE_DEFAULT_USER_QUOTA",
|
||||||
"featureSearchUsers": "ENABLE_FEATURE_SEARCH_USERS",
|
"featureSearchUsers": "ENABLE_FEATURE_SEARCH_USERS",
|
||||||
"featureDisplayEmail": "ENABLE_FEATURE_DISPLAY_EMAIL"
|
"featureDisplayEmail": "ENABLE_FEATURE_DISPLAY_EMAIL",
|
||||||
|
"featureUserQuota": "ENABLE_FEATURE_USER_QUOTA"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -138,7 +138,10 @@
|
|||||||
"drive": {
|
"drive": {
|
||||||
"featureSharedDrive": true,
|
"featureSharedDrive": true,
|
||||||
"featureSearchUsers": true,
|
"featureSearchUsers": true,
|
||||||
"featureDisplayEmail": true
|
"featureDisplayEmail": true,
|
||||||
|
"featureUserQuota": false,
|
||||||
|
"defaultCompany": "00000000-0000-4000-0000-000000000000",
|
||||||
|
"defaultUserQuota": 200000000
|
||||||
},
|
},
|
||||||
"applications": {
|
"applications": {
|
||||||
"grid": [
|
"grid": [
|
||||||
|
|||||||
Generated
+9125
-5105
File diff suppressed because it is too large
Load Diff
@@ -67,7 +67,7 @@
|
|||||||
"@types/eslint": "^7.2.3",
|
"@types/eslint": "^7.2.3",
|
||||||
"@types/fluent-ffmpeg": "^2.1.20",
|
"@types/fluent-ffmpeg": "^2.1.20",
|
||||||
"@types/html-to-text": "^8.1.1",
|
"@types/html-to-text": "^8.1.1",
|
||||||
"@types/jest": "^29.1.0",
|
"@types/jest": "^29.5.12",
|
||||||
"@types/lodash": "^4.14.165",
|
"@types/lodash": "^4.14.165",
|
||||||
"@types/node": "^18.11",
|
"@types/node": "^18.11",
|
||||||
"@types/node-cron": "^3.0.0",
|
"@types/node-cron": "^3.0.0",
|
||||||
@@ -88,13 +88,13 @@
|
|||||||
"eslint-plugin-prettier": "^4.2.1",
|
"eslint-plugin-prettier": "^4.2.1",
|
||||||
"eslint-plugin-unused-imports": "^2.0.0",
|
"eslint-plugin-unused-imports": "^2.0.0",
|
||||||
"form-auto-content": "^2.2.0",
|
"form-auto-content": "^2.2.0",
|
||||||
"jest": "^29.1.0",
|
"jest": "^29.7.0",
|
||||||
"jest-mock-extended": "^3.0.4",
|
"jest-mock-extended": "^3.0.4",
|
||||||
"nodemon": "^2.0.22",
|
"nodemon": "^2.0.22",
|
||||||
"pino-pretty": "^10.0.0",
|
"pino-pretty": "^10.0.0",
|
||||||
"prettier": "^2.1.2",
|
"prettier": "^2.1.2",
|
||||||
"rimraf": "^3.0.2",
|
"rimraf": "^3.0.2",
|
||||||
"ts-jest": "^29.1.0",
|
"ts-jest": "^29.1.2",
|
||||||
"ts-node": "^9.0.0",
|
"ts-node": "^9.0.0",
|
||||||
"ts-node-dev": "^1.1.8",
|
"ts-node-dev": "^1.1.8",
|
||||||
"tsc-watch": "^4.2.9",
|
"tsc-watch": "^4.2.9",
|
||||||
|
|||||||
+13
-2
@@ -124,8 +124,19 @@ export class PostgresConnector extends AbstractConnector<PostgresConnectionOptio
|
|||||||
|
|
||||||
private async alterTablePrimaryKey(entity: EntityDefinition) {
|
private async alterTablePrimaryKey(entity: EntityDefinition) {
|
||||||
if (entity.options.primaryKey) {
|
if (entity.options.primaryKey) {
|
||||||
const query = `ALTER TABLE "${entity.name}" ADD PRIMARY KEY (
|
const query = `
|
||||||
${entity.options.primaryKey.join(", ")});`;
|
do $$
|
||||||
|
begin
|
||||||
|
IF NOT EXISTS
|
||||||
|
(SELECT constraint_name
|
||||||
|
FROM information_schema.table_constraints
|
||||||
|
WHERE table_name = '${entity.name}'
|
||||||
|
and constraint_type = 'PRIMARY KEY')
|
||||||
|
THEN
|
||||||
|
ALTER TABLE "${entity.name}" ADD PRIMARY KEY (
|
||||||
|
${entity.options.primaryKey.join(", ")});
|
||||||
|
END IF;
|
||||||
|
end $$;`;
|
||||||
try {
|
try {
|
||||||
await this.client.query(query);
|
await this.client.query(query);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
import SearchRepository from "../../../core/platform/services/search/repository";
|
import SearchRepository from "../../../core/platform/services/search/repository";
|
||||||
import { getLogger, logger, TdriveLogger } from "../../../core/platform/framework";
|
import { getLogger, logger, TdriveLogger } from "../../../core/platform/framework";
|
||||||
import { CrudException, ListResult } from "../../../core/platform/framework/api/crud-service";
|
import {
|
||||||
|
CrudException,
|
||||||
|
ExecutionContext,
|
||||||
|
ListResult,
|
||||||
|
} from "../../../core/platform/framework/api/crud-service";
|
||||||
import Repository, {
|
import Repository, {
|
||||||
comparisonType,
|
comparisonType,
|
||||||
inType,
|
inType,
|
||||||
@@ -111,6 +115,14 @@ export class DocumentsService {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
userQuota = async (context: CompanyExecutionContext): Promise<number> => {
|
||||||
|
const children = await this.repository.find({
|
||||||
|
parent_id: "user_" + context.user.id,
|
||||||
|
company_id: context.company.id,
|
||||||
|
});
|
||||||
|
return children.getEntities().reduce((sum, child) => sum + child.size, 0);
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetches a drive element
|
* Fetches a drive element
|
||||||
*
|
*
|
||||||
@@ -1021,12 +1033,11 @@ export class DocumentsService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getTab = async (tabId: string, context: CompanyExecutionContext): Promise<DriveTdriveTab> => {
|
getTab = async (tabId: string, context: CompanyExecutionContext): Promise<DriveTdriveTab> => {
|
||||||
const tab = await this.driveTdriveTabRepository.findOne(
|
return await this.driveTdriveTabRepository.findOne(
|
||||||
{ company_id: context.company.id, tab_id: tabId },
|
{ company_id: context.company.id, tab_id: tabId },
|
||||||
{},
|
{},
|
||||||
context,
|
context,
|
||||||
);
|
);
|
||||||
return tab;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
setTab = async (
|
setTab = async (
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { FastifyInstance, FastifyPluginCallback } from "fastify";
|
import { FastifyInstance, FastifyPluginCallback } from "fastify";
|
||||||
import { DocumentsController } from "./controllers";
|
import { DocumentsController } from "./controllers";
|
||||||
import { createDocumentSchema, createVersionSchema } from "./schemas";
|
import { createDocumentSchema, createVersionSchema } from "./schemas";
|
||||||
import profilerPlugin from "../../../utils/profiler";
|
// import profilerPlugin from "../../../utils/profiler";
|
||||||
|
|
||||||
const baseUrl = "/companies/:company_id";
|
const baseUrl = "/companies/:company_id";
|
||||||
const serviceUrl = `${baseUrl}/item`;
|
const serviceUrl = `${baseUrl}/item`;
|
||||||
@@ -9,10 +9,10 @@ const serviceUrl = `${baseUrl}/item`;
|
|||||||
const routes: FastifyPluginCallback = (fastify: FastifyInstance, _options, next) => {
|
const routes: FastifyPluginCallback = (fastify: FastifyInstance, _options, next) => {
|
||||||
const documentsController = new DocumentsController();
|
const documentsController = new DocumentsController();
|
||||||
|
|
||||||
fastify.register(profilerPlugin, {
|
// fastify.register(profilerPlugin, {
|
||||||
active: documentsController.profilingEnabled,
|
// active: documentsController.profilingEnabled,
|
||||||
outputDir: "profiles",
|
// outputDir: "profiles",
|
||||||
});
|
// });
|
||||||
|
|
||||||
fastify.route({
|
fastify.route({
|
||||||
method: "GET",
|
method: "GET",
|
||||||
|
|||||||
@@ -311,7 +311,5 @@ export class FileServiceImpl {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getFilePath(entity: File): string {
|
function getFilePath(entity: File): string {
|
||||||
return `${gr.platformServices.storage.getHomeDir()}/files/${entity.company_id}/${
|
return `/files/${entity.company_id}/${entity.user_id || "anonymous"}/${entity.id}`;
|
||||||
entity.user_id || "anonymous"
|
|
||||||
}/${entity.id}`;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ const routes: FastifyPluginCallback = (fastify: FastifyInstance, options, next)
|
|||||||
{ id: request.currentUser.id },
|
{ id: request.currentUser.id },
|
||||||
);
|
);
|
||||||
|
|
||||||
const authorization = user.role === "owner" || user.role === "admin" ? true : false;
|
const authorization = user.role === "owner" || user.role === "admin";
|
||||||
|
|
||||||
if (!authorization) {
|
if (!authorization) {
|
||||||
throw fastify.httpErrors.unauthorized("Only owner and Company Administrator have permission");
|
throw fastify.httpErrors.unauthorized("Only owner and Company Administrator have permission");
|
||||||
|
|||||||
@@ -33,6 +33,10 @@ import { getPublicUserRoom, getUserRoom } from "../../realtime";
|
|||||||
import NodeCache from "node-cache";
|
import NodeCache from "node-cache";
|
||||||
import gr from "../../../global-resolver";
|
import gr from "../../../global-resolver";
|
||||||
import { formatUser } from "../../../../utils/users";
|
import { formatUser } from "../../../../utils/users";
|
||||||
|
import { getDefaultDriveItem } from "../../../documents/utils";
|
||||||
|
import { CompanyExecutionContext } from "../../../documents/types";
|
||||||
|
import { TYPE as DriveFileType, DriveFile } from "../../../documents/entities/drive-file";
|
||||||
|
import config from "config";
|
||||||
|
|
||||||
export class UserServiceImpl {
|
export class UserServiceImpl {
|
||||||
version: "1";
|
version: "1";
|
||||||
@@ -40,6 +44,7 @@ export class UserServiceImpl {
|
|||||||
searchRepository: SearchRepository<User>;
|
searchRepository: SearchRepository<User>;
|
||||||
companyUserRepository: Repository<CompanyUser>;
|
companyUserRepository: Repository<CompanyUser>;
|
||||||
extUserRepository: Repository<ExternalUser>;
|
extUserRepository: Repository<ExternalUser>;
|
||||||
|
driveFileRepository: Repository<DriveFile>;
|
||||||
private deviceRepository: Repository<Device>;
|
private deviceRepository: Repository<Device>;
|
||||||
private cache: NodeCache;
|
private cache: NodeCache;
|
||||||
|
|
||||||
@@ -56,6 +61,7 @@ export class UserServiceImpl {
|
|||||||
);
|
);
|
||||||
|
|
||||||
this.deviceRepository = await gr.database.getRepository<Device>(DeviceType, Device);
|
this.deviceRepository = await gr.database.getRepository<Device>(DeviceType, Device);
|
||||||
|
this.driveFileRepository = await gr.database.getRepository<DriveFile>(DriveFileType, DriveFile);
|
||||||
|
|
||||||
this.cache = new NodeCache({ stdTTL: 0.2, checkperiod: 120 });
|
this.cache = new NodeCache({ stdTTL: 0.2, checkperiod: 120 });
|
||||||
|
|
||||||
|
|||||||
@@ -64,6 +64,9 @@ export function formatCompany(
|
|||||||
[CompanyFeaturesEnum.COMPANY_DISPLAY_EMAIL]: JSON.parse(
|
[CompanyFeaturesEnum.COMPANY_DISPLAY_EMAIL]: JSON.parse(
|
||||||
config.get("drive.featureDisplayEmail") || "true",
|
config.get("drive.featureDisplayEmail") || "true",
|
||||||
),
|
),
|
||||||
|
[CompanyFeaturesEnum.COMPANY_USER_QUOTA]: JSON.parse(
|
||||||
|
config.get("drive.featureUserQuota") || "false",
|
||||||
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
...(res.plan?.features || {}),
|
...(res.plan?.features || {}),
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ import {
|
|||||||
UserObject,
|
UserObject,
|
||||||
UserParameters,
|
UserParameters,
|
||||||
CompanyUsersParameters,
|
CompanyUsersParameters,
|
||||||
|
UserQuota,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
import Company from "../entities/company";
|
import Company from "../entities/company";
|
||||||
import CompanyUser from "../entities/company_user";
|
import CompanyUser from "../entities/company_user";
|
||||||
@@ -35,6 +36,7 @@ import { getCompanyRooms, getUserRooms } from "../realtime";
|
|||||||
import { formatCompany, getCompanyStats } from "../utils";
|
import { formatCompany, getCompanyStats } from "../utils";
|
||||||
import { formatUser } from "../../../utils/users";
|
import { formatUser } from "../../../utils/users";
|
||||||
import gr from "../../global-resolver";
|
import gr from "../../global-resolver";
|
||||||
|
import config from "config";
|
||||||
|
|
||||||
export class UsersCrudController
|
export class UsersCrudController
|
||||||
implements
|
implements
|
||||||
@@ -318,6 +320,40 @@ export class UsersCrudController
|
|||||||
resources: [],
|
resources: [],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async qouta(
|
||||||
|
request: FastifyRequest<{
|
||||||
|
Params: UserParameters;
|
||||||
|
Querystring: CompanyUsersParameters;
|
||||||
|
}>,
|
||||||
|
_reply: FastifyReply,
|
||||||
|
): Promise<UserQuota> {
|
||||||
|
const context = getExecutionContext(request);
|
||||||
|
|
||||||
|
let id = request.params.id;
|
||||||
|
if (request.params.id === "me") {
|
||||||
|
id = context.user.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (id != context.user.id) {
|
||||||
|
//if admin or application wants to know user quota, it's not implemented yet
|
||||||
|
throw new Error("Not implemented yes");
|
||||||
|
}
|
||||||
|
|
||||||
|
const quota = await gr.services.documents.documents.userQuota({
|
||||||
|
...context,
|
||||||
|
company: { id: request.query.companyId || config.get("drive.defaultCompany") },
|
||||||
|
});
|
||||||
|
|
||||||
|
const total: number = config.has("drive.defaultUserQuota")
|
||||||
|
? config.get("drive.defaultUserQuota")
|
||||||
|
: NaN;
|
||||||
|
return {
|
||||||
|
total: total,
|
||||||
|
remaining: isNaN(total) ? NaN : total - quota,
|
||||||
|
used: quota,
|
||||||
|
} as UserQuota;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getExecutionContext(request: FastifyRequest): ExecutionContext {
|
function getExecutionContext(request: FastifyRequest): ExecutionContext {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import {
|
|||||||
deleteDeviceSchema,
|
deleteDeviceSchema,
|
||||||
getCompanySchema,
|
getCompanySchema,
|
||||||
getDevicesSchema,
|
getDevicesSchema,
|
||||||
|
getQuotaSchema,
|
||||||
getUserCompaniesSchema,
|
getUserCompaniesSchema,
|
||||||
getUserSchema,
|
getUserSchema,
|
||||||
getUsersSchema,
|
getUsersSchema,
|
||||||
@@ -33,6 +34,15 @@ const routes: FastifyPluginCallback = (fastify: FastifyInstance, options, next)
|
|||||||
handler: usersController.get.bind(usersController),
|
handler: usersController.get.bind(usersController),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
fastify.route({
|
||||||
|
method: "GET",
|
||||||
|
url: `${usersUrl}/:id/quota`,
|
||||||
|
preHandler: accessControl,
|
||||||
|
preValidation: fastify.authenticate,
|
||||||
|
schema: getQuotaSchema,
|
||||||
|
handler: usersController.qouta.bind(usersController),
|
||||||
|
});
|
||||||
|
|
||||||
fastify.route({
|
fastify.route({
|
||||||
method: "POST",
|
method: "POST",
|
||||||
url: `${usersUrl}/me/preferences`,
|
url: `${usersUrl}/me/preferences`,
|
||||||
|
|||||||
@@ -95,6 +95,7 @@ export const companyObjectSchema = {
|
|||||||
[CompanyFeaturesEnum.COMPANY_SEARCH_USERS]: { type: "boolean" },
|
[CompanyFeaturesEnum.COMPANY_SEARCH_USERS]: { type: "boolean" },
|
||||||
[CompanyFeaturesEnum.COMPANY_SHARED_DRIVE]: { type: "boolean" },
|
[CompanyFeaturesEnum.COMPANY_SHARED_DRIVE]: { type: "boolean" },
|
||||||
[CompanyFeaturesEnum.COMPANY_DISPLAY_EMAIL]: { type: "boolean" },
|
[CompanyFeaturesEnum.COMPANY_DISPLAY_EMAIL]: { type: "boolean" },
|
||||||
|
[CompanyFeaturesEnum.COMPANY_USER_QUOTA]: { type: "boolean" },
|
||||||
guests: { type: "number" }, // to rename or delete
|
guests: { type: "number" }, // to rename or delete
|
||||||
members: { type: "number" }, // to rename or delete
|
members: { type: "number" }, // to rename or delete
|
||||||
storage: { type: "number" }, // to rename or delete
|
storage: { type: "number" }, // to rename or delete
|
||||||
@@ -143,6 +144,34 @@ export const getUserSchema = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getQuotaSchema = {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
companyId: { type: "string" },
|
||||||
|
},
|
||||||
|
response: {
|
||||||
|
"2xx": {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
used: { type: "number" },
|
||||||
|
remaining: { type: "number" },
|
||||||
|
total: { type: "number" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
tags: ["User"],
|
||||||
|
params: {
|
||||||
|
type: "object",
|
||||||
|
description: "Users",
|
||||||
|
properties: {
|
||||||
|
id: {
|
||||||
|
description: "User ID",
|
||||||
|
type: "string",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
export const setUserPreferencesSchema = {
|
export const setUserPreferencesSchema = {
|
||||||
request: {
|
request: {
|
||||||
properties: {
|
properties: {
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ export interface UserParameters {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface CompanyUsersParameters {
|
export interface CompanyUsersParameters {
|
||||||
/* user id */
|
/* company id */
|
||||||
companyId: string;
|
companyId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -86,6 +86,7 @@ export enum CompanyFeaturesEnum {
|
|||||||
COMPANY_SEARCH_USERS = "company:search_users",
|
COMPANY_SEARCH_USERS = "company:search_users",
|
||||||
COMPANY_SHARED_DRIVE = "company:shared_drive",
|
COMPANY_SHARED_DRIVE = "company:shared_drive",
|
||||||
COMPANY_DISPLAY_EMAIL = "company:display_email",
|
COMPANY_DISPLAY_EMAIL = "company:display_email",
|
||||||
|
COMPANY_USER_QUOTA = "company:user_quota",
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CompanyFeaturesObject = {
|
export type CompanyFeaturesObject = {
|
||||||
@@ -98,6 +99,7 @@ export type CompanyFeaturesObject = {
|
|||||||
[CompanyFeaturesEnum.COMPANY_SEARCH_USERS]?: boolean;
|
[CompanyFeaturesEnum.COMPANY_SEARCH_USERS]?: boolean;
|
||||||
[CompanyFeaturesEnum.COMPANY_SHARED_DRIVE]?: boolean;
|
[CompanyFeaturesEnum.COMPANY_SHARED_DRIVE]?: boolean;
|
||||||
[CompanyFeaturesEnum.COMPANY_DISPLAY_EMAIL]?: boolean;
|
[CompanyFeaturesEnum.COMPANY_DISPLAY_EMAIL]?: boolean;
|
||||||
|
[CompanyFeaturesEnum.COMPANY_USER_QUOTA]?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type CompanyLimitsObject = {
|
export type CompanyLimitsObject = {
|
||||||
@@ -144,3 +146,9 @@ export interface RegisterDeviceParams {
|
|||||||
export interface DeregisterDeviceParams {
|
export interface DeregisterDeviceParams {
|
||||||
value: "string";
|
value: "string";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface UserQuota {
|
||||||
|
used: number;
|
||||||
|
remaining: number;
|
||||||
|
total: number;
|
||||||
|
}
|
||||||
|
|||||||
@@ -9,10 +9,16 @@ import { v1 as uuidv1 } from "uuid";
|
|||||||
import { TestDbService } from "../utils.prepare.db";
|
import { TestDbService } from "../utils.prepare.db";
|
||||||
import { DriveFile } from "../../../src/services/documents/entities/drive-file";
|
import { DriveFile } from "../../../src/services/documents/entities/drive-file";
|
||||||
import { FileVersion } from "../../../src/services/documents/entities/file-version";
|
import { FileVersion } from "../../../src/services/documents/entities/file-version";
|
||||||
import { AccessTokenMockClass, DriveItemDetailsMockClass, SearchResultMockClass } from "./entities/mock_entities";
|
import {
|
||||||
|
AccessTokenMockClass,
|
||||||
|
DriveItemDetailsMockClass,
|
||||||
|
SearchResultMockClass,
|
||||||
|
UserQuotaMockClass
|
||||||
|
} from "./entities/mock_entities";
|
||||||
import { logger } from "../../../src/core/platform/framework";
|
import { logger } from "../../../src/core/platform/framework";
|
||||||
import { expect } from "@jest/globals";
|
import { expect } from "@jest/globals";
|
||||||
import { publicAccessLevel } from "../../../src/services/documents/types";
|
import { publicAccessLevel } from "../../../src/services/documents/types";
|
||||||
|
import { UserQuota } from "../../../src/services/user/web/types";
|
||||||
|
|
||||||
export default class TestHelpers {
|
export default class TestHelpers {
|
||||||
|
|
||||||
@@ -191,22 +197,23 @@ export default class TestHelpers {
|
|||||||
parent_id = "root",
|
parent_id = "root",
|
||||||
) {
|
) {
|
||||||
const file = await this.uploadRandomFile();
|
const file = await this.uploadRandomFile();
|
||||||
const item = {
|
|
||||||
name: file.metadata.name,
|
|
||||||
parent_id: parent_id,
|
|
||||||
company_id: file.company_id,
|
|
||||||
};
|
|
||||||
|
|
||||||
const version = {
|
const doc = await this.createDocumentFromFile(file, parent_id);
|
||||||
file_metadata: {
|
|
||||||
name: file.metadata.name,
|
|
||||||
size: file.upload_data?.size,
|
|
||||||
thumbnails: [],
|
|
||||||
external_id: file.id
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const doc = await this.createDocument(item, version);
|
expect(doc).toBeDefined();
|
||||||
|
expect(doc).not.toBeNull();
|
||||||
|
expect(doc.parent_id).toEqual(parent_id)
|
||||||
|
|
||||||
|
return doc;
|
||||||
|
};
|
||||||
|
|
||||||
|
async createDocumentFromFilename(
|
||||||
|
file_name: "sample.png",
|
||||||
|
parent_id = "root",
|
||||||
|
) {
|
||||||
|
const file = await this.uploadFile(file_name);
|
||||||
|
|
||||||
|
const doc = await this.createDocumentFromFile(file, parent_id);
|
||||||
|
|
||||||
expect(doc).toBeDefined();
|
expect(doc).toBeDefined();
|
||||||
expect(doc).not.toBeNull();
|
expect(doc).not.toBeNull();
|
||||||
@@ -338,5 +345,16 @@ export default class TestHelpers {
|
|||||||
response.body)
|
response.body)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
async quota() {
|
||||||
|
const url = "/internal/services/users/v1/users";
|
||||||
|
|
||||||
|
const response = await this.platform.app.inject({
|
||||||
|
method: "GET",
|
||||||
|
headers: { "authorization": `Bearer ${this.jwt}` },
|
||||||
|
url: `${url}/${this.user.id}/quota?companyId=${this.platform.workspace.company_id}`,
|
||||||
|
})
|
||||||
|
|
||||||
|
return deserialize<UserQuota>(UserQuotaMockClass, response.body)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import {DriveFileAccessLevel, publicAccessLevel} from "../../../../src/services/documents/types";
|
import {DriveFileAccessLevel, publicAccessLevel} from "../../../../src/services/documents/types";
|
||||||
|
import { UserQuota } from "../../../../src/services/user/web/types";
|
||||||
|
|
||||||
export type MockAccessInformation = {
|
export type MockAccessInformation = {
|
||||||
public?: {
|
public?: {
|
||||||
@@ -45,6 +46,12 @@ export class SearchResultMockClass {
|
|||||||
entities: DriveFileMockClass[];
|
entities: DriveFileMockClass[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class UserQuotaMockClass implements UserQuota{
|
||||||
|
remaining: number;
|
||||||
|
total: number;
|
||||||
|
used: number;
|
||||||
|
}
|
||||||
|
|
||||||
export class AccessTokenMockClass {
|
export class AccessTokenMockClass {
|
||||||
access_token: {
|
access_token: {
|
||||||
time: 0;
|
time: 0;
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import { AccessInformation, DriveFile } from "../../../src/services/documents/en
|
|||||||
import { FileVersion } from "../../../src/services/documents/entities/file-version";
|
import { FileVersion } from "../../../src/services/documents/entities/file-version";
|
||||||
import { DriveFileAccessLevel, DriveItemDetails } from "../../../src/services/documents/types";
|
import { DriveFileAccessLevel, DriveItemDetails } from "../../../src/services/documents/types";
|
||||||
import { init, TestPlatform } from "../setup";
|
import { init, TestPlatform } from "../setup";
|
||||||
import { TestDbService } from "../utils.prepare.db";
|
|
||||||
import { e2e_createDocument, e2e_updateDocument } from "./utils";
|
import { e2e_createDocument, e2e_updateDocument } from "./utils";
|
||||||
import TestHelpers from "../common/common_test_helpers";
|
import TestHelpers from "../common/common_test_helpers";
|
||||||
import { AccessTokenMockClass } from "../common/entities/mock_entities";
|
import { AccessTokenMockClass } from "../common/entities/mock_entities";
|
||||||
|
|||||||
@@ -0,0 +1,69 @@
|
|||||||
|
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect} from "@jest/globals";
|
||||||
|
import { init, TestPlatform } from "../setup";
|
||||||
|
import TestHelpers from "../common/common_test_helpers";
|
||||||
|
|
||||||
|
describe("The /users/quota API", () => {
|
||||||
|
let platform: TestPlatform;
|
||||||
|
let currentUser: TestHelpers;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
platform = await init();
|
||||||
|
currentUser = await TestHelpers.getInstance(platform);
|
||||||
|
}, 30000000);
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
await platform.tearDown();
|
||||||
|
platform = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
const platform = await init({
|
||||||
|
services: [
|
||||||
|
"database",
|
||||||
|
"search",
|
||||||
|
"message-queue",
|
||||||
|
"websocket",
|
||||||
|
"applications",
|
||||||
|
"webserver",
|
||||||
|
"user",
|
||||||
|
"auth",
|
||||||
|
"storage",
|
||||||
|
"counter",
|
||||||
|
"console",
|
||||||
|
"workspaces",
|
||||||
|
"statistics",
|
||||||
|
"platform-services",
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}, 30000000);
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
test("should reutrn 200 with available quota", async () => {
|
||||||
|
//given
|
||||||
|
const userQuota = 200000000;
|
||||||
|
const doc = await currentUser.createDocumentFromFilename("sample.png", "user_" + currentUser.user.id)
|
||||||
|
|
||||||
|
//when
|
||||||
|
const quota = await currentUser.quota();
|
||||||
|
|
||||||
|
expect(quota.total).toBe(userQuota);
|
||||||
|
expect(quota.remaining).toBe(userQuota - doc.size); //198346196 //198342406
|
||||||
|
expect(quota.used).toBe(doc.size);
|
||||||
|
}, 30000000);
|
||||||
|
|
||||||
|
test("should return 200 with all empty space", async () => {
|
||||||
|
//given
|
||||||
|
const userQuota = 200000000;
|
||||||
|
|
||||||
|
//when
|
||||||
|
const quota = await currentUser.quota();
|
||||||
|
|
||||||
|
expect(quota.total).toBe(userQuota);
|
||||||
|
expect(quota.remaining).toBe(userQuota); //198346196 //198342406
|
||||||
|
expect(quota.used).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
+1
-1
@@ -54,7 +54,7 @@ describe('The Postgres Connector module', () => {
|
|||||||
expect(normalizeWhitespace(dbQuerySpy.mock.calls[2][0])).toBe(normalizeWhitespace("SELECT table_name, column_name, data_type FROM information_schema.columns WHERE table_name = $1"));
|
expect(normalizeWhitespace(dbQuerySpy.mock.calls[2][0])).toBe(normalizeWhitespace("SELECT table_name, column_name, data_type FROM information_schema.columns WHERE table_name = $1"));
|
||||||
expect(dbQuerySpy.mock.calls[2][1]).toStrictEqual(["test_table"])
|
expect(dbQuerySpy.mock.calls[2][1]).toStrictEqual(["test_table"])
|
||||||
expect(normalizeWhitespace(dbQuerySpy.mock.calls[3][0])).toBe(`ALTER TABLE "test_table" ADD COLUMN id UUID, ADD COLUMN is_in_trash BOOLEAN, ADD COLUMN tags TEXT, ADD COLUMN added BIGINT`)
|
expect(normalizeWhitespace(dbQuerySpy.mock.calls[3][0])).toBe(`ALTER TABLE "test_table" ADD COLUMN id UUID, ADD COLUMN is_in_trash BOOLEAN, ADD COLUMN tags TEXT, ADD COLUMN added BIGINT`)
|
||||||
expect(normalizeWhitespace(dbQuerySpy.mock.calls[4][0])).toBe(`ALTER TABLE "test_table" ADD PRIMARY KEY ( company_id, id);`)
|
expect(normalizeWhitespace(dbQuerySpy.mock.calls[4][0])).toBe(`do $$ begin IF NOT EXISTS (SELECT constraint_name FROM information_schema.table_constraints WHERE table_name = 'test_table' and constraint_type = 'PRIMARY KEY') THEN ALTER TABLE "test_table" ADD PRIMARY KEY ( company_id, id); END IF; end $$;`)
|
||||||
expect(normalizeWhitespace(dbQuerySpy.mock.calls[5][0])).toBe(`CREATE INDEX IF NOT EXISTS index_test_table_company_id_parent_id ON "test_table" ((company_id), parent_id)`)
|
expect(normalizeWhitespace(dbQuerySpy.mock.calls[5][0])).toBe(`CREATE INDEX IF NOT EXISTS index_test_table_company_id_parent_id ON "test_table" ((company_id), parent_id)`)
|
||||||
expect(normalizeWhitespace(dbQuerySpy.mock.calls[6][0])).toBe(`CREATE INDEX IF NOT EXISTS index_test_table_company_id_is_in_trash ON "test_table" ((company_id), is_in_trash)`)
|
expect(normalizeWhitespace(dbQuerySpy.mock.calls[6][0])).toBe(`CREATE INDEX IF NOT EXISTS index_test_table_company_id_is_in_trash ON "test_table" ((company_id), is_in_trash)`)
|
||||||
});
|
});
|
||||||
|
|||||||
+1011
-667
File diff suppressed because it is too large
Load Diff
@@ -5,7 +5,7 @@ version: "3.4"
|
|||||||
services:
|
services:
|
||||||
|
|
||||||
opensearch-node1:
|
opensearch-node1:
|
||||||
image: opensearchproject/opensearch:latest # Specifying the latest available image - modify if you want a specific version
|
image: opensearchproject/opensearch:2.11.0 # Specifying the latest available image - modify if you want a specific version
|
||||||
container_name: opensearch-node1
|
container_name: opensearch-node1
|
||||||
environment:
|
environment:
|
||||||
- cluster.name=opensearch-cluster # Name the cluster
|
- cluster.name=opensearch-cluster # Name the cluster
|
||||||
@@ -13,6 +13,7 @@ services:
|
|||||||
- discovery.seed_hosts=opensearch-node1 # Nodes to look for when discovering the cluster
|
- discovery.seed_hosts=opensearch-node1 # Nodes to look for when discovering the cluster
|
||||||
- cluster.initial_cluster_manager_nodes=opensearch-node1 # Nodes eligible to serve as cluster manager
|
- cluster.initial_cluster_manager_nodes=opensearch-node1 # Nodes eligible to serve as cluster manager
|
||||||
- bootstrap.memory_lock=true # Disable JVM heap memory swapping
|
- bootstrap.memory_lock=true # Disable JVM heap memory swapping
|
||||||
|
# - OPENSEARCH_INITIAL_ADMIN_PASSWORD=admin
|
||||||
- "OPENSEARCH_JAVA_OPTS=-Xms512m -Xmx512m" # Set min and max JVM heap sizes to at least 50% of system RAM
|
- "OPENSEARCH_JAVA_OPTS=-Xms512m -Xmx512m" # Set min and max JVM heap sizes to at least 50% of system RAM
|
||||||
ulimits:
|
ulimits:
|
||||||
memlock:
|
memlock:
|
||||||
|
|||||||
@@ -275,7 +275,8 @@
|
|||||||
"scenes.app.drive.used": "used in this folder",
|
"scenes.app.drive.used": "used in this folder",
|
||||||
"scenes.app.drive.context_menu": "More",
|
"scenes.app.drive.context_menu": "More",
|
||||||
"scenes.app.drive.documents": "Documents",
|
"scenes.app.drive.documents": "Documents",
|
||||||
"components.disk_usage.used": "used,",
|
"components.disk_usage.of": "of",
|
||||||
|
"components.disk_usage.used": "used",
|
||||||
"components.disk_usage.in_trash": "in trash",
|
"components.disk_usage.in_trash": "in trash",
|
||||||
"components.create_modal.create_folder_or_doc": "Create document or folder",
|
"components.create_modal.create_folder_or_doc": "Create document or folder",
|
||||||
"components.create_modal.create_folder": "Create a folder",
|
"components.create_modal.create_folder": "Create a folder",
|
||||||
|
|||||||
@@ -275,7 +275,8 @@
|
|||||||
"scenes.app.drive.used": "utilisés dans ce dossier",
|
"scenes.app.drive.used": "utilisés dans ce dossier",
|
||||||
"scenes.app.drive.documents": "Documents",
|
"scenes.app.drive.documents": "Documents",
|
||||||
"scenes.app.drive.context_menu": "Plus",
|
"scenes.app.drive.context_menu": "Plus",
|
||||||
"components.disk_usage.used": "utilisé,",
|
"components.disk_usage.of": "sur",
|
||||||
|
"components.disk_usage.used": "utilisé",
|
||||||
"components.disk_usage.in_trash": "dans la poubelle",
|
"components.disk_usage.in_trash": "dans la poubelle",
|
||||||
"components.create_modal.create_folder_or_doc": "Créer un document ou un dossier",
|
"components.create_modal.create_folder_or_doc": "Créer un document ou un dossier",
|
||||||
"components.create_modal.create_folder": "Créer un dossier",
|
"components.create_modal.create_folder": "Créer un dossier",
|
||||||
|
|||||||
@@ -275,7 +275,8 @@
|
|||||||
"scenes.app.drive.used": "использовано в этой папке",
|
"scenes.app.drive.used": "использовано в этой папке",
|
||||||
"scenes.app.drive.documents": "Файлы",
|
"scenes.app.drive.documents": "Файлы",
|
||||||
"scenes.app.drive.context_menu": "Еще",
|
"scenes.app.drive.context_menu": "Еще",
|
||||||
"components.disk_usage.used": "использовано,",
|
"components.disk_usage.of": "из",
|
||||||
|
"components.disk_usage.used": "использовано",
|
||||||
"components.disk_usage.in_trash": "в корзине",
|
"components.disk_usage.in_trash": "в корзине",
|
||||||
"components.create_modal.create_folder_or_doc": "Создать папку или файл",
|
"components.create_modal.create_folder_or_doc": "Создать папку или файл",
|
||||||
"components.create_modal.create_folder": "Создать папку",
|
"components.create_modal.create_folder": "Создать папку",
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { DriveItemAtom, DriveItemChildrenAtom } from '../state/store';
|
|||||||
import { BrowseFilter, DriveItem, DriveItemVersion } from '../types';
|
import { BrowseFilter, DriveItem, DriveItemVersion } from '../types';
|
||||||
import { SharedWithMeFilterState } from '../state/shared-with-me-filter';
|
import { SharedWithMeFilterState } from '../state/shared-with-me-filter';
|
||||||
import Languages from 'features/global/services/languages-service';
|
import Languages from 'features/global/services/languages-service';
|
||||||
|
import { useUserQuota } from "features/users/hooks/use-user-quota";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the children of a drive item
|
* Returns the children of a drive item
|
||||||
@@ -15,6 +16,7 @@ import Languages from 'features/global/services/languages-service';
|
|||||||
export const useDriveActions = () => {
|
export const useDriveActions = () => {
|
||||||
const companyId = useRouterCompany();
|
const companyId = useRouterCompany();
|
||||||
const sharedFilter = useRecoilValue(SharedWithMeFilterState);
|
const sharedFilter = useRecoilValue(SharedWithMeFilterState);
|
||||||
|
const { getQuota } = useUserQuota();
|
||||||
|
|
||||||
const refresh = useRecoilCallback(
|
const refresh = useRecoilCallback(
|
||||||
({ set, snapshot }) =>
|
({ set, snapshot }) =>
|
||||||
@@ -52,6 +54,7 @@ export const useDriveActions = () => {
|
|||||||
try {
|
try {
|
||||||
driveFile = await DriveApiClient.create(companyId, { item, version });
|
driveFile = await DriveApiClient.create(companyId, { item, version });
|
||||||
await refresh(driveFile.parent_id!);
|
await refresh(driveFile.parent_id!);
|
||||||
|
await getQuota();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
ToasterService.error(Languages.t('hooks.use-drive-actions.unable_create_file'));
|
ToasterService.error(Languages.t('hooks.use-drive-actions.unable_create_file'));
|
||||||
}
|
}
|
||||||
@@ -89,6 +92,7 @@ export const useDriveActions = () => {
|
|||||||
try {
|
try {
|
||||||
await DriveApiClient.remove(companyId, id);
|
await DriveApiClient.remove(companyId, id);
|
||||||
await refresh(parentId || '');
|
await refresh(parentId || '');
|
||||||
|
await getQuota();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
ToasterService.error(Languages.t('hooks.use-drive-actions.unable_remove_file'));
|
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]}`;
|
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_SEARCH_USERS = 'company:search_users',
|
||||||
COMPANY_SHARED_DRIVE = 'company:shared_drive',
|
COMPANY_SHARED_DRIVE = 'company:shared_drive',
|
||||||
COMPANY_DISPLAY_EMAIL = 'company:display_email',
|
COMPANY_DISPLAY_EMAIL = 'company:display_email',
|
||||||
|
COMPANY_USER_QUOTA = 'company:user_quota',
|
||||||
}
|
}
|
||||||
|
|
||||||
export type FeatureValueType = boolean | number;
|
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_SEARCH_USERS, true);
|
||||||
availableFeaturesWithDefaults.set(FeatureNames.COMPANY_SHARED_DRIVE, true);
|
availableFeaturesWithDefaults.set(FeatureNames.COMPANY_SHARED_DRIVE, true);
|
||||||
availableFeaturesWithDefaults.set(FeatureNames.COMPANY_DISPLAY_EMAIL, 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
|
* ChannelServiceImpl that allow you to manage feature flipping in Tdrive using react feature toggles
|
||||||
@@ -70,7 +72,6 @@ class FeatureTogglesService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public isActiveFeatureName(featureName: FeatureNames) {
|
public isActiveFeatureName(featureName: FeatureNames) {
|
||||||
console.debug(this.activeFeatureNames)
|
|
||||||
const b = this.activeFeatureNames.includes(featureName);
|
const b = this.activeFeatureNames.includes(featureName);
|
||||||
console.debug(`Feature ${featureName} is ${b}`);
|
console.debug(`Feature ${featureName} is ${b}`);
|
||||||
return b;
|
return b;
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import WorkspaceAPIClient from '../../workspaces/api/workspace-api-client';
|
|||||||
import CurrentUser from '../../../deprecated/user/CurrentUser';
|
import CurrentUser from '../../../deprecated/user/CurrentUser';
|
||||||
import { setUserList } from '../hooks/use-user-list';
|
import { setUserList } from '../hooks/use-user-list';
|
||||||
import Logger from 'features/global/framework/logger-service';
|
import Logger from 'features/global/framework/logger-service';
|
||||||
|
import { UserQuota } from "features/users/types/user-quota";
|
||||||
|
|
||||||
export type SearchContextType = {
|
export type SearchContextType = {
|
||||||
scope: 'company' | 'workspace' | 'all';
|
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> {
|
async getCompany(companyId: string): Promise<CompanyType> {
|
||||||
return Api.get<{ resource: CompanyType }>(
|
return Api.get<{ resource: CompanyType }>(
|
||||||
`/internal/services/users/v1/companies/${companyId}`,
|
`/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 { Base } from "@atoms/text";
|
||||||
import { useDriveItem } from '@features/drive/hooks/use-drive-item';
|
import { formatBytesToInt } from "@features/drive/utils";
|
||||||
import { formatBytes } from '@features/drive/utils';
|
|
||||||
import { useCurrentUser } from 'app/features/users/hooks/use-current-user';
|
|
||||||
import Languages from "features/global/services/languages-service";
|
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 (
|
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="bg-zinc-500 dark:bg-zinc-800 bg-opacity-10 rounded-md p-4 w-auto max-w-md">
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
<Title>
|
<div className="overflow-hidden h-4 mb-4 text-xs flex rounded bg-emerald-200">
|
||||||
{formatBytes(item?.size || 0)}
|
{used > 90 && (
|
||||||
<Base> { Languages.t('components.disk_usage.used')} </Base> <Base>{formatBytes(trash?.size || 0)} {Languages.t('components.disk_usage.in_trash')}</Base>
|
<div style={{ width: used + '%',}} className="shadow-none flex flex-col text-center whitespace-nowrap text-white justify-center bg-red-500"></div>
|
||||||
</Title>
|
)}
|
||||||
|
{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>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export default DiskUsage;
|
||||||
Reference in New Issue
Block a user