mirror of
https://github.com/gristlabs/grist-core.git
synced 2026-03-02 04:09:24 +00:00
(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:
@@ -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) {
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user