-
Notifications
You must be signed in to change notification settings - Fork 18
Add Inspector plugin for examining variable values #124
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 20 commits
2ccea5c
053a4a6
8008fbc
9ca19c8
855d484
f2659a9
1a01afc
1cfa5e0
1eaed6d
1b84dbb
b8d3593
8ec4793
52260db
93c374e
02ddee2
0fb5aa4
b9ce8d8
2c94b53
e155ede
3cb626e
1d2ee64
5ec7ad8
f8af03b
9744af0
1c4a5b1
6a5a55e
2df6eed
1b56550
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,195 @@ | ||
| /** | ||
| * Copyright (c) Microsoft Corporation. | ||
| * Licensed under the MIT License. | ||
| */ | ||
|
|
||
| import { IInstance, Plugin } from '../factory.js'; | ||
| import { pluginClassName } from './util.js'; | ||
| import { flaggablePlugin } from './config.js'; | ||
| import { PluginNames } from './interfaces.js'; | ||
| import { InspectorElementProps } from '@microsoft/chartifact-schema'; | ||
|
|
||
| interface InspectorInstance { | ||
| id: string; | ||
| spec: InspectorSpec; | ||
| element: HTMLElement; | ||
| } | ||
|
|
||
| export interface InspectorSpec extends InspectorElementProps { | ||
| } | ||
|
|
||
| const pluginName: PluginNames = 'inspector'; | ||
| const className = pluginClassName(pluginName); | ||
|
|
||
| export const inspectorPlugin: Plugin<InspectorSpec> = { | ||
| ...flaggablePlugin<InspectorSpec>(pluginName, className), | ||
| hydrateComponent: async (renderer, errorHandler, specs) => { | ||
| const { signalBus } = renderer; | ||
| const inspectorInstances: InspectorInstance[] = []; | ||
| for (let index = 0; index < specs.length; index++) { | ||
| const specReview = specs[index]; | ||
| if (!specReview.approvedSpec) { | ||
| continue; | ||
| } | ||
| const container = renderer.element.querySelector(`#${specReview.containerId}`); | ||
|
|
||
| const spec: InspectorSpec = specReview.approvedSpec; | ||
|
|
||
| const html = `<div class="inspector"> | ||
| <div class="inspector-value" id="${spec.variableId || 'all'}-value"></div> | ||
| </div>`; | ||
| container.innerHTML = html; | ||
| const element = container.querySelector('.inspector-value') as HTMLElement; | ||
|
|
||
| const inspectorInstance: InspectorInstance = { id: `${pluginName}-${index}`, spec, element }; | ||
| inspectorInstances.push(inspectorInstance); | ||
| } | ||
|
|
||
| const instances = inspectorInstances.map((inspectorInstance): IInstance => { | ||
| const { element, spec } = inspectorInstance; | ||
|
|
||
| // Special case: if variableId is undefined/omitted, inspect all variables from signalDeps | ||
| const isInspectAll = !spec.variableId; | ||
|
|
||
| const initialSignals = isInspectAll ? [{ | ||
|
||
| name: '*', | ||
| value: null, | ||
| priority: -1, | ||
| isData: false, | ||
| }] : [{ | ||
| name: spec.variableId, | ||
| value: null, | ||
| priority: -1, | ||
| isData: false, | ||
| }]; | ||
|
|
||
| const updateDisplay = (value: unknown) => { | ||
| element.innerHTML = ''; // Clear previous content | ||
|
|
||
| // If raw mode is enabled, always use JSON.stringify without interactivity | ||
| if (spec.raw) { | ||
| element.textContent = JSON.stringify(value, null, 2); | ||
| return; | ||
| } | ||
|
|
||
| // Interactive mode (default) | ||
danmarshall marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| if (Array.isArray(value)) { | ||
| // Create interactive collapsible array display | ||
| renderArray(element, value); | ||
| } else if (typeof value === 'object') { | ||
| // For objects, use JSON.stringify with indentation | ||
| element.textContent = JSON.stringify(value, null, 2); | ||
| } else { | ||
| element.textContent = JSON.stringify(value); | ||
| } | ||
| }; | ||
|
|
||
| const renderArray = (container: HTMLElement, arr: unknown[], depth: number = 0) => { | ||
| const indent = ' '.repeat(depth); | ||
|
|
||
| // Create collapsible array structure | ||
| const arrayWrapper = document.createElement('div'); | ||
| arrayWrapper.className = 'inspector-array'; | ||
|
|
||
| // Array header with toggle | ||
| const header = document.createElement('div'); | ||
| header.className = 'inspector-array-header'; | ||
| header.style.cursor = 'pointer'; | ||
| header.style.userSelect = 'none'; | ||
|
|
||
| const toggleIcon = document.createElement('span'); | ||
| toggleIcon.className = 'inspector-toggle'; | ||
| toggleIcon.textContent = '▶ '; | ||
| toggleIcon.style.display = 'inline-block'; | ||
| toggleIcon.style.width = '1em'; | ||
|
|
||
| const arrayLabel = document.createElement('span'); | ||
| arrayLabel.textContent = `Array(${arr.length})`; | ||
|
|
||
| header.appendChild(toggleIcon); | ||
| header.appendChild(arrayLabel); | ||
|
|
||
| // Array content | ||
| const content = document.createElement('div'); | ||
| content.className = 'inspector-array-content'; | ||
| content.style.paddingLeft = '1.5em'; | ||
|
|
||
| arr.forEach((item, index) => { | ||
| const itemDiv = document.createElement('div'); | ||
| itemDiv.className = 'inspector-array-item'; | ||
|
|
||
| const indexLabel = document.createElement('span'); | ||
| indexLabel.textContent = `[${index}]: `; | ||
| itemDiv.appendChild(indexLabel); | ||
|
|
||
| const valueSpan = document.createElement('span'); | ||
|
|
||
| if (Array.isArray(item)) { | ||
| // Nested array | ||
| renderArray(valueSpan, item, depth + 1); | ||
| } else if (typeof item === 'object') { | ||
| valueSpan.textContent = JSON.stringify(item, null, 2); | ||
| valueSpan.style.whiteSpace = 'pre'; | ||
| } else { | ||
| valueSpan.textContent = JSON.stringify(item); | ||
| } | ||
|
|
||
| itemDiv.appendChild(valueSpan); | ||
| content.appendChild(itemDiv); | ||
| }); | ||
|
|
||
| // Toggle functionality - start collapsed | ||
| let isExpanded = false; | ||
| content.style.display = 'none'; | ||
| const toggle = () => { | ||
| isExpanded = !isExpanded; | ||
| content.style.display = isExpanded ? 'block' : 'none'; | ||
| toggleIcon.textContent = isExpanded ? '▼ ' : '▶ '; | ||
| }; | ||
|
|
||
| header.addEventListener('click', toggle); | ||
|
|
||
| arrayWrapper.appendChild(header); | ||
| arrayWrapper.appendChild(content); | ||
| container.appendChild(arrayWrapper); | ||
| }; | ||
|
|
||
| return { | ||
| ...inspectorInstance, | ||
| initialSignals, | ||
| receiveBatch: async (batch) => { | ||
| if (isInspectAll) { | ||
| // Extract all variable values from signalDeps | ||
| const allVars: { [key: string]: unknown } = {}; | ||
| for (const signalName in signalBus.signalDeps) { | ||
| allVars[signalName] = signalBus.signalDeps[signalName].value; | ||
| } | ||
| updateDisplay(allVars); | ||
| } else if (batch[spec.variableId]) { | ||
| const value = batch[spec.variableId].value; | ||
| updateDisplay(value); | ||
| } | ||
| }, | ||
| beginListening() { | ||
| // Inspector is read-only, no event listeners needed | ||
| // For inspect-all mode, do initial display | ||
| if (isInspectAll) { | ||
| const allVars: { [key: string]: unknown } = {}; | ||
| for (const signalName in signalBus.signalDeps) { | ||
| allVars[signalName] = signalBus.signalDeps[signalName].value; | ||
| } | ||
| updateDisplay(allVars); | ||
| } | ||
| }, | ||
danmarshall marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| getCurrentSignalValue: () => { | ||
| // Inspector doesn't modify the signal, return undefined | ||
| return undefined; | ||
| }, | ||
| destroy: () => { | ||
| // No cleanup needed | ||
| }, | ||
| }; | ||
| }); | ||
| return instances; | ||
| }, | ||
| }; | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@copilot there is a typescript error here because those of type=spec do not have this name. please look at typescript compilation errors. Also I dont even think you need this, see packages\compiler\src\validate\data.ts
what was lacking a check for a variable colliding with a data source. please look at other files in this library before you implement junk like this again.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed in commit (will be in next push). Removed the incorrect duplicate validation from document.ts - you're right that the existing validation in data.ts already handles this properly. The issue was:
validateDataSource()in data.ts already checks for:Also fixed the demo to remove the section trying to inspect
salesData(which is a dataLoader, not a variable).There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed in commit 1d2ee64. Removed the incorrect duplicate validation - the existing validation in data.ts already handles this correctly.