(core) Add document usage banners

Summary:
This also enables the new Usage section for all sites. Currently,
it shows metrics for document row count, but only if the user
has full document read access. Otherwise, a message about
insufficient access is shown.

Test Plan: Browser tests.

Reviewers: jarek

Reviewed By: jarek

Subscribers: alexmojaki

Differential Revision: https://phab.getgrist.com/D3377
This commit is contained in:
George Gevoian
2022-04-21 10:57:33 -07:00
parent 01b1c310b5
commit af5b3c9004
14 changed files with 543 additions and 205 deletions

View File

@@ -10,7 +10,6 @@ import {ActionSummary} from "app/common/ActionSummary";
import {
ApplyUAOptions,
ApplyUAResult,
DataLimitStatus,
DataSourceTransformed,
ForkResult,
ImportOptions,
@@ -41,9 +40,11 @@ import {Features} from 'app/common/Features';
import {FormulaProperties, getFormulaProperties} from 'app/common/GranularAccessClause';
import {byteString, countIf, safeJsonParse} from 'app/common/gutil';
import {InactivityTimer} from 'app/common/InactivityTimer';
import {canEdit} from 'app/common/roles';
import {schema, SCHEMA_VERSION} from 'app/common/schema';
import {MetaRowRecord} from 'app/common/TableData';
import {FetchUrlOptions, UploadResult} from 'app/common/uploads';
import {APPROACHING_LIMIT_RATIO, DataLimitStatus, RowCount} from 'app/common/Usage';
import {DocReplacementOptions, DocState, DocStateComparison} from 'app/common/UserAPI';
import {convertFromColumn} from 'app/common/ValueConverter';
import {guessColInfoWithDocData} from 'app/common/ValueGuesser';
@@ -121,9 +122,6 @@ const REMOVE_UNUSED_ATTACHMENTS_INTERVAL_MS = 60 * 60 * 1000;
// A hook for dependency injection.
export const Deps = {ACTIVEDOC_TIMEOUT};
// Ratio of the row/data size limit where we tell users that they're approaching the limit
const APPROACHING_LIMIT_RATIO = 0.9;
/**
* Represents an active document with the given name. The document isn't actually open until
* either .loadDoc() or .createEmptyDoc() is called.
@@ -172,7 +170,7 @@ export class ActiveDoc extends EventEmitter {
private _lastMemoryMeasurement: number = 0; // Timestamp when memory was last measured.
private _lastDataSizeMeasurement: number = 0; // Timestamp when dbstat data size was last measured.
private _fetchCache = new MapWithTTL<string, Promise<TableDataAction>>(DEFAULT_CACHE_TTL);
private _rowCount?: number;
private _rowCount: RowCount = 'pending';
private _dataSize?: number;
private _productFeatures?: Features;
private _gracePeriodStart: Date|null = null;
@@ -237,11 +235,21 @@ export class ActiveDoc extends EventEmitter {
public get isShuttingDown(): boolean { return this._shuttingDown; }
public get rowLimitRatio() {
return this._rowLimit && this._rowCount ? this._rowCount / this._rowLimit : 0;
if (!this._rowLimit || this._rowLimit <= 0 || typeof this._rowCount !== 'number') {
// Invalid row limits are currently treated as if they are undefined.
return 0;
}
return this._rowCount / this._rowLimit;
}
public get dataSizeLimitRatio() {
return this._dataSizeLimit && this._dataSize ? this._dataSize / this._dataSizeLimit : 0;
if (!this._dataSizeLimit || this._dataSizeLimit <= 0 || !this._dataSize) {
// Invalid data size limits are currently treated as if they are undefined.
return 0;
}
return this._dataSize / this._dataSizeLimit;
}
public get dataLimitRatio() {
@@ -264,15 +272,15 @@ export class ActiveDoc extends EventEmitter {
return null;
}
public async getRowCount(docSession: OptDocSession): Promise<number | undefined> {
if (await this._granularAccess.canReadEverything(docSession)) {
return this._rowCount;
}
public async getRowCount(docSession: OptDocSession): Promise<RowCount> {
const hasFullReadAccess = await this._granularAccess.canReadEverything(docSession);
const hasEditRole = canEdit(await this._granularAccess.getNominalAccess(docSession));
return hasFullReadAccess && hasEditRole ? this._rowCount : 'hidden';
}
public async getDataLimitStatus(): Promise<DataLimitStatus> {
// TODO filter based on session permissions
return this.dataLimitStatus;
public async getDataLimitStatus(docSession: OptDocSession): Promise<DataLimitStatus> {
const hasEditRole = canEdit(await this._granularAccess.getNominalAccess(docSession));
return hasEditRole ? this.dataLimitStatus : null;
}
public async getUserOverride(docSession: OptDocSession) {

View File

@@ -318,7 +318,7 @@ export class DocManager extends EventEmitter {
activeDoc.getRecentMinimalActions(docSession),
activeDoc.getUserOverride(docSession),
activeDoc.getRowCount(docSession),
activeDoc.getDataLimitStatus(),
activeDoc.getDataLimitStatus(docSession),
]);
const result = {

View File

@@ -244,7 +244,7 @@ export class GranularAccess implements GranularAccessForBundle {
// An alternative to this check would be to sandwich user-defined access rules
// between some defaults. Currently the defaults have lower priority than
// user-defined access rules.
if (!canEdit(await this._getNominalAccess(docSession))) {
if (!canEdit(await this.getNominalAccess(docSession))) {
throw new ErrorWithCode('ACL_DENY', 'Only owners or editors can modify documents');
}
if (this._ruler.haveRules()) {
@@ -578,7 +578,7 @@ export class GranularAccess implements GranularAccessForBundle {
* permissions.
*/
public async canReadEverything(docSession: OptDocSession): Promise<boolean> {
const access = await this._getNominalAccess(docSession);
const access = await this.getNominalAccess(docSession);
if (!canView(access)) { return false; }
const permInfo = await this._getAccess(docSession);
return this.getReadPermission(permInfo.getFullAccess()) === 'allow';
@@ -621,7 +621,7 @@ export class GranularAccess implements GranularAccessForBundle {
* Check whether user has owner-level access to the document.
*/
public async isOwner(docSession: OptDocSession): Promise<boolean> {
const access = await this._getNominalAccess(docSession);
const access = await this.getNominalAccess(docSession);
return access === 'owners';
}
@@ -769,6 +769,23 @@ export class GranularAccess implements GranularAccessForBundle {
return result;
}
/**
* Get the role the session user has for this document. User may be overridden,
* in which case the role of the override is returned.
* The forkingAsOwner flag of docSession should not be respected for non-owners,
* so that the pseudo-ownership it offers is restricted to granular access within a
* document (as opposed to document-level operations).
*/
public async getNominalAccess(docSession: OptDocSession): Promise<Role|null> {
const linkParameters = docSession.authorizer?.getLinkParameters() || {};
const baseAccess = getDocSessionAccess(docSession);
if ((linkParameters.aclAsUserId || linkParameters.aclAsUser) && baseAccess === 'owners') {
const info = await this._getUser(docSession);
return info.Access;
}
return baseAccess;
}
// AddOrUpdateRecord requires broad read access to a table.
// But tables can be renamed, and access can be granted and removed
// within a bundle.
@@ -824,23 +841,6 @@ export class GranularAccess implements GranularAccessForBundle {
}
}
/**
* Get the role the session user has for this document. User may be overridden,
* in which case the role of the override is returned.
* The forkingAsOwner flag of docSession should not be respected for non-owners,
* so that the pseudo-ownership it offers is restricted to granular access within a
* document (as opposed to document-level operations).
*/
private async _getNominalAccess(docSession: OptDocSession): Promise<Role> {
const linkParameters = docSession.authorizer?.getLinkParameters() || {};
const baseAccess = getDocSessionAccess(docSession);
if ((linkParameters.aclAsUserId || linkParameters.aclAsUser) && baseAccess === 'owners') {
const info = await this._getUser(docSession);
return info.Access as Role;
}
return baseAccess;
}
/**
* Asserts that user has schema access.
*/