UNPKG

@discoveryjs/discovery

Version:

Frontend framework for rapid data (JSON) analysis, shareable serverless reports and dashboards

160 lines (159 loc) 5.86 kB
import { hasOwn } from "../core/utils/object-utils.js"; import { createElement } from "../core/utils/dom.js"; import { syncLoaderWithProgressbar } from "../core/utils/load-data.js"; import { ViewModel } from "./view-model.js"; import { Progressbar } from "../core/utils/progressbar.js"; import modelfree from "../extensions/modelfree.js"; import upload from "../extensions/upload.js"; import embed from "../extensions/embed-client.js"; import router from "../extensions/router.js"; import * as navButtons from "../nav/buttons.js"; const coalesceOption = (value, fallback) => value !== void 0 ? value : fallback; export class App extends ViewModel { constructor(options = {}) { const extensions = []; extensions.push(navButtons.indexPage); extensions.push(navButtons.discoveryPage); extensions.push(navButtons.colorSchemeToggle); if (coalesceOption(options.router, true)) { extensions.push(router); } if (options.mode === "modelfree") { extensions.push(modelfree); } if (coalesceOption(options.upload, false)) { extensions.push(options.upload === true ? upload : upload.setup(options.upload || {})); extensions.push(navButtons.uploadFile); extensions.push(navButtons.uploadFromClipboard); } if (coalesceOption(options.inspector, true)) { extensions.push(navButtons.inspect); } if (coalesceOption(options.embed, false)) { extensions.push(options.embed === true ? embed : embed.setup(options.embed || {})); } super({ container: document.body, ...options, extensions: options.extensions ? [extensions, options.extensions] : extensions, colorScheme: coalesceOption(options.colorScheme ?? options.darkmode, "auto"), colorSchemePersistent: coalesceOption(options.colorSchemePersistent ?? options.darkmodePersistent, true) }); } setLoadingState(state, options) { const loadingOverlayEl = this.dom.loadingOverlay; const { progressbar } = options || {}; switch (state) { case "init": { loadingOverlayEl.classList.remove("error", "done"); if (progressbar?.el.parentNode) { return; } loadingOverlayEl.replaceChildren(progressbar?.el || ""); break; } case "success": { loadingOverlayEl.classList.add("done"); break; } case "error": { const error = options?.error; const renderContext = this.getRenderContext(); const renderData = { stage: progressbar?.value.stage, errorText: String(error), errorMessage: error.message || String(error), errorStack: (error.stack || "").replace(/^Error:\s*(\S+Error:)/, "$1") }; const renderConfig = [ "app-header:#.model", { view: "block", className: "action-buttons", content: [ { view: "preset/upload", when: this.preset.isDefined("upload") } ] }, { view: "block", className: hasOwn(error, "renderContent") ? "warning-message" : "error-message", content: [ { view: "block", className: "error-type-badge", postRender(el, config, data) { if (data.stage) { el.dataset.stage = ` on ${data.stage}`; } } }, "h3:errorText", error.renderContent || 'text:"(see details in the console)"' ] } ]; loadingOverlayEl.classList.add("error"); loadingOverlayEl.replaceChildren(); this.view.setViewRoot(loadingOverlayEl, "AppOverlay", { inspectable: false, config: renderConfig, data: renderData, context: renderContext }); this.view.render(loadingOverlayEl, renderConfig, renderData, renderContext).then(() => { this.logger.error(error); progressbar?.setState({ error }); }); break; } } } async setDataProgress(data, context, options) { const dataset = options?.dataset; const progressbar = options?.progressbar || this.progressbar({ title: "Set data" }); try { this.setLoadingState("init", { progressbar }); await super.setDataProgress(data, context, { dataset, progressbar }); this.setLoadingState("success"); } catch (error) { this.setLoadingState("error", { error, progressbar }); } } progressbar(options) { return new Progressbar({ domReady: this.dom.ready, onFinish: (timings) => this.logger.perf.groupCollapsed( `${options.title || "Load data"} (${timings[timings.length - 1].duration}ms)`, () => [ ...timings.map((timing) => `${timing.title}: ${timing.duration}ms`), `(await repaint: ${timings.awaitRepaintPenaltyTime}ms)` ] ), ...options }); } async trackLoadDataProgress(loadDataResult) { const progressbar = this.progressbar({ title: loadDataResult.title }); this.cancelScheduledRender(); this.setLoadingState("init", { progressbar }); this.emit("startLoadData", progressbar.subscribeSync.bind(progressbar)); syncLoaderWithProgressbar(loadDataResult, progressbar).then( (dataset) => this.setDataProgress(dataset.data, null, { dataset, progressbar }), (error) => this.setLoadingState("error", { error, progressbar }) ); await loadDataResult.dataset; } initDom(styles) { super.initDom(styles); this.dom.container.append( this.dom.loadingOverlay = createElement("div", "loading-overlay done") ); } renderPage() { document.title = this.info.name || document.title; return super.renderPage(); } }