diff --git a/packages/inula-dev-tools/src/panelX/ActionRunner.tsx b/packages/inula-dev-tools/src/panelX/ActionRunner.tsx new file mode 100644 index 00000000..7b76145b --- /dev/null +++ b/packages/inula-dev-tools/src/panelX/ActionRunner.tsx @@ -0,0 +1,125 @@ +/* + * Copyright (c) 2023 Huawei Technologies Co.,Ltd. + * + * openInula is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * + * http://license.coscl.org.cn/MulanPSL2 + * + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +import Inula, { useState } from 'openinula'; +import { Modal } from './Modal'; +import { highlight, sendMessage } from './utils'; + +function executeAction(storeId: string, name: string, args: any[]) { + sendMessage({ + type: 'inulax run action', + tabId: chrome.devtools.inspectedWindow.tabId, + storeId, + action: name, + args, + }); +} + +function queryAction(storeId: string, name: string, args: any[]) { + sendMessage({ + type: 'inulax queue action', + tabId: chrome.devtools.inspectedWindow.tabId, + storeId, + action: name, + args, + }); +} + +export function ActionRunner({ foo, storeId, actionName }) { + const [data, setState] = useState({ + modal: false, + gatheredAttrs: [], + query: false, + }); + const modalIsOpen = data.modal; + const gatheredAttrs = data.gatheredAttrs; + function setData(val) { + const newData = { + modal: data.modal, + gatheredAttrs: data.gatheredAttrs, + }; + + Object.entries(val).forEach(([key, value]) => (newData[key] = value)); + + setState(newData as any); + } + + const plainFunction = foo.replace(/\{.*}/gms, ''); + const attributes = plainFunction + .replace(/^.*\(/g, '') + .replace(/\).*$/, '') + .split(/, ?/) + .filter((item, index) => index > 0); + + return ( + <> + { + if (attributes.length > 0) { + setData({ modal: false, gatheredAttrs: [], query: false }); + } else { + executeAction(storeId, actionName, gatheredAttrs); + } + }} + > + + ☼ + { + e.preventDefault(); + if (attributes.len > 0) { + setData({ modal: true, gatheredAttrs: [], query: true }); + } else { + queryAction(storeId, actionName, gatheredAttrs); + } + }} + > + ⌛︎{' '} + + + + {plainFunction} + {' {...}'} + + + {modalIsOpen ? ( + { + setData({ modal: false }); + }} + then={data => { + if (gatheredAttrs.length === attributes.length - 1) { + setData({ modal: false }); + executeAction(storeId, actionName, gatheredAttrs.concat(data)); + } else { + setData({ + gatheredAttrs: gatheredAttrs.concat([data]), + }); + } + }} + > +

{data.query ? 'Query action:' : 'Run action:'}

+

{highlight(plainFunction, attributes[gatheredAttrs.length])}

+
+ ) : null} + + ); +} diff --git a/packages/inula-dev-tools/src/panelX/DiffTree.tsx b/packages/inula-dev-tools/src/panelX/DiffTree.tsx new file mode 100644 index 00000000..05c55cfd --- /dev/null +++ b/packages/inula-dev-tools/src/panelX/DiffTree.tsx @@ -0,0 +1,335 @@ +/* + * Copyright (c) 2023 Huawei Technologies Co.,Ltd. + * + * openInula is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * + * http://license.coscl.org.cn/MulanPSL2 + * + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +import { useState } from 'openinula'; +import styles from './PanelX.less'; +import { Tree } from './Tree'; +import {displayValue, omit} from './utils'; + +type Mutation = { + mutation: boolean; + items?: Mutation[]; + attributes?: { [key: string]: Mutation }; + values?: Mutation[]; + entries?: Mutation[]; + from?: any; + to?: any; +}; + +export function DiffTree({ + mutation, + indent = 0, + index = '', + expand = false, + search = '', + forcedExpand = false, + omitAttrs = [], + doNotDisplayIcon = false, + forcedLabel = null, + className, +}: { + mutation: Mutation; + indent: number; + index?: string | number; + expand?: boolean; + search: string; + forcedExpand?: boolean; + omitAttrs: string[]; + doNotDisplayIcon?: boolean; + forcedLabel?: string | number | null; + className?: string; +}) { + if (omitAttrs.length && mutation.attributes) { + mutation.attributes = omit(mutation.attributes, ...omitAttrs); + mutation.from = mutation.from && omit(mutation.from, ...omitAttrs); + mutation.to = mutation.to && omit(mutation.to, ...omitAttrs); + } + const [expanded, setExpanded] = useState(expand); + + const deleted = mutation.mutation && !('to' in mutation); + const newValue = mutation.mutation && !('from' in mutation); + const mutated = mutation.mutation; + + const isArray = mutated && mutation.items; + const isObject = mutated && mutation.attributes; + const isMap = mutated && mutation.entries; + const isSet = mutated && mutation.values; + const isPrimitive = !isArray && !isObject && !isMap && !isSet; + + if (!mutated) { + return ( + + ); + } + + if (newValue) { + return ( + + ); + } + + if (deleted) { + return ( + + ); + } + + return ( +
{ + e.stopPropagation(); + }} + > + { + setExpanded(!expanded); + }} + > + {new Array(Math.max(indent, 0)).fill( )} + {isPrimitive ? ( + // 如果两个 value 是基本变量并且不同,则简单显示不同点 +
{ + e.stopPropagation(); + }} + > + + +
+ ) : ( + // 如果至少有一个是复杂变量,则需要展开按钮 + <> + {forcedExpand ? '' : expanded ? : } + {index === 0 || index ? ( + {displayValue(index, search)}: + ) : ( + '' + )} + {isArray ? ( + // 如果都是数组进行比较 + expanded ? ( + [ + Array(Math.max(mutation.from.length, mutation.to.length)) + .fill(true) + .map((i, index) => { + return ( +
+ {mutation.items[index].mutation ? ( + + ) : ( + + )} +
+ ); + }), + ] + ) : ( + forcedLabel || `Array(${mutation.to?.length})` + ) + ) : isSet ? ( + expanded ? ( +
+
+ {forcedLabel || `Set(${mutation.to?.values.length})`} +
+ {Array( + Math.max( + mutation.from?.values.length, + mutation.to?.values.length + ) + ) + .fill(true) + .map((i ,index) => ( +
+ {mutation.values[index].mutation ? ( + + ) : ( + + )} +
+ ))} +
+ ) : ( + + {forcedLabel || `Set(${mutation.to?.values.length})`} + + ) + ) : isMap ? ( + expanded ? ( + <> + + {forcedLabel || `Map(${mutation.to?.entries.length})`} + + {Array( + Math.max( + mutation.from?.entries.length, + mutation.to?.entries.length + ) + ) + .fill(true) + .map((i, index) => + mutation.entries[index].mutation ? ( +
+ +
+ ) : ( +
+ +
+ ) + )} + + ) : ( + + {forcedLabel || `Map(${mutation.to?.entries.length})`} + + ) + ) : expanded ? ( + // 如果都是 object 进行比较 + Object.entries(mutation.attributes).map(([key, item]) => { + return item.mutation ? ( + e.stopPropagation()}> + { + + } + + ) : ( + e.stopPropagation()}> + { + + } + + ); + }) + ) : ( + forcedLabel || '{ ... }' + )} + + )} +
+
+ ); +} diff --git a/packages/inula-dev-tools/src/panelX/EventLog.tsx b/packages/inula-dev-tools/src/panelX/EventLog.tsx new file mode 100644 index 00000000..265f3243 --- /dev/null +++ b/packages/inula-dev-tools/src/panelX/EventLog.tsx @@ -0,0 +1,402 @@ +/* + * Copyright (c) 2023 Huawei Technologies Co.,Ltd. + * + * openInula is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * + * http://license.coscl.org.cn/MulanPSL2 + * + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +import { useEffect, useState, useRef } from 'openinula'; +import { DevToolPanel } from '../utils/constants'; +import { + initBackgroundConnection, + addBackgroundMessageListener, + removeBackgroundMessageListener, +} from '../panelConnection'; +import { Table } from './Table'; +import { Tree } from './Tree'; +import {fullTextSearch, omit} from './utils'; +import styles from './PanelX.less'; +import { Checkbox } from '../utils/Checkbox'; +import { DiffTree } from './DiffTree'; + +const eventTypes = { + INITIALIZED: 'inulax store initialized', + STATE_CHANGE: 'inulax state change', + SUBSCRIBED: 'inulax subscribed', + UNSUBSCRIBED: 'inulax unsubscribed', + ACTION: 'inulax action', + ACTION_QUEUED: 'inulax action queued', + QUEUE_PENDING: 'inulax queue pending', + QUEUE_FINISHED: 'inulax queue finished', +}; + +const otherTypes = { + GET_EVENTS: 'inulax getEvents', + GET_PERSISTENCE: 'inulax getPersistence', + EVENTS: 'inulax events', + FLUSH_EVENTS: 'inulax flush events', + SET_PERSISTENT: 'inulax setPersistent', + RESET_EVENTS: 'inulax resetEvents', +}; + +function extractDataByType(message, search) { + if (message.type === eventTypes.ACTION) { + return ( +
{ + e.stopPropagation(); + }} + > + +
+ ); + } + + if (message.type === eventTypes.STATE_CHANGE) { + return ( +
{ + e.stopPropagation(); + }} + > + {`${message.data.change.vNodes.length} nodes changed:`} + { + return ( + + {vNode.type}() + + ); + })} + /> +
+ ); + } + + return N/A +} + +export default function EventLog({ setNextStore, setEventFilter, eventFilter }) { + const [log, setLog] = useState([]); + const [initlized, setInitlized] = useState(false); + const [persistent, setPersistent] = useState(false); + const filterField = useRef(null); + + const addFilter = (key, value) => { + const filters = { ...eventFilter }; + filters[key] = value; + + setEventFilter(filters); + }; + + const removeFilter = key => { + const filters = { ...eventFilter }; + delete filters[key]; + + setEventFilter(filters); + }; + + if (!initlized) { + setTimeout(() => { + chrome.runtime.sendMessage({ + type: 'INULA_DEV_TOOLS', + payload: { + type: otherTypes.GET_EVENTS, + tabId: chrome.devtools.inspectedWindow.tabId, + }, + from: DevToolPanel, + }); + + chrome.runtime.sendMessage({ + type: 'INULA_DEV_TOOLS', + payload: { + type: otherTypes.GET_PERSISTENCE, + tabId: chrome.devtools.inspectedWindow.tabId, + }, + from: DevToolPanel, + }); + }, 100); + } + + useEffect(() => { + const lisener = message => { + if (message.payload.type.startsWith('inulax')) { + if (message.payload.type === otherTypes.EVENTS) { + setLog(message.payload.events); + setInitlized(true); + } else if (message.payload.type === otherTypes.SET_PERSISTENT) { + setPersistent(message.payload.persistent); + } else if (message.payload.type === otherTypes.FLUSH_EVENTS) { + chrome.runtime.sendMessage({ + type: 'INULA_DEV_TOOLS', + payload: { + type: otherTypes.GET_EVENTS, + tabId: chrome.devtools.inspectedWindow.tabId, + }, + from: DevToolPanel, + }); + } + } + }; + initBackgroundConnection('panel'); + addBackgroundMessageListener(lisener); + return () => { + removeBackgroundMessageListener(lisener); + }; + }); + + const filters = Object.entries(eventFilter); + const usedTypes = { all: 0 }; + + const processedData = log + .filter(event => { + if (!Object.values(eventTypes).includes(event.message.type)) { + return false; + } + usedTypes.all++; + if (!usedTypes[event.message.type]) { + usedTypes[event.message.type] = 1; + } else { + usedTypes[event.message.type]++; + } + + if (!filters.length) { + return true; + } + + return !filters.some(([key, value]) => { + if (key === 'fulltext') { + const result = fullTextSearch(event, value); + return !result; + } + const keys = key.split('.'); + let search = event; + keys.forEach(attr => { + search = search[attr]; + }); + return value !== search; + }); + }) + .map(event => { + const date = new Date(event.timestamp); + + return { + id: event.id, + timestamp: event.timestamp, + type: event.message.type, + time: `${date.toLocaleTimeString()} - ${date.toLocaleDateString()}`, + state: event.message.type === eventTypes.STATE_CHANGE ? ( + + ) : ( + + ), + storeClick: ( + { + e.preventDefault(); + setNextStore(event.message.data.store.id); + }} + > + {event.message.data.store.id} + + ), + additionalData: extractDataByType( + event.message, + eventFilter['fulltext'] + ), + storeId: event.message.data.store.id, + event, + }; + }); + + return ( +
+
+ { + if (!filterField.current.value) { + removeFilter('fulltext'); + } + addFilter('fulltext', filterField.current.value); + }} + /> + + {' | '} + { + e.stopPropagation(); + + chrome.runtime.sendMessage({ + type: 'INULA_DEV_TOOLS', + payload: { + type: otherTypes.SET_PERSISTENT, + tabId: chrome.devtools.inspectedWindow.tabId, + persistent: !persistent, + }, + from: DevToolPanel, + }); + + setPersistent(!persistent); + }} + > + Persistent events + + {' | '} + + {eventFilter['message.data.store.id'] ? ( + + {' | '} + { + setNextStore(eventFilter['message.data.store.id']); + }} + >{` Displaying: [${eventFilter['message.data.store.id']}] `} + + + ) : null} +
+
+ + {Object.values(eventTypes).map(eventType => { + return ( + + ); + })} +
+ { + const message = data.event.message; + return { + type: data.type, + store: { + actions: Object.fromEntries( + Object.entries(message.data.store.$config.actions).map( + ([id, action]) => { + return [ + id, + (action as string).replace(/\{.*}/gms, '{...}').replace('function ', ''), + ]; + } + ) + ), + computed: Object.fromEntries( + Object.keys(message.data.store.$c).map(key => [ + key, + message.data.store.expanded[key], + ]) + ), + state: message.data.store.$s, + id: message.data.store.id, + }, + // data: omit(data, 'storeClick', 'additionalData'), + }; + }} + search={eventFilter.fulltext ? eventFilter.fulltext : ''} + /> + + ); +} diff --git a/packages/inula-dev-tools/src/panelX/Modal.tsx b/packages/inula-dev-tools/src/panelX/Modal.tsx new file mode 100644 index 00000000..267d5493 --- /dev/null +++ b/packages/inula-dev-tools/src/panelX/Modal.tsx @@ -0,0 +1,103 @@ +/* + * Copyright (c) 2023 Huawei Technologies Co.,Ltd. + * + * openInula is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * + * http://license.coscl.org.cn/MulanPSL2 + * + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +import Inula, { useRef, useState } from 'openinula'; + +export function Modal({ + closeModal, + then, + children, +}: { + closeModal: () => void; + then: (value: any) => void; + children?: any[]; +}) { + const inputRef = useRef(null); + const [error, setError] = useState(null); + + setTimeout(() => { + inputRef.current.focus(); + inputRef.current.value = ''; + }, 10); + + const tryGatherData = () => { + let data; + try { + data = eval(inputRef.current.value); + } catch (err) { + setError(err); + return; + } + + if (then) { + then(data); + } + }; + + return ( +
+
+

{children}

+

+ { + if (key === 'Enter') { + tryGatherData(); + } + }} + /> +

+ {error ?

Variable parsing error

: null} +

+ + +

+
+
+ ); +} diff --git a/packages/inula-dev-tools/src/panelX/PanelX.less b/packages/inula-dev-tools/src/panelX/PanelX.less new file mode 100644 index 00000000..04656c3c --- /dev/null +++ b/packages/inula-dev-tools/src/panelX/PanelX.less @@ -0,0 +1,197 @@ +/* + * Copyright (c) 2023 Huawei Technologies Co.,Ltd. + * + * openInula is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * + * http://license.coscl.org.cn/MulanPSL2 + * + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +@import '../components/assets.less'; + +.displayData { + background-color: rgb(241, 243, 244); +} + +.app { + display: flex; + flex-direction: row; + height: 100%; + font-size: @common-font-size; +} + +div.wrapper { + margin: 15px; + position: relative; + width: calc(100% - 30px); + display: block; +} + +div.table { + display: table; + vertical-align: top; + width: calc(100%); + background-color: white; + position: relative; +} + +div.row { + display: table-row; + + &:nth-child(2n + 1) { + background-color: rgb(241, 243, 244); + .default { + background-color: rgb(241, 243, 244); + } + } +} + +div.cell { + display: table-cell; + cursor: pointer; + padding: 5px; +} + +div.half { + width: calc(50% - 8px); + float: left; +} + +div.header { + background-color: rgb(241, 243, 244); + font-weight: bold; +} + +div.row.active { + background-color: #00a; + color: white; +} + +button.tab { + border: 1px solid grey; + border-top-left-radius: 5px; + border-top-right-radius: 5px; + &.active { + border-bottom: none; + color: black; + font-weight: bold; + background-color: white; + } +} + +span.highlighted { + background-color: #ff0; +} + +.grey { + color: grey; +} + +.red { + color: #a00; +} + +.blue { + color: #00a; +} + +.purple { + color: #909; +} + +.bold { + font-weight: bold; +} + +.link { + font-weight: bold; + text-decoration: underline; + cursor: pointer; + color: #00a; +} + +.compositeInput { + background-color: white; + border: 1px solid grey; + display: inline-block; + border-radius: 0; + padding: 5px; + &.left { + border-right: 0; + margin-right: 0; + padding-right: 0; + } + + &.right { + border-left: 0; + margin-left: 0; + } + + &:focus-visible { + outline: none; + } +} + +.filterButton { + background-color: transparent; + padding: 5px; + border-radius: 5px; + border: 0; + &.active { + background-color: #ddd; + } +} + +.added { + background-color: #afa; + &::before { + font-weight: bold; + color: #0a0; + } +} + +.deleted { + background-color: #faa; + text-decoration-line: line-through; + &::before { + font-weight: bold; + color: #a00; + } +} + +.changed { + background-color: #ffa; + &::before { + font-weight: bold; + color: #ca0; + } +} + +.default { + background-color: white; +} + +.floatingButton { + right: 5px; + position: absolute; + height: 17px; + width: 17px; + font-size: 10px; + padding: 0; + cursor: pointer; +} + +.scrollable { + max-height: calc(100vh - 65px); + overflow: auto; + + div.row { + display: block; + } +} diff --git a/packages/inula-dev-tools/src/panelX/PanelX.tsx b/packages/inula-dev-tools/src/panelX/PanelX.tsx new file mode 100644 index 00000000..b3038c86 --- /dev/null +++ b/packages/inula-dev-tools/src/panelX/PanelX.tsx @@ -0,0 +1,84 @@ +/* + * Copyright (c) 2023 Huawei Technologies Co.,Ltd. + * + * openInula is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * + * http://license.coscl.org.cn/MulanPSL2 + * + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +import { useState } from 'openinula'; +import EventLog from './EventLog'; +import Stores from './Stores'; +import styles from './PanelX.less'; + +export default function PanelX() { + const [active, setActive] = useState('stores'); + const [nextStoreId, setNextStoreId] = useState(null); + const [eventFilter, setEventFilter] = useState({}); + + function showFilterEvents(filter) { + setActive('events'); + setEventFilter(filter); + } + + const tabs = [ + { + id: 'stores', + title: 'Stores', + getComponent: () => ( + + ), + }, + { + id: 'events', + title: 'Events', + getComponents: () => ( + { + setNextStoreId(id); + setActive('stores'); + }} + setEventFilter={setEventFilter} + eventFilter={eventFilter} + /> + ), + }, + ]; + + return ( +
+
+ {tabs.map(tab => + tab.id === active ? ( + + ) : ( + + ) + )} +
+ {tabs.find(item => item.id === active).getComponent()} +
+ ); +} diff --git a/packages/inula-dev-tools/src/panelX/Stores.tsx b/packages/inula-dev-tools/src/panelX/Stores.tsx new file mode 100644 index 00000000..aa7c60e5 --- /dev/null +++ b/packages/inula-dev-tools/src/panelX/Stores.tsx @@ -0,0 +1,74 @@ +/* + * Copyright (c) 2023 Huawei Technologies Co.,Ltd. + * + * openInula is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * + * http://license.coscl.org.cn/MulanPSL2 + * + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +import { useState, useEffect } from 'openinula'; +import { + initBackgroundConnection, + addBackgroundMessageListener, + removeBackgroundMessageListener, + postMessageToBackground, +} from '../panelConnection'; +import { Table } from './Table'; +import { omit, sendMessage } from './utils'; +import styles from './PanelX.less'; +import { Highlight, RemoveHighlight } from '../utils/constants'; +import { ActionRunner } from './ActionRunner'; +import { Tree } from './Tree'; + +export default function Stores({ nextStoreId, showFilteredEvents }) { + const [stores, setStores] = useState([]); + const [initialized, setInitialized] = useState(false); + + if (!initialized) { + setTimeout(() => { + sendMessage({ + type: 'inulax getStores', + tabId: chrome.devtools.inspectedWindow.tabId, + }); + }, 100); + } + + useEffect(() => { + const listener = message => { + if (message.payload.type.startsWith('inulax')) { + // 过滤 inula 消息 + if (message.payload.type === 'inulax stores') { + // Stores 更新 + setStores(message.payload.stores); + setInitialized(true); + } else if (message.payload.type === 'inulax flush stores') { + // Flush store + sendMessage({ + type: 'inulax getStores', + tabId: chrome.devtools.inspectedWindow.tabId, + }); + } else if (message.payload.type === 'inulax observed components') { + // observed components 更新 + setStores( + stores.map(store => { + store.observedComponents = message.payload.data[store.id] || []; + return store; + }) + ); + } + } + }; + initBackgroundConnection('panel'); + addBackgroundMessageListener(listener); + return () => { + removeBackgroundMessageListener(listener); + }; + }); +} diff --git a/packages/inula-dev-tools/src/panelX/Table.tsx b/packages/inula-dev-tools/src/panelX/Table.tsx new file mode 100644 index 00000000..81ab7248 --- /dev/null +++ b/packages/inula-dev-tools/src/panelX/Table.tsx @@ -0,0 +1,164 @@ +/* + * Copyright (c) 2023 Huawei Technologies Co.,Ltd. + * + * openInula is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * + * http://license.coscl.org.cn/MulanPSL2 + * + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +import { useState } from 'openinula'; +import {Tree} from './Tree'; +import styles from './PanelX.less'; + +type displayKeysType = [string, string][]; + +export function Table({ + data, + dataKey = 'id', + displayKeys, + activate, + displayDataProcessor, + search = '', +}: { + data; + dataKey: string; + displayKeys: displayKeysType; + activate?: { + [key: string]: any; + }; + displayDataProcessor: (data: { [key: string]: any }) => { + [key: string]: any; + }; + search: string; +}) { + const [keyToDisplay, setKeyToDisplay] = useState(false); + const [manualOverride, setManualOverride] = useState(false); + let displayRow = null; + + if (!manualOverride && activate) { + data.forEach(item => { + if (displayRow) { + return; + } + let notMatch = false; + Object.entries(activate).forEach(([key, value]) => { + if (notMatch) { + return; + } + if (item[key] !== value) { + notMatch = true; + } + }); + if (notMatch) { + return; + } + displayRow = item; + }); + } else if (manualOverride && keyToDisplay) { + data.forEach(item => { + if (displayRow) { + return; + } + if (item[dataKey] === keyToDisplay) { + displayRow = item; + } + }); + } + + if (displayRow) { + const [attr, title] = displayKeys[0]; + return ( +
+
+
+
{title}
+
+
+ + {data.map(row => ( +
{ + setManualOverride(true); + setKeyToDisplay( + keyToDisplay === row[dataKey] ? null : row[dataKey] + ); + }} + > +
{row?.[attr] || ''}
+
+ ))} +
+
+ +
+
+
+ Data: + +
+
+
+ +
+
+ +
+
+
+
+
+ ); + } else { + return ( +
+
+
+ {displayKeys.map(([key, title]) => ( +
{title}
+ ))} +
+ {data.map(item => ( +
{ + setManualOverride(true); + setKeyToDisplay(item[dataKey]); + }} + className={styles.row} + > + {displayKeys.map(([key, title]) => ( +
{item[key]}
+ ))} +
+ ))} +
+
+ ); + } +} diff --git a/packages/inula-dev-tools/src/panelX/Tree.tsx b/packages/inula-dev-tools/src/panelX/Tree.tsx new file mode 100644 index 00000000..3091c1a4 --- /dev/null +++ b/packages/inula-dev-tools/src/panelX/Tree.tsx @@ -0,0 +1,230 @@ +/* + * Copyright (c) 2023 Huawei Technologies Co.,Ltd. + * + * openInula is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * + * http://license.coscl.org.cn/MulanPSL2 + * + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +import { useState } from 'openinula'; +import styles from './PanelX.less'; +import {Modal} from './Modal'; +import { displayValue, omit } from './utils'; + +export function Tree({ + data, + indent = 0, + index = '', + expand = false, + search = '', + forcedExpand = false, + onEdit = null, + omitAttrs = [], + className, + forcedLabel = null, +}: { + data: any; + indent?: number; + index?: string | number; + expand?: boolean; + search?: string; + forcedExpand?: boolean; + className?: string | undefined + omitAttrs?: string[]; + onEdit?: (path: any[], value: any) => void | null; + forcedLabel?: string | number | null; +}) { + const [expanded, setExpanded] = useState(expand); + const [modal, setModal] = useState(false); + const isArray = Array.isArray(data); + const isObject = data && !isArray && typeof data === 'object'; + const isSet = isObject && data?._type === 'Set'; + const isWeakSet = isObject && data?._type === 'WeakSet'; + const isMap = isObject && data?._type === 'Map'; + const isWeakMap = isObject && data?._type === 'WeakMap'; + const isVNode = isObject && data.vtype; + const canBeExpanded = isArray || (isObject && !isWeakSet && !isWeakMap); + + if (isObject && omitAttrs?.length) { + data = omit(data, ...omitAttrs); + } + + return canBeExpanded ? ( +
{ + e.stopPropagation(); + }} + > + { + setExpanded(!expanded); + }} + > + {new Array(Math.max(indent, 0)).fill( )} + {forcedExpand || isVNode ? null : expanded ? ( + + ) : ( + + )} + {index === 0 || index ? ( + <> + {displayValue(index, search)}: + + ) : ( + '' + )} + {forcedLabel + ? forcedLabel + : expanded + ? isVNode + ? null + : Array.isArray(data) + ? `Array(${data.length})` + : isMap + ? `Map(${data.entries.length})` + : isSet + ? `Set(${data.values.length})` + : '{ ... }' + : isWeakMap + ? 'WeakMap()' + : isWeakSet + ? 'WeakSet()' + : isMap + ? `Map(${data.entries.length})` + : isSet + ? `Set(${data.values.length})` + : Array.isArray(data) + ? `Array(${data.length})` + : '{ ... }'} + + {expanded || isVNode ? ( + isArray ? ( + <> + {data.map((value, index) => { + return ( +
+ { + onEdit(path.concat([index]), val); + } + : null + } + /> +
+ ); + })} + + ) : isVNode ? ( + data + ) : isMap ? ( +
+ {data.entries.map(([key, value]) => { + return ( + + ); + })} +
+ ) : isSet ? ( + data.values.map(item => { + return ( +
+ +
+ ); + }) + ) : ( + Object.entries(data).map(([key, value]) => { + return ( +
+ { + onEdit(path.concat([key]), val); + } + : null + } + /> +
+ ); + }) + ) + ) : ( + '' + )} +
+ ) : ( +
+ {new Array(indent).fill( )} + + {typeof index !== 'undefined' ? ( + <> + {displayValue(index, search)}: + + ) : ( + '' + )} + {displayValue(data, search)} + {onEdit && !isWeakSet && !isWeakMap ? ( // TODO: editable weak set and map + <> + { + setModal(true); + }} + > + ☼ + + {onEdit && modal ? ( + { + setModal(false); + }} + then={data => { + onEdit([], data); + setModal(false); + }} + > +

Edit value:

{index} +
+ ) : null} + + ) : null} +
+
+ ); +} diff --git a/packages/inula-dev-tools/src/panelX/index.tsx b/packages/inula-dev-tools/src/panelX/index.tsx new file mode 100644 index 00000000..7e46144c --- /dev/null +++ b/packages/inula-dev-tools/src/panelX/index.tsx @@ -0,0 +1,18 @@ +/* + * Copyright (c) 2023 Huawei Technologies Co.,Ltd. + * + * openInula is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * + * http://license.coscl.org.cn/MulanPSL2 + * + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +import PanelX from './PanelX'; + +export default PanelX; diff --git a/packages/inula-dev-tools/src/panelX/panelX.html b/packages/inula-dev-tools/src/panelX/panelX.html new file mode 100644 index 00000000..4a211dcf --- /dev/null +++ b/packages/inula-dev-tools/src/panelX/panelX.html @@ -0,0 +1,49 @@ + + + + + + + + + + + + + +
+ + + + diff --git a/packages/inula-dev-tools/src/panelX/utils.tsx b/packages/inula-dev-tools/src/panelX/utils.tsx new file mode 100644 index 00000000..1f0d807f --- /dev/null +++ b/packages/inula-dev-tools/src/panelX/utils.tsx @@ -0,0 +1,228 @@ +/* + * Copyright (c) 2023 Huawei Technologies Co.,Ltd. + * + * openInula is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * + * http://license.coscl.org.cn/MulanPSL2 + * + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +import * as Inula from 'openinula'; +import styles from './PanelX.less'; +import { DevToolPanel } from '../utils/constants'; + +export function highlight(source, search) { + if (!search || !source?.split) { + return source; + } + + const parts = source.split(search); + const result = []; + + for (let i= 0; i < parts.length * 2 - 1; i++) { + if (i % 2) { + result.push({search}); + } else { + result.push(parts[i / 2]); + } + } + + return result; +} + +export function displayValue(val: any, search = '') { + if (typeof val === 'boolean') { + return ( + + {highlight(val ? 'true' : 'false', search)} + + ); + } + + if (val === '') { + return {'""'}; + } + + if (typeof val === 'undefined') { + return {highlight('undefined', search)}; + } + + if (val === 'null') { + return {highlight('null', search)}; + } + + if (typeof val === 'string') { + if (val.match(/^function\s?\(/)) { + return ( + + ƒ + {highlight( + val.match(/^function\s?\([\w,]*\)/g)[0].replace(/^function\s?/, ''), + search + )} + + ); + } + return "{highlight(val, search)}"; + } + if (typeof val === 'number') { + return {highlight('' + val, search)}; + } + if (typeof val === 'function') { + const args = val.toString().match(/^function\s?\([\w,]*\)/g)[0].replace(/^function\s?/, ''); + return ( + + ƒ + {highlight(args, search)} + + ); + } + if (typeof val === 'object') { + if (val?._type === 'WeakSet') { + return WeakSet(); + } + + if (val?._type === 'WeakMap') { + return WeakMap(); + } + } +} + +export function fullTextSearch(value, search) { + if (!value) { + return false; + } + + if (Array.isArray(value)) { + return value.some(val => fullTextSearch(val, search)); + } + + if (typeof value === 'object') { + if (value?._type === 'Set') { + return value.values.some(val => fullTextSearch(val, search)); + } + if (value?._type === 'Map') { + return value.entries.some( + (key, val) => fullTextSearch(key, search) || fullTextSearch(val, search) + ); + } + return Object.values(value).some(val => fullTextSearch(val, search)); + } + + return value.toString().includes(search); +} + +export function omit(obj, ...attrs) { + const res = { ...obj }; + attrs.forEach(attr => delete res[attr]); + return res; +} + +export function stringify(data) { + if (typeof data === 'string' && data.startsWith('function(')) { + return ( + + ƒ + {data.match(/^function\([\w,]*\)/g)[0].substring(8)} + + ); + } + + if (!data) { + return displayValue(data); + } + + if (Array.isArray(data)) { + return `Array(${data.length})`; + } + + if (typeof data === 'object') { + return `{${Object.entries(data).map(([key, value]) => { + if (typeof value === 'string' && value.startsWith('function(')) { + return ( + + {key} + + ƒ + {value.match(/^function\([\w,]*\)/g)[0].substring(8)} + + + ); + } + if (!value) { + return ( + + {key}:{displayValue(value)} + + ); + } + if (Array.isArray(value)) { + return ( + + {key}:{' '} + {`Array(${value.length})`} + + ); + } + if (typeof value === 'object') { + if ((value as any)?._type === 'WeakSet') { + return ( + + {key}: {'WeakSet()'} + + ); + } + if ((value as any)?._type === 'WeakMap') { + return ( + + {key}: {'WeakMap'} + + ); + } + if ((value as any)?._type === 'Set') { + return ( + + {key}:{' '} + {`Set(${(value as Set).size})`} + + ); + } + if ((value as any)?._type === 'Map') { + return ( + + {key}:{' '} + {`Map(${(value as Map).size})`} + + ); + } + + // object + return ( + + {key}: {'{...}'} + + ); + } + return ( + + {key}: {displayValue(value)} + + ); + })}}`; + } + return data; +} + +export function sendMessage(payload) { + chrome.runtime.sendMessage({ + type: 'INULA_DEV_TOOLS', + payload, + from: DevToolPanel, + }); +}