mirror of
https://github.com/gristlabs/grist-core.git
synced 2026-03-02 04:09:24 +00:00
(core) Broadcast doc usage updates to clients
Summary: Introduces a new message type, docUsage, that's broadcast to all connected clients whenever document usage is updated in ActiveDoc. Test Plan: Browser tests. Reviewers: jarek Reviewed By: jarek Differential Revision: https://phab.getgrist.com/D3451
This commit is contained in:
@@ -4,11 +4,13 @@ import {reportError, setErrorNotifier} from 'app/client/models/errors';
|
||||
import {urlState} from 'app/client/models/gristUrlState';
|
||||
import {Notifier} from 'app/client/models/NotifyModel';
|
||||
import {getFlavor, ProductFlavor} from 'app/client/ui/CustomThemes';
|
||||
import {OrgUsageSummary} from 'app/common/DocUsage';
|
||||
import {Features} from 'app/common/Features';
|
||||
import {GristLoadConfig} from 'app/common/gristUrls';
|
||||
import {FullUser} from 'app/common/LoginSessionAPI';
|
||||
import {LocalPlugin} from 'app/common/plugin';
|
||||
import {UserPrefs} from 'app/common/Prefs';
|
||||
import {isOwner} from 'app/common/roles';
|
||||
import {getTagManagerScript} from 'app/common/tagManager';
|
||||
import {getGristConfig} from 'app/common/urlUtils';
|
||||
import {getOrgName, Organization, OrgError, UserAPI, UserAPIImpl} from 'app/common/UserAPI';
|
||||
@@ -60,6 +62,7 @@ export interface AppModel {
|
||||
|
||||
currentOrg: Organization|null; // null if no access to currentSubdomain
|
||||
currentOrgName: string; // Our best guess for human-friendly name.
|
||||
currentOrgUsage: Observable<OrgUsageSummary|null>;
|
||||
isPersonal: boolean; // Is it a personal site?
|
||||
isTeamSite: boolean; // Is it a team site?
|
||||
orgError?: OrgError; // If currentOrg is null, the error that caused it.
|
||||
@@ -70,6 +73,8 @@ export interface AppModel {
|
||||
pageType: Observable<PageType>;
|
||||
|
||||
notifier: Notifier;
|
||||
|
||||
refreshOrgUsage(): Promise<void>;
|
||||
}
|
||||
|
||||
export class TopAppModelImpl extends Disposable implements TopAppModel {
|
||||
@@ -182,6 +187,8 @@ export class AppModelImpl extends Disposable implements AppModel {
|
||||
// Figure out the org name, or blank if details are unavailable.
|
||||
public readonly currentOrgName = getOrgNameOrGuest(this.currentOrg, this.currentUser);
|
||||
|
||||
public readonly currentOrgUsage: Observable<OrgUsageSummary|null> = Observable.create(this, null);
|
||||
|
||||
public readonly isPersonal = Boolean(this.currentOrg?.owner);
|
||||
public readonly isTeamSite = Boolean(this.currentOrg) && !this.isPersonal;
|
||||
|
||||
@@ -206,6 +213,23 @@ export class AppModelImpl extends Disposable implements AppModel {
|
||||
this._recordSignUpIfIsNewUser();
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch and update the current org's usage.
|
||||
*/
|
||||
public async refreshOrgUsage() {
|
||||
const currentOrg = this.currentOrg;
|
||||
if (!isOwner(currentOrg)) {
|
||||
// Note: getOrgUsageSummary already checks for owner access; we do an early return
|
||||
// here to skip making unnecessary API calls.
|
||||
return;
|
||||
}
|
||||
|
||||
const usage = await this.api.getOrgUsageSummary(currentOrg.id);
|
||||
if (!this.isDisposed()) {
|
||||
this.currentOrgUsage.set(usage);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* If the current user is a new user, record a sign-up event via Google Tag Manager.
|
||||
*/
|
||||
|
||||
@@ -18,6 +18,7 @@ import {AsyncFlow, CancelledError, FlowRunner} from 'app/common/AsyncFlow';
|
||||
import {delay} from 'app/common/delay';
|
||||
import {OpenDocMode, UserOverride} from 'app/common/DocListAPI';
|
||||
import {FilteredDocUsageSummary} from 'app/common/DocUsage';
|
||||
import {Product} from 'app/common/Features';
|
||||
import {IGristUrlState, parseUrlId, UrlIdParts} from 'app/common/gristUrls';
|
||||
import {getReconnectTimeout} from 'app/common/gutil';
|
||||
import {canEdit, isOwner} from 'app/common/roles';
|
||||
@@ -46,6 +47,12 @@ export interface DocPageModel {
|
||||
currentDoc: Observable<DocInfo|null>;
|
||||
currentDocUsage: Observable<FilteredDocUsageSummary|null>;
|
||||
|
||||
/**
|
||||
* Initially set to the product referenced by `currentDoc`, and updated whenever `currentDoc`
|
||||
* changes, or a doc usage message is received from the server.
|
||||
*/
|
||||
currentProduct: Observable<Product|null>;
|
||||
|
||||
// This block is to satisfy previous interface, but usable as this.currentDoc.get().id, etc.
|
||||
currentDocId: Observable<string|undefined>;
|
||||
currentWorkspace: Observable<Workspace|null>;
|
||||
@@ -90,6 +97,12 @@ export class DocPageModelImpl extends Disposable implements DocPageModel {
|
||||
public readonly currentDoc = Observable.create<DocInfo|null>(this, null);
|
||||
public readonly currentDocUsage = Observable.create<FilteredDocUsageSummary|null>(this, null);
|
||||
|
||||
/**
|
||||
* Initially set to the product referenced by `currentDoc`, and updated whenever `currentDoc`
|
||||
* changes, or a doc usage message is received from the server.
|
||||
*/
|
||||
public readonly currentProduct = Observable.create<Product|null>(this, null);
|
||||
|
||||
public readonly currentUrlId = Computed.create(this, this.currentDoc, (use, doc) => doc ? doc.urlId : undefined);
|
||||
public readonly currentDocId = Computed.create(this, this.currentDoc, (use, doc) => doc ? doc.id : undefined);
|
||||
public readonly currentWorkspace = Computed.create(this, this.currentDoc, (use, doc) => doc && doc.workspace);
|
||||
@@ -146,6 +159,14 @@ export class DocPageModelImpl extends Disposable implements DocPageModel {
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
this.autoDispose(this.currentOrg.addListener((org) => {
|
||||
// Whenever the current doc is updated, set the current product to be the
|
||||
// one referenced by the updated doc.
|
||||
if (org?.billingAccount?.product.name !== this.currentProduct.get()?.name) {
|
||||
this.currentProduct.set(org?.billingAccount?.product ?? null);
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
public createLeftPane(leftPanelOpen: Observable<boolean>) {
|
||||
|
||||
@@ -7,7 +7,6 @@ import {AppModel, reportError} from 'app/client/models/AppModel';
|
||||
import {reportMessage, UserError} from 'app/client/models/errors';
|
||||
import {urlState} from 'app/client/models/gristUrlState';
|
||||
import {ownerName} from 'app/client/models/WorkspaceInfo';
|
||||
import {OrgUsageSummary} from 'app/common/DocUsage';
|
||||
import {IHomePage} from 'app/common/gristUrls';
|
||||
import {isLongerThan} from 'app/common/gutil';
|
||||
import {SortPref, UserOrgPrefs, ViewPref} from 'app/common/Prefs';
|
||||
@@ -76,8 +75,6 @@ export interface HomeModel {
|
||||
// user isn't allowed to create a doc.
|
||||
newDocWorkspace: Observable<Workspace|null|"unsaved">;
|
||||
|
||||
currentOrgUsage: Observable<OrgUsageSummary|null>;
|
||||
|
||||
createWorkspace(name: string): Promise<void>;
|
||||
renameWorkspace(id: number, name: string): Promise<void>;
|
||||
deleteWorkspace(id: number, forever: boolean): Promise<void>;
|
||||
@@ -158,8 +155,6 @@ export class HomeModelImpl extends Disposable implements HomeModel, ViewSettings
|
||||
wss.every((ws) => ws.isSupportWorkspace || ws.docs.length === 0) &&
|
||||
Boolean(use(this.newDocWorkspace))));
|
||||
|
||||
public readonly currentOrgUsage: Observable<OrgUsageSummary|null> = Observable.create(this, null);
|
||||
|
||||
private _userOrgPrefs = Observable.create<UserOrgPrefs|undefined>(this, this._app.currentOrg?.userOrgPrefs);
|
||||
|
||||
constructor(private _app: AppModel, clientScope: ClientScope) {
|
||||
@@ -193,7 +188,7 @@ export class HomeModelImpl extends Disposable implements HomeModel, ViewSettings
|
||||
const importSources = ImportSourceElement.fromArray(pluginManager.pluginsList);
|
||||
this.importSources.set(importSources);
|
||||
|
||||
this._updateCurrentOrgUsage().catch(reportError);
|
||||
this._app.refreshOrgUsage().catch(reportError);
|
||||
}
|
||||
|
||||
// Accessor for the AppModel containing this HomeModel.
|
||||
@@ -386,14 +381,6 @@ export class HomeModelImpl extends Disposable implements HomeModel, ViewSettings
|
||||
await this._app.api.updateOrg('current', {userOrgPrefs: org.userOrgPrefs});
|
||||
}
|
||||
}
|
||||
|
||||
private async _updateCurrentOrgUsage() {
|
||||
const currentOrg = this.app.currentOrg;
|
||||
if (!roles.isOwner(currentOrg)) { return; }
|
||||
|
||||
const api = this.app.api;
|
||||
this.currentOrgUsage.set(await api.getOrgUsageSummary(currentOrg.id));
|
||||
}
|
||||
}
|
||||
|
||||
// Check if active product allows just a single workspace.
|
||||
|
||||
Reference in New Issue
Block a user