From 02e69fb685f0f7ec84852578a4a1903acaa0b44f Mon Sep 17 00:00:00 2001 From: Alex Hall Date: Mon, 14 Mar 2022 20:16:30 +0200 Subject: [PATCH] (core) Crudely show row count and limit in UI Summary: Add rowCount returned from sandbox when applying user actions to ActionGroup which is broadcast to clients. Add rowCount to ActiveDoc and update it after applying user actions. Add rowCount to OpenLocalDocResult using ActiveDoc value, to show when a client opens a doc before any user actions happen. Add rowCount observable to DocPageModel which is set when the doc is opened and when action groups are received. Add crude UI (commented out) in Tool.ts showing the row count and the limit in AppModel.currentFeatures. The actual UI doesn't have a place to go yet. Followup tasks: - Real, pretty UI - Counts per table - Keep count(s) secret from users with limited access? - Data size indicator? - Banner when close to or above limit - Measure row counts outside of sandbox to avoid spoofing with formula - Handle changes to the limit when the plan is changed or extra rows are purchased Test Plan: Tested UI manually, including with free team site, opening a fresh doc, opening an initialised doc, adding rows, undoing, and changes from another tab. Automated tests seem like they should wait for a proper UI. Reviewers: georgegevoian Reviewed By: georgegevoian Differential Revision: https://phab.getgrist.com/D3318 --- app/client/components/GristDoc.ts | 3 +++ app/client/models/DocPageModel.ts | 5 +++++ app/client/ui/Tools.ts | 16 ++++++++++++---- app/common/ActionGroup.ts | 1 + app/common/DocListAPI.ts | 1 + app/server/lib/ActiveDoc.ts | 8 ++++++++ app/server/lib/DocManager.ts | 9 ++++++--- app/server/lib/GranularAccess.ts | 2 +- app/server/lib/Sharing.ts | 1 + 9 files changed, 38 insertions(+), 8 deletions(-) diff --git a/app/client/components/GristDoc.ts b/app/client/components/GristDoc.ts index b66fca5f..b3b09a4c 100644 --- a/app/client/components/GristDoc.ts +++ b/app/client/components/GristDoc.ts @@ -452,6 +452,9 @@ export class GristDoc extends DisposableWithEvents { if (schemaUpdated) { this.trigger('schemaUpdateAction', docActions); } + if (typeof actionGroup.rowCount === "number") { + this.docPageModel.rowCount.set(actionGroup.rowCount); + } } } diff --git a/app/client/models/DocPageModel.ts b/app/client/models/DocPageModel.ts index 4403c273..9a7ae63a 100644 --- a/app/client/models/DocPageModel.ts +++ b/app/client/models/DocPageModel.ts @@ -65,6 +65,8 @@ export interface DocPageModel { gristDoc: Observable; // Instance of GristDoc once it exists. + rowCount: Observable; + createLeftPane(leftPanelOpen: Observable): DomArg; renameDoc(value: string): Promise; updateCurrentDoc(urlId: string, openMode: OpenDocMode): Promise; @@ -105,6 +107,8 @@ export class DocPageModelImpl extends Disposable implements DocPageModel { // Observable set to the instance of GristDoc once it's created. public readonly gristDoc = Observable.create(this, null); + public readonly rowCount = Observable.create(this, undefined); + // Combination of arguments needed to open a doc (docOrUrlId + openMod). It's obtained from the // URL, and when it changes, we need to re-open. // If making a comparison, the id of the document we are comparing with is also included @@ -253,6 +257,7 @@ export class DocPageModelImpl extends Disposable implements DocPageModel { doc.userOverride = openDocResponse.userOverride || null; this.currentDoc.set({...doc}); } + this.rowCount.set(openDocResponse.rowCount); const gdModule = await gristDocModulePromise; const docComm = gdModule.DocComm.create(flow, comm, openDocResponse, doc.id, this.appModel.notifier); flow.checkIfCancelled(); diff --git a/app/client/ui/Tools.ts b/app/client/ui/Tools.ts index 21602667..0843f60c 100644 --- a/app/client/ui/Tools.ts +++ b/app/client/ui/Tools.ts @@ -19,8 +19,9 @@ import {Computed, Disposable, dom, makeTestId, Observable, observable, styled} f const testId = makeTestId('test-tools-'); export function tools(owner: Disposable, gristDoc: GristDoc, leftPanelOpen: Observable): Element { - const isOwner = gristDoc.docPageModel.currentDoc.get()?.access === 'owners'; - const isOverridden = Boolean(gristDoc.docPageModel.userOverride.get()); + const docPageModel = gristDoc.docPageModel; + const isOwner = docPageModel.currentDoc.get()?.access === 'owners'; + const isOverridden = Boolean(docPageModel.userOverride.get()); const hasDocTour = Computed.create(owner, use => use(gristDoc.docModel.allTableIds.getObservable()).includes('GristDocTour')); const canViewAccessRules = observable(false); @@ -34,6 +35,13 @@ export function tools(owner: Disposable, gristDoc: GristDoc, leftPanelOpen: Obse cssTools.cls('-collapsed', (use) => !use(leftPanelOpen)), cssSectionHeader("TOOLS"), + // TODO proper UI + // cssPageEntry( + // dom.domComputed(docPageModel.rowCount, rowCount => + // `${rowCount} of ${docPageModel.appModel.currentFeatures.baseMaxRowsPerDocument || "infinity"} rows used` + // ), + // ), + cssPageEntry( cssPageEntry.cls('-selected', (use) => use(gristDoc.activeViewId) === 'acl'), cssPageEntry.cls('-disabled', (use) => !use(canViewAccessRules)), @@ -78,7 +86,7 @@ export function tools(owner: Disposable, gristDoc: GristDoc, leftPanelOpen: Obse testId('code'), ), cssSpacer(), - dom.maybe(gristDoc.docPageModel.currentDoc, (doc) => { + dom.maybe(docPageModel.currentDoc, (doc) => { const ex = examples.find(e => e.urlId === doc.urlId); if (!ex || !ex.tutorialUrl) { return null; } return cssPageEntry( @@ -125,7 +133,7 @@ export function tools(owner: Disposable, gristDoc: GristDoc, leftPanelOpen: Obse ) ), ), - createHelpTools(gristDoc.docPageModel.appModel, false) + createHelpTools(docPageModel.appModel, false) ); } diff --git a/app/common/ActionGroup.ts b/app/common/ActionGroup.ts index 4b9d6f15..05c42c2c 100644 --- a/app/common/ActionGroup.ts +++ b/app/common/ActionGroup.ts @@ -25,4 +25,5 @@ export interface ActionGroup extends MinimalActionGroup { user: string; primaryAction: string; // The name of the first user action in the ActionGroup. internal: boolean; // True if it is inappropriate to log/undo the action. + rowCount?: number; } diff --git a/app/common/DocListAPI.ts b/app/common/DocListAPI.ts index 4534fd0a..91e85576 100644 --- a/app/common/DocListAPI.ts +++ b/app/common/DocListAPI.ts @@ -44,6 +44,7 @@ export interface OpenLocalDocResult { log: MinimalActionGroup[]; recoveryMode?: boolean; userOverride?: UserOverride; + rowCount?: number; } export interface UserOverride { diff --git a/app/server/lib/ActiveDoc.ts b/app/server/lib/ActiveDoc.ts index 0d777f30..dd319edd 100644 --- a/app/server/lib/ActiveDoc.ts +++ b/app/server/lib/ActiveDoc.ts @@ -159,6 +159,7 @@ export class ActiveDoc extends EventEmitter { private _fullyLoaded: boolean = false; // Becomes true once all columns are loaded/computed. private _lastMemoryMeasurement: number = 0; // Timestamp when memory was last measured. private _fetchCache = new MapWithTTL>(DEFAULT_CACHE_TTL); + private _rowCount?: number; // Timer for shutting down the ActiveDoc a bit after all clients are gone. private _inactivityTimer = new InactivityTimer(() => this.shutdown(), Deps.ACTIVEDOC_TIMEOUT * 1000); @@ -212,6 +213,12 @@ export class ActiveDoc extends EventEmitter { public get isShuttingDown(): boolean { return this._shuttingDown; } + public async getRowCount(docSession: OptDocSession): Promise { + if (await this._granularAccess.canReadEverything(docSession)) { + return this._rowCount; + } + } + public async getUserOverride(docSession: OptDocSession) { return this._granularAccess.getUserOverride(docSession); } @@ -1221,6 +1228,7 @@ export class ActiveDoc extends EventEmitter { ...this.getLogMeta(docSession), rowCount: sandboxActionBundle.rowCount }); + this._rowCount = sandboxActionBundle.rowCount; await this._reportDataEngineMemory(); } else { // Create default SandboxActionBundle to use if the data engine is not called. diff --git a/app/server/lib/DocManager.ts b/app/server/lib/DocManager.ts index a11846d8..c462208f 100644 --- a/app/server/lib/DocManager.ts +++ b/app/server/lib/DocManager.ts @@ -311,9 +311,11 @@ export class DocManager extends EventEmitter { } } - const [metaTables, recentActions] = await Promise.all([ + const [metaTables, recentActions, userOverride, rowCount] = await Promise.all([ activeDoc.fetchMetaTables(docSession), - activeDoc.getRecentMinimalActions(docSession) + activeDoc.getRecentMinimalActions(docSession), + activeDoc.getUserOverride(docSession), + activeDoc.getRowCount(docSession), ]); const result = { @@ -322,7 +324,8 @@ export class DocManager extends EventEmitter { doc: metaTables, log: recentActions, recoveryMode: activeDoc.recoveryMode, - userOverride: await activeDoc.getUserOverride(docSession), + userOverride, + rowCount, } as OpenLocalDocResult; if (!activeDoc.muted) { diff --git a/app/server/lib/GranularAccess.ts b/app/server/lib/GranularAccess.ts index 3ff8a2cb..2eeedf57 100644 --- a/app/server/lib/GranularAccess.ts +++ b/app/server/lib/GranularAccess.ts @@ -365,10 +365,10 @@ export class GranularAccess implements GranularAccessForBundle { public async filterActionGroup(docSession: OptDocSession, actionGroup: ActionGroup): Promise { if (await this.allowActionGroup(docSession, actionGroup)) { return actionGroup; } // For now, if there's any nuance at all, suppress the summary and description. - // TODO: create an empty action summary, to be sure not to leak anything important. const result: ActionGroup = { ...actionGroup }; result.actionSummary = createEmptyActionSummary(); result.desc = ''; + result.rowCount = undefined; return result; } diff --git a/app/server/lib/Sharing.ts b/app/server/lib/Sharing.ts index 6185b9e0..79eff733 100644 --- a/app/server/lib/Sharing.ts +++ b/app/server/lib/Sharing.ts @@ -309,6 +309,7 @@ export class Sharing { internal: isCalculate, }); actionGroup.actionSummary = actionSummary; + actionGroup.rowCount = sandboxActionBundle.rowCount; await accessControl.appliedBundle(); await accessControl.sendDocUpdateForBundle(actionGroup); if (docSession) {