@discoveryjs/discovery
Version:
Frontend framework for rapid data (JSON) analysis, shareable serverless reports and dashboards
289 lines (288 loc) • 10.4 kB
JavaScript
import { colorSchemeStateValues, serializeColorSchemeState } from "../core/color-scheme.js";
import { randomId } from "../core/utils/id.js";
import { loadDataFromPush, loadDataFromStream } from "../core/utils/load-data.js";
export default Object.assign(setup(), { setup });
const noop = () => {
};
const navSection = ["primary", "secondary", "menu"];
const navAction = ["insert", "prepend", "append", "before", "after", "replace", "remove"];
function createNavItemConfig(rawConfig, sendMessage, rawCommands) {
const commands = Array.isArray(rawCommands) ? rawCommands : [];
return JSON.parse(
JSON.stringify(rawConfig),
(_, value) => typeof value === "string" && commands.includes(value) ? () => sendMessage("navMethod", value) : value
);
}
function setup(options) {
const hostId = options?.hostId || randomId();
const postponeMessages = options?.postponeMessages;
const onNotify = typeof options?.onNotify === "function" ? options.onNotify : () => {
};
return function(host) {
let loadChunkedDataStatus = null;
let loadDataUnsubscribe = noop;
const cancelLoadChunkedDataApi = (error) => {
loadChunkedDataStatus?.finish();
loadChunkedDataStatus = null;
if (error) {
host.logger.debug("Cancel chunked data load from host with error:", error);
}
};
const parentWindow = window.parent;
const actionCalls = /* @__PURE__ */ new Map();
const sendMessage = (type, payload) => {
const message = {
from: "discoveryjs-app",
id: hostId,
type,
payload
};
parentWindow.postMessage(message, "*");
};
const setLoadDataUnsubscribe = (fn) => {
loadDataUnsubscribe = () => {
if (fn !== null) {
loadDataUnsubscribe = noop;
fn();
}
};
};
const trackDataLoading = (loadDataStatus) => {
const result = host.trackLoadDataProgress(loadDataStatus);
result.catch(() => {
});
};
const processIncomingMessage = ({ data }) => {
const { id, type, payload } = data || {};
if (id === hostId) {
switch (type) {
case "notification": {
const { name, details } = payload;
onNotify(name, details);
break;
}
case "defineAction": {
const name = payload;
host.action.define(
name,
(...args) => new Promise((resolve, reject) => {
const callId = randomId();
actionCalls.set(callId, { resolve, reject });
setTimeout(() => {
actionCalls.delete(callId);
reject(new Error("Timeout"));
}, 3e4);
sendMessage("action", {
callId,
name,
args
});
})
);
break;
}
case "actionResult": {
const { callId } = payload;
if (!actionCalls.has(callId)) {
host.logger.error(`[Discovery.js] Unknown action call id "${callId}"`);
break;
}
const { resolve, reject } = actionCalls.get(callId);
if ("error" in payload) {
reject(payload.error);
} else {
resolve(payload.value);
}
break;
}
case "setPageHash": {
const { replace, hash } = payload || {};
host.setPageHash(hash || "", replace || false);
break;
}
case "setPageHashState": {
const { replace, id: id2, ref, params } = payload || {};
host.setPageHashState({ id: id2, ref, params }, replace);
break;
}
case "setPageHashStateWithAnchor": {
const { replace, id: id2, ref, params, anchor } = payload || {};
host.setPageHashStateWithAnchor({ id: id2, ref, params, anchor }, replace);
break;
}
case "setPage": {
const { replace, id: id2, ref, params } = payload || {};
host.setPage(id2, ref, params, replace);
break;
}
case "setPageRef": {
const { replace, ref } = payload || {};
host.setPageRef(ref, replace);
break;
}
case "setPageParams": {
const { replace, params } = payload || {};
host.setPageParams(params, replace);
break;
}
case "setPageAnchor": {
const { replace, anchor } = payload || {};
host.setPageAnchor(anchor, replace);
break;
}
case "setColorSchemeState": {
const colorSchemeState = payload;
if (!colorSchemeStateValues.includes(colorSchemeState)) {
host.logger.warn(`Wrong value for colorScheme "${colorSchemeState}", supported values: ${colorSchemeStateValues.map((value) => JSON.stringify(value)).join(", ")}`);
break;
}
host.colorScheme.set(colorSchemeState);
break;
}
case "setRouterPreventLocationUpdate": {
host.action.call("setPreventLocationUpdate", Boolean(payload));
break;
}
case "changeNavButtons": {
const {
section = "primary",
action = "unknown"
} = payload;
if (!navSection.includes(section)) {
host.logger.warn(`Wrong value for nav button place "${section}", supported values: ${navSection.map((value) => JSON.stringify(value)).join(", ")}`);
break;
}
switch (payload.action) {
case "insert": {
const { name, position, config, commands } = payload;
host.nav[section].insert(createNavItemConfig(config, sendMessage, commands), position, name);
break;
}
case "prepend":
case "append": {
const { config, commands } = payload;
host.nav[section][payload.action](createNavItemConfig(config, sendMessage, commands));
break;
}
case "before":
case "after":
case "replace": {
const { name, config, commands } = payload;
host.nav[section][payload.action](name, createNavItemConfig(config, sendMessage, commands));
break;
}
case "remove": {
const { name } = payload;
host.nav[section].remove(name);
break;
}
default:
host.logger.warn(`Wrong value for nav button action "${action}", supported values: ${navAction.map((value) => JSON.stringify(value)).join(", ")}`);
}
break;
}
case "unloadData": {
cancelLoadChunkedDataApi();
host.unloadData();
break;
}
case "dataStream": {
const { stream, resource } = payload;
cancelLoadChunkedDataApi();
trackDataLoading(loadDataFromStream(stream, { resource }));
break;
}
case "startChunkedDataUpload": {
const { acceptToken, resource } = payload;
cancelLoadChunkedDataApi();
loadChunkedDataStatus = Object.assign(loadDataFromPush({ resource }), { acceptToken });
trackDataLoading(loadChunkedDataStatus);
break;
}
case "cancelChunkedDataUpload": {
const { acceptToken, error } = payload;
if (loadChunkedDataStatus?.acceptToken === acceptToken) {
cancelLoadChunkedDataApi(error);
}
}
case "dataChunk": {
const { acceptToken } = payload;
if (loadChunkedDataStatus === null) {
host.logger.warn("Loading data is not inited");
break;
}
if (loadChunkedDataStatus.acceptToken !== acceptToken) {
host.logger.warn("Bad accept token");
break;
}
if ("value" in payload && payload.value !== void 0) {
loadChunkedDataStatus.push(payload.value);
}
if ("done" in payload && payload.done) {
cancelLoadChunkedDataApi();
}
break;
}
default:
host.logger.warn(`Got a post-message addressed to discovery app but with unknown "${type}" type`);
}
}
};
if (parentWindow === window) {
return;
}
host.on("pageHashChange", (replace) => {
sendMessage("pageHashChanged", {
replace,
hash: host.pageHash || "#",
id: host.pageId,
ref: host.pageRef,
params: host.pageParams,
anchor: host.pageAnchor
});
});
host.on("startLoadData", (subscribe) => {
loadDataUnsubscribe();
setLoadDataUnsubscribe(subscribe((state) => sendMessage("loadingState", state)));
});
host.on("startSetData", (subscribe) => {
loadDataUnsubscribe();
setLoadDataUnsubscribe(subscribe((state) => sendMessage("loadingState", state)));
});
host.on("data", () => {
sendMessage("data", null);
});
host.on("unloadData", () => {
loadDataUnsubscribe();
sendMessage("unloadData", null);
});
host.colorScheme.subscribe(
(_, state) => sendMessage("colorSchemeChanged", {
state,
value: serializeColorSchemeState(state)
})
);
if (Array.isArray(postponeMessages)) {
Promise.resolve().then(() => {
for (const message of postponeMessages) {
processIncomingMessage({ data: message });
}
});
}
addEventListener("message", processIncomingMessage, false);
addEventListener("unload", () => sendMessage("destroy", null), false);
sendMessage("ready", {
page: {
hash: host.pageHash || "#",
id: host.pageId,
ref: host.pageRef,
params: host.pageParams,
anchor: host.pageAnchor
},
colorScheme: {
state: host.colorScheme.state,
value: host.colorScheme.serializedValue
}
});
};
}