Skip to content

Commit

Permalink
wip
Browse files Browse the repository at this point in the history
  • Loading branch information
jordigh committed Jul 16, 2024
1 parent 5afd3cd commit 3139328
Show file tree
Hide file tree
Showing 10 changed files with 254 additions and 46 deletions.
23 changes: 23 additions & 0 deletions app/client/models/ToggleEnterpriseModel.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import {AppModel, getHomeUrl} from 'app/client/models/AppModel';
import {bundleChanges, Disposable, Observable} from "grainjs";
import {ConfigAPI, ConfigAPIImpl} from 'app/common/ConfigAPI';

export class ToggleEnterpriseModel extends Disposable {
public readonly edition: Observable<string | null> = Observable.create(this, null);
private readonly _configAPI: ConfigAPI = new ConfigAPIImpl(getHomeUrl());
constructor (_appModel: AppModel) {
super();
}

public async fetchEnterpriseToggle(): Promise<void> {
const edition = await this._configAPI.getValue('edition');
bundleChanges(() => {
this.edition.set(edition);
});
}

public async updateEnterpriseToggle(edition: string): Promise<void> {
await this._configAPI.setValue({edition});
await this.fetchEnterpriseToggle();
}
}
10 changes: 9 additions & 1 deletion app/client/ui/AdminPanel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {AppHeader} from 'app/client/ui/AppHeader';
import {leftPanelBasic} from 'app/client/ui/LeftPanelCommon';
import {pagePanels} from 'app/client/ui/PagePanels';
import {SupportGristPage} from 'app/client/ui/SupportGristPage';
import {ToggleEnterprisePage} from 'app/client/ui/ToggleEnterprisePage';
import {createTopBarHome} from 'app/client/ui/TopBar';
import {cssBreadcrumbs, separator} from 'app/client/ui2018/breadcrumbs';
import {basicButton} from 'app/client/ui2018/buttons';
Expand All @@ -25,7 +26,6 @@ import {Computed, Disposable, dom, IDisposable,
IDisposableOwner, MultiHolder, Observable, styled} from 'grainjs';
import {AdminSection, AdminSectionItem, HidableToggle} from 'app/client/ui/AdminPanelCss';


const t = makeT('AdminPanel');

// Translated "Admin Panel" name, made available to other modules.
Expand All @@ -35,6 +35,7 @@ export function getAdminPanelName() {

export class AdminPanel extends Disposable {
private _supportGrist = SupportGristPage.create(this, this._appModel);
private _toggleEnterprise = ToggleEnterprisePage.create(this, this._appModel);
private readonly _installAPI: InstallAPI = new InstallAPIImpl(getHomeUrl());
private _checks: AdminChecks;

Expand Down Expand Up @@ -161,6 +162,13 @@ Please log in as an administrator.`)),
description: t('Current version of Grist'),
value: cssValueLabel(`Version ${version.version}`),
}),
dom.create(AdminSectionItem, {
id: 'enterprise',
name: t('Enterprise'),
description: t('Enable Grist Enterprise features'),
value: dom.create(HidableToggle, this._toggleEnterprise.getEnterpriseToggleObservable()),
expandedContent: this._toggleEnterprise.buildEnterpriseSection(),
}),
this._buildUpdates(owner),
]),
dom.create(AdminSection, t('Self Checks'), [
Expand Down
45 changes: 45 additions & 0 deletions app/client/ui/AdminTogglesCss.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import {bigBasicButton, bigBasicButtonLink, bigPrimaryButton} from 'app/client/ui2018/buttons';
import {theme} from 'app/client/ui2018/cssVars';
import {styled} from 'grainjs';

export const cssSection = styled('div', ``);

export const cssParagraph = styled('div', `
color: ${theme.text};
font-size: 14px;
line-height: 20px;
margin-bottom: 12px;
`);

export const cssOptInOutMessage = styled(cssParagraph, `
line-height: 40px;
font-weight: 600;
margin-top: 24px;
margin-bottom: 0px;
`);

export const cssOptInButton = styled(bigPrimaryButton, `
margin-top: 24px;
`);

export const cssOptOutButton = styled(bigBasicButton, `
margin-top: 24px;
`);

export const cssSponsorButton = styled(bigBasicButtonLink, `
margin-top: 24px;
`);

export const cssButtonIconAndText = styled('div', `
display: flex;
align-items: center;
`);

export const cssButtonText = styled('span', `
margin-left: 8px;
`);

export const cssSpinnerBox = styled('div', `
margin-top: 24px;
text-align: center;
`);
59 changes: 14 additions & 45 deletions app/client/ui/SupportGristPage.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,25 @@
import {makeT} from 'app/client/lib/localization';
import {AppModel} from 'app/client/models/AppModel';
import {TelemetryModel, TelemetryModelImpl} from 'app/client/models/TelemetryModel';
import {basicButtonLink, bigBasicButton, bigBasicButtonLink, bigPrimaryButton} from 'app/client/ui2018/buttons';
import {theme} from 'app/client/ui2018/cssVars';
import {icon} from 'app/client/ui2018/icons';
import {cssLink} from 'app/client/ui2018/links';
import {loadingSpinner} from 'app/client/ui2018/loaders';
import {commonUrls} from 'app/common/gristUrls';
import {TelemetryPrefsWithSources} from 'app/common/InstallAPI';
import {Computed, Disposable, dom, makeTestId, styled} from 'grainjs';
import {Computed, Disposable, dom, makeTestId} from 'grainjs';
import {basicButtonLink} from 'app/client/ui2018/buttons';
import {
cssButtonIconAndText,
cssButtonText,
cssOptInButton,
cssOptInOutMessage,
cssOptOutButton,
cssParagraph,
cssSection,
cssSpinnerBox,
cssSponsorButton,
} from 'app/client/ui/AdminTogglesCss';


const testId = makeTestId('test-support-grist-page-');

Expand Down Expand Up @@ -164,45 +175,3 @@ function gristCoreLink() {
{href: commonUrls.githubGristCore, target: '_blank'},
);
}

const cssSection = styled('div', ``);

const cssParagraph = styled('div', `
color: ${theme.text};
font-size: 14px;
line-height: 20px;
margin-bottom: 12px;
`);

const cssOptInOutMessage = styled(cssParagraph, `
line-height: 40px;
font-weight: 600;
margin-top: 24px;
margin-bottom: 0px;
`);

const cssOptInButton = styled(bigPrimaryButton, `
margin-top: 24px;
`);

const cssOptOutButton = styled(bigBasicButton, `
margin-top: 24px;
`);

const cssSponsorButton = styled(bigBasicButtonLink, `
margin-top: 24px;
`);

const cssButtonIconAndText = styled('div', `
display: flex;
align-items: center;
`);

const cssButtonText = styled('span', `
margin-left: 8px;
`);

const cssSpinnerBox = styled('div', `
margin-top: 24px;
text-align: center;
`);
88 changes: 88 additions & 0 deletions app/client/ui/ToggleEnterprisePage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import {makeT} from 'app/client/lib/localization';
import {Computed, Disposable, dom, makeTestId} from "grainjs";
import {AppModel} from "app/client/models/AppModel";
import {loadingSpinner} from 'app/client/ui2018/loaders';
import {commonUrls} from "app/common/gristUrls";
import {cssLink} from 'app/client/ui2018/links';
import {ToggleEnterpriseModel} from 'app/client/models/ToggleEnterpriseModel';
import {
cssOptInButton,
cssOptInOutMessage,
cssOptOutButton,
cssParagraph,
cssSection,
cssSpinnerBox
} from 'app/client/ui/AdminTogglesCss';


const t = makeT('ToggleEnterprsiePage');
const testId = makeTestId('test-toggle-enterprise-page-');

export class ToggleEnterprisePage extends Disposable {
private readonly _model: ToggleEnterpriseModel = new ToggleEnterpriseModel(this._appModel);
private readonly _isEnterprise = Computed.create(this, this._model.edition, (_use, edition) => {
return edition === 'enterprise';
})
.onWrite(async (enabled) => {
await this._model.updateEnterpriseToggle(enabled ? 'enterprise' : 'core');
});
constructor(private _appModel: AppModel) {
super();
this._model.fetchEnterpriseToggle().catch(reportError);
}

public getEnterpriseToggleObservable() {
return this._isEnterprise;
}

public buildEnterpriseSection() {
return cssSection(
dom.domComputed(this._isEnterprise, isEnterprise => {
if (isEnterprise === null) {
return cssSpinnerBox(loadingSpinner());
}
return [
cssParagraph(
t('Activation keys are used to run Grist Enterprise after a trial period ' +
'of 30 days has expired. Get an activation key by signing up for Grist ' +
'Enterprise. You do not need an activation key to run Grist Core.')
),
cssParagraph(t('Learn more in our {{link}}.', {
link: enterpriseHelpCenterLink(),
})),
this._buildEnterpriseSectionButtons(),
];
}),
testId('enterprise-opt-in-section'),
);
}

public _buildEnterpriseSectionButtons() {
return dom.domComputed(this._isEnterprise, (enterpriseEnabled) => {
if (enterpriseEnabled) {
return [
cssOptInOutMessage(
t('Grist Enterprise Edition is enabled.'),
testId('enterprise-opt-out-message'),
),
cssOptOutButton(t('Disable Grist Enterprise Edition'),
dom.on('click', () => this._isEnterprise.set(false)),
),
];
} else {
return [
cssOptInButton(t('Enable Grist Enterprise Edition'),
dom.on('click', () => this._isEnterprise.set(true)),
),
];
}
});
}
}

function enterpriseHelpCenterLink() {
return cssLink(
t('Help Center'),
{href: commonUrls.helpEnterpriseOptIn, target: '_blank'},
);
}
28 changes: 28 additions & 0 deletions app/common/ConfigAPI.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { BaseAPI, IOptions } from "app/common/BaseAPI";
import {addCurrentOrgToPath} from 'app/common/urlUtils';

export interface ConfigAPI {
getValue(key: string): Promise<any>;
setValue(value: any): Promise<void>;
}

export class ConfigAPIImpl extends BaseAPI implements ConfigAPI {
constructor(private _homeUrl: string, options: IOptions = {}) {
super(options);
}

public async getValue(key: string): Promise<any> {
return (await this.requestJson(`${this._url}/api/config/${key}`, {method: 'GET'})).value;
}

public async setValue(value: any): Promise<void> {
await this.request(`${this._url}/api/config`, {
method: 'PATCH',
body: JSON.stringify(value),
});
}

private get _url(): string {
return addCurrentOrgToPath(this._homeUrl);
}
}
1 change: 1 addition & 0 deletions app/common/gristUrls.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ export const commonUrls = {
helpTryingOutChanges: "https://support.getgrist.com/copying-docs/#trying-out-changes",
helpCustomWidgets: "https://support.getgrist.com/widget-custom",
helpTelemetryLimited: "https://support.getgrist.com/telemetry-limited",
helpEnterpriseOptIn: "https://support.getgrist.com/self-managed/#how-do-i-activate-grist-enterprise",
helpCalendarWidget: "https://support.getgrist.com/widget-calendar",
helpLinkKeys: "https://support.getgrist.com/examples/2021-04-link-keys",
helpFilteringReferenceChoices: "https://support.getgrist.com/col-refs/#filtering-reference-choices-in-dropdown",
Expand Down
35 changes: 35 additions & 0 deletions app/server/lib/ConfigBackendAPI.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import * as express from 'express';
import {expressWrap} from 'app/server/lib/expressWrap';

import {IGristCoreConfig, loadGristCoreConfigFile} from 'app/server/lib/configCore';
import log from "app/server/lib/log";

export class ConfigBackendAPI {
private _config: IGristCoreConfig;
public async init () {
this._config = await loadGristCoreConfigFile();
}

public addEndpoints(app: express.Express, requireMiddleware: express.RequestHandler) {
app.get('/api/config/:key', requireMiddleware, expressWrap(async (req, resp) => {
log.debug('config: requesting configuration', req.params);

// Only one key is valid for now
if (req.params.key === 'edition') {
resp.send({value: this._config.edition.get()});
} else {
resp.status(404).send('Configuration key not found.');
}
}));

app.patch('/api/config', requireMiddleware, expressWrap(async (req, resp) => {
log.debug('config: received new configuration item', req.body);
if(req.body.edition !== undefined) {
await this._config.edition.set(req.body.edition);
resp.sendStatus(200);
} else {
resp.status(400).send('Invalid configuration key');
}
}));
}
}
10 changes: 10 additions & 0 deletions app/server/lib/FlexServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ import fetch from 'node-fetch';
import * as path from 'path';
import * as serveStatic from "serve-static";
import {IGristCoreConfig} from "./configCore";
import {ConfigBackendAPI} from "app/server/lib/ConfigBackendAPI";

// Health checks are a little noisy in the logs, so we don't show them all.
// We show the first N health checks:
Expand Down Expand Up @@ -1943,6 +1944,15 @@ export class FlexServer implements GristServer {
}));
}

public async addConfigEndpoints() {
// Need to be an admin to change the Grist config
const requireInstallAdmin = this.getInstallAdmin().getMiddlewareRequireAdmin();

const configBackendAPI = new ConfigBackendAPI();
await configBackendAPI.init();
configBackendAPI.addEndpoints(this.app, requireInstallAdmin);
}

// Get the HTML template sent for document pages.
public async getDocTemplate(): Promise<DocTemplate> {
const page = await fse.readFile(path.join(getAppPathTo(this.appRoot, 'static'),
Expand Down
1 change: 1 addition & 0 deletions app/server/mergedServerMain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,7 @@ export async function main(port: number, serverTypes: ServerType[],
server.addLogEndpoint();
server.addGoogleAuthEndpoint();
server.addInstallEndpoints();
await server.addConfigEndpoints();
}

if (includeDocs) {
Expand Down

0 comments on commit 3139328

Please sign in to comment.