mirror of
https://github.com/gristlabs/grist-core.git
synced 2026-03-02 04:09:24 +00:00
(core) Adding UI for timing API
Summary: Adding new buttons to control the `timing` API and a way to view the results using virtual table features. Test Plan: Added new Reviewers: georgegevoian Reviewed By: georgegevoian Subscribers: paulfitz Differential Revision: https://phab.getgrist.com/D4252
This commit is contained in:
@@ -295,7 +295,7 @@ export interface TimingInfo {
|
||||
/**
|
||||
* Total time spend evaluating a formula.
|
||||
*/
|
||||
total: number;
|
||||
sum: number;
|
||||
/**
|
||||
* Number of times the formula was evaluated (for all rows).
|
||||
*/
|
||||
@@ -320,9 +320,10 @@ export interface FormulaTimingInfo extends TimingInfo {
|
||||
*/
|
||||
export interface TimingStatus {
|
||||
/**
|
||||
* If true, timing info is being collected.
|
||||
* If disabled then 'disabled', else 'active' or 'pending'. Pending means that the engine is busy
|
||||
* and can't respond to confirm the status (but it used to be active before that).
|
||||
*/
|
||||
status: boolean;
|
||||
status: 'active'|'pending'|'disabled';
|
||||
/**
|
||||
* Will be undefined if we can't get the timing info (e.g. if the document is locked by other call).
|
||||
* Otherwise, contains the intermediate results gathered so far.
|
||||
|
||||
@@ -107,6 +107,9 @@ export interface CommDocChatter extends CommMessageBase {
|
||||
},
|
||||
// This could also be a fine place to send updated info
|
||||
// about other users of the document.
|
||||
timing?: {
|
||||
status: 'active'|'disabled';
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -74,6 +74,7 @@ export interface OpenLocalDocResult {
|
||||
clientId: string; // the docFD is meaningful only in the context of this session
|
||||
doc: {[tableId: string]: TableDataAction};
|
||||
log: MinimalActionGroup[];
|
||||
isTimingOn: boolean;
|
||||
recoveryMode?: boolean;
|
||||
userOverride?: UserOverride;
|
||||
docUsage?: FilteredDocUsageSummary;
|
||||
|
||||
@@ -23,16 +23,17 @@ export namespace Sort {
|
||||
* Object base representation for column expression.
|
||||
*/
|
||||
export interface ColSpecDetails {
|
||||
colRef: number;
|
||||
colRef: ColRef;
|
||||
direction: Direction;
|
||||
orderByChoice?: boolean;
|
||||
emptyLast?: boolean;
|
||||
naturalSort?: boolean;
|
||||
}
|
||||
/**
|
||||
* Column expression type.
|
||||
* Column expression type. Either number, an object, or virtual id string _vid\d+
|
||||
*/
|
||||
export type ColSpec = number | string;
|
||||
export type ColRef = number | string;
|
||||
/**
|
||||
* Sort expression type, for example [1,-2, '3:emptyLast', '-4:orderByChoice']
|
||||
*/
|
||||
@@ -75,7 +76,7 @@ export namespace Sort {
|
||||
tail.push("orderByChoice");
|
||||
}
|
||||
if (!tail.length) {
|
||||
return +head;
|
||||
return maybeNumber(head);
|
||||
}
|
||||
return head + (tail.length ? OPTION_SEPARATOR : "") + tail.join(FLAG_SEPARATOR);
|
||||
}
|
||||
@@ -92,21 +93,33 @@ export namespace Sort {
|
||||
: parseColSpec(colSpec);
|
||||
}
|
||||
|
||||
function maybeNumber(colRef: string): ColRef {
|
||||
const num = parseInt(colRef, 10);
|
||||
return isNaN(num) ? colRef : num;
|
||||
}
|
||||
|
||||
function parseColSpec(colString: string): ColSpecDetails {
|
||||
const REGEX = /^(-)?(\d+)(:([\w\d;]+))?$/;
|
||||
if (!colString) {
|
||||
throw new Error("Empty column expression");
|
||||
}
|
||||
const REGEX = /^(?<sign>-)?(?<colRef>(_vid)?(\d+))(:(?<flag>[\w\d;]+))?$/;
|
||||
const match = colString.match(REGEX);
|
||||
if (!match) {
|
||||
throw new Error("Error parsing sort expression " + colString);
|
||||
}
|
||||
const [, sign, colRef, , flag] = match;
|
||||
const {sign, colRef, flag} = match.groups || {};
|
||||
const flags = flag?.split(";");
|
||||
return {
|
||||
colRef: +colRef,
|
||||
return onlyDefined({
|
||||
colRef: maybeNumber(colRef),
|
||||
direction: sign === "-" ? DESC : ASC,
|
||||
orderByChoice: flags?.includes("orderByChoice"),
|
||||
emptyLast: flags?.includes("emptyLast"),
|
||||
naturalSort: flags?.includes("naturalSort"),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function onlyDefined<T extends Record<string, any>>(obj: T): T{
|
||||
return Object.fromEntries(Object.entries(obj).filter(([_, v]) => v !== undefined)) as T;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -138,17 +151,26 @@ export namespace Sort {
|
||||
* Converts column expression order.
|
||||
*/
|
||||
export function setColDirection(colSpec: ColSpec, dir: Direction): ColSpec {
|
||||
if (typeof colSpec === "number") {
|
||||
if (typeof colSpec == "number") {
|
||||
return Math.abs(colSpec) * dir;
|
||||
} else if (colSpec.startsWith(VirtualId.PREFIX)) {
|
||||
return dir === DESC ? `-${colSpec}` : colSpec;
|
||||
} else if (colSpec.startsWith(`-${VirtualId.PREFIX}`)) {
|
||||
return dir === ASC ? colSpec.slice(1) : colSpec;
|
||||
} else {
|
||||
return detailsToSpec({ ...parseColSpec(colSpec), direction: dir });
|
||||
}
|
||||
return detailsToSpec({ ...parseColSpec(colSpec), direction: dir });
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates simple column expression.
|
||||
*/
|
||||
export function createColSpec(colRef: number, dir: Direction): ColSpec {
|
||||
return colRef * dir;
|
||||
export function createColSpec(colRef: ColRef, dir: Direction): ColSpec {
|
||||
if (typeof colRef === "number") {
|
||||
return colRef * dir;
|
||||
} else {
|
||||
return dir === ASC ? colRef : `-${colRef}`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -187,7 +209,7 @@ export namespace Sort {
|
||||
/**
|
||||
* Swaps column id in column expression. Primary use for display columns.
|
||||
*/
|
||||
export function swapColRef(colSpec: ColSpec, colRef: number): ColSpec {
|
||||
export function swapColRef(colSpec: ColSpec, colRef: ColRef): ColSpec {
|
||||
if (typeof colSpec === "number") {
|
||||
return colSpec >= 0 ? colRef : -colRef;
|
||||
}
|
||||
@@ -220,7 +242,7 @@ export namespace Sort {
|
||||
* @param colRef Column id to remove
|
||||
* @param newSpec New column sort options to put in place of the old one.
|
||||
*/
|
||||
export function replace(sortSpec: SortSpec, colRef: number, newSpec: ColSpec | ColSpecDetails): SortSpec {
|
||||
export function replace(sortSpec: SortSpec, colRef: ColRef, newSpec: ColSpec | ColSpecDetails): SortSpec {
|
||||
const index = findColIndex(sortSpec, colRef);
|
||||
if (index >= 0) {
|
||||
const updated = sortSpec.slice();
|
||||
@@ -322,3 +344,26 @@ export namespace Sort {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let _virtualIdCounter = 1;
|
||||
const _virtualSymbols = new Map<string, string>();
|
||||
/**
|
||||
* Creates a virtual id for virtual tables. Can remember some generated ids if called with a
|
||||
* name (this feature used only in tests for now).
|
||||
*
|
||||
* The resulting id looks like _vid\d+.
|
||||
*/
|
||||
export function VirtualId(symbol = '') {
|
||||
if (symbol) {
|
||||
if (!_virtualSymbols.has(symbol)) {
|
||||
const generated = `${VirtualId.PREFIX}${_virtualIdCounter++}`;
|
||||
_virtualSymbols.set(symbol, generated);
|
||||
return generated;
|
||||
} else {
|
||||
return _virtualSymbols.get(symbol)!;
|
||||
}
|
||||
} else {
|
||||
return `${VirtualId.PREFIX}${_virtualIdCounter++}`;
|
||||
}
|
||||
}
|
||||
VirtualId.PREFIX = '_vid';
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import {ActionSummary} from 'app/common/ActionSummary';
|
||||
import {ApplyUAResult, ForkResult, PermissionDataWithExtraUsers, QueryFilters} from 'app/common/ActiveDocAPI';
|
||||
import {ApplyUAResult, ForkResult, FormulaTimingInfo,
|
||||
PermissionDataWithExtraUsers, QueryFilters, TimingStatus} from 'app/common/ActiveDocAPI';
|
||||
import {AssistanceRequest, AssistanceResponse} from 'app/common/AssistancePrompts';
|
||||
import {BaseAPI, IOptions} from 'app/common/BaseAPI';
|
||||
import {BillingAPI, BillingAPIImpl} from 'app/common/BillingAPI';
|
||||
@@ -512,14 +513,18 @@ export interface DocAPI {
|
||||
getAssistance(params: AssistanceRequest): Promise<AssistanceResponse>;
|
||||
/**
|
||||
* Check if the document is currently in timing mode.
|
||||
* Status is either
|
||||
* - 'active' if timings are enabled.
|
||||
* - 'pending' if timings are enabled but we can't get the data yet (as engine is blocked)
|
||||
* - 'disabled' if timings are disabled.
|
||||
*/
|
||||
timing(): Promise<{status: boolean}>;
|
||||
timing(): Promise<TimingStatus>;
|
||||
/**
|
||||
* Starts recording timing information for the document. Throws exception if timing is already
|
||||
* in progress or you don't have permission to start timing.
|
||||
*/
|
||||
startTiming(): Promise<void>;
|
||||
stopTiming(): Promise<void>;
|
||||
stopTiming(): Promise<FormulaTimingInfo[]>;
|
||||
}
|
||||
|
||||
// Operations that are supported by a doc worker.
|
||||
@@ -1134,7 +1139,7 @@ export class DocAPIImpl extends BaseAPI implements DocAPI {
|
||||
});
|
||||
}
|
||||
|
||||
public async timing(): Promise<{status: boolean}> {
|
||||
public async timing(): Promise<TimingStatus> {
|
||||
return this.requestJson(`${this._url}/timing`);
|
||||
}
|
||||
|
||||
@@ -1142,8 +1147,8 @@ export class DocAPIImpl extends BaseAPI implements DocAPI {
|
||||
await this.request(`${this._url}/timing/start`, {method: 'POST'});
|
||||
}
|
||||
|
||||
public async stopTiming(): Promise<void> {
|
||||
await this.request(`${this._url}/timing/stop`, {method: 'POST'});
|
||||
public async stopTiming(): Promise<FormulaTimingInfo[]> {
|
||||
return await this.requestJson(`${this._url}/timing/stop`, {method: 'POST'});
|
||||
}
|
||||
|
||||
private _getRecords(tableId: string, endpoint: 'data' | 'records', options?: GetRowsParams): Promise<any> {
|
||||
|
||||
@@ -14,7 +14,7 @@ import clone = require('lodash/clone');
|
||||
import pickBy = require('lodash/pickBy');
|
||||
import slugify from 'slugify';
|
||||
|
||||
export const SpecialDocPage = StringUnion('code', 'acl', 'data', 'GristDocTour', 'settings', 'webhook');
|
||||
export const SpecialDocPage = StringUnion('code', 'acl', 'data', 'GristDocTour', 'settings', 'webhook', 'timing');
|
||||
type SpecialDocPage = typeof SpecialDocPage.type;
|
||||
export type IDocPage = number | SpecialDocPage;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user