mirror of
https://github.com/gristlabs/grist-core.git
synced 2026-03-02 04:09:24 +00:00
(core) Add API endpoint to get site usage summary
Summary: The summary includes a count of documents that are approaching limits, in grace period, or delete-only. The endpoint is only accessible to site owners, and is currently unused. A follow-up diff will add usage banners to the site home page, which will use the response from the endpoint to communicate usage information to owners. Test Plan: Browser and server tests. Reviewers: alexmojaki Reviewed By: alexmojaki Differential Revision: https://phab.getgrist.com/D3420
This commit is contained in:
74
app/common/DocLimits.ts
Normal file
74
app/common/DocLimits.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
import {ApiError} from 'app/common/ApiError';
|
||||
import {APPROACHING_LIMIT_RATIO, DataLimitStatus, DocumentUsage, getUsageRatio} from 'app/common/DocUsage';
|
||||
import {Features} from 'app/common/Features';
|
||||
import * as moment from 'moment-timezone';
|
||||
|
||||
/**
|
||||
* Error class indicating failure due to limits being exceeded.
|
||||
*/
|
||||
export class LimitExceededError extends ApiError {
|
||||
constructor(message: string) {
|
||||
super(message, 413);
|
||||
}
|
||||
}
|
||||
|
||||
export interface GetDataLimitStatusParams {
|
||||
docUsage: DocumentUsage | null;
|
||||
productFeatures: Features | undefined;
|
||||
gracePeriodStart: Date | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a set of params that includes document usage, current product features, and
|
||||
* a grace-period start (if any), returns the data limit status of a document.
|
||||
*/
|
||||
export function getDataLimitStatus(params: GetDataLimitStatusParams): DataLimitStatus {
|
||||
const {docUsage, productFeatures, gracePeriodStart} = params;
|
||||
const ratio = getDataLimitRatio(docUsage, productFeatures);
|
||||
if (ratio > 1) {
|
||||
const start = gracePeriodStart;
|
||||
const days = productFeatures?.gracePeriodDays;
|
||||
if (start && days && moment().diff(moment(start), 'days') >= days) {
|
||||
return 'deleteOnly';
|
||||
} else {
|
||||
return 'gracePeriod';
|
||||
}
|
||||
} else if (ratio > APPROACHING_LIMIT_RATIO) {
|
||||
return 'approachingLimit';
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Given `docUsage` and `productFeatures`, returns the highest usage ratio
|
||||
* across all data-related limits (currently only row count and data size).
|
||||
*/
|
||||
export function getDataLimitRatio(
|
||||
docUsage: DocumentUsage | null,
|
||||
productFeatures: Features | undefined
|
||||
): number {
|
||||
if (!docUsage) { return 0; }
|
||||
|
||||
const {rowCount, dataSizeBytes} = docUsage;
|
||||
const maxRows = productFeatures?.baseMaxRowsPerDocument;
|
||||
const maxDataSize = productFeatures?.baseMaxDataSizePerDocument;
|
||||
const rowRatio = getUsageRatio(rowCount, maxRows);
|
||||
const dataSizeRatio = getUsageRatio(dataSizeBytes, maxDataSize);
|
||||
return Math.max(rowRatio, dataSizeRatio);
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps `dataLimitStatus` status to an integer and returns it; larger integer
|
||||
* values indicate a more "severe" status.
|
||||
*
|
||||
* Useful for relatively comparing the severity of two statuses.
|
||||
*/
|
||||
export function getSeverity(dataLimitStatus: DataLimitStatus): number {
|
||||
switch (dataLimitStatus) {
|
||||
case null: { return 0; }
|
||||
case 'approachingLimit': { return 1; }
|
||||
case 'gracePeriod': { return 2; }
|
||||
case 'deleteOnly': { return 3; }
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import {MinimalActionGroup} from 'app/common/ActionGroup';
|
||||
import {TableDataAction} from 'app/common/DocActions';
|
||||
import {DocUsage} from 'app/common/DocUsage';
|
||||
import {FilteredDocUsageSummary} from 'app/common/DocUsage';
|
||||
import {Role} from 'app/common/roles';
|
||||
import {StringUnion} from 'app/common/StringUnion';
|
||||
import {FullUser} from 'app/common/UserAPI';
|
||||
@@ -45,7 +45,7 @@ export interface OpenLocalDocResult {
|
||||
log: MinimalActionGroup[];
|
||||
recoveryMode?: boolean;
|
||||
userOverride?: UserOverride;
|
||||
docUsage?: DocUsage;
|
||||
docUsage?: FilteredDocUsageSummary;
|
||||
}
|
||||
|
||||
export interface UserOverride {
|
||||
|
||||
@@ -1,29 +1,60 @@
|
||||
import {ApiError} from 'app/common/ApiError';
|
||||
|
||||
export interface DocUsage {
|
||||
dataLimitStatus: DataLimitStatus;
|
||||
rowCount: RowCount;
|
||||
dataSizeBytes: DataSize;
|
||||
attachmentsSizeBytes: AttachmentsSize;
|
||||
export interface DocumentUsage {
|
||||
rowCount?: number;
|
||||
dataSizeBytes?: number;
|
||||
attachmentsSizeBytes?: number;
|
||||
}
|
||||
|
||||
type NumberOrStatus = number | 'hidden' | 'pending';
|
||||
|
||||
export type RowCount = NumberOrStatus;
|
||||
|
||||
export type DataSize = NumberOrStatus;
|
||||
|
||||
export type AttachmentsSize = NumberOrStatus;
|
||||
|
||||
export type DataLimitStatus = 'approachingLimit' | 'gracePeriod' | 'deleteOnly' | null;
|
||||
|
||||
export type NonHidden<T> = Exclude<T, 'hidden'>;
|
||||
type DocUsageOrPending = {
|
||||
[Metric in keyof Required<DocumentUsage>]: Required<DocumentUsage>[Metric] | 'pending'
|
||||
}
|
||||
|
||||
export interface DocUsageSummary extends DocUsageOrPending {
|
||||
dataLimitStatus: DataLimitStatus;
|
||||
}
|
||||
|
||||
// Count of non-removed documents in an org, grouped by data limit status.
|
||||
export type OrgUsageSummary = Record<NonNullable<DataLimitStatus>, number>;
|
||||
|
||||
type FilteredDocUsage = {
|
||||
[Metric in keyof DocUsageOrPending]: DocUsageOrPending[Metric] | 'hidden'
|
||||
}
|
||||
|
||||
export interface FilteredDocUsageSummary extends FilteredDocUsage {
|
||||
dataLimitStatus: DataLimitStatus;
|
||||
}
|
||||
|
||||
// Ratio of usage at which we start telling users that they're approaching limits.
|
||||
export const APPROACHING_LIMIT_RATIO = 0.9;
|
||||
|
||||
export class LimitExceededError extends ApiError {
|
||||
constructor(message: string) {
|
||||
super(message, 413);
|
||||
/**
|
||||
* Computes a ratio of `usage` to `limit`, if possible. Returns 0 if `usage` or `limit`
|
||||
* is invalid or undefined.
|
||||
*/
|
||||
export function getUsageRatio(usage: number | undefined, limit: number | undefined): number {
|
||||
if (!isEnforceableLimit(limit) || usage === undefined || usage < 0) {
|
||||
// Treat undefined or invalid values as having 0 usage.
|
||||
return 0;
|
||||
}
|
||||
|
||||
return usage / limit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an empty org usage summary with values initialized to 0.
|
||||
*/
|
||||
export function createEmptyOrgUsageSummary(): OrgUsageSummary {
|
||||
return {
|
||||
approachingLimit: 0,
|
||||
gracePeriod: 0,
|
||||
deleteOnly: 0,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if `limit` is defined and is a valid, positive number.
|
||||
*/
|
||||
function isEnforceableLimit(limit: number | undefined): limit is number {
|
||||
return limit !== undefined && limit > 0;
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import {BillingAPI, BillingAPIImpl} from 'app/common/BillingAPI';
|
||||
import {BrowserSettings} from 'app/common/BrowserSettings';
|
||||
import {BulkColValues, TableColValues, TableRecordValue, TableRecordValues, UserAction} from 'app/common/DocActions';
|
||||
import {DocCreationInfo, OpenDocMode} from 'app/common/DocListAPI';
|
||||
import {OrgUsageSummary} from 'app/common/DocUsage';
|
||||
import {Features} from 'app/common/Features';
|
||||
import {ICustomWidget} from 'app/common/CustomWidget';
|
||||
import {isClient} from 'app/common/gristUrls';
|
||||
@@ -288,6 +289,7 @@ export interface UserAPI {
|
||||
getWorkspace(workspaceId: number): Promise<Workspace>;
|
||||
getOrg(orgId: number|string): Promise<Organization>;
|
||||
getOrgWorkspaces(orgId: number|string): Promise<Workspace[]>;
|
||||
getOrgUsageSummary(orgId: number|string): Promise<OrgUsageSummary>;
|
||||
getTemplates(onlyFeatured?: boolean): Promise<Workspace[]>;
|
||||
getDoc(docId: string): Promise<Document>;
|
||||
newOrg(props: Partial<OrganizationProperties>): Promise<number>;
|
||||
@@ -448,6 +450,10 @@ export class UserAPIImpl extends BaseAPI implements UserAPI {
|
||||
{ method: 'GET' });
|
||||
}
|
||||
|
||||
public async getOrgUsageSummary(orgId: number|string): Promise<OrgUsageSummary> {
|
||||
return this.requestJson(`${this._url}/api/orgs/${orgId}/usage`, { method: 'GET' });
|
||||
}
|
||||
|
||||
public async getTemplates(onlyFeatured: boolean = false): Promise<Workspace[]> {
|
||||
return this.requestJson(`${this._url}/api/templates?onlyFeatured=${onlyFeatured ? 1 : 0}`, { method: 'GET' });
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user