(core) updates from grist-core

This commit is contained in:
Paul Fitzpatrick
2024-08-07 14:06:59 -04:00
34 changed files with 986 additions and 224 deletions

View 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));
}

View 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);
}
}
}

View File

@@ -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'), [

View 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;
`);

View File

@@ -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;
`);

View 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)),
),
];
}
});
}
}

View File

@@ -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 ?