diff --git a/spectre-frontend/package.json b/spectre-frontend/package.json index 114f7b2..549c148 100644 --- a/spectre-frontend/package.json +++ b/spectre-frontend/package.json @@ -5,8 +5,9 @@ "type": "module", "scripts": { "dev": "vite", - "build": "tsc -b && dotenvx run -f .env -- vite build", + "build": "eslint . && tsc -b && dotenvx run -f .env -- vite build", "lint": "eslint . --fix", + "check": "eslint . && tsc -b", "preview": "vite preview", "generate": "dotenvx run -f .env.local -f .env -- gql-gen --config codegen.ts" }, diff --git a/spectre-frontend/src/components/CopyableValue.tsx b/spectre-frontend/src/components/CopyableValue.tsx new file mode 100644 index 0000000..333dc6f --- /dev/null +++ b/spectre-frontend/src/components/CopyableValue.tsx @@ -0,0 +1,58 @@ +import React, { useState } from 'react' +import type { TooltipProps } from '@heroui/react' +import { Tooltip } from '@heroui/react' +import SvgIcon from '@/components/icon/SvgIcon.tsx' +import Icon from '@/components/icon/icon.ts' + +interface CopyableValueProps { + content: string + placement?: TooltipProps['placement'] +} +const CopyableValue: React.FC = ({ + content, + placement, +}) => { + const [isCopied, setCopied] = useState(false) + let tooltip: React.ReactNode + if (content.length > 256) { + tooltip = ( + {content.length} chars + ) + } else { + tooltip = content + } + + const doCopy = () => { + if (isCopied) { + return + } + setCopied(true) + const clipboardItem = new ClipboardItem({ + 'text/plain': content, + }) + navigator.clipboard.write([clipboardItem]).finally(() => { + setTimeout(() => { + setCopied(false) + }, 1000) + }) + } + + return ( +
+ + {content} + + +
+ ) +} + +export default CopyableValue diff --git a/spectre-frontend/src/components/KVGird/index.tsx b/spectre-frontend/src/components/KVGird/index.tsx index f0d5267..3593904 100644 --- a/spectre-frontend/src/components/KVGird/index.tsx +++ b/spectre-frontend/src/components/KVGird/index.tsx @@ -6,22 +6,26 @@ interface KVGirdProps { children: ReactElement[] | ReactElement } +const MAX_ROW_COUNT = 4 + const KVGird: React.FC = (props) => { const nodes = Array.isArray(props.children) ? props.children : [props.children] return (
{nodes.map((detail, index) => (
0 ? 'border-l-divider border-l-1 px-3' : undefined, + index > 0 || (index === 0 && nodes.length > MAX_ROW_COUNT) + ? 'border-l-divider border-l-1 px-3' + : undefined, )} > {detail} diff --git a/spectre-frontend/src/components/SimpleList.tsx b/spectre-frontend/src/components/SimpleList.tsx new file mode 100644 index 0000000..28f06f2 --- /dev/null +++ b/spectre-frontend/src/components/SimpleList.tsx @@ -0,0 +1,27 @@ +import { Code } from '@heroui/react' + +interface SimpleListProps { + entities: string[] + name: string + color?: 'default' | 'primary' | 'secondary' | 'success' | 'warning' | 'danger' +} +const SimpleList: React.FC = ({ entities, color, name }) => { + return ( +
+ {name}: + {entities.length > 0 ? ( +
    + {entities.map((entity) => ( +
  • + {entity} +
  • + ))} +
+ ) : ( + + )} +
+ ) +} + +export default SimpleList diff --git a/spectre-frontend/src/components/TreeView/index.tsx b/spectre-frontend/src/components/TreeView/index.tsx deleted file mode 100644 index e69de29..0000000 diff --git a/spectre-frontend/src/components/icon/icon.ts b/spectre-frontend/src/components/icon/icon.ts index 5d27fee..07f4b11 100644 --- a/spectre-frontend/src/components/icon/icon.ts +++ b/spectre-frontend/src/components/icon/icon.ts @@ -29,6 +29,8 @@ const Icon = { BOOKMARK: 'fc-bookmark', LOCK: 'fc-lock', EXTERNAL: 'fc-arrow-up-right-from-square', + COPY: 'fc-copy', + CHECK: 'fc-check', } as const export type Icons = (typeof Icon)[keyof typeof Icon] diff --git a/spectre-frontend/src/components/icon/svg-symbols.tsx b/spectre-frontend/src/components/icon/svg-symbols.tsx index 714a86e..a31a1ca 100644 --- a/spectre-frontend/src/components/icon/svg-symbols.tsx +++ b/spectre-frontend/src/components/icon/svg-symbols.tsx @@ -348,6 +348,28 @@ const SvgSymbols: React.FC = () => { d="M384 64C366.3 64 352 78.3 352 96C352 113.7 366.3 128 384 128L466.7 128L265.3 329.4C252.8 341.9 252.8 362.2 265.3 374.7C277.8 387.2 298.1 387.2 310.6 374.7L512 173.3L512 256C512 273.7 526.3 288 544 288C561.7 288 576 273.7 576 256L576 96C576 78.3 561.7 64 544 64L384 64zM144 160C99.8 160 64 195.8 64 240L64 496C64 540.2 99.8 576 144 576L400 576C444.2 576 480 540.2 480 496L480 416C480 398.3 465.7 384 448 384C430.3 384 416 398.3 416 416L416 496C416 504.8 408.8 512 400 512L144 512C135.2 512 128 504.8 128 496L128 240C128 231.2 135.2 224 144 224L224 224C241.7 224 256 209.7 256 192C256 174.3 241.7 160 224 160L144 160z" /> + + {/**/} + + + + {/**/} + + , document.body, diff --git a/spectre-frontend/src/pages/channel/[channelId]/_channel_icons/svg-symbols.tsx b/spectre-frontend/src/pages/channel/[channelId]/_channel_icons/svg-symbols.tsx index b60097c..29757e8 100644 --- a/spectre-frontend/src/pages/channel/[channelId]/_channel_icons/svg-symbols.tsx +++ b/spectre-frontend/src/pages/channel/[channelId]/_channel_icons/svg-symbols.tsx @@ -21,21 +21,21 @@ const ChannelSvgSymbols: React.FC = () => { viewBox="0 0 16 16" > {/**/} - + - + @@ -72,7 +72,7 @@ const ChannelSvgSymbols: React.FC = () => { xmlns="http://www.w3.org/2000/svg" > {/**/} - + { fill="#3574F0" /> diff --git a/spectre-frontend/src/pages/channel/[channelId]/_component/QuickCommand/commands.ts b/spectre-frontend/src/pages/channel/[channelId]/_component/QuickCommand/commands.ts index bb4c06b..bf2c99d 100644 --- a/spectre-frontend/src/pages/channel/[channelId]/_component/QuickCommand/commands.ts +++ b/spectre-frontend/src/pages/channel/[channelId]/_component/QuickCommand/commands.ts @@ -130,11 +130,48 @@ const jad: FormHandle = { isSync: true, } +type TTValues = { + index: string + count: number + depth: number + expression: string + extraArgs?: string +} + +const tt: FormHandle = { + name: 'Time Tunnel', + buildCommand(values) { + let base = `tt -w '${values.expression}' -x ${values.depth} -i ${values.index}` + if (values.count > 0) { + base += ' -n ' + values.count + } + if (values.extraArgs) { + base += ' ' + values.extraArgs + } + return base + }, + defaultValues: { + count: -1, + depth: 3, + expression: DEFAULT_EXPRESSION, + }, + items: [ + { name: 'index', isRequired: true, label: 'Index', type: 'number' }, + [ + { name: 'count', label: '监听数量', type: 'number' }, + { name: 'depth', label: '递归深度', type: 'number' }, + ], + { name: 'expression', label: '表达式' }, + { name: 'extraArgs', label: '额外参数' }, + ], +} + export const quickCommandHandles = { watch, stack, trace, jad, + tt, } export type QuickCommandKeys = keyof typeof quickCommandHandles diff --git a/spectre-frontend/src/pages/channel/[channelId]/_component/QuickCommand/index.tsx b/spectre-frontend/src/pages/channel/[channelId]/_component/QuickCommand/index.tsx index 20e0e75..4d6f02f 100644 --- a/spectre-frontend/src/pages/channel/[channelId]/_component/QuickCommand/index.tsx +++ b/spectre-frontend/src/pages/channel/[channelId]/_component/QuickCommand/index.tsx @@ -33,6 +33,9 @@ interface QuickCommands { jad: { classname: string } + tt: { + index: number + } } type OpenArgs = @@ -77,6 +80,7 @@ const QuickCommand: React.FC = (props) => { const onAction0 = (key: string | number, args: object = {}): boolean => { const qck = key as QuickCommandKeys const handle = quickCommandHandles[qck] + console.log('1') if (!handle) { return false } diff --git a/spectre-frontend/src/pages/channel/[channelId]/_message_view/ArthasResponseDetail.tsx b/spectre-frontend/src/pages/channel/[channelId]/_message_view/ArthasResponseDetail.tsx index bb2f56c..0032552 100644 --- a/spectre-frontend/src/pages/channel/[channelId]/_message_view/ArthasResponseDetail.tsx +++ b/spectre-frontend/src/pages/channel/[channelId]/_message_view/ArthasResponseDetail.tsx @@ -68,13 +68,15 @@ const ArthasResponseDetail: React.FC = (props) => { const currentId = props.message.id return ( <> -
- 命令: {props.message.context.command} -
+ {props.message.context.command ? ( +
+ 命令: {props.message.context.command} +
+ ) : null} {/* 策略:对于脏组件,我们全部渲染但在 CSS 上隐藏;对于非脏组件,动态切换 */} {Array.from(componentCache.current.entries()).map(([id, node]) => (
@@ -84,7 +86,7 @@ const ArthasResponseDetail: React.FC = (props) => { {/* 处理尚未变脏且未进入缓存的新组件 */} {!componentCache.current.has(currentId) && ( -
+
{renderDetail(currentId, Component)}
)} diff --git a/spectre-frontend/src/pages/channel/[channelId]/_message_view/_component/RowAffectDetail.tsx b/spectre-frontend/src/pages/channel/[channelId]/_message_view/_component/RowAffectDetail.tsx new file mode 100644 index 0000000..3749898 --- /dev/null +++ b/spectre-frontend/src/pages/channel/[channelId]/_message_view/_component/RowAffectDetail.tsx @@ -0,0 +1,15 @@ +import type { DetailComponentProps } from '@/pages/channel/[channelId]/_message_view/factory.ts' + +type RowAffectedMessage = { + type: 'row_affect' + jobId: number + rowCount: number +} + +const RowAffectDetail: React.FC> = ( + props, +) => { + return
影响了 {props.msg.rowCount} 个类
+} + +export default RowAffectDetail diff --git a/spectre-frontend/src/pages/channel/[channelId]/_message_view/_component/ScMessageDetail.tsx b/spectre-frontend/src/pages/channel/[channelId]/_message_view/_component/ScMessageDetail.tsx index 86df8ad..d459d66 100644 --- a/spectre-frontend/src/pages/channel/[channelId]/_message_view/_component/ScMessageDetail.tsx +++ b/spectre-frontend/src/pages/channel/[channelId]/_message_view/_component/ScMessageDetail.tsx @@ -3,7 +3,6 @@ import type { DetailComponentProps } from '../factory.ts' import { Card, CardBody, - Code, Link, Table, TableBody, @@ -17,6 +16,7 @@ import { updateChannelContext } from '@/store/channelSlice.ts' import { useDispatch } from 'react-redux' import KVGird from '@/components/KVGird' import KVGridItem from '@/components/KVGird/KVGridItem.tsx' +import SimpleList from '@/components/SimpleList.tsx' type Fields = { annotations: string[] @@ -58,29 +58,6 @@ type ScMessage = { classInfo?: ClassInfo } -const ListDisplay: React.FC<{ - entities: string[] - name: string - color?: 'default' | 'primary' | 'secondary' | 'success' | 'warning' | 'danger' -}> = ({ entities, color, name }) => { - return ( -
- {name}: - {entities.length > 0 ? ( -
    - {entities.map((entity) => ( -
  • - {entity} -
  • - ))} -
- ) : ( - - )} -
- ) -} - const ClassInfoDisplay: React.FC<{ classInfo: ClassInfo }> = ({ classInfo, }) => { @@ -135,17 +112,17 @@ const ClassInfoDisplay: React.FC<{ classInfo: ClassInfo }> = ({ - - - + {classInfo.fields ? ( <>
字段信息
@@ -179,7 +156,7 @@ const ScMessageDetail: React.FC> = ({ msg, }) => { if (msg.classNames) { - return + return } else if (msg.classInfo) { return } diff --git a/spectre-frontend/src/pages/channel/[channelId]/_message_view/_component/SmMessageDetail.tsx b/spectre-frontend/src/pages/channel/[channelId]/_message_view/_component/SmMessageDetail.tsx new file mode 100644 index 0000000..2beded3 --- /dev/null +++ b/spectre-frontend/src/pages/channel/[channelId]/_message_view/_component/SmMessageDetail.tsx @@ -0,0 +1,105 @@ +import React, { useCallback } from 'react' +import type { DetailComponentProps } from '@/pages/channel/[channelId]/_message_view/factory.ts' +import { Card, CardBody, Code, Link, Tooltip } from '@heroui/react' +import KVGird from '@/components/KVGird' +import KVGridItem from '@/components/KVGird/KVGridItem.tsx' +import { useDispatch } from 'react-redux' +import { updateChannelContext } from '@/store/channelSlice.ts' +import SimpleList from '@/components/SimpleList.tsx' + +type MethodInfo = { + constructor: boolean + declaringClass: string + descriptor: string + methodName: string +} + +type MethodDetail = MethodInfo & { + annotations: string[] + classLoaderHash: string + exceptions: string[] + modifier: string + parameters: string[] + returnType: string +} + +type ScMessage = { + type: 'sc' + segment: number + jobId: number + detail: boolean + withField: boolean + methodInfo?: MethodInfo | MethodDetail +} + +const SmWithDetail: React.FC<{ detail: MethodDetail }> = ({ detail }) => { + const dispatch = useDispatch() + const applyClassloader = useCallback(() => { + dispatch( + updateChannelContext({ + classloaderHash: detail.classLoaderHash, + }), + ) + }, [detail.classLoaderHash, dispatch]) + return ( +
+
+ {detail.declaringClass}#{detail.methodName} +
+ + +
基础信息
+ + {detail.methodName} + {detail.modifier} + {detail.returnType} + + + + #{detail.classLoaderHash} + + + + + + + +
+
+
+ ) +} + +const SmMessageDetail: React.FC> = ({ + msg, +}) => { + if (msg.detail) { + return + } else if (msg.methodInfo) { + return ( +
+
搜索到以下方法名称:
+ {msg.methodInfo.methodName} +
+ ) + } +} + +export default SmMessageDetail diff --git a/spectre-frontend/src/pages/channel/[channelId]/_message_view/_component/StackMessageDetail.tsx b/spectre-frontend/src/pages/channel/[channelId]/_message_view/_component/StackMessageDetail.tsx index 398d765..1a6685e 100644 --- a/spectre-frontend/src/pages/channel/[channelId]/_message_view/_component/StackMessageDetail.tsx +++ b/spectre-frontend/src/pages/channel/[channelId]/_message_view/_component/StackMessageDetail.tsx @@ -1,23 +1,11 @@ import type { DetailComponentProps } from '../factory.ts' -import React, { - type MouseEvent, - useCallback, - useContext, - useMemo, - useRef, - useState, -} from 'react' +import React, { useCallback, useMemo } from 'react' import Time from '@/components/Time.tsx' -import Icon from '@/components/icon/icon.ts' -import clsx from 'clsx' -import ChannelContext from '@/pages/channel/[channelId]/context.ts' import { useDispatch, useSelector } from 'react-redux' import type { RootState } from '@/store' -import { Button, ListboxItem, Tooltip } from '@heroui/react' +import { Button, Tooltip } from '@heroui/react' import { setTipRead } from '@/store/tipSlice' -import SvgIcon from '@/components/icon/SvgIcon.tsx' -import useRightClickMenu from '@/components/RightClickMenu/useRightClickMenu.ts' -import RightClickMenu from '@/components/RightClickMenu/RightClickMenu.tsx' +import StackTrace from '@/pages/channel/[channelId]/_message_view/_component/_common/StackTrace.tsx' type Trace = { fileName: string @@ -44,73 +32,14 @@ type KV = { value: React.ReactNode } -interface PackageHiderProps { - trace: Trace - forceExpand?: boolean -} - -type ClassInfo = { - package: string - lightPackage: string - classname: string -} - -const PackageHider: React.FC = ({ trace, forceExpand }) => { - const [lightMode, setLightMode] = useState(true) - const info: ClassInfo = useMemo(() => { - const packageCharacters: string[] = [] - const pkgs = trace.className.split('.') - for (let i = 0; i < pkgs.length - 1; i++) { - if (i === 1) { - // usually company name - packageCharacters.push(pkgs[i]) - } else { - packageCharacters.push(pkgs[i].charAt(0)) - } - } - return { - classname: pkgs[pkgs.length - 1], - package: trace.className.substring(0, trace.className.lastIndexOf('.')), - lightPackage: packageCharacters.join('.'), - } - }, [trace.className]) - - return ( - <> - {lightMode && !forceExpand ? ( - setLightMode(false)} - > - {info.lightPackage} - - ) : ( - {info.package} - )} - .{info.classname} - - ) -} - -const Actions = { - WATCH: 'watch', - TRACE: 'trace', - STACK: 'stack', - FLAG: 'flag', - JAD: 'jad', -} as const - const StackMessageDetail: React.FC> = ({ msg, onDirty, }) => { - const [markedLines, setMarkedLines] = useState(new Set()) - const selectedIndex = useRef(-1) const channelRightClickMenuTip = useSelector( (state) => state.tip.channelRightClickMenuTip, ) const dispatch = useDispatch() - const context = useContext(ChannelContext) const keyValues: KV[] = useMemo( () => [ { @@ -129,29 +58,6 @@ const StackMessageDetail: React.FC> = ({ [msg], ) - const { onContextMenu, menuProps } = useRightClickMenu() - - const onContextMenu0 = useCallback( - (e: MouseEvent, index: number) => { - selectedIndex.current = index - onContextMenu(e) - }, - [onContextMenu], - ) - - const changeFlag = useCallback(() => { - setMarkedLines((prevState) => { - const r = new Set(prevState) - if (r.has(selectedIndex.current)) { - r.delete(selectedIndex.current) - } else { - r.add(selectedIndex.current) - } - onDirty?.() - return r - }) - }, [onDirty]) - const hideRightClickTip = useCallback(() => { dispatch( setTipRead({ @@ -160,31 +66,6 @@ const StackMessageDetail: React.FC> = ({ ) }, [dispatch]) - const onAction = useCallback( - (key: string | number) => { - const trace = msg.stackTrace[selectedIndex.current] - if ( - context.getQuickCommandExecutor().handleActions(key, { - classname: trace.className, - methodName: trace.methodName, - }) - ) { - return - } - switch (key) { - case Actions.JAD: - context - .getTabsController() - .openTab('JAD', {}, { classname: trace.className }) - break - case Actions.FLAG: - changeFlag() - break - } - }, - [changeFlag, context, msg.stackTrace], - ) - return (
> = ({ )
-
- {msg.stackTrace.map((trace, index) => ( -
- onContextMenu0(e, index)} - className={clsx( - 'cursor-pointer hover:opacity-80', - markedLines.has(index) ? 'bg-yellow-200' : undefined, - )} - > - - #{trace.methodName}: - {trace.lineNumber} - -
- ))} -
- - 反编译 - Watch - Stack - Trace - } - > - {markedLines.has(selectedIndex.current) ? '取消标记' : '标记'} - - +
) } diff --git a/spectre-frontend/src/pages/channel/[channelId]/_message_view/_component/SysEnvMessageDetail.tsx b/spectre-frontend/src/pages/channel/[channelId]/_message_view/_component/SysEnvMessageDetail.tsx new file mode 100644 index 0000000..d30e66e --- /dev/null +++ b/spectre-frontend/src/pages/channel/[channelId]/_message_view/_component/SysEnvMessageDetail.tsx @@ -0,0 +1,42 @@ +import type { DetailComponentProps } from '@/pages/channel/[channelId]/_message_view/factory.ts' +import { + Table, + TableBody, + TableCell, + TableColumn, + TableHeader, + TableRow, +} from '@heroui/react' +import React from 'react' +import CopyableValue from '@/components/CopyableValue.tsx' + +type SysEnvMessage = { + env: Record + jobId: number + type: 'sysenv' +} + +const SysEnvMessageDetail: React.FC> = ({ + msg, +}) => { + return ( + + + 名称 + + + + {([key, value]) => ( + + {key} + + + + + )} + +
+ ) +} + +export default SysEnvMessageDetail diff --git a/spectre-frontend/src/pages/channel/[channelId]/_message_view/_component/SysPropMessageDetail.tsx b/spectre-frontend/src/pages/channel/[channelId]/_message_view/_component/SysPropMessageDetail.tsx new file mode 100644 index 0000000..505bd8a --- /dev/null +++ b/spectre-frontend/src/pages/channel/[channelId]/_message_view/_component/SysPropMessageDetail.tsx @@ -0,0 +1,41 @@ +import React from 'react' +import type { DetailComponentProps } from '@/pages/channel/[channelId]/_message_view/factory.ts' +import { + Table, + TableBody, + TableCell, + TableColumn, + TableHeader, + TableRow, +} from '@heroui/react' +import CopyableValue from '@/components/CopyableValue.tsx' + +type SysPropsMessage = { + jobId: number + props: Record + type: 'sysprop' +} +const SysEnvMessageDetail: React.FC> = ({ + msg, +}) => { + return ( + + + 名称 + + + + {([key, value]) => ( + + {key} + + + + + )} + +
+ ) +} + +export default SysEnvMessageDetail diff --git a/spectre-frontend/src/pages/channel/[channelId]/_message_view/_component/TTMessageDetail.tsx b/spectre-frontend/src/pages/channel/[channelId]/_message_view/_component/TTMessageDetail.tsx new file mode 100644 index 0000000..e407f6b --- /dev/null +++ b/spectre-frontend/src/pages/channel/[channelId]/_message_view/_component/TTMessageDetail.tsx @@ -0,0 +1,116 @@ +import type { DetailComponentProps } from '@/pages/channel/[channelId]/_message_view/factory.ts' +import OgnlMessageView from '@/pages/channel/[channelId]/_message_view/_component/_ognl_result/OgnlMessageView.tsx' +import { + ListboxItem, + Table, + TableBody, + TableCell, + TableColumn, + TableHeader, + TableRow, +} from '@heroui/react' +import PackageHider from '@/pages/channel/[channelId]/_message_view/_component/_common/PackageHider.tsx' +import RightClickMenu from '@/components/RightClickMenu/RightClickMenu.tsx' +import useRightClickMenu from '@/components/RightClickMenu/useRightClickMenu.ts' +import React, { useContext, useRef } from 'react' +import ChannelContext from '@/pages/channel/[channelId]/context.ts' + +type TimeFragment = { + className: string + cost: number + index: number + methodName: string + object: string + params: string[] + return: boolean + returnObj: string + throw: boolean + throwExp: string + timestamp: string +} + +type TTMessage = { + type: 'tt' + first: boolean + jobId: number + timeFragmentList?: TimeFragment[] + timeFragment?: TimeFragment + watchValue?: string +} + +const TimeFragmentListDisplay: React.FC<{ + timeFragmentList: TimeFragment[] +}> = ({ timeFragmentList }) => { + const { onContextMenu, menuProps } = useRightClickMenu() + const context = useContext(ChannelContext) + const selectedFragment = useRef(null) + + const onContextMenu0 = ( + fragment: TimeFragment, + e: React.MouseEvent, + ) => { + selectedFragment.current = fragment + onContextMenu(e) + } + + const onAction = () => { + context.getQuickCommandExecutor().open('tt', { + index: selectedFragment.current!.index, + }) + } + + return ( +
+ + + INDEX + CLASS + METHOD + IS-RET + IS-EXP + COST(ms) + OBJECT + TIMESTAMP + + + {timeFragmentList.map((fragment) => ( + onContextMenu0(fragment, e)} + > + {fragment.index} + + + + {fragment.methodName} + {fragment.return.toString()} + {fragment.throwExp.toString()} + {fragment.cost} + {fragment.object} + {fragment.timestamp} + + ))} + +
+
+ 提示: 右键可以进行额外操作 +
+ + 执行表达式 + +
+ ) +} + +const TTMessageDetail: React.FC> = ({ + msg, +}) => { + if (msg.watchValue) { + return + } else if (msg.timeFragmentList) { + return + } else if (msg.timeFragment) { + return + } +} +export default TTMessageDetail diff --git a/spectre-frontend/src/pages/channel/[channelId]/_message_view/_component/ThreadMessageDetail.tsx b/spectre-frontend/src/pages/channel/[channelId]/_message_view/_component/ThreadMessageDetail.tsx new file mode 100644 index 0000000..6420baa --- /dev/null +++ b/spectre-frontend/src/pages/channel/[channelId]/_message_view/_component/ThreadMessageDetail.tsx @@ -0,0 +1,171 @@ +import type { DetailComponentProps } from '@/pages/channel/[channelId]/_message_view/factory.ts' +import { Accordion, AccordionItem, Card, CardBody } from '@heroui/react' +import KVGird from '@/components/KVGird' +import KVGridItem from '@/components/KVGird/KVGridItem.tsx' +import ThreadTable from '@/pages/channel/[channelId]/_tabs/_dashboard/ThreadTable.tsx' +import React from 'react' +import PercentageData from '@/pages/channel/[channelId]/_tabs/_dashboard/PercentageData.tsx' +import StackTrace from '@/pages/channel/[channelId]/_message_view/_component/_common/StackTrace.tsx' + +type Thread = { + cpu: number + daemon: boolean + deltaTime: number + group: string + id: number + interrupted: boolean + name: string + priority: number + state: string + time: number +} + +type Trace = { + fileName: string + lineNumber: number + className: string + methodName: string +} + +type BusyThread = { + blockedCount: number + blockedTime: number + cpu: number + daemon: boolean + deltaTime: number + group: string + id: number + isNative: boolean + lockInfo?: Record + lockName?: string + lockOwnerId: number + lockedMonitors: string[] + lockedSynchronizers: string[] + name: string + priority: number + stackTrace: Trace[] + state: string + suspended: boolean + time: number + waitedCount: number + waitedTime: number +} + +type ThreadInfo = { + blockedCount: number + blockedTime: number + daemon: boolean + inNative: boolean + lockInfo?: { + className: string + identityHashCode: number + } + lockName: string + lockOwnerId: number + lockedMonitors: string[] + lockedSynchronizers: string[] + priority: number + stackTrace: Trace[] + suspended: boolean + threadId: number + threadName: string + threadState: string + waitedCount: number + waitedTime: number +} + +type ThreadMessage = { + all: boolean + jobId: number + type: 'thread' + threadStats?: Thread[] + threadStateCount?: Record + busyThreads?: BusyThread[] + threadInfo?: ThreadInfo +} + +const BusyThreadsDisplay: React.FC<{ + busyThreads: BusyThread[] + onDirty?: () => void +}> = ({ busyThreads, onDirty }) => { + return ( + + {busyThreads.map((thread) => ( + + {thread.name} + Id={thread.id} cpuUsage= + + + deltaTime={thread.deltaTime}ms time={thread.time}ms{' '} + + {thread.state} +
+ } + > + + + ))} + + ) +} + +const SingleThreadInfo: React.FC<{ + threadInfo: ThreadInfo + onDirty?: () => void +}> = ({ threadInfo, onDirty }) => { + return ( +
+
+ {threadInfo.threadName} + + Id={threadInfo.threadId} {threadInfo.threadState} + + {threadInfo.lockInfo ? ( + +  on {threadInfo.lockInfo.className}@ + {threadInfo.lockInfo.identityHashCode} + + ) : null} +
+ +
+ ) +} + +const ThreadMessageDetail: React.FC> = ({ + msg, + onDirty, +}) => { + if (msg.threadStateCount && msg.threadStats) { + return ( +
+ + +
线程统计
+ + {Object.entries(msg.threadStateCount).map((cnt) => ( + + {cnt[1]} + + ))} + +
+
+ {/*偷个懒*/} + +
+ ) + } else if (msg.busyThreads) { + return ( + + ) + } else if (msg.threadInfo) { + return + } +} +export default ThreadMessageDetail diff --git a/spectre-frontend/src/pages/channel/[channelId]/_message_view/_component/VersionMessageDetail.tsx b/spectre-frontend/src/pages/channel/[channelId]/_message_view/_component/VersionMessageDetail.tsx new file mode 100644 index 0000000..00ec390 --- /dev/null +++ b/spectre-frontend/src/pages/channel/[channelId]/_message_view/_component/VersionMessageDetail.tsx @@ -0,0 +1,16 @@ +import type { DetailComponentProps } from '@/pages/channel/[channelId]/_message_view/factory.ts' + +export type VersionMessage = { + type: 'version' + version: string + jobId: number + fid: number +} + +const VersionMessageDetail: React.FC> = ({ + msg, +}) => { + return
当前版本: {msg.version}
+} + +export default VersionMessageDetail diff --git a/spectre-frontend/src/pages/channel/[channelId]/_message_view/_component/VmOptionsMessageDetail.tsx b/spectre-frontend/src/pages/channel/[channelId]/_message_view/_component/VmOptionsMessageDetail.tsx new file mode 100644 index 0000000..3965d87 --- /dev/null +++ b/spectre-frontend/src/pages/channel/[channelId]/_message_view/_component/VmOptionsMessageDetail.tsx @@ -0,0 +1,49 @@ +import type { DetailComponentProps } from '@/pages/channel/[channelId]/_message_view/factory.ts' +import { + Table, + TableBody, + TableCell, + TableColumn, + TableHeader, + TableRow, +} from '@heroui/react' +import CopyableValue from '@/components/CopyableValue.tsx' + +type VmOption = { + name: string + origin: string + value: string + writeable: boolean +} +type VmOptionsMessage = { + jobId: number + type: 'vmoption' + vmOptions: VmOption[] +} + +const VmOptionsMessageDetail: React.FC< + DetailComponentProps +> = ({ msg }) => { + return ( + + + name + value + writeable + + + {msg.vmOptions.map((vmOption) => ( + + {vmOption.name} + + + + {vmOption.writeable.toString()} + + ))} + +
+ ) +} + +export default VmOptionsMessageDetail diff --git a/spectre-frontend/src/pages/channel/[channelId]/_message_view/_component/_common/PackageHider.tsx b/spectre-frontend/src/pages/channel/[channelId]/_message_view/_component/_common/PackageHider.tsx new file mode 100644 index 0000000..fd0771c --- /dev/null +++ b/spectre-frontend/src/pages/channel/[channelId]/_message_view/_component/_common/PackageHider.tsx @@ -0,0 +1,54 @@ +import React, { useMemo, useState } from 'react' + +type ClassInfo = { + package: string + lightPackage: string + classname: string +} + +interface PackageHiderProps { + classname: string + forceExpand?: boolean +} + +const PackageHider: React.FC = ({ + classname, + forceExpand, +}) => { + const [lightMode, setLightMode] = useState(true) + const info: ClassInfo = useMemo(() => { + const packageCharacters: string[] = [] + const pkgs = classname.split('.') + for (let i = 0; i < pkgs.length - 1; i++) { + if (i === 1) { + // usually company name + packageCharacters.push(pkgs[i]) + } else { + packageCharacters.push(pkgs[i].charAt(0)) + } + } + return { + classname: pkgs[pkgs.length - 1], + package: classname.substring(0, classname.lastIndexOf('.')), + lightPackage: packageCharacters.join('.'), + } + }, [classname]) + + return ( + <> + {lightMode && !forceExpand ? ( + setLightMode(false)} + > + {info.lightPackage} + + ) : ( + {info.package} + )} + .{info.classname} + + ) +} + +export default PackageHider diff --git a/spectre-frontend/src/pages/channel/[channelId]/_message_view/_component/_common/StackTrace.tsx b/spectre-frontend/src/pages/channel/[channelId]/_message_view/_component/_common/StackTrace.tsx new file mode 100644 index 0000000..3391e0d --- /dev/null +++ b/spectre-frontend/src/pages/channel/[channelId]/_message_view/_component/_common/StackTrace.tsx @@ -0,0 +1,127 @@ +import React, { + type MouseEvent, + useCallback, + useContext, + useRef, + useState, +} from 'react' +import clsx from 'clsx' +import RightClickMenu from '@/components/RightClickMenu/RightClickMenu.tsx' +import { ListboxItem } from '@heroui/react' +import SvgIcon from '@/components/icon/SvgIcon.tsx' +import Icon from '@/components/icon/icon.ts' +import useRightClickMenu from '@/components/RightClickMenu/useRightClickMenu.ts' +import ChannelContext from '@/pages/channel/[channelId]/context.ts' +import PackageHider from '@/pages/channel/[channelId]/_message_view/_component/_common/PackageHider.tsx' + +export type Trace = { + fileName: string + lineNumber: number + className: string + methodName: string +} + +interface StackTraceProps { + traces: Trace[] + onDirty?: () => void +} + +const Actions = { + WATCH: 'watch', + TRACE: 'trace', + STACK: 'stack', + FLAG: 'flag', + JAD: 'jad', +} as const + +const StackTrace: React.FC = ({ traces, onDirty }) => { + const { onContextMenu, menuProps } = useRightClickMenu() + const [markedLines, setMarkedLines] = useState(new Set()) + const selectedIndex = useRef(-1) + const context = useContext(ChannelContext) + + const onContextMenu0 = useCallback( + (e: MouseEvent, index: number) => { + selectedIndex.current = index + onContextMenu(e) + }, + [onContextMenu], + ) + + const changeFlag = useCallback(() => { + setMarkedLines((prevState) => { + const r = new Set(prevState) + if (r.has(selectedIndex.current)) { + r.delete(selectedIndex.current) + } else { + r.add(selectedIndex.current) + } + onDirty?.() + return r + }) + }, [onDirty]) + + const onAction = useCallback( + (key: string | number) => { + const trace = traces[selectedIndex.current] + if ( + context.getQuickCommandExecutor().handleActions(key, { + classname: trace.className, + methodName: trace.methodName, + }) + ) { + return + } + switch (key) { + case Actions.JAD: + context + .getTabsController() + .openTab('JAD', {}, { classname: trace.className }) + break + case Actions.FLAG: + changeFlag() + break + } + }, + [changeFlag, context, traces], + ) + + return ( +
+
+ {traces.map((trace, index) => ( +
+ onContextMenu0(e, index)} + className={clsx( + 'cursor-pointer hover:opacity-80', + markedLines.has(index) ? 'bg-yellow-200' : undefined, + )} + > + + #{trace.methodName}: + {trace.lineNumber} + +
+ ))} +
+ + 反编译 + Watch + Stack + Trace + } + > + {markedLines.has(selectedIndex.current) ? '取消标记' : '标记'} + + +
+ ) +} + +export default StackTrace diff --git a/spectre-frontend/src/pages/channel/[channelId]/_message_view/_register/row-affect.ts b/spectre-frontend/src/pages/channel/[channelId]/_message_view/_register/row-affect.ts new file mode 100644 index 0000000..a9061ea --- /dev/null +++ b/spectre-frontend/src/pages/channel/[channelId]/_message_view/_register/row-affect.ts @@ -0,0 +1,12 @@ +import { registerMessageView } from '@/pages/channel/[channelId]/_message_view/factory.ts' +import RowAffectDetail from '@/pages/channel/[channelId]/_message_view/_component/RowAffectDetail.tsx' + +registerMessageView({ + type: 'row_affect', + detailComponent: RowAffectDetail, + display: (message) => ({ + name: `影响了 ${message.value.rowCount} 个类`, + color: 'secondary', + tag: '影响数量', + }), +}) diff --git a/spectre-frontend/src/pages/channel/[channelId]/_message_view/_register/sm.ts b/spectre-frontend/src/pages/channel/[channelId]/_message_view/_register/sm.ts new file mode 100644 index 0000000..dab9420 --- /dev/null +++ b/spectre-frontend/src/pages/channel/[channelId]/_message_view/_register/sm.ts @@ -0,0 +1,11 @@ +import { registerMessageView } from '@/pages/channel/[channelId]/_message_view/factory.ts' +import SmMessageDetail from '@/pages/channel/[channelId]/_message_view/_component/SmMessageDetail.tsx' + +registerMessageView({ + detailComponent: SmMessageDetail, + type: 'sm', + display: () => ({ + tag: 'sm', + name: '搜索方法', + }), +}) diff --git a/spectre-frontend/src/pages/channel/[channelId]/_message_view/_register/sysenv.ts b/spectre-frontend/src/pages/channel/[channelId]/_message_view/_register/sysenv.ts new file mode 100644 index 0000000..e2ffa13 --- /dev/null +++ b/spectre-frontend/src/pages/channel/[channelId]/_message_view/_register/sysenv.ts @@ -0,0 +1,10 @@ +import { registerMessageView } from '@/pages/channel/[channelId]/_message_view/factory.ts' +import SysEnvMessageDetail from '@/pages/channel/[channelId]/_message_view/_component/SysEnvMessageDetail.tsx' + +registerMessageView({ + detailComponent: SysEnvMessageDetail, + type: 'sysenv', + display: () => ({ + name: '环境变量', + }), +}) diff --git a/spectre-frontend/src/pages/channel/[channelId]/_message_view/_register/sysprop.ts b/spectre-frontend/src/pages/channel/[channelId]/_message_view/_register/sysprop.ts new file mode 100644 index 0000000..8579786 --- /dev/null +++ b/spectre-frontend/src/pages/channel/[channelId]/_message_view/_register/sysprop.ts @@ -0,0 +1,10 @@ +import { registerMessageView } from '@/pages/channel/[channelId]/_message_view/factory.ts' +import SysPropMessageDetail from '@/pages/channel/[channelId]/_message_view/_component/SysPropMessageDetail.tsx' + +registerMessageView({ + detailComponent: SysPropMessageDetail, + display: () => ({ + name: '系统参数', + }), + type: 'sysprop', +}) diff --git a/spectre-frontend/src/pages/channel/[channelId]/_message_view/_register/thread.ts b/spectre-frontend/src/pages/channel/[channelId]/_message_view/_register/thread.ts new file mode 100644 index 0000000..13c6f1f --- /dev/null +++ b/spectre-frontend/src/pages/channel/[channelId]/_message_view/_register/thread.ts @@ -0,0 +1,10 @@ +import { registerMessageView } from '@/pages/channel/[channelId]/_message_view/factory.ts' +import ThreadMessageDetail from '@/pages/channel/[channelId]/_message_view/_component/ThreadMessageDetail.tsx' + +registerMessageView({ + detailComponent: ThreadMessageDetail, + type: 'thread', + display: () => ({ + name: '线程信息', + }), +}) diff --git a/spectre-frontend/src/pages/channel/[channelId]/_message_view/_register/tt.ts b/spectre-frontend/src/pages/channel/[channelId]/_message_view/_register/tt.ts new file mode 100644 index 0000000..bfd2255 --- /dev/null +++ b/spectre-frontend/src/pages/channel/[channelId]/_message_view/_register/tt.ts @@ -0,0 +1,10 @@ +import { registerMessageView } from '@/pages/channel/[channelId]/_message_view/factory.ts' +import TTMessageDetail from '@/pages/channel/[channelId]/_message_view/_component/TTMessageDetail.tsx' + +registerMessageView({ + type: 'tt', + display: (message) => ({ + name: message.context.command, + }), + detailComponent: TTMessageDetail, +}) diff --git a/spectre-frontend/src/pages/channel/[channelId]/_message_view/_register/uncomplete.ts b/spectre-frontend/src/pages/channel/[channelId]/_message_view/_register/uncomplete.ts index bd4e935..7abc6c3 100644 --- a/spectre-frontend/src/pages/channel/[channelId]/_message_view/_register/uncomplete.ts +++ b/spectre-frontend/src/pages/channel/[channelId]/_message_view/_register/uncomplete.ts @@ -3,38 +3,6 @@ import type { InputStatusResponse } from '@/api/impl/arthas.ts' // 存放还没完成的 -type VersionMessage = { - type: 'version' - version: string - jobId: number - fid: number -} - -type RowAffectedMessage = { - type: 'row_affect' - jobId: number - rowCount: number - fid: number -} - -registerMessageView({ - type: 'row_affect', - display: (message) => ({ - name: `影响了 ${message.value.rowCount} 个类`, - color: 'secondary', - tag: '影响数量', - }), -}) - -registerMessageView({ - type: 'version', - display: (message) => ({ - name: message.value.version, - color: 'secondary', - tag: 'version', - }), -}) - registerMessageView({ type: 'input_status', display: (message) => ({ diff --git a/spectre-frontend/src/pages/channel/[channelId]/_message_view/_register/version.ts b/spectre-frontend/src/pages/channel/[channelId]/_message_view/_register/version.ts new file mode 100644 index 0000000..6494a91 --- /dev/null +++ b/spectre-frontend/src/pages/channel/[channelId]/_message_view/_register/version.ts @@ -0,0 +1,14 @@ +import VersionMessageDetail, { + type VersionMessage, +} from '@/pages/channel/[channelId]/_message_view/_component/VersionMessageDetail.tsx' +import { registerMessageView } from '@/pages/channel/[channelId]/_message_view/factory.ts' + +registerMessageView({ + type: 'version', + display: (message) => ({ + name: message.value.version, + color: 'secondary', + tag: 'version', + }), + detailComponent: VersionMessageDetail, +}) diff --git a/spectre-frontend/src/pages/channel/[channelId]/_message_view/_register/vmoption.ts b/spectre-frontend/src/pages/channel/[channelId]/_message_view/_register/vmoption.ts new file mode 100644 index 0000000..cfa7af7 --- /dev/null +++ b/spectre-frontend/src/pages/channel/[channelId]/_message_view/_register/vmoption.ts @@ -0,0 +1,10 @@ +import { registerMessageView } from '@/pages/channel/[channelId]/_message_view/factory.ts' +import VmOptionsMessageDetail from '@/pages/channel/[channelId]/_message_view/_component/VmOptionsMessageDetail.tsx' + +registerMessageView({ + type: 'vmoption', + detailComponent: VmOptionsMessageDetail, + display: () => ({ + name: '虚拟机选项', + }), +}) diff --git a/spectre-frontend/src/pages/channel/[channelId]/_tabs/_console/ArthasResponseListTab.tsx b/spectre-frontend/src/pages/channel/[channelId]/_tabs/_console/ArthasResponseListTab.tsx index bb089ff..31ba6d2 100644 --- a/spectre-frontend/src/pages/channel/[channelId]/_tabs/_console/ArthasResponseListTab.tsx +++ b/spectre-frontend/src/pages/channel/[channelId]/_tabs/_console/ArthasResponseListTab.tsx @@ -13,12 +13,13 @@ import ArthasResponseItem, { import ChannelContext from '@/pages/channel/[channelId]/context.ts' import type { ArthasMessage } from '@/pages/channel/[channelId]/db.ts' import type { ArthasMessageBus } from '@/pages/channel/[channelId]/useArthasMessageBus.tsx' +import type { StatusMessage } from '@/pages/channel/[channelId]/_message_view/_component/StatusMessageDetail.tsx' interface ArthasResponseListProps { onEntitySelect: (e: ArthasMessage) => void } -const IGNORED_TYPES = new Set(['input_status', 'command', 'status']) +const IGNORED_TYPES = new Set(['input_status', 'command']) function buildArray0(bus: ArthasMessageBus) { const channelSlice = store.getState().channel const isDebugMode = channelSlice.context.isDebugMode @@ -44,6 +45,11 @@ function buildArray( const type = entity.value.type if (IGNORED_TYPES.has(type)) { continue + } else if ( + type === 'status' && + (entity.value as StatusMessage).statusCode === 0 + ) { + continue } // dashboard 仅显示第一条 if ('dashboard' === type && lastMsgType === type) { @@ -106,7 +112,7 @@ const ArthasResponseListTab: React.FC = (props) => { useEffect(() => { setFilteredResponse(buildArray0(context.messageBus)) - }, [isDebugMode]) + }, [context.messageBus, isDebugMode]) useLayoutEffect(() => { const container = scrollRef.current diff --git a/spectre-frontend/src/pages/channel/[channelId]/_tabs/_console/CommandExecuteBlock.tsx b/spectre-frontend/src/pages/channel/[channelId]/_tabs/_console/CommandExecuteBlock.tsx index ab2a309..75a79dd 100644 --- a/spectre-frontend/src/pages/channel/[channelId]/_tabs/_console/CommandExecuteBlock.tsx +++ b/spectre-frontend/src/pages/channel/[channelId]/_tabs/_console/CommandExecuteBlock.tsx @@ -82,7 +82,7 @@ const CommandExecuteBlock: React.FC = () => { if (fail) { setValue('command', command) } - setRunningCommand(command) + setRunningCommand(command.trim()) }, }) return () => { @@ -113,7 +113,7 @@ const CommandExecuteBlock: React.FC = () => { {runningCommand} ) : undefined}
-
+