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) {