mirror of
https://github.com/gristlabs/grist-core.git
synced 2024-10-27 20:44:07 +00:00
253 lines
7.2 KiB
TypeScript
253 lines
7.2 KiB
TypeScript
|
import BaseView = require('app/client/components/BaseView');
|
||
|
import {GristDoc} from 'app/client/components/GristDoc';
|
||
|
import {ViewSectionHelper} from 'app/client/components/ViewLayout';
|
||
|
import {makeT} from 'app/client/lib/localization';
|
||
|
import {IEdit, IExternalTable, VirtualTable} from 'app/client/models/VirtualTable';
|
||
|
import {urlState} from 'app/client/models/gristUrlState';
|
||
|
import {docListHeader} from 'app/client/ui/DocMenuCss';
|
||
|
import {isNarrowScreenObs, mediaSmall} from 'app/client/ui2018/cssVars';
|
||
|
import {loadingSpinner} from 'app/client/ui2018/loaders';
|
||
|
import {FormulaTimingInfo} from 'app/common/ActiveDocAPI';
|
||
|
import {DisposableWithEvents} from 'app/common/DisposableWithEvents';
|
||
|
import {
|
||
|
DocAction,
|
||
|
getColValues,
|
||
|
TableDataAction} from 'app/common/DocActions';
|
||
|
import {VirtualId} from 'app/common/SortSpec';
|
||
|
import {not} from 'app/common/gutil';
|
||
|
import {Disposable, dom, makeTestId, Observable, styled} from 'grainjs';
|
||
|
import omit = require('lodash/omit');
|
||
|
import range = require('lodash/range');
|
||
|
|
||
|
const t = makeT('TimingPage');
|
||
|
const testId = makeTestId('test-timing-page-');
|
||
|
|
||
|
/**
|
||
|
* A list of columns for a virtual table about formula timings.
|
||
|
*/
|
||
|
const COLUMNS = [
|
||
|
{
|
||
|
id: VirtualId(),
|
||
|
colId: 'tableId',
|
||
|
type: 'Text',
|
||
|
label: t('Table ID'),
|
||
|
},
|
||
|
{
|
||
|
id: VirtualId(),
|
||
|
colId: 'colId',
|
||
|
type: 'Text',
|
||
|
label: t('Column ID'),
|
||
|
},
|
||
|
{
|
||
|
id: VirtualId(),
|
||
|
colId: 'sum',
|
||
|
type: 'Numeric',
|
||
|
label: t('Total Time (s)')
|
||
|
},
|
||
|
{
|
||
|
id: VirtualId(),
|
||
|
colId: 'calls',
|
||
|
type: 'Numeric',
|
||
|
label: t('Number of Calls')
|
||
|
},
|
||
|
{
|
||
|
id: VirtualId(),
|
||
|
colId: 'average',
|
||
|
type: 'Numeric',
|
||
|
label: t('Average Time (s)')
|
||
|
},
|
||
|
// max time
|
||
|
{
|
||
|
id: VirtualId(),
|
||
|
colId: 'max',
|
||
|
type: 'Numeric',
|
||
|
label: t('Max Time (s)')
|
||
|
},
|
||
|
] as const;
|
||
|
|
||
|
interface TimingRecord {
|
||
|
tableId: string;
|
||
|
colId: string;
|
||
|
sum: number;
|
||
|
calls: number;
|
||
|
average: number;
|
||
|
max: number;
|
||
|
}
|
||
|
|
||
|
const VIRTUAL_SECTION_ID = VirtualId();
|
||
|
const VIRTUAL_TABLE_ID = VirtualId();
|
||
|
|
||
|
/**
|
||
|
* Layout of fields in a view, with a specific ordering.
|
||
|
*/
|
||
|
const FIELDS: Array<(typeof COLUMNS)[number]['colId']> = [
|
||
|
'tableId', 'colId', 'sum', 'calls', 'average', 'max'
|
||
|
];
|
||
|
|
||
|
|
||
|
class TimingExternalTable extends Disposable implements IExternalTable {
|
||
|
public name = 'GristHidden_TimingTable';
|
||
|
public initialActions = _prepareInitialActions(this.name);
|
||
|
public saveableFields = [];
|
||
|
|
||
|
public constructor(private _initialData: FormulaTimingInfo[]) {
|
||
|
super();
|
||
|
}
|
||
|
|
||
|
public async fetchAll(): Promise<TableDataAction> {
|
||
|
const timingInfo = this._initialData;
|
||
|
console.debug('Timing info:', timingInfo);
|
||
|
const data = timingInfo || [];
|
||
|
const indicies = range(data.length).map(i => i + 1);
|
||
|
return ['TableData', this.name, indicies,
|
||
|
getColValues(indicies.map(rowId => _mapModelValues(rowId, data[rowId - 1])))];
|
||
|
}
|
||
|
|
||
|
// Not used.
|
||
|
public async beforeEdit(editor: IEdit) {}
|
||
|
public async afterEdit(editor: IEdit) {}
|
||
|
public async afterAnySchemaChange(editor: IEdit) {}
|
||
|
public async sync(editor: IEdit): Promise<void> {}
|
||
|
}
|
||
|
|
||
|
export class TimingPage extends DisposableWithEvents {
|
||
|
private _data: Observable<FormulaTimingInfo[]|null> = Observable.create(this, null);
|
||
|
|
||
|
constructor(private _gristDoc: GristDoc) {
|
||
|
super();
|
||
|
if (this._gristDoc.isTimingOn.get() === false) {
|
||
|
// Just redirect back to the settings page.
|
||
|
this._openSettings();
|
||
|
} else {
|
||
|
this._start().catch(ex => {
|
||
|
this._openSettings();
|
||
|
reportError(ex);
|
||
|
});
|
||
|
}
|
||
|
}
|
||
|
|
||
|
public buildDom() {
|
||
|
return cssContainer(
|
||
|
dom.maybe(this._data, () =>
|
||
|
dom('div', {style: 'display: flex; justify-content: space-between; align-items: baseline'},
|
||
|
cssHeader(t('Formula timer')),
|
||
|
)
|
||
|
),
|
||
|
dom.maybeOwned(this._data, (owner) => {
|
||
|
const viewSectionModel = this._gristDoc.docModel.viewSections.getRowModel(VIRTUAL_SECTION_ID as any);
|
||
|
ViewSectionHelper.create(owner, this._gristDoc, viewSectionModel);
|
||
|
return dom.maybe(use => use(viewSectionModel.viewInstance), (view: BaseView) =>
|
||
|
dom('div.active_section.view_data_pane_container.flexvbox', view.viewPane,
|
||
|
dom.maybe(use => !use(isNarrowScreenObs()), () => view.selectionSummary?.buildDom()),
|
||
|
)
|
||
|
);
|
||
|
}),
|
||
|
dom.maybe(not(this._data), () => cssLoaderScreen(
|
||
|
loadingSpinner(),
|
||
|
dom('div', t('Loading timing data. Don\'t close this tab.')),
|
||
|
testId('spinner'),
|
||
|
))
|
||
|
);
|
||
|
}
|
||
|
|
||
|
private _openSettings() {
|
||
|
urlState().pushUrl({docPage: 'settings'}).catch(reportError);
|
||
|
}
|
||
|
|
||
|
private async _start() {
|
||
|
const docApi = this._gristDoc.docPageModel.appModel.api.getDocAPI(this._gristDoc.docId());
|
||
|
|
||
|
// Get the data from the server (and wait for the engine to calculate everything if it hasn't already).
|
||
|
const data = await docApi.stopTiming();
|
||
|
if (this.isDisposed()) { return; }
|
||
|
|
||
|
// And wire up the UI.
|
||
|
const ext = this.autoDispose(new TimingExternalTable(data));
|
||
|
new VirtualTable(this, this._gristDoc, ext);
|
||
|
this._data.set(data);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// See the WebhookPage for more details on how this works.
|
||
|
function _prepareInitialActions(tableId: string): DocAction[] {
|
||
|
return [[
|
||
|
// Add the virtual table.
|
||
|
'AddTable', tableId,
|
||
|
COLUMNS.map(col => ({
|
||
|
isFormula: true,
|
||
|
type: 'Any',
|
||
|
formula: '',
|
||
|
id: col.colId
|
||
|
}))
|
||
|
], [
|
||
|
// Add an entry for the virtual table.
|
||
|
'AddRecord', '_grist_Tables', VIRTUAL_TABLE_ID as any, {tableId, primaryViewId: 0},
|
||
|
], [
|
||
|
// Add entries for the columns of the virtual table.
|
||
|
'BulkAddRecord', '_grist_Tables_column',
|
||
|
COLUMNS.map(col => col.id) as any, getColValues(COLUMNS.map(rec =>
|
||
|
Object.assign({
|
||
|
isFormula: false,
|
||
|
formula: '',
|
||
|
widgetOptions: '',
|
||
|
parentId: VIRTUAL_TABLE_ID as any,
|
||
|
}, omit(rec, ['id']) as any))),
|
||
|
], [
|
||
|
// Add a view section.
|
||
|
'AddRecord', '_grist_Views_section', VIRTUAL_SECTION_ID as any,
|
||
|
{
|
||
|
tableRef: VIRTUAL_TABLE_ID, parentKey: 'record',
|
||
|
title: 'Timing', layout: 'vertical', showHeader: true,
|
||
|
borderWidth: 1, defaultWidth: 100,
|
||
|
}
|
||
|
], [
|
||
|
// List the fields shown in the view section.
|
||
|
'BulkAddRecord', '_grist_Views_section_field', FIELDS.map(VirtualId.bind(null, undefined)) as any, {
|
||
|
colRef: FIELDS.map(colId => COLUMNS.find(r => r.colId === colId)!.id),
|
||
|
parentId: FIELDS.map(() => VIRTUAL_SECTION_ID),
|
||
|
parentPos: FIELDS.map((_, i) => i),
|
||
|
}
|
||
|
]];
|
||
|
}
|
||
|
|
||
|
// See the WebhookPage for more details on how this works.
|
||
|
function _mapModelValues(rowId: number, model: FormulaTimingInfo): Partial<TimingRecord & {id: number}> {
|
||
|
return {
|
||
|
id: rowId,
|
||
|
tableId: model.tableId,
|
||
|
colId: model.colId,
|
||
|
sum: model.sum,
|
||
|
calls: model.count,
|
||
|
average: model.average,
|
||
|
max: model.max,
|
||
|
};
|
||
|
}
|
||
|
|
||
|
const cssHeader = styled(docListHeader, `
|
||
|
margin-bottom: 18px;
|
||
|
`);
|
||
|
|
||
|
const cssContainer = styled('div', `
|
||
|
overflow-y: auto;
|
||
|
position: relative;
|
||
|
height: 100%;
|
||
|
padding: 32px 64px 24px 64px;
|
||
|
|
||
|
display: flex;
|
||
|
flex-direction: column;
|
||
|
@media ${mediaSmall} {
|
||
|
& {
|
||
|
padding: 32px 24px 24px 24px;
|
||
|
}
|
||
|
}
|
||
|
`);
|
||
|
|
||
|
const cssLoaderScreen = styled('div', `
|
||
|
display: flex;
|
||
|
flex-direction: column;
|
||
|
align-items: center;
|
||
|
justify-content: center;
|
||
|
gap: 8px;
|
||
|
flex: 1;
|
||
|
`);
|