(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
This commit is contained in:
Alex Hall 2022-03-14 20:16:30 +02:00
parent d154b9afa7
commit 02e69fb685
9 changed files with 38 additions and 8 deletions

View File

@ -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);
}
}
}

View File

@ -65,6 +65,8 @@ export interface DocPageModel {
gristDoc: Observable<GristDoc|null>; // Instance of GristDoc once it exists.
rowCount: Observable<number|undefined>;
createLeftPane(leftPanelOpen: Observable<boolean>): DomArg;
renameDoc(value: string): Promise<void>;
updateCurrentDoc(urlId: string, openMode: OpenDocMode): Promise<Document>;
@ -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<GristDoc|null>(this, null);
public readonly rowCount = Observable.create<number|undefined>(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();

View File

@ -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<boolean>): 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)
);
}

View File

@ -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;
}

View File

@ -44,6 +44,7 @@ export interface OpenLocalDocResult {
log: MinimalActionGroup[];
recoveryMode?: boolean;
userOverride?: UserOverride;
rowCount?: number;
}
export interface UserOverride {

View File

@ -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<string, Promise<TableDataAction>>(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<number | undefined> {
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.

View File

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

View File

@ -365,10 +365,10 @@ export class GranularAccess implements GranularAccessForBundle {
public async filterActionGroup(docSession: OptDocSession, actionGroup: ActionGroup): Promise<ActionGroup> {
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;
}

View File

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