!84 [inula-dev-tools]<feat> 状态管理器调试页面合入

Merge pull request !84 from 涂旭辉/master
This commit is contained in:
openInula-robot 2023-11-28 07:49:23 +00:00 committed by Gitee
commit 02be3494b5
No known key found for this signature in database
GPG Key ID: 173E9B9CA92EEF8F
12 changed files with 2009 additions and 0 deletions

View File

@ -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 (
<>
<span
title={'Run action'}
onClick={() => {
if (attributes.length > 0) {
setData({ modal: false, gatheredAttrs: [], query: false });
} else {
executeAction(storeId, actionName, gatheredAttrs);
}
}}
>
<b
style={{
cursor: 'pointer',
}}
>
<span
title={'Add to action queue'}
onClick={e => {
e.preventDefault();
if (attributes.len > 0) {
setData({ modal: true, gatheredAttrs: [], query: true });
} else {
queryAction(storeId, actionName, gatheredAttrs);
}
}}
>
{' '}
</span>
</b>
<span>
<i>{plainFunction}</i>
{' {...}'}
</span>
</span>
{modalIsOpen ? (
<Modal
closeModal={() => {
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]),
});
}
}}
>
<h3>{data.query ? 'Query action:' : 'Run action:'}</h3>
<p>{highlight(plainFunction, attributes[gatheredAttrs.length])}</p>
</Modal>
) : null}
</>
);
}

View File

@ -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 (
<Tree
data={mutation.to}
indent={indent}
search={search}
expand={expand}
forcedExpand={forcedExpand}
omitAttrs={omitAttrs}
forcedLabel={forcedLabel}
/>
);
}
if (newValue) {
return (
<Tree
data={mutation.to}
indent={indent}
search={search}
expand={expand}
forcedExpand={forcedExpand}
className={styles.added}
omitAttrs={omitAttrs}
forcedLabel={forcedLabel}
/>
);
}
if (deleted) {
return (
<Tree
data={mutation.from}
indent={indent}
search={search}
expand={expand}
forcedExpand={forcedExpand}
className={styles.deleted}
omitAttrs={omitAttrs}
forcedLabel={forcedLabel}
/>
);
}
return (
<div
style={{
fontFamily: 'monospace',
}}
className={`${
expanded
? 'expanded'
: `not-expanded ${
mutated && !isPrimitive && !expanded ? styles.changed : ''
}`
}`}
onClick={e => {
e.stopPropagation();
}}
>
<span
style={{
cursor: 'pointer',
}}
onClick={() => {
setExpanded(!expanded);
}}
>
{new Array(Math.max(indent, 0)).fill(<span>&nbsp;</span>)}
{isPrimitive ? (
// 如果两个 value 是基本变量并且不同,则简单显示不同点
<div
onClick={e => {
e.stopPropagation();
}}
>
<Tree
data={mutation.from}
indent={indent}
search={search}
index={index}
className={styles.deleted}
omitAttrs={omitAttrs}
/>
<Tree
data={mutation.to}
indent={indent}
search={search}
index={index}
className={styles.added}
omitAttrs={omitAttrs}
/>
</div>
) : (
// 如果至少有一个是复杂变量,则需要展开按钮
<>
{forcedExpand ? '' : expanded ? <span></span> : <span></span>}
{index === 0 || index ? (
<b className={styles.purple}>{displayValue(index, search)}: </b>
) : (
''
)}
{isArray ? (
// 如果都是数组进行比较
expanded ? (
[
Array(Math.max(mutation.from.length, mutation.to.length))
.fill(true)
.map((i, index) => {
return (
<div>
{mutation.items[index].mutation ? (
<DiffTree
mutation={{
...mutation.items[index],
to: mutation.to[index],
}}
indent={indent}
search={search}
omitAttrs={omitAttrs}
forcedLabel={index}
/>
) : (
<Tree
data={mutation.to[index]}
indent={indent}
search={search}
index={index}
className={styles.default}
omitAttrs={omitAttrs}
/>
)}
</div>
);
}),
]
) : (
forcedLabel || `Array(${mutation.to?.length})`
)
) : isSet ? (
expanded ? (
<div>
<div>
{forcedLabel || `Set(${mutation.to?.values.length})`}
</div>
{Array(
Math.max(
mutation.from?.values.length,
mutation.to?.values.length
)
)
.fill(true)
.map((i ,index) => (
<div>
{mutation.values[index].mutation ? (
<DiffTree
mutation={{
...mutation.values[index],
}}
indent={indent + 2}
search={search}
omitAttrs={omitAttrs}
/>
) : (
<Tree
data={mutation.to?.values[index]}
indent={indent + 2}
search={search}
className={styles.default}
omitAttrs={omitAttrs}
/>
)}
</div>
))}
</div>
) : (
<span>
{forcedLabel || `Set(${mutation.to?.values.length})`}
</span>
)
) : isMap ? (
expanded ? (
<>
<span>
{forcedLabel || `Map(${mutation.to?.entries.length})`}
</span>
{Array(
Math.max(
mutation.from?.entries.length,
mutation.to?.entries.length
)
)
.fill(true)
.map((i, index) =>
mutation.entries[index].mutation ? (
<div>
<DiffTree
mutation={{
...mutation.entries[index],
}}
indent={indent + 2}
search={search}
omitAttrs={omitAttrs}
forcedLabel={'[map item]'}
/>
</div>
) : (
<div>
<Tree
data={mutation.to?.entries[index]}
indent={indent + 2}
search={search}
className={styles.default}
omitAttrs={omitAttrs}
forcedLabel={'[map item]'}
/>
</div>
)
)}
</>
) : (
<span>
{forcedLabel || `Map(${mutation.to?.entries.length})`}
</span>
)
) : expanded ? (
// 如果都是 object 进行比较
Object.entries(mutation.attributes).map(([key, item]) => {
return item.mutation ? (
<span onClick={e => e.stopPropagation()}>
{
<DiffTree
mutation={item}
index={key}
indent={indent}
search={search}
className={!expanded && mutated ? '' : styles.changed}
omitAttrs={omitAttrs}
/>
}
</span>
) : (
<span onClick={e => e.stopPropagation()}>
{
<Tree
data={mutation.to[key]}
index={key}
indent={indent}
search={search}
className={styles.default}
omitAttrs={omitAttrs}
/>
}
</span>
);
})
) : (
forcedLabel || '{ ... }'
)}
</>
)}
</span>
</div>
);
}

View File

@ -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 (
<div
onClick={e => {
e.stopPropagation();
}}
>
<Tree
data={{
Action: `${message.data.action.action}${
message.data.fromQueue ? ' (queued)' : ''
}`
}}
expand={true}
indent={-4}
forcedExpand={true}
search={search}
omitAttrs={['_inulaObserver']}
/>
</div>
);
}
if (message.type === eventTypes.STATE_CHANGE) {
return (
<div
onClick={e => {
e.stopPropagation();
}}
>
<b>{`${message.data.change.vNodes.length} nodes changed:`}</b>
<Tree
data={message.data.change.vNodes.map(vNode => {
return (
<span>
<i>{vNode.type}</i>()
</span>
);
})}
/>
</div>
);
}
return <span className={styles.grey}>N/A</span>
}
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 ? (
<DiffTree
mutation={event.message.data.change.mutation}
expand={true}
forcedExpand={true}
indent={0}
search={eventFilter['fulltext']}
omitAttrs={['_inulaObserver']}
doNotDisplayIcon={true}
/>
) : (
<Tree
data={event.message.data.store.$s}
expand={true}
search={eventFilter['fulltext']}
forcedExpand={true}
indent={-4}
omitAttrs={['_inulaObserver']}
/>
),
storeClick: (
<span
className={styles.link}
onClick={e => {
e.preventDefault();
setNextStore(event.message.data.store.id);
}}
>
{event.message.data.store.id}
</span>
),
additionalData: extractDataByType(
event.message,
eventFilter['fulltext']
),
storeId: event.message.data.store.id,
event,
};
});
return (
<div>
<div style={{ marginTop: '0px', margin: '5px' }}>
<input
ref={filterField}
type={'text'}
placeholder={'Filter:'}
className={`${styles.compositeInput} ${styles.left}`}
onKeyUp={() => {
if (!filterField.current.value) {
removeFilter('fulltext');
}
addFilter('fulltext', filterField.current.value);
}}
/>
<button
className={`${styles.bold} ${styles.compositeInput} ${styles.right}`}
onClick={() => {
filterField.current.value = '';
removeFilter('fulltext');
}}
>
X
</button>
<span className={styles.grey}>{' | '}</span>
<span
style={{
cursor: 'pointer'
}}
onClick={e => {
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);
}}
>
<Checkbox value={persistent}></Checkbox> Persistent events
</span>
{' | '}
<button
onClick={() => {
// 重置 events
chrome.runtime.sendMessage({
type: 'INULA_DEV_TOOLS',
payload: {
type: otherTypes.RESET_EVENTS,
tabId: chrome.devtools.inspectedWindow.tabId,
},
from: DevToolPanel,
});
}}
>
Reset
</button>
{eventFilter['message.data.store.id'] ? (
<span>
{' | '}
<b
style={{
cursor: 'pointer',
}}
onClick={() => {
setNextStore(eventFilter['message.data.store.id']);
}}
>{` Displaying: [${eventFilter['message.data.store.id']}] `}</b>
<button
onClick={() => {
removeFilter('message.data.store.id');
}}
>
X
</button>
</span>
) : null}
</div>
<div style={{ marginTop: '0px', margin: '5px' }}>
<button
className={`${styles.filterButton} ${log.length ? '' : styles.grey} ${
eventFilter['message.type'] ? '' : styles.active
}`}
onClick={() => {
removeFilter('message.type');
}}
>
All({usedTypes.all})
</button>
{Object.values(eventTypes).map(eventType => {
return (
<button
className={`${styles.filterButton} ${
usedTypes[eventType] ? '' : styles.grey
} ${
eventFilter['message.type'] === eventType ? styles.active : ''
}`}
onClick={() => {
addFilter('message.type', eventType);
}}
>
{`${eventType.replace('inulax ', '')}(${
usedTypes[eventType] || 0
})`}
</button>
);
})}
</div>
<Table
data={processedData}
dataKey={'id'}
displayKeys={[
['type', 'Event type:'],
['storeClick', 'Store:'],
['time', 'Time:'],
['state', 'State:'],
['additionalData', 'Additional data:'],
]}
displayDataProcessor={data => {
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 : ''}
/>
</div>
);
}

View File

@ -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 (
<div
style={{
position: 'fixed',
width: '100vw',
height: '100vh',
top: 0,
left: 0,
backgroundColor: 'rgba(0, 0, 0 , 0.3)',
}}
>
<div
style={{
top: 'calc(50vh - 50px)',
left: 'calc(50vw - 125px)',
width: '250px',
backgroundColor: 'white',
border: '1px solid black',
position: 'fixed',
textAlign: 'center',
}}
>
<p>{children}</p>
<p>
<input
ref={inputRef}
type={'text'}
onKeyPress={({key}) => {
if (key === 'Enter') {
tryGatherData();
}
}}
/>
</p>
{error ? <p>Variable parsing error</p> : null}
<p>
<button
onClick={() => {
tryGatherData();
}}
>
OK
</button>
<button
onClick={() => {
closeModal();
}}
>
Cancel
</button>
</p>
</div>
</div>
);
}

View File

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

View File

@ -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: () => (
<Stores
nextStoreId={nextStoreId}
showFilteredEvents={showFilterEvents}
/>
),
},
{
id: 'events',
title: 'Events',
getComponents: () => (
<EventLog
setNextStore={id => {
setNextStoreId(id);
setActive('stores');
}}
setEventFilter={setEventFilter}
eventFilter={eventFilter}
/>
),
},
];
return (
<div>
<div style={{ marginBottom: '10px' }}>
{tabs.map(tab =>
tab.id === active ? (
<button
className={`${styles.tab} ${styles.active}`}
disabled={true}
>
{tab.title}
</button>
) : (
<button
className={styles.tab}
onClick={() => {
setActive(tab.id);
}}
>
{tab.title}
</button>
)
)}
</div>
{tabs.find(item => item.id === active).getComponent()}
</div>
);
}

View File

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

View File

@ -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 (
<div className={styles.wrapper}>
<div className={`${styles.table} ${styles.half}`}>
<div className={styles.row}>
<div className={`${styles.cell} ${styles.header}`}>{title}</div>
</div>
<div className={styles.scrollable}>
<span></span>
{data.map(row => (
<div
className={`${styles.row} ${
keyToDisplay === row[dataKey] ? styles.active : ''
}`}
onClick={() => {
setManualOverride(true);
setKeyToDisplay(
keyToDisplay === row[dataKey] ? null : row[dataKey]
);
}}
>
<div className={styles.cell}>{row?.[attr] || ''}</div>
</div>
))}
</div>
</div>
<div className={`${styles.table} ${styles.half} ${styles.displayData}`}>
<div className={styles.row}>
<div className={styles.cell}>
<b>Data:</b>
<button
className={styles.floatingButton}
onClick={() => {
setKeyToDisplay(null);
}}
>
X
</button>
</div>
</div>
<div className={styles.scrollable}>
<span></span>
<div className={styles.row}>
<div className={styles.cell}>
<Tree
data={
displayDataProcessor
? displayDataProcessor(displayRow)
: displayRow
}
indent={displayRow[displayKeys[0][0]]}
expand={true}
search={search}
forcedExpand={true}
/>
</div>
</div>
</div>
</div>
</div>
);
} else {
return (
<div className={styles.wrapper}>
<div className={styles.table}>
<div className={`${styles.row} ${styles.header}`}>
{displayKeys.map(([key, title]) => (
<div className={styles.cell}>{title}</div>
))}
</div>
{data.map(item => (
<div
onClick={() => {
setManualOverride(true);
setKeyToDisplay(item[dataKey]);
}}
className={styles.row}
>
{displayKeys.map(([key, title]) => (
<div className={styles.cell}>{item[key]}</div>
))}
</div>
))}
</div>
</div>
);
}
}

View File

@ -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 ? (
<div
style={{ fontFamily: 'monoSpace' }}
className={`${expanded ? 'expanded' : 'not-expanded'} ${className}`}
onClick={e => {
e.stopPropagation();
}}
>
<span
style={{ cursor: 'pointer' }}
onClick={() => {
setExpanded(!expanded);
}}
>
{new Array(Math.max(indent, 0)).fill(<span>&nbsp;</span>)}
{forcedExpand || isVNode ? null : expanded ? (
<span></span>
) : (
<span></span>
)}
{index === 0 || index ? (
<>
<b className={styles.purple}>{displayValue(index, search)}: </b>
</>
) : (
''
)}
{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})`
: '{ ... }'}
</span>
{expanded || isVNode ? (
isArray ? (
<>
{data.map((value, index) => {
return (
<div>
<Tree
data={value}
indent={indent + 4}
index={index}
search={search}
className={className}
onEdit={
onEdit
? (path, val) => {
onEdit(path.concat([index]), val);
}
: null
}
/>
</div>
);
})}
</>
) : isVNode ? (
data
) : isMap ? (
<div>
{data.entries.map(([key, value]) => {
return (
<Tree
data={{key, value}}
indent={indent + 4}
search={search}
className={className}
// TODO: editable sets
/>
);
})}
</div>
) : isSet ? (
data.values.map(item => {
return (
<div>
<Tree
data={item}
indent={indent + 4}
search={search}
className={className}
// TODO: editable sets
/>
</div>
);
})
) : (
Object.entries(data).map(([key, value]) => {
return (
<div>
<Tree
data={value}
indent={indent + 4}
index={key}
search={search}
className={className}
onEdit={
onEdit
? (path, val) => {
onEdit(path.concat([key]), val);
}
: null
}
/>
</div>
);
})
)
) : (
''
)}
</div>
) : (
<div className={'not-expanded'}>
{new Array(indent).fill(<span>&nbsp;</span>)}
<span className={`${className}`}>
{typeof index !== 'undefined' ? (
<>
<b className={styles.purple}>{displayValue(index, search)}: </b>
</>
) : (
''
)}
{displayValue(data, search)}
{onEdit && !isWeakSet && !isWeakMap ? ( // TODO: editable weak set and map
<>
<b
style={{ cursor: 'pointer' }}
onClick={() => {
setModal(true);
}}
>
</b>
{onEdit && modal ? (
<Modal
closeModal={() => {
setModal(false);
}}
then={data => {
onEdit([], data);
setModal(false);
}}
>
<h3>Edit value:</h3> {index}
</Modal>
) : null}
</>
) : null}
</span>
</div>
);
}

View File

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

View File

@ -0,0 +1,49 @@
<!--
~ 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.
-->
<!doctype html>
<html style="display: flex">
<head>
<meta charset="utf8">
<meta http-equiv="Content-Security-Policy"
content="default-src *; style-src 'self' 'unsafe-inline'; script-src 'self' 'unsafe-inline' 'unsafe-eval' ">
<style>
html {
width: 100%;
height: 100%;
}
body {
margin: 0;
padding: 0;
width: 100%;
height: 100%;
}
#root {
width: 100%;
height: 100%;
}
</style>
<script src="inula.development.js"></script>
</head>
<body>
<div id="root"></div>
<script src="panelX.js"></script>
</body>
</html>

View File

@ -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(<span className={styles.highlighted}>{search}</span>);
} else {
result.push(parts[i / 2]);
}
}
return result;
}
export function displayValue(val: any, search = '') {
if (typeof val === 'boolean') {
return (
<span>
{highlight(val ? 'true' : 'false', search)}
</span>
);
}
if (val === '') {
return <span className={styles.red}>{'""'}</span>;
}
if (typeof val === 'undefined') {
return <span className={styles.grey}>{highlight('undefined', search)}</span>;
}
if (val === 'null') {
return <span className={styles.grey}>{highlight('null', search)}</span>;
}
if (typeof val === 'string') {
if (val.match(/^function\s?\(/)) {
return (
<span>
<i>ƒ</i>
{highlight(
val.match(/^function\s?\([\w,]*\)/g)[0].replace(/^function\s?/, ''),
search
)}
</span>
);
}
return <span className={styles.red}>"{highlight(val, search)}"</span>;
}
if (typeof val === 'number') {
return <span className={styles.blue}>{highlight('' + val, search)}</span>;
}
if (typeof val === 'function') {
const args = val.toString().match(/^function\s?\([\w,]*\)/g)[0].replace(/^function\s?/, '');
return (
<span>
<i>ƒ</i>
{highlight(args, search)}
</span>
);
}
if (typeof val === 'object') {
if (val?._type === 'WeakSet') {
return <span>WeakSet()</span>;
}
if (val?._type === 'WeakMap') {
return <span>WeakMap()</span>;
}
}
}
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 (
<span>
<i>ƒ</i>
{data.match(/^function\([\w,]*\)/g)[0].substring(8)}
</span>
);
}
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 (
<span>
<span className={styles.purple}>{key}</span>
<span>
<i>ƒ</i>
{value.match(/^function\([\w,]*\)/g)[0].substring(8)}
</span>
</span>
);
}
if (!value) {
return (
<span>
<span className={styles.purple}>{key}</span>:{displayValue(value)}
</span>
);
}
if (Array.isArray(value)) {
return (
<span>
<span className={styles.purple}>{key}</span>:{' '}
{`Array(${value.length})`}
</span>
);
}
if (typeof value === 'object') {
if ((value as any)?._type === 'WeakSet') {
return (
<span>
<span className={styles.purple}>{key}</span>: {'WeakSet()'}
</span>
);
}
if ((value as any)?._type === 'WeakMap') {
return (
<span>
<span className={styles.purple}>{key}</span>: {'WeakMap'}
</span>
);
}
if ((value as any)?._type === 'Set') {
return (
<span>
<span className={styles.purple}>{key}</span>:{' '}
{`Set(${(value as Set<any>).size})`}
</span>
);
}
if ((value as any)?._type === 'Map') {
return (
<span>
<span className={styles.purple}>{key}</span>:{' '}
{`Map(${(value as Map<any, any>).size})`}
</span>
);
}
// object
return (
<span>
<span className={styles.purple}>{key}</span>: {'{...}'}
</span>
);
}
return (
<span>
<span className={styles.purple}>{key}</span>: {displayValue(value)}
</span>
);
})}}`;
}
return data;
}
export function sendMessage(payload) {
chrome.runtime.sendMessage({
type: 'INULA_DEV_TOOLS',
payload,
from: DevToolPanel,
});
}