Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add new admin toggle for turning Enterprise on and off #1095

Merged
merged 13 commits into from
Jul 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,13 @@
## docker buildx build -t ... --build-context=ext=<path> .
## The code in <path> will then be built along with the rest of Grist.
################################################################################
FROM scratch as ext
FROM scratch AS ext

################################################################################
## Javascript build stage
################################################################################

FROM node:18-buster as builder
FROM node:18-buster AS builder

# Install all node dependencies.
WORKDIR /grist
Expand Down Expand Up @@ -46,7 +46,7 @@ RUN \
################################################################################

# Fetch python3.11 and python2.7
FROM python:3.11-slim-buster as collector
FROM python:3.11-slim-buster AS collector

# Install all python dependencies.
ADD sandbox/requirements.txt requirements.txt
Expand All @@ -66,7 +66,7 @@ RUN \
# Fetch gvisor-based sandbox. Note, to enable it to run within default
# unprivileged docker, layers of protection that require privilege have
# been stripped away, see https://github.com/google/gvisor/issues/4371
FROM docker.io/gristlabs/gvisor-unprivileged:buster as sandbox
FROM docker.io/gristlabs/gvisor-unprivileged:buster AS sandbox

################################################################################
## Run-time stage
Expand Down
11 changes: 11 additions & 0 deletions app/client/lib/markdown.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { sanitizeHTML } from 'app/client/ui/sanitizeHTML';
import { BindableValue, DomElementMethod, subscribeElem } from 'grainjs';
import { marked } from 'marked';

export function markdown(markdownObs: BindableValue<string>): DomElementMethod {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some comments around purpose would be useful for these two methods.

return elem => subscribeElem(elem, markdownObs, value => setMarkdownValue(elem, value));
}

function setMarkdownValue(elem: Element, markdownValue: string): void {
elem.innerHTML = sanitizeHTML(marked(markdownValue));
}
44 changes: 44 additions & 0 deletions app/client/models/ToggleEnterpriseModel.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import {getHomeUrl} from 'app/client/models/AppModel';
import {Disposable, Observable} from "grainjs";
import {ConfigAPI} from 'app/common/ConfigAPI';
import {delay} from 'app/common/delay';

export class ToggleEnterpriseModel extends Disposable {
jordigh marked this conversation as resolved.
Show resolved Hide resolved
public readonly edition: Observable<string | null> = Observable.create(this, null);
private readonly _configAPI: ConfigAPI = new ConfigAPI(getHomeUrl());

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

public async updateEnterpriseToggle(edition: string): Promise<void> {
// We may be restarting the server, so these requests may well
// fail if done in quick succession.
await retryOnNetworkError(() => this._configAPI.setValue({edition}));
this.edition.set(edition);
await retryOnNetworkError(() => this._configAPI.restartServer());
}
}

// Copied from DocPageModel.ts
const reconnectIntervals = [1000, 1000, 2000, 5000, 10000];
async function retryOnNetworkError<R>(func: () => Promise<R>): Promise<R> {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not great to have copies of retry code that get independent fixes, but also ok given schedule.

Hmm might be a bit weird if multiple requests overlap, there's no cancellation.

It is acceptable, given what it is.

for (let attempt = 0; ; attempt++) {
try {
return await func();
} catch (err) {
// fetch() promises that network errors are reported as TypeError. We'll accept NetworkError too.
if (err.name !== "TypeError" && err.name !== "NetworkError") {
throw err;
}
// We really can't reach the server. Make it known.
if (attempt >= reconnectIntervals.length) {
throw err;
}
const reconnectTimeout = reconnectIntervals[attempt];
console.warn(`Call to ${func.name} failed, will retry in ${reconnectTimeout} ms`, err);
await delay(reconnectTimeout);
}
}
}
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 {ToggleEnterpriseWidget} from 'app/client/ui/ToggleEnterpriseWidget';
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 = ToggleEnterpriseWidget.create(this);
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'),
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;
`);
58 changes: 13 additions & 45 deletions app/client/ui/SupportGristPage.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,24 @@
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 {
cssButtonIconAndText,
cssButtonText,
cssOptInButton,
cssOptInOutMessage,
cssOptOutButton,
cssParagraph,
cssSection,
cssSpinnerBox,
cssSponsorButton,
} from 'app/client/ui/AdminTogglesCss';
import {basicButtonLink} from 'app/client/ui2018/buttons';
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';

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

Expand Down Expand Up @@ -164,45 +174,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;
`);
78 changes: 78 additions & 0 deletions app/client/ui/ToggleEnterpriseWidget.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import {makeT} from 'app/client/lib/localization';
import {markdown} from 'app/client/lib/markdown';
import {Computed, Disposable, dom, makeTestId} from "grainjs";
import {commonUrls} from "app/common/gristUrls";
import {ToggleEnterpriseModel} from 'app/client/models/ToggleEnterpriseModel';
import {
cssOptInButton,
cssOptOutButton,
cssParagraph,
cssSection,
} from 'app/client/ui/AdminTogglesCss';


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

export class ToggleEnterpriseWidget extends Disposable {
private readonly _model: ToggleEnterpriseModel = new ToggleEnterpriseModel();
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() {
super();
this._model.fetchEnterpriseToggle().catch(reportError);
}

public getEnterpriseToggleObservable() {
return this._isEnterprise;
}

public buildEnterpriseSection() {
return cssSection(
dom.domComputed(this._isEnterprise, (enterpriseEnabled) => {
return [
enterpriseEnabled ?
cssParagraph(
markdown(t('Grist Enterprise is **enabled**.')),
testId('enterprise-opt-out-message'),
) : null,
cssParagraph(
markdown(t(`An activation key is used to run Grist Enterprise after a trial period
of 30 days has expired. Get an activation key by [signing up for Grist
Enterprise]({{signupLink}}). You do not need an activation key to run
Grist Core.

Learn more in our [Help Center]({{helpCenter}}).`, {
signupLink: commonUrls.plans,
helpCenter: commonUrls.helpEnterpriseOptIn
}))
),
this._buildEnterpriseSectionButtons(),
];
}),
testId('enterprise-opt-in-section'),
);
}

public _buildEnterpriseSectionButtons() {
return dom.domComputed(this._isEnterprise, (enterpriseEnabled) => {
if (enterpriseEnabled) {
return [
cssOptOutButton(t('Disable Grist Enterprise'),
dom.on('click', () => this._isEnterprise.set(false)),
),
];
} else {
return [
cssOptInButton(t('Enable Grist Enterprise'),
dom.on('click', () => this._isEnterprise.set(true)),
),
];
}
});
}
}
31 changes: 31 additions & 0 deletions app/common/ConfigAPI.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import {BaseAPI, IOptions} from "app/common/BaseAPI";
import {addCurrentOrgToPath} from 'app/common/urlUtils';

/**
* An API for accessing the internal Grist configuration, stored in
* config.json.
*/
export class ConfigAPI extends BaseAPI {
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, restart=false): Promise<void> {
await this.request(`${this._url}/api/config`, {
method: 'PATCH',
body: JSON.stringify({config: value, restart}),
});
}

public async restartServer(): Promise<void> {
await this.request(`${this._url}/api/admin/restart`, {method: 'POST'});
jordigh marked this conversation as resolved.
Show resolved Hide resolved
}

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
Loading
Loading