@adaptabletools/adaptable
Version:
Powerful AG Grid extension which provides advanced, cutting-edge functionality to meet all DataGrid requirements
405 lines (404 loc) • 14.6 kB
JavaScript
import { AG_GRID_VERSION } from './agGrid/AgGridModulesAdapter';
import { ADAPTABLE_VERSION } from './EnvVars';
import Emitter from './Utilities/Emitter';
import { debug } from '@infinite-table/infinite-react';
import { warnOnce } from './agGrid/AdaptableLogger';
let DEV_TOOLS_LISTENING_STARTED = false;
let DEV_TOOLS_READY = false;
const __ADAPTABLE_DEVTOOLS_EMITTER = new Emitter();
const LOG_LEVELS = ['info', 'success', 'warn', 'error', 'perf'];
function serializeForPostMessage(value) {
try {
const json = JSON.stringify(value);
return json === undefined ? String(value) : JSON.parse(json);
}
catch (error) {
console.warn('Adaptable DevTools: error serializing value', error);
const seen = new WeakSet();
const json = JSON.stringify(value, (_key, val) => {
if (typeof val === 'object' && val !== null) {
if (seen.has(val))
return undefined;
seen.add(val);
}
return val;
});
return json === undefined ? String(value) : JSON.parse(json);
}
}
function serializeAdaptableStateForPostMessage(state, parentKey) {
return Object.entries(state).reduce((acc, [key, value]) => {
if (key === 'Internal') {
acc[key] = serializeAdaptableStateForPostMessage(value, key);
return acc;
}
try {
const json = JSON.stringify(value);
acc[key] = json === undefined ? String(value) : JSON.parse(json);
}
catch (_error) {
warnOnce(`Adaptable DevTools: skipping serializing state key "${parentKey ? `${parentKey}.` : ''}${key}"`);
}
return acc;
}, {});
}
function formatLogArgForPostMessage(value) {
if (typeof value === 'string') {
return value;
}
if (value == null) {
return String(value);
}
if (typeof value === 'object') {
try {
return JSON.stringify(value, null, 2);
}
catch {
return String(value);
}
}
return String(value);
}
function isMessageFromDevToolsContentScript(data) {
return (data &&
typeof data.source === 'string' &&
typeof data.target === 'string' &&
data.target === 'adaptable-page' &&
data.source.startsWith('adaptable-devtools-contentscript'));
}
function ensureDevToolsReady() {
if (DEV_TOOLS_READY) {
return Promise.resolve();
}
DEV_TOOLS_READY = true;
console.log('Adaptable DevTools is listening for messages!');
debug.onLogIntent('*', (logOptions) => {
const { channel, color, timestamp, args } = logOptions;
const channelParts = channel.split(':');
const [channelBase, adaptableId] = channelParts;
if (!['Adaptable', 'LayoutManager'].includes(channelBase)) {
return;
}
if (channelBase === 'LayoutManager' && adaptableId === 'Diffing') {
return;
}
if (!adaptableId) {
return;
}
window.postMessage({
...messageBase,
url: getPageUrlOfWindow(),
type: 'log',
payload: {
adaptableId,
channel,
color,
level: channelParts.find((part) => LOG_LEVELS.includes(part)) ?? 'info',
timestamp,
args: args.map((arg) => serializeForPostMessage(formatLogArgForPostMessage(arg))),
},
});
});
return __ADAPTABLE_DEVTOOLS_EMITTER.emit('ready');
}
function handleOpenSettingsPanelMessage(payload) {
const { adaptableId, moduleName } = payload ?? {};
if (!adaptableId) {
console.warn('Adaptable DevTools: open-settings-panel message missing adaptableId');
return;
}
const instance = REGISTERED_ADAPTABLE_INSTANCES.get(adaptableId);
if (instance?.isDestroyed) {
console.warn(`Adaptable DevTools: instance "${adaptableId}" is destroyed; cannot open settings panel`);
return;
}
const api = instance?.api ?? REGISTERED_ADAPTABLE_APIS.get(adaptableId);
const settingsPanelApi = api?.settingsPanelApi;
if (!settingsPanelApi) {
console.warn(`Adaptable DevTools: settings panel is not available for "${adaptableId}" (instance not ready or destroyed)`);
return;
}
settingsPanelApi.openSettingsPanel(moduleName);
}
function applyThemeToDocumentElement(payload) {
const style = document.documentElement.style;
for (let i = style.length - 1; i >= 0; i--) {
const prop = style[i];
if (prop.startsWith('--ab-')) {
style.removeProperty(prop);
}
}
for (const [name, value] of Object.entries(payload.light)) {
if (value != null && value !== '') {
style.setProperty(name, String(value));
}
}
if (payload.variant === 'dark') {
for (const [name, value] of Object.entries(payload.dark)) {
if (value != null && value !== '') {
style.setProperty(name, String(value));
}
}
}
}
function handleThemeVarChangeMessage(payload) {
const { name, value } = payload ?? {};
if (!name) {
return;
}
document.documentElement.style.setProperty(name, String(value ?? ''));
}
function handleApplyThemeMessage(payload) {
if (!payload || (payload.variant !== 'light' && payload.variant !== 'dark')) {
return;
}
applyThemeToDocumentElement(payload);
}
function handleLoadVariantMessage(payload) {
const { adaptableId, variant } = payload ?? {};
if (!adaptableId || (variant !== 'light' && variant !== 'dark')) {
console.warn('Adaptable DevTools: load-variant message missing adaptableId or variant');
return;
}
const api = REGISTERED_ADAPTABLE_APIS.get(adaptableId);
if (!api) {
console.warn(`Adaptable DevTools: no registered instance for "${adaptableId}"`);
return;
}
console.log(`Adaptable DevTools: loading "${variant}" theme variant for "${adaptableId}"`);
if (variant === 'dark') {
api.themeApi.loadDarkTheme();
}
else {
api.themeApi.loadLightTheme();
}
}
function handleRestoreStateMessage(payload) {
const { adaptableId, state } = payload ?? {};
if (!adaptableId || !state) {
console.warn('Adaptable DevTools: restore-state message missing adaptableId or state');
return;
}
const api = REGISTERED_ADAPTABLE_APIS.get(adaptableId);
if (!api) {
console.warn(`Adaptable DevTools: no registered instance for "${adaptableId}"`);
return;
}
void api.stateApi.internalApi.restoreState(state).catch((error) => {
console.error(`Adaptable DevTools: failed to restore state for "${adaptableId}"`, error);
});
}
export function listenForDevTools(adaptableInstance) {
if (typeof window !== 'undefined' && !DEV_TOOLS_LISTENING_STARTED) {
DEV_TOOLS_LISTENING_STARTED = true;
window.addEventListener('message', (event) => {
if (!isMessageFromDevToolsContentScript(event.data)) {
return;
}
void ensureDevToolsReady();
const handlers = {
'restore-state': handleRestoreStateMessage,
'open-settings-panel': handleOpenSettingsPanelMessage,
'load-variant': handleLoadVariantMessage,
'theme-var-change': handleThemeVarChangeMessage,
'apply-theme': handleApplyThemeMessage,
};
handlers[event.data.type]?.(event.data.payload);
});
}
__ADAPTABLE_DEVTOOLS_EMITTER.onIncludeFiredOnce('ready').then(() => {
const { api, adaptableOptions } = adaptableInstance;
if (!api || !adaptableOptions) {
console.error('Cannot register Adaptable instance in DevTools because it is not ready', adaptableInstance);
return;
}
registerAdaptableInstanceInDevTools(adaptableOptions.adaptableId, { api }, adaptableInstance);
});
}
const REGISTERED_ADAPTABLE_IDS = new Set();
const REGISTERED_ADAPTABLE_APIS = new Map();
const REGISTERED_ADAPTABLE_INSTANCES = new Map();
const pendingGridRuntimeUpdates = new Map();
const GRID_RUNTIME_FALLBACK_DELAY_MS = 100;
const INSTANCE_REGISTRATIONS = new Map();
function buildGridRuntime(api) {
const selectedRowInfo = api.gridApi.getSelectedRowInfo();
return {
allRowsCount: api.gridApi.getRowCount(),
visibleRowsCount: api.gridApi.getVisibleRowCount(),
selectedRowsCount: selectedRowInfo?.gridRows?.length ?? 0,
};
}
function postMountMessage(adaptableId, api) {
window.postMessage(buildMessageForExtension({
type: 'mount',
adaptableId,
adaptableVersion: ADAPTABLE_VERSION,
agGridVersion: AG_GRID_VERSION,
licenseKey: api.optionsApi.getAdaptableOptions().licenseKey,
}));
}
function cancelScheduledGridRuntimeUpdate(adaptableId) {
const pending = pendingGridRuntimeUpdates.get(adaptableId);
if (pending != null) {
clearTimeout(pending);
pendingGridRuntimeUpdates.delete(adaptableId);
}
}
function scheduleGridRuntimeUpdate(adaptableId, api, delayMs = GRID_RUNTIME_FALLBACK_DELAY_MS) {
cancelScheduledGridRuntimeUpdate(adaptableId);
pendingGridRuntimeUpdates.set(adaptableId, setTimeout(() => {
pendingGridRuntimeUpdates.delete(adaptableId);
postGridRuntimeUpdate(adaptableId, api);
}, delayMs));
}
function postStateChangeMessage(adaptableId, options) {
const api = options?.api ?? REGISTERED_ADAPTABLE_APIS.get(adaptableId);
if (!api) {
return;
}
const state = api.stateApi.internalApi.getAllStateForDevTools
? api.stateApi.internalApi.getAllStateForDevTools()
: api.stateApi.getAllState();
window.postMessage(buildMessageForExtension({
type: 'state-change',
adaptableId,
state: serializeAdaptableStateForPostMessage(state),
actionName: options?.actionName,
}));
if (options?.api) {
scheduleGridRuntimeUpdate(adaptableId, options.api);
}
}
function postGridRuntimeUpdate(adaptableId, api) {
window.postMessage(buildMessageForExtension({
type: 'state-change',
adaptableId,
gridRuntime: buildGridRuntime(api),
}));
}
function detachAdaptableInstanceListeners(adaptableId) {
cancelScheduledGridRuntimeUpdate(adaptableId);
const registration = INSTANCE_REGISTRATIONS.get(adaptableId);
if (registration) {
registration.unsubscribe();
INSTANCE_REGISTRATIONS.delete(adaptableId);
}
REGISTERED_ADAPTABLE_IDS.delete(adaptableId);
REGISTERED_ADAPTABLE_APIS.delete(adaptableId);
REGISTERED_ADAPTABLE_INSTANCES.delete(adaptableId);
}
function unregisterAdaptableInstanceFromDevTools(adaptableId) {
detachAdaptableInstanceListeners(adaptableId);
window.postMessage(buildMessageForExtension({ type: 'unmount', adaptableId }));
}
const messageBase = {
source: 'adaptable-page',
target: 'adaptable-devtools-background',
};
export function getPageUrlOfWindow() {
const url = new URL(window.location.href);
return url.origin + url.pathname;
}
function buildMessageForExtension(params) {
const { type, adaptableId, ...payload } = params;
const url = getPageUrlOfWindow();
if (type === 'unmount') {
return {
...messageBase,
url,
type,
payload: {
adaptableId,
...(payload ?? {}),
},
};
}
if (type === 'mount') {
return {
...messageBase,
url,
type,
payload: {
adaptableId,
adaptableVersion: payload?.adaptableVersion,
agGridVersion: payload?.agGridVersion,
licenseKey: payload?.licenseKey,
},
};
}
if (type === 'state-change') {
return {
...messageBase,
url,
type,
payload: {
adaptableId,
state: payload?.state,
actionName: payload?.actionName,
gridRuntime: payload?.gridRuntime,
},
};
}
const message = {
...messageBase,
url,
type,
payload: {
adaptableId,
...(payload ?? {}),
},
};
return message;
}
function publishStateSnapshot(adaptableId) {
const api = REGISTERED_ADAPTABLE_APIS.get(adaptableId);
if (!api) {
return;
}
postStateChangeMessage(adaptableId, { api });
}
function registerAdaptableInstanceInDevTools(adaptableId, options, adaptableInstance) {
const { api } = options;
detachAdaptableInstanceListeners(adaptableId);
console.log(`Registering instance "${adaptableId}" into Adaptable DevTools`);
REGISTERED_ADAPTABLE_IDS.add(adaptableId);
REGISTERED_ADAPTABLE_APIS.set(adaptableId, api);
if (adaptableInstance) {
REGISTERED_ADAPTABLE_INSTANCES.set(adaptableId, adaptableInstance);
}
const onStateChange = (info) => {
postStateChangeMessage(adaptableId, { actionName: info.actionName, api });
};
const onStateReloaded = () => {
postStateChangeMessage(adaptableId, {
actionName: 'AdaptableStateReloaded',
api,
});
};
const unsubscribeStateChanged = api.eventApi.on('AdaptableStateChanged', onStateChange);
const unsubscribeStateReloaded = api.eventApi.on('AdaptableStateReloaded', onStateReloaded);
const unsubscribeRowSelectionChanged = api.eventApi.on('RowSelectionChanged', () => {
postGridRuntimeUpdate(adaptableId, api);
});
const unsubscribeDestroy = api.eventApi.on('AdaptableDestroy', () => {
unregisterAdaptableInstanceFromDevTools(adaptableId);
});
const adaptableGrid = adaptableInstance ?? REGISTERED_ADAPTABLE_INSTANCES.get(adaptableId);
const unsubscribeFiltersApplied = adaptableGrid?._on('AdapTableFiltersApplied', () => {
cancelScheduledGridRuntimeUpdate(adaptableId);
postGridRuntimeUpdate(adaptableId, api);
});
INSTANCE_REGISTRATIONS.set(adaptableId, {
api,
unsubscribe: () => {
unsubscribeStateChanged();
unsubscribeStateReloaded();
unsubscribeRowSelectionChanged();
unsubscribeFiltersApplied?.();
unsubscribeDestroy();
},
});
postMountMessage(adaptableId, api);
publishStateSnapshot(adaptableId);
}