mirror of
https://github.com/gristlabs/grist-core.git
synced 2026-03-02 04:09:24 +00:00
reconcile boot and admin pages further (#963)
This adds some remaining parts of the boot page to the admin panel, and then removes the boot page.
This commit is contained in:
@@ -1,160 +0,0 @@
|
||||
import { AppModel } from 'app/client/models/AppModel';
|
||||
import { AdminChecks, ProbeDetails } from 'app/client/models/AdminChecks';
|
||||
import { createAppPage } from 'app/client/ui/createAppPage';
|
||||
import { pagePanels } from 'app/client/ui/PagePanels';
|
||||
import { BootProbeInfo, BootProbeResult } from 'app/common/BootProbe';
|
||||
import { getGristConfig } from 'app/common/urlUtils';
|
||||
import { Disposable, dom, Observable, styled, UseCBOwner } from 'grainjs';
|
||||
|
||||
const cssBody = styled('div', `
|
||||
padding: 20px;
|
||||
overflow: auto;
|
||||
`);
|
||||
|
||||
const cssHeader = styled('div', `
|
||||
padding: 20px;
|
||||
`);
|
||||
|
||||
const cssResult = styled('div', `
|
||||
max-width: 500px;
|
||||
`);
|
||||
|
||||
/**
|
||||
*
|
||||
* A "boot" page for inspecting the state of the Grist installation.
|
||||
*
|
||||
* TODO: deferring using any localization machinery so as not
|
||||
* to have to worry about its failure modes yet, but it should be
|
||||
* fine as long as assets served locally are used.
|
||||
*
|
||||
*/
|
||||
export class Boot extends Disposable {
|
||||
|
||||
private _checks: AdminChecks;
|
||||
|
||||
constructor(_appModel: AppModel) {
|
||||
super();
|
||||
// Setting title in constructor seems to be how we are doing this,
|
||||
// based on other similar pages.
|
||||
document.title = 'Booting Grist';
|
||||
this._checks = new AdminChecks(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set up the page. Uses the generic Grist layout with an empty
|
||||
* side panel, just for convenience. Could be made a lot prettier.
|
||||
*/
|
||||
public buildDom() {
|
||||
this._checks.fetchAvailableChecks().catch(e => reportError(e));
|
||||
|
||||
const config = getGristConfig();
|
||||
const errMessage = config.errMessage;
|
||||
const rootNode = dom('div',
|
||||
dom.domComputed(
|
||||
use => {
|
||||
return pagePanels({
|
||||
leftPanel: {
|
||||
panelWidth: Observable.create(this, 240),
|
||||
panelOpen: Observable.create(this, false),
|
||||
hideOpener: true,
|
||||
header: null,
|
||||
content: null,
|
||||
},
|
||||
headerMain: cssHeader(dom('h1', 'Grist Boot')),
|
||||
contentMain: this.buildBody(use, {errMessage}),
|
||||
});
|
||||
}
|
||||
),
|
||||
);
|
||||
return rootNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* The body of the page is very simple right now, basically a
|
||||
* placeholder. Make a section for each probe, and kick them off in
|
||||
* parallel, showing results as they come in.
|
||||
*/
|
||||
public buildBody(use: UseCBOwner, options: {errMessage?: string}) {
|
||||
if (options.errMessage) {
|
||||
return cssBody(cssResult(this.buildError()));
|
||||
}
|
||||
return cssBody([
|
||||
...use(this._checks.probes).map(probe => {
|
||||
const req = this._checks.requestCheck(probe);
|
||||
return cssResult(
|
||||
this.buildResult(req.probe, use(req.result), req.details));
|
||||
}),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* This is used when there is an attempt to access the boot page
|
||||
* but something isn't right - either the page isn't enabled, or
|
||||
* the key in the URL is wrong. Give the user some information about
|
||||
* how to set things up.
|
||||
*/
|
||||
public buildError() {
|
||||
return dom(
|
||||
'div',
|
||||
dom('p',
|
||||
'A diagnostics page can be made available at:',
|
||||
dom('blockquote', '/boot/GRIST_BOOT_KEY'),
|
||||
'GRIST_BOOT_KEY is an environment variable ',
|
||||
' set before Grist starts. It should only',
|
||||
' contain characters that are valid in a URL.',
|
||||
' It should be a secret, since no authentication is needed',
|
||||
' to visit the diagnostics page.'),
|
||||
dom('p',
|
||||
'You are seeing this page because either the key is not set,',
|
||||
' or it is not in the URL.'),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* An ugly rendering of information returned by the probe.
|
||||
*/
|
||||
public buildResult(info: BootProbeInfo, result: BootProbeResult,
|
||||
details: ProbeDetails|undefined) {
|
||||
const out: (HTMLElement|string|null)[] = [];
|
||||
out.push(dom('h2', info.name));
|
||||
if (details) {
|
||||
out.push(dom('p', '> ', details.info));
|
||||
}
|
||||
if (result.verdict) {
|
||||
out.push(dom('pre', result.verdict));
|
||||
}
|
||||
if (result.success !== undefined) {
|
||||
out.push(result.success ? '✅' : '❌');
|
||||
}
|
||||
if (result.done === true) {
|
||||
out.push(dom('p', 'no fault detected'));
|
||||
}
|
||||
if (result.details) {
|
||||
for (const [key, val] of Object.entries(result.details)) {
|
||||
out.push(dom(
|
||||
'div',
|
||||
cssLabel(key),
|
||||
dom('input', dom.prop('value', JSON.stringify(val)))));
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a stripped down page to show boot information.
|
||||
* Make sure the API isn't used since it may well be unreachable
|
||||
* due to a misconfiguration, especially in multi-server setups.
|
||||
*/
|
||||
createAppPage(appModel => {
|
||||
return dom.create(Boot, appModel);
|
||||
}, {
|
||||
useApi: false,
|
||||
});
|
||||
|
||||
export const cssLabel = styled('div', `
|
||||
display: inline-block;
|
||||
min-width: 100px;
|
||||
text-align: right;
|
||||
padding-right: 5px;
|
||||
`);
|
||||
@@ -1,5 +1,6 @@
|
||||
import { reportError } from 'app/client/models/errors';
|
||||
import { BootProbeIds, BootProbeInfo, BootProbeResult } from 'app/common/BootProbe';
|
||||
import { removeTrailingSlash } from 'app/common/gutil';
|
||||
import { InstallAPI } from 'app/common/InstallAPI';
|
||||
import { getGristConfig } from 'app/common/urlUtils';
|
||||
import { Disposable, Observable, UseCBOwner } from 'grainjs';
|
||||
|
||||
@@ -19,7 +20,7 @@ export class AdminChecks {
|
||||
// Keep track of probe results we have received, by probe ID.
|
||||
private _results: Map<string, Observable<BootProbeResult>>;
|
||||
|
||||
constructor(private _parent: Disposable) {
|
||||
constructor(private _parent: Disposable, private _installAPI: InstallAPI) {
|
||||
this.probes = Observable.create(_parent, []);
|
||||
this._results = new Map();
|
||||
this._requests = new Map();
|
||||
@@ -32,18 +33,16 @@ export class AdminChecks {
|
||||
const config = getGristConfig();
|
||||
const errMessage = config.errMessage;
|
||||
if (!errMessage) {
|
||||
// Probe tool URLs are relative to the current URL. Don't trust configuration,
|
||||
// because it may be buggy if the user is here looking at the boot page
|
||||
// to figure out some problem.
|
||||
//
|
||||
// We have been careful to make URLs available with appropriate
|
||||
// middleware relative to both of the admin panel and the boot page.
|
||||
const url = new URL(removeTrailingSlash(document.location.href));
|
||||
url.pathname += '/probe';
|
||||
const resp = await fetch(url.href);
|
||||
const _probes = await resp.json();
|
||||
this.probes.set(_probes.probes);
|
||||
const _probes = await this._installAPI.getChecks().catch(reportError);
|
||||
if (!this._parent.isDisposed()) {
|
||||
// Currently, probes are forbidden if not admin.
|
||||
// TODO: May want to relax this to allow some probes that help
|
||||
// diagnose some initial auth problems.
|
||||
this.probes.set(_probes ? _probes.probes : []);
|
||||
}
|
||||
return _probes;
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -54,12 +53,12 @@ export class AdminChecks {
|
||||
const {id} = probe;
|
||||
let result = this._results.get(id);
|
||||
if (!result) {
|
||||
result = Observable.create(this._parent, {});
|
||||
result = Observable.create(this._parent, {status: 'none'});
|
||||
this._results.set(id, result);
|
||||
}
|
||||
let request = this._requests.get(id);
|
||||
if (!request) {
|
||||
request = new AdminCheckRunner(id, this._results, this._parent);
|
||||
request = new AdminCheckRunner(this._installAPI, id, this._results, this._parent);
|
||||
this._requests.set(id, request);
|
||||
}
|
||||
request.start();
|
||||
@@ -93,15 +92,15 @@ export interface AdminCheckRequest {
|
||||
* Manage a single check.
|
||||
*/
|
||||
export class AdminCheckRunner {
|
||||
constructor(public id: string, public results: Map<string, Observable<BootProbeResult>>,
|
||||
constructor(private _installAPI: InstallAPI,
|
||||
public id: string,
|
||||
public results: Map<string, Observable<BootProbeResult>>,
|
||||
public parent: Disposable) {
|
||||
const url = new URL(removeTrailingSlash(document.location.href));
|
||||
url.pathname = url.pathname + '/probe/' + id;
|
||||
fetch(url.href).then(async resp => {
|
||||
const _probes: BootProbeResult = await resp.json();
|
||||
this._installAPI.runCheck(id).then(result => {
|
||||
if (parent.isDisposed()) { return; }
|
||||
const ob = results.get(id);
|
||||
if (ob) {
|
||||
ob.set(_probes);
|
||||
ob.set(result);
|
||||
}
|
||||
}).catch(e => console.error(e));
|
||||
}
|
||||
@@ -109,7 +108,7 @@ export class AdminCheckRunner {
|
||||
public start() {
|
||||
let result = this.results.get(this.id);
|
||||
if (!result) {
|
||||
result = Observable.create(this.parent, {});
|
||||
result = Observable.create(this.parent, {status: 'none'});
|
||||
this.results.set(this.id, result);
|
||||
}
|
||||
}
|
||||
@@ -120,7 +119,7 @@ export class AdminCheckRunner {
|
||||
* but it can be useful to show extra details and tips in the
|
||||
* client.
|
||||
*/
|
||||
const probeDetails: Record<string, ProbeDetails> = {
|
||||
export const probeDetails: Record<string, ProbeDetails> = {
|
||||
'boot-page': {
|
||||
info: `
|
||||
This boot page should not be too easy to access. Either turn
|
||||
@@ -144,6 +143,17 @@ is set.
|
||||
`,
|
||||
},
|
||||
|
||||
'sandboxing': {
|
||||
info: `
|
||||
Grist allows for very powerful formulas, using Python.
|
||||
We recommend setting the environment variable
|
||||
GRIST_SANDBOX_FLAVOR to gvisor if your hardware
|
||||
supports it (most will), to run formulas in each document
|
||||
within a sandbox isolated from other documents and isolated
|
||||
from the network.
|
||||
`
|
||||
},
|
||||
|
||||
'system-user': {
|
||||
info: `
|
||||
It is good practice not to run Grist as the root user.
|
||||
@@ -153,6 +163,15 @@ It is good practice not to run Grist as the root user.
|
||||
'reachable': {
|
||||
info: `
|
||||
The main page of Grist should be available.
|
||||
`
|
||||
},
|
||||
|
||||
'websockets': {
|
||||
// TODO: add a link to https://support.getgrist.com/self-managed/#how-do-i-run-grist-on-a-server
|
||||
info: `
|
||||
Websocket connections need HTTP 1.1 and the ability to pass a few
|
||||
extra headers in order to work. Sometimes a reverse proxy can
|
||||
interfere with these requirements.
|
||||
`
|
||||
},
|
||||
};
|
||||
|
||||
@@ -531,10 +531,52 @@ export function getOrgNameOrGuest(org: Organization|null, user: FullUser|null) {
|
||||
return getOrgName(org);
|
||||
}
|
||||
|
||||
export function getHomeUrl(): string {
|
||||
/**
|
||||
* If we don't know what the home URL is, the top level of the site
|
||||
* we are on may work. This should always work for single-server installs
|
||||
* that don't encode organization information in domains. Even for other
|
||||
* cases, this should be a good enough home URL for many purposes, it
|
||||
* just may still have some organization information encoded in it from
|
||||
* the domain that could influence results that might be supposed to be
|
||||
* organization-neutral.
|
||||
*/
|
||||
export function getFallbackHomeUrl(): string {
|
||||
const {host, protocol} = window.location;
|
||||
return `${protocol}//${host}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the official home URL sent to us from the back end.
|
||||
*/
|
||||
export function getConfiguredHomeUrl(): string {
|
||||
const gristConfig: any = (window as any).gristConfig;
|
||||
return (gristConfig && gristConfig.homeUrl) || `${protocol}//${host}`;
|
||||
return (gristConfig && gristConfig.homeUrl) || getFallbackHomeUrl();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the home URL, using fallback if on admin page rather
|
||||
* than trusting back end configuration.
|
||||
*/
|
||||
export function getPreferredHomeUrl(): string|undefined {
|
||||
const gristUrl = urlState().state.get();
|
||||
if (gristUrl.adminPanel) {
|
||||
// On the admin panel, we should not trust configuration much,
|
||||
// since we want the user to be able to access it to diagnose
|
||||
// problems with configuration. So we access the API via the
|
||||
// site we happen to be on rather than anything configured on
|
||||
// the back end. Couldn't we just always do this? Maybe!
|
||||
// It could require adjustments for calls that are meant
|
||||
// to be site-neutral if the domain has an org encoded in it.
|
||||
// But that's a small price to pay. Grist Labs uses a setup
|
||||
// where api calls go to a dedicated domain distinct from all
|
||||
// other sites, but there's no particular advantage to it.
|
||||
return getFallbackHomeUrl();
|
||||
}
|
||||
return getConfiguredHomeUrl();
|
||||
}
|
||||
|
||||
export function getHomeUrl(): string {
|
||||
return getPreferredHomeUrl() || getConfiguredHomeUrl();
|
||||
}
|
||||
|
||||
export function newUserAPIImpl(): UserAPIImpl {
|
||||
|
||||
@@ -2,8 +2,8 @@ import {buildHomeBanners} from 'app/client/components/Banners';
|
||||
import {makeT} from 'app/client/lib/localization';
|
||||
import {localStorageJsonObs} from 'app/client/lib/localStorageObs';
|
||||
import {getTimeFromNow} from 'app/client/lib/timeUtils';
|
||||
import {AdminChecks, probeDetails, ProbeDetails} from 'app/client/models/AdminChecks';
|
||||
import {AppModel, getHomeUrl, reportError} from 'app/client/models/AppModel';
|
||||
import {AdminChecks} from 'app/client/models/AdminChecks';
|
||||
import {urlState} from 'app/client/models/gristUrlState';
|
||||
import {AppHeader} from 'app/client/ui/AppHeader';
|
||||
import {leftPanelBasic} from 'app/client/ui/LeftPanelCommon';
|
||||
@@ -15,7 +15,7 @@ import {basicButton} from 'app/client/ui2018/buttons';
|
||||
import {toggle} from 'app/client/ui2018/checkbox';
|
||||
import {mediaSmall, testId, theme, vars} from 'app/client/ui2018/cssVars';
|
||||
import {cssLink, makeLinks} from 'app/client/ui2018/links';
|
||||
import {SandboxingBootProbeDetails} from 'app/common/BootProbe';
|
||||
import {BootProbeInfo, BootProbeResult, SandboxingBootProbeDetails} from 'app/common/BootProbe';
|
||||
import {commonUrls, getPageTitleSuffix} from 'app/common/gristUrls';
|
||||
import {InstallAPI, InstallAPIImpl, LatestVersion} from 'app/common/InstallAPI';
|
||||
import {naturalCompare} from 'app/common/SortFunc';
|
||||
@@ -41,7 +41,7 @@ export class AdminPanel extends Disposable {
|
||||
constructor(private _appModel: AppModel) {
|
||||
super();
|
||||
document.title = getAdminPanelName() + getPageTitleSuffix(getGristConfig());
|
||||
this._checks = new AdminChecks(this);
|
||||
this._checks = new AdminChecks(this, this._installAPI);
|
||||
}
|
||||
|
||||
public buildDom() {
|
||||
@@ -78,9 +78,42 @@ export class AdminPanel extends Disposable {
|
||||
}
|
||||
|
||||
private _buildMainContent(owner: MultiHolder) {
|
||||
// If probes are available, show the panel as normal.
|
||||
// Otherwise say it is unavailable, and describe a fallback
|
||||
// mechanism for access.
|
||||
return cssPageContainer(
|
||||
dom.cls('clipboard'),
|
||||
{tabIndex: "-1"},
|
||||
dom.maybe(this._checks.probes, probes => {
|
||||
return probes.length > 0
|
||||
? this._buildMainContentForAdmin(owner)
|
||||
: this._buildMainContentForOthers(owner);
|
||||
}),
|
||||
testId('admin-panel'),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Show something helpful to those without access to the panel,
|
||||
* which could include a legit adminstrator if auth is misconfigured.
|
||||
*/
|
||||
private _buildMainContentForOthers(owner: MultiHolder) {
|
||||
const exampleKey = 'example-' + window.crypto.randomUUID();
|
||||
return dom.create(AdminSection, t('Administrator Panel Unavailable'), [
|
||||
dom('p', t(`You do not have access to the administrator panel.
|
||||
Please log in as an administrator.`)),
|
||||
dom(
|
||||
'p',
|
||||
t(`Or, as a fallback, you can set: {{bootKey}} in the environment and visit: {{url}}`, {
|
||||
bootKey: dom('pre', `GRIST_BOOT_KEY=${exampleKey}`),
|
||||
url: dom('pre', `/admin?boot-key=${exampleKey}`)
|
||||
}),
|
||||
),
|
||||
]);
|
||||
}
|
||||
|
||||
private _buildMainContentForAdmin(owner: MultiHolder) {
|
||||
return [
|
||||
dom.create(AdminSection, t('Support Grist'), [
|
||||
dom.create(AdminSectionItem, {
|
||||
id: 'telemetry',
|
||||
@@ -113,7 +146,6 @@ export class AdminPanel extends Disposable {
|
||||
expandedContent: this._buildAuthenticationNotice(owner),
|
||||
})
|
||||
]),
|
||||
|
||||
dom.create(AdminSection, t('Version'), [
|
||||
dom.create(AdminSectionItem, {
|
||||
id: 'version',
|
||||
@@ -123,8 +155,23 @@ export class AdminPanel extends Disposable {
|
||||
}),
|
||||
this._buildUpdates(owner),
|
||||
]),
|
||||
testId('admin-panel'),
|
||||
);
|
||||
dom.create(AdminSection, t('Self Checks'), [
|
||||
this._buildProbeItems(owner, {
|
||||
showRedundant: false,
|
||||
showNovel: true,
|
||||
}),
|
||||
dom.create(AdminSectionItem, {
|
||||
id: 'probe-other',
|
||||
name: 'more...',
|
||||
description: '',
|
||||
value: '',
|
||||
expandedContent: this._buildProbeItems(owner, {
|
||||
showRedundant: true,
|
||||
showNovel: false,
|
||||
}),
|
||||
}),
|
||||
]),
|
||||
];
|
||||
}
|
||||
|
||||
private _buildSandboxingDisplay(owner: IDisposableOwner) {
|
||||
@@ -132,7 +179,7 @@ export class AdminPanel extends Disposable {
|
||||
use => {
|
||||
const req = this._checks.requestCheckById(use, 'sandboxing');
|
||||
const result = req ? use(req.result) : undefined;
|
||||
const success = result?.success;
|
||||
const success = result?.status === 'success';
|
||||
const details = result?.details as SandboxingBootProbeDetails|undefined;
|
||||
if (!details) {
|
||||
return cssValueLabel(t('unknown'));
|
||||
@@ -150,11 +197,9 @@ export class AdminPanel extends Disposable {
|
||||
|
||||
private _buildSandboxingNotice() {
|
||||
return [
|
||||
t('Grist allows for very powerful formulas, using Python. \
|
||||
We recommend setting the environment variable GRIST_SANDBOX_FLAVOR to gvisor \
|
||||
if your hardware supports it (most will), \
|
||||
to run formulas in each document within a sandbox \
|
||||
isolated from other documents and isolated from the network.'),
|
||||
// Use AdminChecks text for sandboxing, in order not to
|
||||
// duplicate.
|
||||
probeDetails['sandboxing'].info,
|
||||
dom(
|
||||
'div',
|
||||
{style: 'margin-top: 8px'},
|
||||
@@ -172,7 +217,8 @@ isolated from other documents and isolated from the network.'),
|
||||
return cssValueLabel(cssErrorText('unavailable'));
|
||||
}
|
||||
|
||||
const { success, details } = result;
|
||||
const { status, details } = result;
|
||||
const success = status === 'success';
|
||||
const loginSystemId = details?.loginSystemId;
|
||||
|
||||
if (!success || !loginSystemId) {
|
||||
@@ -412,8 +458,112 @@ isolated from other documents and isolated from the network.'),
|
||||
)
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the results of various checks. Of the checks, some are considered
|
||||
* "redundant" (already covered elsewhere in the Admin Panel) and the
|
||||
* remainder are "novel".
|
||||
*/
|
||||
private _buildProbeItems(owner: MultiHolder, options: {
|
||||
showRedundant: boolean,
|
||||
showNovel: boolean,
|
||||
}) {
|
||||
return dom.domComputed(
|
||||
use => [
|
||||
...use(this._checks.probes).map(probe => {
|
||||
const isRedundant = probe.id === 'sandboxing';
|
||||
const show = isRedundant ? options.showRedundant : options.showNovel;
|
||||
if (!show) { return null; }
|
||||
const req = this._checks.requestCheck(probe);
|
||||
return this._buildProbeItem(owner, req.probe, use(req.result), req.details);
|
||||
}),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the result of an individual check.
|
||||
*/
|
||||
private _buildProbeItem(owner: MultiHolder,
|
||||
info: BootProbeInfo,
|
||||
result: BootProbeResult,
|
||||
details: ProbeDetails|undefined) {
|
||||
|
||||
const status = this._encodeSuccess(result);
|
||||
return dom.create(AdminSectionItem, {
|
||||
id: `probe-${info.id}`,
|
||||
name: info.id,
|
||||
description: info.name,
|
||||
value: cssStatus(status),
|
||||
expandedContent: [
|
||||
cssCheckHeader(
|
||||
t('Results'),
|
||||
{ style: 'margin-top: 0px; padding-top: 0px;' },
|
||||
),
|
||||
result.verdict ? dom('pre', result.verdict) : null,
|
||||
(result.status === 'none') ? null :
|
||||
dom('p',
|
||||
(result.status === 'success') ? t('Check succeeded.') : t('Check failed.')),
|
||||
(result.status !== 'none') ? null :
|
||||
dom('p', t('No fault detected.')),
|
||||
(details?.info === undefined) ? null : [
|
||||
cssCheckHeader(t('Notes')),
|
||||
details.info,
|
||||
],
|
||||
(result.details === undefined) ? null : [
|
||||
cssCheckHeader(t('Details')),
|
||||
...Object.entries(result.details).map(([key, val]) => {
|
||||
return dom(
|
||||
'div',
|
||||
cssLabel(key),
|
||||
dom('input', dom.prop(
|
||||
'value',
|
||||
typeof val === 'string' ? val : JSON.stringify(val))));
|
||||
}),
|
||||
],
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Give an icon summarizing success or failure. Factor in the
|
||||
* severity of the result for failures. This is crude, the
|
||||
* visualization of the results can be elaborated in future.
|
||||
*/
|
||||
private _encodeSuccess(result: BootProbeResult) {
|
||||
switch (result.status) {
|
||||
case 'success':
|
||||
return '✅';
|
||||
case 'fault':
|
||||
return '❌';
|
||||
case 'warning':
|
||||
return '❗';
|
||||
case 'hmm':
|
||||
return '?';
|
||||
case 'none':
|
||||
return '―';
|
||||
default:
|
||||
// should not arrive here
|
||||
return '??';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Ugh I'm not a front end person. h5 small-caps, sure why not.
|
||||
// Hopefully someone with taste will edit someday!
|
||||
const cssCheckHeader = styled('h5', `
|
||||
margin-bottom: 5px;
|
||||
font-variant: small-caps;
|
||||
`);
|
||||
|
||||
const cssStatus = styled('div', `
|
||||
display: inline-block;
|
||||
text-align: center;
|
||||
width: 40px;
|
||||
padding: 5px;
|
||||
`);
|
||||
|
||||
const cssPageContainer = styled('div', `
|
||||
overflow: auto;
|
||||
padding: 40px;
|
||||
@@ -491,3 +641,10 @@ export const cssDangerText = styled('div', `
|
||||
const cssHappyText = styled('span', `
|
||||
color: ${theme.controlFg};
|
||||
`);
|
||||
|
||||
export const cssLabel = styled('div', `
|
||||
display: inline-block;
|
||||
min-width: 100px;
|
||||
text-align: right;
|
||||
padding-right: 5px;
|
||||
`);
|
||||
|
||||
Reference in New Issue
Block a user