mirror of
https://github.com/gristlabs/grist-core.git
synced 2024-10-27 20:44:07 +00:00
(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:
parent
d154b9afa7
commit
02e69fb685
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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();
|
||||
|
@ -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)
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -44,6 +44,7 @@ export interface OpenLocalDocResult {
|
||||
log: MinimalActionGroup[];
|
||||
recoveryMode?: boolean;
|
||||
userOverride?: UserOverride;
|
||||
rowCount?: number;
|
||||
}
|
||||
|
||||
export interface UserOverride {
|
||||
|
@ -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.
|
||||
|
@ -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) {
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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) {
|
||||
|
Loading…
Reference in New Issue
Block a user