Merge branch 'master' of gitee.com:openInula/inula
This commit is contained in:
commit
530fb24289
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"name": "openinula",
|
"name": "inula",
|
||||||
"description": "OpenInula is a JavaScript framework library.",
|
"description": "OpenInula is a JavaScript framework library.",
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"private": true,
|
"private": true,
|
||||||
|
|
|
@ -0,0 +1,68 @@
|
||||||
|
/*
|
||||||
|
* 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 { injectSrc, injectCode } from '../utils/injectUtils';
|
||||||
|
import { checkMessage } from '../utils/transferUtils';
|
||||||
|
import { DevToolContentScript, DevToolHook, DevToolBackground } from '../utils/constants';
|
||||||
|
import { changeSource } from '../utils/transferUtils';
|
||||||
|
|
||||||
|
// 页面的 window 对象不能直接通过 contentScript 代码修改,只能通过添加 js 代码往页面 window 注入 hook
|
||||||
|
const rendererURL = chrome.runtime.getURL('/injector.js');
|
||||||
|
if (window.performance.getEntriesByType('navigation')) {
|
||||||
|
const entryType = (window.performance.getEntriesByType('navigation')[0] as any).type;
|
||||||
|
if (entryType === 'navigate') {
|
||||||
|
injectSrc(rendererURL);
|
||||||
|
} else if (entryType === 'reload' && !(window as any).__INULA_DEV_HOOK__) {
|
||||||
|
let rendererCode;
|
||||||
|
const request = new XMLHttpRequest();
|
||||||
|
request.addEventListener('load', function () {
|
||||||
|
rendererCode = this.responseText;
|
||||||
|
});
|
||||||
|
request.open('GET', rendererURL, false);
|
||||||
|
request.send();
|
||||||
|
injectCode(rendererCode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 监听来自页面的信息
|
||||||
|
window.addEventListener(
|
||||||
|
'message',
|
||||||
|
event => {
|
||||||
|
// 只监听来自本页面的消息
|
||||||
|
if (event.source !== window) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = event.data;
|
||||||
|
if (checkMessage(data, DevToolHook)) {
|
||||||
|
changeSource(data, DevToolContentScript);
|
||||||
|
// 传递给 background
|
||||||
|
chrome.runtime.sendMessage(data);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
false
|
||||||
|
);
|
||||||
|
|
||||||
|
// 监听来自 background 的消息
|
||||||
|
chrome.runtime.onMessage.addListener(function (message, sender, sendResponse) {
|
||||||
|
// 该方法可以监听页面 contentScript 和插件的消息
|
||||||
|
// 没有 tab 信息说明消息来自插件
|
||||||
|
if (!sender.tab && checkMessage(message, DevToolBackground)) {
|
||||||
|
changeSource(message, DevToolContentScript);
|
||||||
|
// 传递消息给页面
|
||||||
|
window.postMessage(message, '*');
|
||||||
|
}
|
||||||
|
sendResponse({ status: 'ok' });
|
||||||
|
});
|
|
@ -0,0 +1,101 @@
|
||||||
|
/*
|
||||||
|
* 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 { render, createElement } from 'openinula';
|
||||||
|
import Panel from '../panel/Panel';
|
||||||
|
import PanelX from '../panelX/PanelX';
|
||||||
|
|
||||||
|
let panelCreated = false;
|
||||||
|
|
||||||
|
const viewSource = () => {
|
||||||
|
setTimeout(() => {
|
||||||
|
chrome.devtools.inspectedWindow.eval(`
|
||||||
|
if (window.$type != null) {
|
||||||
|
if (
|
||||||
|
window.$type &&
|
||||||
|
window.$type.prototype &&
|
||||||
|
window.$type.prototype.render
|
||||||
|
) {
|
||||||
|
// 类组件
|
||||||
|
inspect(window.$type.prototype.render);
|
||||||
|
} else {
|
||||||
|
// 函数组件
|
||||||
|
inspect(window.$type);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
}, 100);
|
||||||
|
};
|
||||||
|
|
||||||
|
const inspectVNode = () => {
|
||||||
|
chrome.devtools.inspectedWindow.eval(
|
||||||
|
`
|
||||||
|
window.__INULA_DEV_HOOK__ && window.__INULA_DEV_HOOK__.$0 !== $0
|
||||||
|
? (inspect(window.__INULA_DEV_HOOK__.$0.realNode), true)
|
||||||
|
: false
|
||||||
|
`,
|
||||||
|
(_, error) => {
|
||||||
|
if (error) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
let currentPanel = null;
|
||||||
|
|
||||||
|
chrome.devtools.inspectedWindow.eval(
|
||||||
|
'window.__INULA_DEV_HOOK__',
|
||||||
|
function (isInula, error) {
|
||||||
|
if (!isInula || panelCreated) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
panelCreated = true;
|
||||||
|
chrome.devtools.panels.create(
|
||||||
|
'Inula',
|
||||||
|
'',
|
||||||
|
'panel.html',
|
||||||
|
(extensionPanel) => {
|
||||||
|
extensionPanel.onShown.addListener((panel) => {
|
||||||
|
if (currentPanel === panel) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
currentPanel = panel;
|
||||||
|
const container = panel.document.getElementById('root');
|
||||||
|
const element = createElement(Panel, { viewSource, inspectVNode });
|
||||||
|
render(element, container);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
chrome.devtools.panels.create(
|
||||||
|
'InulaX',
|
||||||
|
'',
|
||||||
|
'panelX.html',
|
||||||
|
(extensionPanel) => {
|
||||||
|
extensionPanel.onShown.addListener((panel) => {
|
||||||
|
if (currentPanel === panel) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
currentPanel = panel;
|
||||||
|
const container = panel.document.getElementById('root');
|
||||||
|
const element = createElement(PanelX, {});
|
||||||
|
render(element, container);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
|
@ -0,0 +1,30 @@
|
||||||
|
<!--
|
||||||
|
~ 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>
|
||||||
|
<head>
|
||||||
|
<meta http-equiv="Content-Security-Policy"
|
||||||
|
content="default-src *; style-src 'self' 'unsafe-inline'; srcipt-src 'self' 'unsafe-inline' 'unsafe-eval' ">
|
||||||
|
<script src="inula.development.js"></script>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div>
|
||||||
|
<p>Inula dev tools!</p>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
<script> src="main.js"</script>
|
||||||
|
</html>
|
|
@ -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}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
|
@ -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> </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>
|
||||||
|
);
|
||||||
|
}
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
|
@ -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);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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> </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> </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>
|
||||||
|
);
|
||||||
|
}
|
|
@ -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;
|
|
@ -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>
|
|
@ -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,
|
||||||
|
});
|
||||||
|
}
|
|
@ -38,7 +38,7 @@ export function injectSrc(src) {
|
||||||
).appendChild(script);
|
).appendChild(script);
|
||||||
}
|
}
|
||||||
|
|
||||||
function injectCode(code) {
|
export function injectCode(code) {
|
||||||
const script = document.createElement('script');
|
const script = document.createElement('script');
|
||||||
script.textContent = code;
|
script.textContent = code;
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "inula-intl",
|
"name": "inula-intl",
|
||||||
"version": "0.0.2",
|
"version": "0.0.3",
|
||||||
"description": "",
|
"description": "",
|
||||||
"main": "build/intl.umd.js",
|
"main": "build/intl.umd.js",
|
||||||
"type": "commonjs",
|
"type": "commonjs",
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "inula-request",
|
"name": "inula-request",
|
||||||
"version": "0.0.5",
|
"version": "0.0.7",
|
||||||
"description": "Inula-request brings you a convenient request experience!",
|
"description": "Inula-request brings you a convenient request experience!",
|
||||||
"main": "./dist/inulaRequest.js",
|
"main": "./dist/inulaRequest.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|
|
@ -41,4 +41,7 @@ export default {
|
||||||
presets: ['@babel/preset-env']
|
presets: ['@babel/preset-env']
|
||||||
})
|
})
|
||||||
],
|
],
|
||||||
|
external:[
|
||||||
|
'openinula'
|
||||||
|
],
|
||||||
};
|
};
|
||||||
|
|
|
@ -157,10 +157,6 @@ openinula团队会关注所有Pull Request,我们会review以及合入你的
|
||||||
1. `npm run build` 同时构建openinula UMD的prod版本和dev版本
|
1. `npm run build` 同时构建openinula UMD的prod版本和dev版本
|
||||||
2. `build-types` 单独构建openinula的类型提示@types目录
|
2. `build-types` 单独构建openinula的类型提示@types目录
|
||||||
|
|
||||||
#### 配套开发工具
|
|
||||||
|
|
||||||
- [openinula-devtool](https://www.XXXX.com): 可视化openinula项目页面的vDom树
|
|
||||||
|
|
||||||
## 开源许可协议
|
## 开源许可协议
|
||||||
|
|
||||||
请查阅 License 获取开源许可协议的更多信息.
|
请查阅 License 获取开源许可协议的更多信息.
|
||||||
|
|
|
@ -187,7 +187,7 @@ describe('Redux adapter', () => {
|
||||||
reduxStore.dispatch({ type: 'toggle' });
|
reduxStore.dispatch({ type: 'toggle' });
|
||||||
reduxStore.dispatch({ type: 'toggle' });
|
reduxStore.dispatch({ type: 'toggle' });
|
||||||
|
|
||||||
expect(counter).toBe(3); // NOTE: first action is always store initialization
|
expect(counter).toBe(2); // execute dispatch two times, applyMiddleware was called same times
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Should apply multiple enhancers', async () => {
|
it('Should apply multiple enhancers', async () => {
|
||||||
|
@ -226,7 +226,7 @@ describe('Redux adapter', () => {
|
||||||
|
|
||||||
reduxStore.dispatch({ type: 'toggle' });
|
reduxStore.dispatch({ type: 'toggle' });
|
||||||
|
|
||||||
expect(counter).toBe(2); // NOTE: first action is always store initialization
|
expect(counter).toBe(1); // execute dispatch two times, applyMiddleware was called same times
|
||||||
expect(lastAction).toBe('toggle');
|
expect(lastAction).toBe('toggle');
|
||||||
expect(middlewareCallList[0]).toBe('callCounter');
|
expect(middlewareCallList[0]).toBe('callCounter');
|
||||||
expect(middlewareCallList[1]).toBe('lastFunctionStorage');
|
expect(middlewareCallList[1]).toBe('lastFunctionStorage');
|
||||||
|
|
|
@ -67,18 +67,50 @@ export function isPromise(obj: any): boolean {
|
||||||
return isObject(obj) && typeof obj.then === 'function';
|
return isObject(obj) && typeof obj.then === 'function';
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isSame(x, y) {
|
export function isSame(x: unknown, y: unknown): boolean {
|
||||||
if (typeof Object.is !== 'function') {
|
// 如果两个对象是同一个引用,直接返回true
|
||||||
if (x === y) {
|
if (x === y) {
|
||||||
// +0 != -0
|
return true;
|
||||||
return x !== 0 || 1 / x === 1 / y;
|
|
||||||
} else {
|
|
||||||
// NaN == NaN
|
|
||||||
return x !== x && y !== y;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return Object.is(x, y);
|
|
||||||
}
|
}
|
||||||
|
// 如果两个对象类型不同,直接返回false
|
||||||
|
if (typeof x !== typeof y) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// 如果两个对象都是null或undefined,直接返回true
|
||||||
|
if (x == null || y == null) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// 如果两个对象都是基本类型,比较他们的值是否相等
|
||||||
|
if (typeof x !== 'object') {
|
||||||
|
return x === y;
|
||||||
|
}
|
||||||
|
// 如果两个对象都是数组,比较他们的长度是否相等,然后递归比较每个元素是否相等
|
||||||
|
if (Array.isArray(x) && Array.isArray(y)) {
|
||||||
|
if (x.length !== y.length) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
for (let i = 0; i < x.length; i++) {
|
||||||
|
if (!isSame(x[i], y[i])) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// 两个对象都是普通对象,首先比较他们的属性数量是否相等,然后递归比较每个属性的值是否相等
|
||||||
|
if (typeof x === 'object' && typeof y === 'object') {
|
||||||
|
const keys1 = Object.keys(x!).sort();
|
||||||
|
const keys2 = Object.keys(y!).sort();
|
||||||
|
if (keys1.length !== keys2.length) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
for (let i = 0; i < keys1.length; i++) {
|
||||||
|
if (!isSame(x![keys1[i]], y![keys2[i]])) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getDetailedType(val: any) {
|
export function getDetailedType(val: any) {
|
||||||
|
|
|
@ -27,12 +27,12 @@ export {
|
||||||
createDispatchHook,
|
createDispatchHook,
|
||||||
} from './reduxReact';
|
} from './reduxReact';
|
||||||
|
|
||||||
export type ReduxStoreHandler = {
|
export type ReduxStoreHandler<T = any> = {
|
||||||
reducer: (state: any, action: { type: string }) => any;
|
reducer(state: T, action: { type: string }): any;
|
||||||
dispatch: (action: { type: string }) => void;
|
dispatch(action: { type: string }): void;
|
||||||
getState: () => any;
|
getState(): T;
|
||||||
subscribe: (listener: () => void) => () => void;
|
subscribe(listener: () => void): () => void;
|
||||||
replaceReducer: (reducer: (state: any, action: { type: string }) => any) => void;
|
replaceReducer(reducer: (state: T, action: { type: string }) => any): void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ReduxAction = {
|
export type ReduxAction = {
|
||||||
|
@ -53,6 +53,9 @@ export type ReduxMiddleware = (
|
||||||
|
|
||||||
type Reducer = (state: any, action: ReduxAction) => any;
|
type Reducer = (state: any, action: ReduxAction) => any;
|
||||||
|
|
||||||
|
type StoreCreator = (reducer: Reducer, preloadedState?: any) => ReduxStoreHandler;
|
||||||
|
type StoreEnhancer = (next: StoreCreator) => StoreCreator;
|
||||||
|
|
||||||
function mergeData(state, data) {
|
function mergeData(state, data) {
|
||||||
if (!data) {
|
if (!data) {
|
||||||
state.stateWrapper = data;
|
state.stateWrapper = data;
|
||||||
|
@ -87,7 +90,7 @@ function mergeData(state, data) {
|
||||||
state.stateWrapper = data;
|
state.stateWrapper = data;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createStore(reducer: Reducer, preloadedState?: any, enhancers?): ReduxStoreHandler {
|
export function createStore(reducer: Reducer, preloadedState?: any, enhancers?: StoreEnhancer): ReduxStoreHandler {
|
||||||
const store = createStoreX({
|
const store = createStoreX({
|
||||||
id: 'defaultStore',
|
id: 'defaultStore',
|
||||||
state: { stateWrapper: preloadedState },
|
state: { stateWrapper: preloadedState },
|
||||||
|
@ -130,12 +133,14 @@ export function createStore(reducer: Reducer, preloadedState?: any, enhancers?):
|
||||||
dispatch: store.$a.dispatch,
|
dispatch: store.$a.dispatch,
|
||||||
};
|
};
|
||||||
|
|
||||||
enhancers && enhancers(result);
|
|
||||||
|
|
||||||
result.dispatch({ type: 'InulaX' });
|
result.dispatch({ type: 'InulaX' });
|
||||||
|
|
||||||
store.reduxHandler = result;
|
store.reduxHandler = result;
|
||||||
|
|
||||||
|
if (typeof enhancers === 'function') {
|
||||||
|
return enhancers(createStore)(reducer, preloadedState);
|
||||||
|
}
|
||||||
|
|
||||||
return result as ReduxStoreHandler;
|
return result as ReduxStoreHandler;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -150,19 +155,23 @@ export function combineReducers(reducers: { [key: string]: Reducer }): Reducer {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function applyMiddlewares(store: ReduxStoreHandler, middlewares: ReduxMiddleware[]): void {
|
function applyMiddlewares(createStore: StoreCreator, middlewares: ReduxMiddleware[]): StoreCreator {
|
||||||
middlewares = middlewares.slice();
|
return (reducer, preloadedState) => {
|
||||||
middlewares.reverse();
|
middlewares = middlewares.slice();
|
||||||
let dispatch = store.dispatch;
|
middlewares.reverse();
|
||||||
middlewares.forEach(middleware => {
|
const storeObj = createStore(reducer, preloadedState);
|
||||||
dispatch = middleware(store)(dispatch);
|
let dispatch = storeObj.dispatch;
|
||||||
});
|
middlewares.forEach(middleware => {
|
||||||
store.dispatch = dispatch;
|
dispatch = middleware(storeObj)(dispatch);
|
||||||
|
});
|
||||||
|
storeObj.dispatch = dispatch;
|
||||||
|
return storeObj;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function applyMiddleware(...middlewares: ReduxMiddleware[]): (store: ReduxStoreHandler) => void {
|
export function applyMiddleware(...middlewares: ReduxMiddleware[]): (createStore: StoreCreator) => StoreCreator {
|
||||||
return store => {
|
return createStore => {
|
||||||
return applyMiddlewares(store, middlewares);
|
return applyMiddlewares(createStore, middlewares);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -170,7 +179,7 @@ type ActionCreator = (...params: any[]) => ReduxAction;
|
||||||
type ActionCreators = { [key: string]: ActionCreator };
|
type ActionCreators = { [key: string]: ActionCreator };
|
||||||
export type BoundActionCreator = (...params: any[]) => void;
|
export type BoundActionCreator = (...params: any[]) => void;
|
||||||
type BoundActionCreators = { [key: string]: BoundActionCreator };
|
type BoundActionCreators = { [key: string]: BoundActionCreator };
|
||||||
type Dispatch = (action) => any;
|
type Dispatch = (action: ReduxAction) => any;
|
||||||
|
|
||||||
export function bindActionCreators(actionCreators: ActionCreators, dispatch: Dispatch): BoundActionCreators {
|
export function bindActionCreators(actionCreators: ActionCreators, dispatch: Dispatch): BoundActionCreators {
|
||||||
const boundActionCreators = {};
|
const boundActionCreators = {};
|
||||||
|
@ -183,12 +192,12 @@ export function bindActionCreators(actionCreators: ActionCreators, dispatch: Dis
|
||||||
return boundActionCreators;
|
return boundActionCreators;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function compose(...middlewares: ReduxMiddleware[]) {
|
export function compose<T = StoreCreator>(...middlewares: ((...args: any[]) => any)[]): (...args: any[]) => T {
|
||||||
return (store: ReduxStoreHandler, extraArgument: any) => {
|
return (...args) => {
|
||||||
let val;
|
let val: any;
|
||||||
middlewares.reverse().forEach((middleware: ReduxMiddleware, index) => {
|
middlewares.reverse().forEach((middleware, index) => {
|
||||||
if (!index) {
|
if (!index) {
|
||||||
val = middleware(store, extraArgument);
|
val = middleware(...args);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
val = middleware(val);
|
val = middleware(val);
|
||||||
|
|
|
@ -17,6 +17,7 @@ import { useState, useContext, useEffect, useRef } from '../../renderer/hooks/Ho
|
||||||
import { createContext } from '../../renderer/components/context/CreateContext';
|
import { createContext } from '../../renderer/components/context/CreateContext';
|
||||||
import { createElement } from '../../external/JSXElement';
|
import { createElement } from '../../external/JSXElement';
|
||||||
import type { ReduxStoreHandler, ReduxAction, BoundActionCreator } from './redux';
|
import type { ReduxStoreHandler, ReduxAction, BoundActionCreator } from './redux';
|
||||||
|
import { forwardRef } from '../../renderer/components/ForwardRef';
|
||||||
|
|
||||||
const DefaultContext = createContext(null);
|
const DefaultContext = createContext(null);
|
||||||
type Context = typeof DefaultContext;
|
type Context = typeof DefaultContext;
|
||||||
|
@ -40,29 +41,27 @@ export function createStoreHook(context: Context): () => ReduxStoreHandler {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createSelectorHook(context: Context): (selector?: (any) => any) => any {
|
export function createSelectorHook(context: Context): (selector?: ((state: unknown) => any) | undefined) => any {
|
||||||
const store = createStoreHook(context)() as unknown as ReduxStoreHandler;
|
const store = createStoreHook(context)();
|
||||||
return function (selector = state => state) {
|
return function useSelector(selector = state => state) {
|
||||||
const [b, fr] = useState(false);
|
const [state, setState] = useState(() => store.getState());
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const unsubscribe = store.subscribe(() => fr(!b));
|
const unsubscribe = store.subscribe(() => {
|
||||||
return () => {
|
setState(store.getState());
|
||||||
unsubscribe();
|
});
|
||||||
};
|
return () => unsubscribe();
|
||||||
});
|
}, []);
|
||||||
|
|
||||||
return selector(store.getState());
|
return selector(state);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createDispatchHook(context: Context): () => BoundActionCreator {
|
export function createDispatchHook(context: Context): () => BoundActionCreator {
|
||||||
const store = createStoreHook(context)() as unknown as ReduxStoreHandler;
|
const store = createStoreHook(context)();
|
||||||
return function () {
|
return function useDispatch() {
|
||||||
return action => {
|
return store.dispatch;
|
||||||
store.dispatch(action);
|
};
|
||||||
};
|
|
||||||
}.bind(store);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useSelector = selector => {
|
export const useSelector = selector => {
|
||||||
|
@ -90,6 +89,11 @@ type MergePropsP<StateProps, DispatchProps, OwnProps, MergedProps> = (
|
||||||
type WrappedComponent<OwnProps> = (props: OwnProps) => ReturnType<typeof createElement>;
|
type WrappedComponent<OwnProps> = (props: OwnProps) => ReturnType<typeof createElement>;
|
||||||
type OriginalComponent<MergedProps> = (props: MergedProps) => ReturnType<typeof createElement>;
|
type OriginalComponent<MergedProps> = (props: MergedProps) => ReturnType<typeof createElement>;
|
||||||
type Connector<OwnProps, MergedProps> = (Component: OriginalComponent<MergedProps>) => WrappedComponent<OwnProps>;
|
type Connector<OwnProps, MergedProps> = (Component: OriginalComponent<MergedProps>) => WrappedComponent<OwnProps>;
|
||||||
|
type ConnectOption<State = any> = {
|
||||||
|
areStatesEqual?: (oldState: State, newState: State) => boolean;
|
||||||
|
context?: Context;
|
||||||
|
forwardRef?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
export function connect<StateProps, DispatchProps, OwnProps, MergedProps>(
|
export function connect<StateProps, DispatchProps, OwnProps, MergedProps>(
|
||||||
mapStateToProps: MapStateToPropsP<StateProps, OwnProps> = () => ({} as StateProps),
|
mapStateToProps: MapStateToPropsP<StateProps, OwnProps> = () => ({} as StateProps),
|
||||||
|
@ -99,10 +103,7 @@ export function connect<StateProps, DispatchProps, OwnProps, MergedProps>(
|
||||||
dispatchProps,
|
dispatchProps,
|
||||||
ownProps
|
ownProps
|
||||||
): MergedProps => ({ ...stateProps, ...dispatchProps, ...ownProps } as unknown as MergedProps),
|
): MergedProps => ({ ...stateProps, ...dispatchProps, ...ownProps } as unknown as MergedProps),
|
||||||
options?: {
|
options?: ConnectOption
|
||||||
areStatesEqual?: (oldState: any, newState: any) => boolean;
|
|
||||||
context?: Context;
|
|
||||||
}
|
|
||||||
): Connector<OwnProps, MergedProps> {
|
): Connector<OwnProps, MergedProps> {
|
||||||
if (!options) {
|
if (!options) {
|
||||||
options = {};
|
options = {};
|
||||||
|
@ -114,37 +115,31 @@ export function connect<StateProps, DispatchProps, OwnProps, MergedProps>(
|
||||||
|
|
||||||
//this component should mimic original type of component used
|
//this component should mimic original type of component used
|
||||||
const Wrapper: WrappedComponent<OwnProps> = (props: OwnProps) => {
|
const Wrapper: WrappedComponent<OwnProps> = (props: OwnProps) => {
|
||||||
const [f, forceReload] = useState(true);
|
const store = useStore() as ReduxStoreHandler;
|
||||||
|
const [state, setState] = useState(() => store.getState());
|
||||||
const store = useStore() as unknown as ReduxStoreHandler;
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const unsubscribe = store.subscribe(() => forceReload(!f));
|
const unsubscribe = store.subscribe(() => {
|
||||||
return () => {
|
setState(store.getState());
|
||||||
unsubscribe();
|
});
|
||||||
};
|
return () => unsubscribe();
|
||||||
});
|
}, []);
|
||||||
|
|
||||||
const previous = useRef({
|
const previous = useRef<{ state: { [key: string]: any }; mappedState: StateProps }>({
|
||||||
state: {},
|
state: {},
|
||||||
mappedState: {},
|
mappedState: {} as StateProps,
|
||||||
}) as {
|
});
|
||||||
current: {
|
|
||||||
state: { [key: string]: any };
|
|
||||||
mappedState: StateProps;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
let mappedState: StateProps;
|
let mappedState: StateProps;
|
||||||
if (options?.areStatesEqual) {
|
if (options?.areStatesEqual) {
|
||||||
if (options.areStatesEqual(previous.current.state, store.getState())) {
|
if (options.areStatesEqual(previous.current.state, state)) {
|
||||||
mappedState = previous.current.mappedState as StateProps;
|
mappedState = previous.current.mappedState as StateProps;
|
||||||
} else {
|
} else {
|
||||||
mappedState = mapStateToProps ? mapStateToProps(store.getState(), props) : ({} as StateProps);
|
mappedState = mapStateToProps ? mapStateToProps(state, props) : ({} as StateProps);
|
||||||
previous.current.mappedState = mappedState;
|
previous.current.mappedState = mappedState;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
mappedState = mapStateToProps ? mapStateToProps(store.getState(), props) : ({} as StateProps);
|
mappedState = mapStateToProps ? mapStateToProps(state, props) : ({} as StateProps);
|
||||||
previous.current.mappedState = mappedState;
|
previous.current.mappedState = mappedState;
|
||||||
}
|
}
|
||||||
let mappedDispatch: DispatchProps = {} as DispatchProps;
|
let mappedDispatch: DispatchProps = {} as DispatchProps;
|
||||||
|
@ -153,12 +148,14 @@ export function connect<StateProps, DispatchProps, OwnProps, MergedProps>(
|
||||||
Object.entries(mapDispatchToProps).forEach(([key, value]) => {
|
Object.entries(mapDispatchToProps).forEach(([key, value]) => {
|
||||||
mappedDispatch[key] = (...args: ReduxAction[]) => {
|
mappedDispatch[key] = (...args: ReduxAction[]) => {
|
||||||
store.dispatch(value(...args));
|
store.dispatch(value(...args));
|
||||||
|
setState(store.getState());
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
mappedDispatch = mapDispatchToProps(store.dispatch, props);
|
mappedDispatch = mapDispatchToProps(store.dispatch, props);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
mappedDispatch = Object.assign({}, mappedDispatch, { dispatch: store.dispatch });
|
||||||
const mergedProps = (
|
const mergedProps = (
|
||||||
mergeProps ||
|
mergeProps ||
|
||||||
((state, dispatch, originalProps) => {
|
((state, dispatch, originalProps) => {
|
||||||
|
@ -166,12 +163,18 @@ export function connect<StateProps, DispatchProps, OwnProps, MergedProps>(
|
||||||
})
|
})
|
||||||
)(mappedState, mappedDispatch, props);
|
)(mappedState, mappedDispatch, props);
|
||||||
|
|
||||||
previous.current.state = store.getState();
|
previous.current.state = state;
|
||||||
|
|
||||||
const node = createElement(Component, mergedProps);
|
return createElement(Component, mergedProps);
|
||||||
return node;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (options?.forwardRef) {
|
||||||
|
const forwarded = forwardRef((props, ref) => {
|
||||||
|
return Wrapper({ ...props, ref: ref });
|
||||||
|
});
|
||||||
|
return forwarded as WrappedComponent<OwnProps>;
|
||||||
|
}
|
||||||
|
|
||||||
return Wrapper;
|
return Wrapper;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -35,5 +35,5 @@ function createThunkMiddleware(extraArgument?: any): ReduxMiddleware {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const thunk = createThunkMiddleware();
|
export const thunk = createThunkMiddleware();
|
||||||
// @ts-ignore
|
|
||||||
thunk.withExtraArgument = createThunkMiddleware;
|
export const withExtraArgument = createThunkMiddleware;
|
||||||
|
|
|
@ -16,7 +16,7 @@
|
||||||
import { isMap, isSet, isWeakMap, isWeakSet } from '../CommonUtils';
|
import { isMap, isSet, isWeakMap, isWeakSet } from '../CommonUtils';
|
||||||
import { getStore, getAllStores } from '../store/StoreHandler';
|
import { getStore, getAllStores } from '../store/StoreHandler';
|
||||||
import { OBSERVED_COMPONENTS } from './constants';
|
import { OBSERVED_COMPONENTS } from './constants';
|
||||||
import { VNode } from "../../renderer/vnode/VNode";
|
import { VNode } from '../../renderer/vnode/VNode';
|
||||||
|
|
||||||
const sessionId = Date.now();
|
const sessionId = Date.now();
|
||||||
|
|
||||||
|
|
|
@ -21,7 +21,7 @@ import { setStateChange } from '../render/FunctionComponent';
|
||||||
import { getHookStage, HookStage } from './HookStage';
|
import { getHookStage, HookStage } from './HookStage';
|
||||||
import type { VNode } from '../Types';
|
import type { VNode } from '../Types';
|
||||||
import { getProcessingVNode } from '../GlobalVar';
|
import { getProcessingVNode } from '../GlobalVar';
|
||||||
import { markUpdatedInRender } from "./HookMain";
|
import { markUpdatedInRender } from './HookMain';
|
||||||
|
|
||||||
// 构造新的Update数组
|
// 构造新的Update数组
|
||||||
function insertUpdate<S, A>(action: A, hook: Hook<S, A>): Update<S, A> {
|
function insertUpdate<S, A>(action: A, hook: Hook<S, A>): Update<S, A> {
|
||||||
|
|
|
@ -74,7 +74,7 @@ export function getLazyVNodeTag(lazyComp: any): string {
|
||||||
} else if (lazyComp !== undefined && lazyComp !== null && typeLazyMap[lazyComp.vtype]) {
|
} else if (lazyComp !== undefined && lazyComp !== null && typeLazyMap[lazyComp.vtype]) {
|
||||||
return typeLazyMap[lazyComp.vtype];
|
return typeLazyMap[lazyComp.vtype];
|
||||||
}
|
}
|
||||||
throw Error("Inula can't resolve the content of lazy");
|
throw Error('Inula can\'t resolve the content of lazy');
|
||||||
}
|
}
|
||||||
|
|
||||||
// 创建processing
|
// 创建processing
|
||||||
|
|
Loading…
Reference in New Issue