mirror of
https://github.com/gristlabs/grist-core.git
synced 2024-10-27 20:44:07 +00:00
(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:
parent
8da89b0a3d
commit
80f8168cab
@ -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': {
|
||||
|
@ -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};
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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.
|
||||
|
@ -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;
|
||||
|
@ -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) {
|
||||
|
@ -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';
|
||||
|
@ -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, {
|
||||
|
Loading…
Reference in New Issue
Block a user