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 (
+
+
+
+
+
+ {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,
+ });
+}