From 273954ddb85090d1811b6f39c2c3392311ebcb41 Mon Sep 17 00:00:00 2001 From: andy-haynes <36863574+andy-haynes@users.noreply.github.com> Date: Fri, 22 Mar 2024 10:17:31 -0700 Subject: [PATCH] feat: add Container Messages tab to dev panel (#384) * restore trusted-author for StateAndTrust.Root * fix indent * dev panel tab for container messages * selection indicator * remove unused package * remove and ban console.log * feedback Co-authored-by: Pavel <156711570+pavelisnear@users.noreply.github.com> * feedback --------- Co-authored-by: Pavel <156711570+pavelisnear@users.noreply.github.com> --- apps/demos/src/StateAndTrust/Root.tsx | 2 +- apps/web/package.json | 1 + apps/web/src/components/ComponentMonitor.tsx | 284 ------------------ apps/web/src/components/Inspector.tsx | 11 + apps/web/src/components/Messaging.module.css | 58 ++++ apps/web/src/components/Messaging.tsx | 203 +++++++++++++ apps/web/src/components/WebEngineVariants.tsx | 10 +- apps/web/src/stores/container-messages.ts | 17 ++ .../src/components/SandboxedIframe.tsx | 2 +- packages/eslint-config-custom/index.js | 1 + pnpm-lock.yaml | 65 +++- 11 files changed, 362 insertions(+), 292 deletions(-) delete mode 100644 apps/web/src/components/ComponentMonitor.tsx create mode 100644 apps/web/src/components/Messaging.module.css create mode 100644 apps/web/src/components/Messaging.tsx create mode 100644 apps/web/src/stores/container-messages.ts diff --git a/apps/demos/src/StateAndTrust/Root.tsx b/apps/demos/src/StateAndTrust/Root.tsx index 428b7d34..b1585137 100644 --- a/apps/demos/src/StateAndTrust/Root.tsx +++ b/apps/demos/src/StateAndTrust/Root.tsx @@ -18,7 +18,7 @@ function StateAndTrust() {
{ - const source = component.componentId?.split('##')[0] || ''; - if (!componentsBySource[source]) { - componentsBySource[source] = []; - } - - componentsBySource[source].push(component); - return componentsBySource; - }, - {} as { [key: string]: ComponentInstance[] } - ); - - const sortedByFrequency = Object.entries(groupedComponents) as [ - string, - ComponentInstance[], - ][]; - sortedByFrequency.sort( - ([, aComponents], [, bComponents]) => - bComponents.length - aComponents.length - ); - - const reversedEvents = [...metrics.messages]; - reversedEvents.reverse(); - - const messageMetrics = metrics.messages.reduce((grouped, bweMessage) => { - const { message } = bweMessage; - if (!message) { - return grouped; - } - - if (!grouped.has(message.type)) { - grouped.set(message.type, []); - } - - grouped.set(message.type, [...grouped.get(message.type)!, bweMessage]); - return grouped; - }, new Map()); - - const displayMetrics = { - 'Containers Loaded': metrics.componentsLoaded.length, - 'Component Renders': messageMetrics.get('component.render')?.length || 0, - 'Updates Requested': messageMetrics.get('component.update')?.length || 0, - 'DOM Handlers Invoked': - messageMetrics.get('component.domCallback')?.length || 0, - 'Callbacks Invoked': - messageMetrics.get('component.callbackInvocation')?.length || 0, - 'Callbacks Returned': - messageMetrics.get('component.callbackResponse')?.length || 0, - }; - - const parseComponentId = (componentId: string): ComponentId | null => { - if (!componentId) { - return null; - } - - const [path, id, , ...ancestors] = componentId.split('##'); - const [author, name] = path.split('/'); - return { - author, - name, - path, - id, - parent: parseComponentId(ancestors.join('##')), - }; - }; - - const formatProps = (props: any, isRoot = false): any => { - if (!props || typeof props === 'number') { - return props; - } - - if (Array.isArray(props)) { - return `[${props.map((p) => formatProps(p)).join(', ')}]`; - } - - if (typeof props === 'object') { - const formatted = Object.entries(props) - .map(([k, v]) => `${k}=${formatProps(v)}`) - .join(', '); - if (isRoot) { - return formatted; - } - - return `{ ${formatted} }`; - } - - return `"${props.toString()}"`; - }; - - const formatComponentId = (componentId: ComponentId | null) => { - if (!componentId) { - return ''; - } - - if (!componentId.id) { - return componentId.name; - } - - return `${componentId.name}#${componentId.id}`; - }; - - const buildEventSummary = (params: BWEMessage): ComponentMessage | null => { - const { toComponent, fromComponent, message } = params; - const isFromComponent = fromComponent !== undefined; - switch (message.type) { - case 'component.render': { - const { type, props } = message.node; - const formattedChildren = message.childComponents?.length - ? `with children ${message.childComponents - .map(({ componentId }) => - formatComponentId(parseComponentId(componentId)) - ) - .join(', ')}` - : ''; - - return { - message, - isFromComponent, - badgeClass: 'bg-danger', - name: 'render', - componentId: parseComponentId(message.componentId)!, - summary: `rendered <${type} ${formatProps(props, true).slice( - 0, - 64 - )}...> ${formattedChildren}`, - }; - } - case 'component.callbackInvocation': { - const targetComponent = formatComponentId( - parseComponentId(message.targetId || '') - ); - const { requestId, method, args } = message; - return { - message, - isFromComponent, - badgeClass: 'bg-primary', - name: 'invoke', - componentId: parseComponentId(message.originator)!, - summary: `[${requestId.split('-')[0]}] called ${targetComponent}.${ - method.split('::')[0] - }(${args})${!isFromComponent ? ' for' : ''}`, - }; - } - case 'component.callbackResponse': { - const { requestId, result } = message; - return { - message, - isFromComponent, - badgeClass: 'bg-success', - name: 'return', - componentId: parseComponentId( - isFromComponent ? message.containerId : message.targetId - )!, - summary: `[${requestId.split('-')[0]}] returned ${result} ${ - !isFromComponent ? 'to' : '' - }`, - }; - } - case 'component.update': { - return { - message, - isFromComponent, - badgeClass: 'bg-warning', - name: 'update', - componentId: parseComponentId(toComponent!)!, - summary: `updated props ${JSON.stringify(message.props || {})} on`, - }; - } - case 'component.domCallback': { - return { - message, - isFromComponent, - badgeClass: 'bg-info', - name: 'DOM', - componentId: parseComponentId(toComponent!)!, - summary: `invoked event DOM handler [${ - message.method.split('::')[0] - }()] on`, - }; - } - default: - return null; - } - }; - - return ( -
-
-
Stats
-
Containers
-
Messages
-
-
-
- {Object.entries(displayMetrics).map(([label, value], i) => ( -
-
{label}
-
{value}
-
- ))} -
-
- {sortedByFrequency.map(([source, componentsBySource], i) => ( -
-
{source}
-
- {componentsBySource.length} -
-
- ))} -
-
- {reversedEvents.map(buildEventSummary).map( - (event: ComponentMessage | null, i) => - event && ( -
console.log(event.message)} - > - - {reversedEvents.length - i}| - - - {event.name} - - {!event.isFromComponent && ( - - Application - - )} - {event.isFromComponent && event.componentId && ( - - {formatComponentId(event.componentId)} - - )} -  {event.summary}  - {!event.isFromComponent && event.componentId && ( - - {formatComponentId(event.componentId)} - - )} -
- ) - )} -
-
-
- ); -} diff --git a/apps/web/src/components/Inspector.tsx b/apps/web/src/components/Inspector.tsx index f8b1ba20..ee40b800 100644 --- a/apps/web/src/components/Inspector.tsx +++ b/apps/web/src/components/Inspector.tsx @@ -6,8 +6,10 @@ import { oneDark } from 'react-syntax-highlighter/dist/cjs/styles/prism'; import s from './Inspector.module.css'; +import { Messaging } from '@/components/Messaging'; import { useMediaQuery } from '@/hooks/useMediaQuery'; import { useComponentSourcesStore } from '@/stores/component-sources'; +import { useContainerMessagesStore } from '@/stores/container-messages'; import { useFlagsStore } from '@/stores/flags'; import { usePortalStore } from '@/stores/portal'; @@ -33,6 +35,10 @@ export function Inspector() { [componentSources] ); + const containerMessages = useContainerMessagesStore( + (state) => state.messages + ); + // path of selected component, will need to be modified once we support version locking // since it will be possible to have multiple versions of the same component const [selectedComponent, setSelectedComponent] = useState(); @@ -99,6 +105,7 @@ export function Inspector() { Component Source + Container Messages Flags @@ -188,6 +195,10 @@ export function Inspector() {
+ + + +
{ + if (!componentId) { + return null; + } + + const [path, id, , ...ancestors] = componentId.split('##'); + const [author, name] = path.split('/'); + return { + author, + name, + path, + id, + parent: parseComponentId(ancestors.join('##')), + }; +}; + +const formatProps = (props: any, isRoot = false): any => { + if (!props || typeof props === 'number') { + return props; + } + + if (Array.isArray(props)) { + return `[${props.map((p) => formatProps(p)).join(', ')}]`; + } + + if (typeof props === 'object') { + const formatted = Object.entries(props) + .map(([k, v]) => `${k}=${formatProps(v)}`) + .join(', '); + if (isRoot) { + return formatted; + } + + return `{ ${formatted} }`; + } + + return `"${props.toString()}"`; +}; + +const formatComponentId = (componentId: ComponentId | null) => { + if (!componentId) { + return ''; + } + + if (!componentId.id) { + return componentId.name; + } + + return `${componentId.name.split('.').slice(1).join('.')}#${componentId.id}`; +}; + +const buildMessageSummary = (params: BWEMessage): ComponentMessage | null => { + const { toComponent, fromComponent, message } = params; + const isFromComponent = fromComponent !== undefined; + switch (message.type) { + case 'component.render': { + const { type, props } = message.node; + const formattedChildren = message.childComponents?.length + ? `with children ${message.childComponents + .map(({ componentId }) => + formatComponentId(parseComponentId(componentId)) + ) + .join(', ')}` + : ''; + + return { + message, + isFromComponent, + name: 'render', + componentId: parseComponentId(message.componentId)!, + summary: `rendered <${type} ${formatProps(props, true).slice( + 0, + 64 + )}...> ${formattedChildren}`, + }; + } + case 'component.callbackInvocation': { + const targetComponent = formatComponentId( + parseComponentId(message.targetId || '') + ); + const { requestId, method, args } = message; + return { + message, + isFromComponent, + name: 'invoke', + componentId: parseComponentId(message.originator)!, + summary: `[${requestId.split('-')[0]}] called ${targetComponent}.${ + method.split('::')[0] + }(${args})${isFromComponent ? '' : ' for'}`, + }; + } + case 'component.callbackResponse': { + const { requestId, result } = message; + return { + message, + isFromComponent, + name: 'return', + componentId: parseComponentId( + isFromComponent ? message.containerId : message.targetId + )!, + summary: `[${requestId.split('-')[0]}] returned ${result} ${ + isFromComponent ? '' : 'to' + }`, + }; + } + case 'component.update': { + return { + message, + isFromComponent, + name: 'update', + componentId: parseComponentId(toComponent!)!, + summary: `updated props ${JSON.stringify(message.props || {})} on`, + }; + } + case 'component.domCallback': { + return { + message, + isFromComponent, + name: 'DOM', + componentId: parseComponentId(toComponent!)!, + summary: `invoked message DOM handler [${ + message.method.split('::')[0] + }()] on`, + }; + } + default: + return null; + } +}; + +export function Messaging({ + containerMessages, +}: { + containerMessages: BWEMessage[]; +}) { + const [selectedMessage, setSelectedMessage] = useState(null); + + const reversedMessages = [...containerMessages]; + reversedMessages.reverse(); + + return ( +
+
+ {reversedMessages.map(buildMessageSummary).map( + (message: ComponentMessage | null, i) => + message && ( +
setSelectedMessage(message.message)} + > +
+ {reversedMessages.length - i} +
+
+
{message.name}
+
+
+ {!message.isFromComponent && 'Application'} + {message.isFromComponent && + message.componentId && + formatComponentId(message.componentId)} +
+
+ {!message.isFromComponent && + message.componentId && + formatComponentId(message.componentId)} +
+
+ ) + )} +
+
+ {selectedMessage && } +
+
+ ); +} diff --git a/apps/web/src/components/WebEngineVariants.tsx b/apps/web/src/components/WebEngineVariants.tsx index ebfd50bd..4677bc76 100644 --- a/apps/web/src/components/WebEngineVariants.tsx +++ b/apps/web/src/components/WebEngineVariants.tsx @@ -7,8 +7,8 @@ import { import { AccountState } from '@near-wallet-selector/core'; import { useEffect, useState } from 'react'; -import { useComponentMetrics } from '@/hooks/useComponentMetrics'; import { useComponentSourcesStore } from '@/stores/component-sources'; +import { useContainerMessagesStore } from '@/stores/container-messages'; const PREACT_VERSION = '10.17.1'; @@ -24,8 +24,8 @@ export function WebEngine({ rootComponentPath, flags, }: WebEnginePropsVariantProps) { - const { /* metrics, */ reportMessage } = useComponentMetrics(); const addSource = useComponentSourcesStore((store) => store.addSource); + const addMessage = useContainerMessagesStore((store) => store.addMessage); const { components, error } = useWebEngine({ config: { @@ -37,7 +37,7 @@ export function WebEngine({ hooks: { containerSourceCompiled: ({ componentPath, rawSource }) => addSource(componentPath, rawSource), - messageReceived: reportMessage, + messageReceived: addMessage, }, }, rootComponentPath, @@ -60,8 +60,8 @@ export function SandboxWebEngine({ rootComponentPath, flags, }: WebEnginePropsVariantProps) { - const { /* metrics, */ reportMessage } = useComponentMetrics(); const addSource = useComponentSourcesStore((store) => store.addSource); + const addMessage = useContainerMessagesStore((store) => store.addMessage); const [localComponents, setLocalComponents] = useState(); @@ -99,7 +99,7 @@ export function SandboxWebEngine({ hooks: { containerSourceCompiled: ({ componentPath, rawSource }) => addSource(componentPath, rawSource), - messageReceived: reportMessage, + messageReceived: addMessage, }, }, localComponents, diff --git a/apps/web/src/stores/container-messages.ts b/apps/web/src/stores/container-messages.ts new file mode 100644 index 00000000..73b1a38e --- /dev/null +++ b/apps/web/src/stores/container-messages.ts @@ -0,0 +1,17 @@ +import type { BWEMessage } from '@bos-web-engine/application'; +import { create } from 'zustand'; + +interface ContainerMessagesState { + messages: BWEMessage[]; + addMessage: (message: BWEMessage) => void; + clearMessages: () => void; +} + +export const useContainerMessagesStore = create( + (set) => ({ + messages: [], + addMessage: (message) => + set((state) => ({ messages: [...state.messages, message] })), + clearMessages: () => set({ messages: [] }), + }) +); diff --git a/packages/application/src/components/SandboxedIframe.tsx b/packages/application/src/components/SandboxedIframe.tsx index 81738275..35ce6bdc 100644 --- a/packages/application/src/components/SandboxedIframe.tsx +++ b/packages/application/src/components/SandboxedIframe.tsx @@ -36,7 +36,7 @@ function buildSandboxedComponent({ }