diff --git a/libraries/compute-utils/function-views/src/custom-function-view.ts b/libraries/compute-utils/function-views/src/custom-function-view.ts new file mode 100644 index 0000000000..44df0c762e --- /dev/null +++ b/libraries/compute-utils/function-views/src/custom-function-view.ts @@ -0,0 +1,192 @@ +import * as grok from 'datagrok-api/grok'; +import * as ui from 'datagrok-api/ui'; +import * as DG from 'datagrok-api/dg'; +import {BehaviorSubject} from 'rxjs'; +import {deepCopy} from '../../shared-utils/utils'; +import {historyUtils} from '../../history-utils'; + +export abstract class CustomFunctionView extends DG.ViewBase { + public showHistory = new BehaviorSubject(false); + public isHistorical = new BehaviorSubject(false); + public isReady = new BehaviorSubject(false); + + public historyRoot = ui.div(); + + public funcCall?: DG.FuncCall; + + constructor(private initValue: string) { + super(); + this.box = true; + this.init(); + } + + public abstract buildIO(): HTMLElement; + + exportConfig: { + export: ((format: string) => Promise); + filename: ((format: string) => string); + supportedFormats: string[]; + supportedExtensions: Record; + } | null = null; + + getMocks: ({mockName: string, action: () => Promise}[]) | null = null; + + getTemplates: ({name: string, action: () => Promise}[]) | null = null; + + getHelp: (() => Promise) | null = null; + + reportBug: (() => Promise) | null = null; + + requestFeature: (() => Promise) | null = null; + + public async init() { + const func = DG.Func.byName(this.initValue); + this.linkFunccall(func.prepare({})); + await this.onFuncCallReady(); + this.isReady.next(true); + } + + public linkFunccall(funcCall: DG.FuncCall) { + this.funcCall = funcCall; + } + + public async onFuncCallReady() { + this.name = this.funcCall!.func.friendlyName; + await this.getPackageData(); + this.build(); + } + + public async onBeforeSaveRun(callToSave: DG.FuncCall) { } + public async onAfterSaveRun(savedCall: DG.FuncCall) { } + + public async saveRun(callToSave: DG.FuncCall): Promise { + const callCopy = deepCopy(callToSave); + await this.onBeforeSaveRun(callCopy); + + if (callCopy.id) callCopy.newId(); + + const savedCall = await historyUtils.saveRun(callCopy); + const loadedCall = await historyUtils.loadRun(savedCall.id); + + this.linkFunccall(loadedCall); + this.isHistorical.next(true); + + await this.onAfterSaveRun(savedCall); + return savedCall; + } + + public async onBeforeLoadRun() {} + public async onAfterLoadRun(funcCall: DG.FuncCall) {} + + public async loadRun(funcCallId: string): Promise { + await this.onBeforeLoadRun(); + + const pulledRun = await historyUtils.loadRun(funcCallId); + + this.linkFunccall(pulledRun); + this.isHistorical.next(true); + + await this.onAfterLoadRun(pulledRun); + return pulledRun; + } + + public build(): void { + ui.empty(this.root); + const rootItem = ui.div([ + this.buildIO(), + this.historyRoot, + ]) + this.root.appendChild(rootItem); + + this.buildRibbonMenu(); + this.buildRibbonPanels(); + } + + public buildRibbonPanels(): HTMLElement[][] { + const historyButton = ui.iconFA('history', () => this.showHistory.next(!this.showHistory.value)); + const saveButton = ui.iconFA('save', () => this.funcCall ? this.saveRun(this.funcCall) : undefined); + const exportBtn = ui.comboPopup( + ui.iconFA('arrow-to-bottom'), + this.getFormats(), + this.exportRun.bind(this), + ); + const newRibbonPanels: HTMLElement[][] = [[historyButton, saveButton, exportBtn]]; + this.setRibbonPanels(newRibbonPanels); + return newRibbonPanels; + } + + public buildRibbonMenu() { + this.ribbonMenu.clear(); + + if (!this.exportConfig && !this.reportBug && !this.requestFeature && !this.getHelp && !this.getMocks && !this.getTemplates) return; + + const ribbonMenu = this.ribbonMenu.group('Model'); + + if (this.getMocks && this.getMocks.length > 0) { + if (this.getMocks.length === 1) + ribbonMenu.item('Input data mock', this.getMocks[0].action); + else { + const dataGroup = ribbonMenu.group('Input data mocks'); + this.getMocks.forEach((val) => { + dataGroup.item(val.mockName, val.action); + }); + ribbonMenu.endGroup(); + } + } + + if (this.getTemplates && this.getTemplates.length > 0) { + if (this.getTemplates.length === 1) + ribbonMenu.item('Input data template', this.getTemplates[0].action); + else { + const dataGroup = ribbonMenu.group('Input data templates'); + this.getTemplates.forEach((val) => { + dataGroup.item(val.name, val.action); + }); + ribbonMenu.endGroup(); + } + } + + if (this.exportConfig && this.exportConfig.supportedFormats.length > 0) { + ribbonMenu + .group('Export') + .items(this.getFormats(), this.exportRun.bind(this)) + .endGroup(); + } + + if (this.reportBug) + ribbonMenu.item('Report a bug', () => this.reportBug!()); + + if (this.requestFeature) + ribbonMenu.item('Request a feature', () => this.requestFeature!()); + + if (this.getHelp) + ribbonMenu.item('Help', () => this.getHelp!()); + } + + public async exportRun(format: string) { + DG.Utils.download(this.exportConfig!.filename(format), await this.exportConfig!.export(format)); + } + + private getFormats() { + return (this.exportConfig?.supportedFormats ?? []); + } + + private async getPackageData() { + const pack = this.func?.package; + if (!pack) + return; + + const reportBugUrl = (await pack?.getProperties() as any)?.REPORT_BUG_URL; + if (reportBugUrl && !this.reportBug) + this.reportBug = async () => {window.open(reportBugUrl, '_blank');}; + + const reqFeatureUrl = (await pack?.getProperties() as any)?.REQUEST_FEATURE_URL; + if (reqFeatureUrl && !this.requestFeature) + this.requestFeature = async () => {window.open(reqFeatureUrl, '_blank');}; + } + + get func() { + return this.funcCall?.func; + } + +}