mirror of
https://github.com/gristlabs/grist-core.git
synced 2026-03-02 04:09:24 +00:00
(core) Admin Panel and InstallAdmin class to identify installation admins.
Summary: - Add InstallAdmin class to identify users who can manage Grist installation. This is overridable by different Grist flavors (e.g. different in SaaS). It generalizes previous logic used to decide who can control Activation settings (e.g. enable telemetry). - Implement a basic Admin Panel at /admin, and move items previously in the "Support Grist" page into the "Support Grist" section of the Admin Panel. - Replace "Support Grist" menu items with "Admin Panel" and show only to admins. - Add "Support Grist" links to Github sponsorship to user-account menu. - Add "Support Grist" button to top-bar, which - for admins, replaces the previous "Contribute" button and reopens the "Support Grist / opt-in to telemetry" nudge (unchanged) - for everyone else, links to Github sponsorship - in either case, user can dismiss it. Test Plan: Shuffled some test cases between Support Grist and the new Admin Panel, and added some new cases. Reviewers: jarek, paulfitz Reviewed By: jarek, paulfitz Differential Revision: https://phab.getgrist.com/D4194
This commit is contained in:
@@ -25,8 +25,9 @@ import {getDefaultThemePrefs, Theme, ThemeColors, ThemePrefs,
|
||||
ThemePrefsChecker} from 'app/common/ThemePrefs';
|
||||
import {getThemeColors} from 'app/common/Themes';
|
||||
import {getGristConfig} from 'app/common/urlUtils';
|
||||
import {ExtendedUser} from 'app/common/UserAPI';
|
||||
import {getOrgName, isTemplatesOrg, Organization, OrgError, UserAPI, UserAPIImpl} from 'app/common/UserAPI';
|
||||
import {getUserPrefObs, getUserPrefsObs, markAsSeen, markAsUnSeen} from 'app/client/models/UserPrefs';
|
||||
import {getUserPrefObs, getUserPrefsObs, markAsSeen} from 'app/client/models/UserPrefs';
|
||||
import {bundleChanges, Computed, Disposable, Observable, subscribe} from 'grainjs';
|
||||
|
||||
const t = makeT('AppModel');
|
||||
@@ -40,7 +41,7 @@ export type PageType =
|
||||
| "billing"
|
||||
| "welcome"
|
||||
| "account"
|
||||
| "support"
|
||||
| "admin"
|
||||
| "activation";
|
||||
|
||||
const G = getBrowserGlobals('document', 'window');
|
||||
@@ -99,8 +100,8 @@ export interface AppModel {
|
||||
topAppModel: TopAppModel;
|
||||
api: UserAPI;
|
||||
|
||||
currentUser: FullUser|null;
|
||||
currentValidUser: FullUser|null; // Like currentUser, but null when anonymous
|
||||
currentUser: ExtendedUser|null;
|
||||
currentValidUser: ExtendedUser|null; // Like currentUser, but null when anonymous
|
||||
|
||||
currentOrg: Organization|null; // null if no access to currentSubdomain
|
||||
currentOrgName: string; // Our best guess for human-friendly name.
|
||||
@@ -141,8 +142,8 @@ export interface AppModel {
|
||||
isSupport(): boolean; // If user is a Support user
|
||||
isOwner(): boolean; // If user is an owner of this org
|
||||
isOwnerOrEditor(): boolean; // If user is an owner or editor of this org
|
||||
/** Creates an computed observable to dismiss a popup or check if it was dismissed */
|
||||
dismissedPopup(name: DismissedPopup): Observable<boolean>;
|
||||
isInstallAdmin(): boolean; // Is user an admin of this installation
|
||||
dismissPopup(name: DismissedPopup, isSeen: boolean): void; // Mark popup as dismissed or not.
|
||||
switchUser(user: FullUser, org?: string): Promise<void>;
|
||||
}
|
||||
|
||||
@@ -283,7 +284,7 @@ export class AppModelImpl extends Disposable implements AppModel {
|
||||
public readonly api: UserAPI = this.topAppModel.api;
|
||||
|
||||
// Compute currentValidUser, turning anonymous into null.
|
||||
public readonly currentValidUser: FullUser|null =
|
||||
public readonly currentValidUser: ExtendedUser|null =
|
||||
this.currentUser && !this.currentUser.anonymous ? this.currentUser : null;
|
||||
|
||||
// Figure out the org name, or blank if details are unavailable.
|
||||
@@ -324,8 +325,8 @@ export class AppModelImpl extends Disposable implements AppModel {
|
||||
return 'welcome';
|
||||
} else if (state.account) {
|
||||
return 'account';
|
||||
} else if (state.supportGrist) {
|
||||
return 'support';
|
||||
} else if (state.adminPanel) {
|
||||
return 'admin';
|
||||
} else if (state.activation) {
|
||||
return 'activation';
|
||||
} else {
|
||||
@@ -340,7 +341,7 @@ export class AppModelImpl extends Disposable implements AppModel {
|
||||
state.billing === 'scheduled' ||
|
||||
Boolean(state.account) ||
|
||||
Boolean(state.activation) ||
|
||||
Boolean(state.supportGrist)
|
||||
Boolean(state.adminPanel)
|
||||
);
|
||||
});
|
||||
|
||||
@@ -353,7 +354,7 @@ export class AppModelImpl extends Disposable implements AppModel {
|
||||
|
||||
constructor(
|
||||
public readonly topAppModel: TopAppModel,
|
||||
public readonly currentUser: FullUser|null,
|
||||
public readonly currentUser: ExtendedUser|null,
|
||||
public readonly currentOrg: Organization|null,
|
||||
public readonly orgError?: OrgError,
|
||||
) {
|
||||
@@ -419,6 +420,10 @@ export class AppModelImpl extends Disposable implements AppModel {
|
||||
return Boolean(this.currentOrg && isOwnerOrEditor(this.currentOrg));
|
||||
}
|
||||
|
||||
public isInstallAdmin(): boolean {
|
||||
return Boolean(this.currentUser?.isInstallAdmin);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch and update the current org's usage.
|
||||
*/
|
||||
@@ -435,16 +440,8 @@ export class AppModelImpl extends Disposable implements AppModel {
|
||||
}
|
||||
}
|
||||
|
||||
public dismissedPopup(name: DismissedPopup): Computed<boolean> {
|
||||
const computed = Computed.create(null, use => use(this.dismissedPopups).includes(name));
|
||||
computed.onWrite(value => {
|
||||
if (value) {
|
||||
markAsSeen(this.dismissedPopups, name);
|
||||
} else {
|
||||
markAsUnSeen(this.dismissedPopups, name);
|
||||
}
|
||||
});
|
||||
return computed;
|
||||
public dismissPopup(name: DismissedPopup, isSeen: boolean): void {
|
||||
markAsSeen(this.dismissedPopups, name, isSeen);
|
||||
}
|
||||
|
||||
public async switchUser(user: FullUser, org?: string) {
|
||||
|
||||
@@ -89,12 +89,16 @@ export const {getPrefsObs: getUserPrefsObs, getPrefObs: getUserPrefObs} = makePr
|
||||
// For preferences that store a list of items (such as seen docTours), this helper updates the
|
||||
// preference to add itemId to it (e.g. to avoid auto-starting the docTour again in the future).
|
||||
// prefKey is used only to log a more informative warning on error.
|
||||
export function markAsSeen<T>(seenIdsObs: Observable<T[] | undefined>, itemId: T) {
|
||||
export function markAsSeen<T>(seenIdsObs: Observable<T[] | undefined>, itemId: T, isSeen = true) {
|
||||
const seenIds = seenIdsObs.get() || [];
|
||||
try {
|
||||
if (!seenIds.includes(itemId)) {
|
||||
const seen = new Set(seenIds);
|
||||
seen.add(itemId);
|
||||
if (isSeen) {
|
||||
seen.add(itemId);
|
||||
} else {
|
||||
seen.delete(itemId);
|
||||
}
|
||||
seenIdsObs.set([...seen].sort());
|
||||
}
|
||||
} catch (e) {
|
||||
@@ -104,17 +108,3 @@ export function markAsSeen<T>(seenIdsObs: Observable<T[] | undefined>, itemId: T
|
||||
console.warn("Failed to save preference in markAsSeen", e);
|
||||
}
|
||||
}
|
||||
|
||||
export function markAsUnSeen<T>(seenIdsObs: Observable<T[] | undefined>, itemId: T) {
|
||||
const seenIds = seenIdsObs.get() || [];
|
||||
try {
|
||||
if (seenIds.includes(itemId)) {
|
||||
const seen = new Set(seenIds);
|
||||
seen.delete(itemId);
|
||||
seenIdsObs.set([...seen].sort());
|
||||
}
|
||||
} catch (e) {
|
||||
// tslint:disable-next-line:no-console
|
||||
console.warn("Failed to save preference in markAsUnSeen", e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -171,7 +171,7 @@ export class UrlStateImpl {
|
||||
public updateState(prevState: IGristUrlState, newState: IGristUrlState): IGristUrlState {
|
||||
const keepState = (newState.org || newState.ws || newState.homePage || newState.doc || isEmpty(newState) ||
|
||||
newState.account || newState.billing || newState.activation || newState.welcome ||
|
||||
newState.supportGrist) ?
|
||||
newState.adminPanel) ?
|
||||
(prevState.org ? {org: prevState.org} : {}) :
|
||||
prevState;
|
||||
return {...keepState, ...newState};
|
||||
@@ -205,10 +205,10 @@ export class UrlStateImpl {
|
||||
const signupReload = [prevState.login, newState.login].includes('signup')
|
||||
&& prevState.login !== newState.login;
|
||||
// Reload when moving to/from the support Grist page.
|
||||
const supportGristReload = Boolean(prevState.supportGrist) !== Boolean(newState.supportGrist);
|
||||
const adminPanelReload = Boolean(prevState.adminPanel) !== Boolean(newState.adminPanel);
|
||||
return Boolean(orgReload || accountReload || billingReload || activationReload ||
|
||||
gristConfig.errPage || docReload || welcomeReload || linkKeysReload || signupReload ||
|
||||
supportGristReload);
|
||||
adminPanelReload);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user