UNPKG

@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
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); }