(core) DocLimits: display days remaining instead of days of grace period

Summary:
Before this change we would always say there are 14 days remaining,
regardless of how many actually are remaining. Let's pass around a
different `dataLimitsInfo` object that also reports the number of days
remaining.

Test Plan: Ensure the test suite passes.

Reviewers: georgegevoian

Reviewed By: georgegevoian

Differential Revision: https://phab.getgrist.com/D4332
This commit is contained in:
Jordi Gutiérrez Hermoso 2024-08-27 21:27:59 -04:00
parent 8da89b0a3d
commit 80f8168cab
7 changed files with 40 additions and 30 deletions

View File

@ -7,7 +7,7 @@ import {withInfoTooltip} from 'app/client/ui/tooltips';
import {mediaXSmall, theme} from 'app/client/ui2018/cssVars';
import {icon} from 'app/client/ui2018/icons';
import {loadingDots, loadingSpinner} from 'app/client/ui2018/loaders';
import {APPROACHING_LIMIT_RATIO, DataLimitStatus} from 'app/common/DocUsage';
import {APPROACHING_LIMIT_RATIO, DataLimitInfo} from 'app/common/DocUsage';
import {Features, isFreePlan} from 'app/common/Features';
import {capitalizeFirstWord} from 'app/common/gutil';
import {canUpgradeOrg} from 'app/common/roles';
@ -40,8 +40,8 @@ export class DocumentUsage extends Disposable {
// TODO: Update this whenever the rest of the UI is internationalized.
private readonly _rowCountFormatter = new Intl.NumberFormat('en-US');
private readonly _dataLimitStatus = Computed.create(this, this._currentDocUsage, (_use, usage) => {
return usage?.dataLimitStatus ?? null;
private readonly _dataLimitInfo = Computed.create(this, this._currentDocUsage, (_use, usage) => {
return usage?.dataLimitInfo;
});
private readonly _rowCount = Computed.create(this, this._currentDocUsage, (_use, usage) => {
@ -158,11 +158,11 @@ export class DocumentUsage extends Disposable {
const org = use(this._currentOrg);
const product = use(this._currentProduct);
const features = use(this._currentFeatures);
const status = use(this._dataLimitStatus);
if (!org || !status) { return null; }
const usageInfo = use(this._dataLimitInfo);
if (!org || !usageInfo?.status) { return null; }
return buildMessage([
buildLimitStatusMessage(status, features, {
buildLimitStatusMessage(usageInfo, features, {
disableRawDataLink: true
}),
(product && isFreePlan(product.name)
@ -196,13 +196,14 @@ export class DocumentUsage extends Disposable {
}
export function buildLimitStatusMessage(
status: NonNullable<DataLimitStatus>,
usageInfo: NonNullable<DataLimitInfo>,
features?: Features|null,
options: {
disableRawDataLink?: boolean;
} = {}
) {
const {disableRawDataLink = false} = options;
const {status, daysRemaining} = usageInfo;
switch (status) {
case 'approachingLimit': {
return [
@ -224,7 +225,7 @@ export function buildLimitStatusMessage(
return [
'Document limits ',
disableRawDataLink ? 'exceeded' : buildRawDataPageLink('exceeded'),
`. In ${gracePeriodDays} days, this document will be read-only.`
`. In ${daysRemaining} days, this document will be read-only.`
];
}
case 'deleteOnly': {

View File

@ -1,5 +1,6 @@
import {ApiError} from 'app/common/ApiError';
import {APPROACHING_LIMIT_RATIO, DataLimitStatus, DocumentUsage, getUsageRatio} from 'app/common/DocUsage';
import {APPROACHING_LIMIT_RATIO, DataLimitInfo, DataLimitStatus,
DocumentUsage, getUsageRatio} from 'app/common/DocUsage';
import {Features} from 'app/common/Features';
import moment from 'moment-timezone';
@ -22,22 +23,24 @@ export interface GetDataLimitStatusParams {
* 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 {
export function getDataLimitInfo(params: GetDataLimitStatusParams): DataLimitInfo {
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';
// In case we forgot to define a grace period, we'll default to two weeks.
const days = productFeatures?.gracePeriodDays ?? 14;
const daysRemaining = start && days ? days - moment().diff(moment(start), 'days') : NaN;
if (daysRemaining > 0) {
return {status: 'gracePeriod', daysRemaining};
} else {
return 'gracePeriod';
return {status: 'deleteOnly'};
}
} else if (ratio > APPROACHING_LIMIT_RATIO) {
return 'approachingLimit';
} else {
return null;
return {status: 'approachingLimit'};
}
return {status: null};
}
/**

View File

@ -10,13 +10,17 @@ export interface RowCounts {
}
export type DataLimitStatus = 'approachingLimit' | 'gracePeriod' | 'deleteOnly' | null;
export interface DataLimitInfo {
status: DataLimitStatus;
daysRemaining?: number;
}
type DocUsageOrPending = {
[Metric in keyof Required<DocumentUsage>]: Required<DocumentUsage>[Metric] | 'pending'
}
export interface DocUsageSummary extends DocUsageOrPending {
dataLimitStatus: DataLimitStatus;
dataLimitInfo: DataLimitInfo;
}
// Count of non-removed documents in an org, grouped by data limit status.
@ -27,7 +31,7 @@ type FilteredDocUsage = {
}
export interface FilteredDocUsageSummary extends FilteredDocUsage {
dataLimitStatus: DataLimitStatus;
dataLimitInfo: DataLimitInfo;
}
// Ratio of usage at which we start telling users that they're approaching limits.

View File

@ -1,7 +1,7 @@
import {ShareInfo} from 'app/common/ActiveDocAPI';
import {ApiError, LimitType} from 'app/common/ApiError';
import {mapGetOrSet, mapSetOrClear, MapWithTTL} from 'app/common/AsyncCreate';
import {getDataLimitStatus} from 'app/common/DocLimits';
import {getDataLimitInfo} from 'app/common/DocLimits';
import {createEmptyOrgUsageSummary, DocumentUsage, OrgUsageSummary} from 'app/common/DocUsage';
import {normalizeEmail} from 'app/common/emails';
import {ANONYMOUS_PLAN, canAddOrgMembers, Features} from 'app/common/Features';
@ -764,7 +764,7 @@ export class HomeDBManager extends EventEmitter {
// Return an aggregate count of documents, grouped by data limit status.
const summary = createEmptyOrgUsageSummary();
for (const {usage: docUsage, gracePeriodStart} of docs) {
const dataLimitStatus = getDataLimitStatus({docUsage, gracePeriodStart, productFeatures});
const dataLimitStatus = getDataLimitInfo({docUsage, gracePeriodStart, productFeatures}).status;
if (dataLimitStatus) { summary[dataLimitStatus] += 1; }
}
return summary;

View File

@ -50,12 +50,12 @@ import {
UserAction
} from 'app/common/DocActions';
import {DocData} from 'app/common/DocData';
import {getDataLimitRatio, getDataLimitStatus, getSeverity, LimitExceededError} from 'app/common/DocLimits';
import {getDataLimitInfo, getDataLimitRatio, getSeverity, LimitExceededError} from 'app/common/DocLimits';
import {DocSnapshots} from 'app/common/DocSnapshot';
import {DocumentSettings} from 'app/common/DocumentSettings';
import {
APPROACHING_LIMIT_RATIO,
DataLimitStatus,
DataLimitInfo,
DocumentUsage,
DocUsageSummary,
FilteredDocUsageSummary,
@ -421,8 +421,8 @@ export class ActiveDoc extends EventEmitter {
return getDataLimitRatio(this._docUsage, this._product?.features);
}
public get dataLimitStatus(): DataLimitStatus {
return getDataLimitStatus({
public get dataLimitInfo(): DataLimitInfo {
return getDataLimitInfo({
docUsage: this._docUsage,
productFeatures: this._product?.features,
gracePeriodStart: this._gracePeriodStart,
@ -431,7 +431,7 @@ export class ActiveDoc extends EventEmitter {
public getDocUsageSummary(): DocUsageSummary {
return {
dataLimitStatus: this.dataLimitStatus,
dataLimitInfo: this.dataLimitInfo,
rowCount: this._docUsage?.rowCount ?? 'pending',
dataSizeBytes: this._docUsage?.dataSizeBytes ?? 'pending',
attachmentsSizeBytes: this._docUsage?.attachmentsSizeBytes ?? 'pending',
@ -2176,7 +2176,7 @@ export class ActiveDoc extends EventEmitter {
await this.waitForInitialization();
if (
this.dataLimitStatus === "deleteOnly" &&
this.dataLimitInfo.status === "deleteOnly" &&
!actions.every(action => [
'RemoveTable', 'RemoveColumn', 'RemoveRecord', 'BulkRemoveRecord',
'RemoveViewSection', 'RemoveView', 'ApplyUndoActions', 'RespondToRequests',
@ -2253,13 +2253,13 @@ export class ActiveDoc extends EventEmitter {
*/
private async _updateDocUsage(usage: Partial<DocumentUsage>, options: UpdateUsageOptions = {}) {
const {syncUsageToDatabase = true, broadcastUsageToClients = true} = options;
const oldStatus = this.dataLimitStatus;
const oldStatus = this.dataLimitInfo.status;
this._docUsage = {...(this._docUsage || {}), ...usage};
if (syncUsageToDatabase) {
/* If status decreased, we'll update usage in the database with minimal delay, so site usage
* banners show up-to-date statistics. If status increased or stayed the same, we'll schedule
* a delayed update, since it's less critical for banners to update immediately. */
const didStatusDecrease = getSeverity(this.dataLimitStatus) < getSeverity(oldStatus);
const didStatusDecrease = getSeverity(this.dataLimitInfo.status) < getSeverity(oldStatus);
this._syncDocUsageToDatabase(didStatusDecrease);
}
if (broadcastUsageToClients) {

View File

@ -792,7 +792,7 @@ export class GranularAccess implements GranularAccessForBundle {
}
const role = options.role ?? await this.getNominalAccess(docSession);
const hasEditRole = canEdit(role);
if (!hasEditRole) { result.dataLimitStatus = null; }
if (!hasEditRole) { result.dataLimitInfo.status = null; }
const hasFullReadAccess = await this.canReadEverything(docSession);
if (!hasEditRole || !hasFullReadAccess) {
result.rowCount = 'hidden';

View File

@ -2212,6 +2212,7 @@ describe('ApiServer', function() {
await server.dbManager.setDocsMetadata({[id]: {usage}});
}
await server.dbManager.setDocGracePeriodStart(docIds[docIds.length - 1], new Date(2000, 1, 1));
await server.dbManager.setDocGracePeriodStart(docIds[docIds.length - 2], new Date());
// Check that what's reported by /usage is accurate.
await assertOrgUsage(freeTeamOrgId, chimpy, {
@ -2238,6 +2239,7 @@ describe('ApiServer', function() {
dataSizeBytes: 999999999,
attachmentsSizeBytes: 999999999,
}}});
await server.dbManager.setDocGracePeriodStart(docId, new Date());
// Check that /usage includes that document in the count.
await assertOrgUsage(freeTeamOrgId, chimpy, {