From 80f8168cab6998250e594d96f6e53996882cdd7c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jordi=20Guti=C3=A9rrez=20Hermoso?= Date: Tue, 27 Aug 2024 21:27:59 -0400 Subject: [PATCH] (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 --- app/client/components/DocumentUsage.ts | 17 +++++++++-------- app/common/DocLimits.ts | 21 ++++++++++++--------- app/common/DocUsage.ts | 8 ++++++-- app/gen-server/lib/homedb/HomeDBManager.ts | 4 ++-- app/server/lib/ActiveDoc.ts | 16 ++++++++-------- app/server/lib/GranularAccess.ts | 2 +- test/gen-server/ApiServer.ts | 2 ++ 7 files changed, 40 insertions(+), 30 deletions(-) diff --git a/app/client/components/DocumentUsage.ts b/app/client/components/DocumentUsage.ts index bb1023fc..e7edf5bd 100644 --- a/app/client/components/DocumentUsage.ts +++ b/app/client/components/DocumentUsage.ts @@ -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, + usageInfo: NonNullable, 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': { diff --git a/app/common/DocLimits.ts b/app/common/DocLimits.ts index dae8dfed..138aa2f7 100644 --- a/app/common/DocLimits.ts +++ b/app/common/DocLimits.ts @@ -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}; } /** diff --git a/app/common/DocUsage.ts b/app/common/DocUsage.ts index 0ecb0c84..afddbe48 100644 --- a/app/common/DocUsage.ts +++ b/app/common/DocUsage.ts @@ -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]: Required[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. diff --git a/app/gen-server/lib/homedb/HomeDBManager.ts b/app/gen-server/lib/homedb/HomeDBManager.ts index a4f89721..e70884d1 100644 --- a/app/gen-server/lib/homedb/HomeDBManager.ts +++ b/app/gen-server/lib/homedb/HomeDBManager.ts @@ -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; diff --git a/app/server/lib/ActiveDoc.ts b/app/server/lib/ActiveDoc.ts index 5a0e5648..4a1ff82e 100644 --- a/app/server/lib/ActiveDoc.ts +++ b/app/server/lib/ActiveDoc.ts @@ -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, 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) { diff --git a/app/server/lib/GranularAccess.ts b/app/server/lib/GranularAccess.ts index b90e8104..b54005ba 100644 --- a/app/server/lib/GranularAccess.ts +++ b/app/server/lib/GranularAccess.ts @@ -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'; diff --git a/test/gen-server/ApiServer.ts b/test/gen-server/ApiServer.ts index 39aba8c3..2f651604 100644 --- a/test/gen-server/ApiServer.ts +++ b/test/gen-server/ApiServer.ts @@ -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, {