mirror of
https://github.com/gristlabs/grist-core.git
synced 2026-03-02 04:09:24 +00:00
(core) updates from grist-core
This commit is contained in:
29
app/client/lib/markdown.ts
Normal file
29
app/client/lib/markdown.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { sanitizeHTML } from 'app/client/ui/sanitizeHTML';
|
||||
import { BindableValue, DomElementMethod, subscribeElem } from 'grainjs';
|
||||
import { marked } from 'marked';
|
||||
|
||||
/**
|
||||
* Helper function for using Markdown in grainjs elements. It accepts
|
||||
* both plain Markdown strings, as well as methods that use an observable.
|
||||
* Example usage:
|
||||
*
|
||||
* cssSection(markdown(t(`# New Markdown Function
|
||||
*
|
||||
* We can _write_ [the usual Markdown](https://markdownguide.org) *inside*
|
||||
* a Grainjs element.`)));
|
||||
*
|
||||
* or
|
||||
*
|
||||
* cssSection(markdown(use => use(toggle) ? t('The toggle is **on**') : t('The toggle is **off**'));
|
||||
*
|
||||
* Markdown strings are easier for our translators to handle, as it's possible
|
||||
* to include all of the context around a single markdown string without
|
||||
* breaking it up into separate strings for grainjs elements.
|
||||
*/
|
||||
export function markdown(markdownObs: BindableValue<string>): DomElementMethod {
|
||||
return elem => subscribeElem(elem, markdownObs, value => setMarkdownValue(elem, value));
|
||||
}
|
||||
|
||||
function setMarkdownValue(elem: Element, markdownValue: string): void {
|
||||
elem.innerHTML = sanitizeHTML(marked(markdownValue));
|
||||
}
|
||||
44
app/client/models/ToggleEnterpriseModel.ts
Normal file
44
app/client/models/ToggleEnterpriseModel.ts
Normal file
@@ -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 {
|
||||
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> {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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';
|
||||
@@ -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.
|
||||
@@ -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;
|
||||
|
||||
@@ -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'), [
|
||||
|
||||
45
app/client/ui/AdminTogglesCss.ts
Normal file
45
app/client/ui/AdminTogglesCss.ts
Normal file
@@ -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;
|
||||
`);
|
||||
@@ -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-');
|
||||
|
||||
@@ -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
app/client/ui/ToggleEnterpriseWidget.ts
Normal file
78
app/client/ui/ToggleEnterpriseWidget.ts
Normal file
@@ -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)),
|
||||
),
|
||||
];
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -6,8 +6,9 @@
|
||||
* It can be instantiated by calling showUserManagerModal with the UserAPI and IUserManagerOptions.
|
||||
*/
|
||||
import { makeT } from 'app/client/lib/localization';
|
||||
import {commonUrls} from 'app/common/gristUrls';
|
||||
import {commonUrls, isOrgInPathOnly} from 'app/common/gristUrls';
|
||||
import {capitalizeFirstWord, isLongerThan} from 'app/common/gutil';
|
||||
import {getGristConfig} from 'app/common/urlUtils';
|
||||
import {FullUser} from 'app/common/LoginSessionAPI';
|
||||
import * as roles from 'app/common/roles';
|
||||
import {Organization, PermissionData, UserAPI} from 'app/common/UserAPI';
|
||||
@@ -816,15 +817,25 @@ const cssMemberPublicAccess = styled(cssMemberSecondary, `
|
||||
function renderTitle(resourceType: ResourceType, resource?: Resource, personal?: boolean) {
|
||||
switch (resourceType) {
|
||||
case 'organization': {
|
||||
if (personal) { return t('Your role for this team site'); }
|
||||
return [
|
||||
t('Manage members of team site'),
|
||||
!resource ? null : cssOrgName(
|
||||
`${(resource as Organization).name} (`,
|
||||
cssOrgDomain(`${(resource as Organization).domain}.getgrist.com`),
|
||||
')',
|
||||
)
|
||||
];
|
||||
if (personal) {
|
||||
return t('Your role for this team site');
|
||||
}
|
||||
|
||||
function getOrgDisplay() {
|
||||
if (!resource) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const org = resource as Organization;
|
||||
const gristConfig = getGristConfig();
|
||||
const gristHomeHost = gristConfig.homeUrl ? new URL(gristConfig.homeUrl).host : '';
|
||||
const baseDomain = gristConfig.baseDomain || gristHomeHost;
|
||||
const orgDisplay = isOrgInPathOnly() ? `${baseDomain}/o/${org.domain}` : `${org.domain}${baseDomain}`;
|
||||
|
||||
return cssOrgName(`${org.name} (`, cssOrgDomain(orgDisplay), ')');
|
||||
}
|
||||
|
||||
return [t('Manage members of team site'), getOrgDisplay()];
|
||||
}
|
||||
default: {
|
||||
return personal ?
|
||||
|
||||
31
app/common/ConfigAPI.ts
Normal file
31
app/common/ConfigAPI.ts
Normal file
@@ -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'});
|
||||
}
|
||||
|
||||
private get _url(): string {
|
||||
return addCurrentOrgToPath(this._homeUrl);
|
||||
}
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -22,6 +22,15 @@ export class Activation extends BaseEntity {
|
||||
@Column({name: 'updated_at', default: () => "CURRENT_TIMESTAMP"})
|
||||
public updatedAt: Date;
|
||||
|
||||
// When the enterprise activation was first enabled, so we know when
|
||||
// to start counting the trial date.
|
||||
//
|
||||
// Activations are created at Grist installation to track other
|
||||
// things such as prefs, but the user might not enable Enterprise
|
||||
// until later.
|
||||
@Column({name: 'enabled_at', type: nativeValues.dateTimeType, nullable: true})
|
||||
public enabledAt: Date|null;
|
||||
|
||||
public checkProperties(props: any): props is Partial<InstallProperties> {
|
||||
for (const key of Object.keys(props)) {
|
||||
if (!installPropertyKeys.includes(key)) {
|
||||
|
||||
18
app/gen-server/migration/1722529827161-Activation-Enabled.ts
Normal file
18
app/gen-server/migration/1722529827161-Activation-Enabled.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import * as sqlUtils from "app/gen-server/sqlUtils";
|
||||
import { MigrationInterface, QueryRunner, TableColumn } from "typeorm";
|
||||
|
||||
export class ActivationEnabled1722529827161 implements MigrationInterface {
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
const dbType = queryRunner.connection.driver.options.type;
|
||||
const datetime = sqlUtils.datetime(dbType);
|
||||
await queryRunner.addColumn('activations', new TableColumn({
|
||||
name: 'enabled_at',
|
||||
type: datetime,
|
||||
isNullable: true,
|
||||
}));
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.dropColumn('activations', 'enabled_at');
|
||||
}
|
||||
}
|
||||
35
app/server/lib/ConfigBackendAPI.ts
Normal file
35
app/server/lib/ConfigBackendAPI.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import * as express from 'express';
|
||||
import {expressWrap} from 'app/server/lib/expressWrap';
|
||||
|
||||
import {getGlobalConfig} from 'app/server/lib/globalConfig';
|
||||
|
||||
import log from "app/server/lib/log";
|
||||
|
||||
export class ConfigBackendAPI {
|
||||
public addEndpoints(app: express.Express, requireInstallAdmin: express.RequestHandler) {
|
||||
app.get('/api/config/:key', requireInstallAdmin, expressWrap((req, resp) => {
|
||||
log.debug('config: requesting configuration', req.params);
|
||||
|
||||
// Only one key is valid for now
|
||||
if (req.params.key === 'edition') {
|
||||
resp.send({value: getGlobalConfig().edition.get()});
|
||||
} else {
|
||||
resp.status(404).send({ error: 'Configuration key not found.' });
|
||||
}
|
||||
}));
|
||||
|
||||
app.patch('/api/config', requireInstallAdmin, expressWrap(async (req, resp) => {
|
||||
const config = req.body.config;
|
||||
log.debug('config: received new configuration item', config);
|
||||
|
||||
// Only one key is valid for now
|
||||
if(config.edition !== undefined) {
|
||||
await getGlobalConfig().edition.set(config.edition);
|
||||
|
||||
resp.send({ msg: 'ok' });
|
||||
} else {
|
||||
resp.status(400).send({ error: 'Invalid configuration key' });
|
||||
}
|
||||
}));
|
||||
}
|
||||
}
|
||||
@@ -87,7 +87,8 @@ import {AddressInfo} from 'net';
|
||||
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";
|
||||
import {IGristCoreConfig} from "app/server/lib/configCore";
|
||||
|
||||
// Health checks are a little noisy in the logs, so we don't show them all.
|
||||
// We show the first N health checks:
|
||||
@@ -1876,20 +1877,24 @@ export class FlexServer implements GristServer {
|
||||
const probes = new BootProbes(this.app, this, '/api', adminMiddleware);
|
||||
probes.addEndpoints();
|
||||
|
||||
this.app.post('/api/admin/restart', requireInstallAdmin, expressWrap(async (req, resp) => {
|
||||
const newConfig = req.body.newConfig;
|
||||
this.app.post('/api/admin/restart', requireInstallAdmin, expressWrap(async (_, resp) => {
|
||||
resp.on('finish', () => {
|
||||
// If we have IPC with parent process (e.g. when running under
|
||||
// Docker) tell the parent that we have a new environment so it
|
||||
// can restart us.
|
||||
if (process.send) {
|
||||
process.send({ action: 'restart', newConfig });
|
||||
process.send({ action: 'restart' });
|
||||
}
|
||||
});
|
||||
// On the topic of http response codes, thus spake MDN:
|
||||
// "409: This response is sent when a request conflicts with the current state of the server."
|
||||
const status = process.send ? 200 : 409;
|
||||
return resp.status(status).send();
|
||||
|
||||
if(!process.env.GRIST_RUNNING_UNDER_SUPERVISOR) {
|
||||
// On the topic of http response codes, thus spake MDN:
|
||||
// "409: This response is sent when a request conflicts with the current state of the server."
|
||||
return resp.status(409).send({
|
||||
error: "Cannot automatically restart the Grist server to enact changes. Please restart server manually."
|
||||
});
|
||||
}
|
||||
return resp.status(200).send({ msg: 'ok' });
|
||||
}));
|
||||
|
||||
// Restrict this endpoint to install admins
|
||||
@@ -1948,6 +1953,14 @@ export class FlexServer implements GristServer {
|
||||
}));
|
||||
}
|
||||
|
||||
public addConfigEndpoints() {
|
||||
// Need to be an admin to change the Grist config
|
||||
const requireInstallAdmin = this.getInstallAdmin().getMiddlewareRequireAdmin();
|
||||
|
||||
const configBackendAPI = new ConfigBackendAPI();
|
||||
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'),
|
||||
|
||||
@@ -98,7 +98,7 @@ export class FileConfig<FileContents> {
|
||||
}
|
||||
|
||||
public async persistToDisk() {
|
||||
await Deps.writeFile(this._filePath, JSON.stringify(this._rawConfig, null, 2));
|
||||
await Deps.writeFile(this._filePath, JSON.stringify(this._rawConfig, null, 2) + "\n");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,8 @@ import {
|
||||
fileConfigAccessorFactory,
|
||||
IWritableConfigValue
|
||||
} from "./config";
|
||||
import { convertToCoreFileContents, IGristCoreConfigFileLatest } from "./configCoreFileFormats";
|
||||
import {convertToCoreFileContents, IGristCoreConfigFileLatest} from "./configCoreFileFormats";
|
||||
import {isAffirmative} from 'app/common/gutil';
|
||||
|
||||
export type Edition = "core" | "enterprise";
|
||||
|
||||
@@ -23,6 +24,9 @@ export function loadGristCoreConfigFile(configPath?: string): IGristCoreConfig {
|
||||
export function loadGristCoreConfig(fileConfig?: FileConfig<IGristCoreConfigFileLatest>): IGristCoreConfig {
|
||||
const fileConfigValue = fileConfigAccessorFactory(fileConfig);
|
||||
return {
|
||||
edition: createConfigValue<Edition>("core", fileConfigValue("edition"))
|
||||
edition: createConfigValue<Edition>(
|
||||
isAffirmative(process.env.TEST_ENABLE_ACTIVATION) ? "enterprise" : "core",
|
||||
fileConfigValue("edition")
|
||||
)
|
||||
};
|
||||
}
|
||||
|
||||
@@ -165,6 +165,7 @@ export async function main(port: number, serverTypes: ServerType[],
|
||||
server.addLogEndpoint();
|
||||
server.addGoogleAuthEndpoint();
|
||||
server.addInstallEndpoints();
|
||||
server.addConfigEndpoints();
|
||||
}
|
||||
|
||||
if (includeDocs) {
|
||||
|
||||
Reference in New Issue
Block a user