@discoveryjs/discovery
Version:
Frontend framework for rapid data (JSON) analysis, shareable serverless reports and dashboards
160 lines (159 loc) • 5.86 kB
JavaScript
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();
}
}