Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions spectre-frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
"echarts": "^6.0.0",
"framer-motion": "^12.23.9",
"i18next": "^25.7.3",
"idb": "^8.0.3",
"react": "~19.1.2",
"react-dom": "~19.1.2",
"react-hook-form": "^7.61.1",
Expand Down
8 changes: 8 additions & 0 deletions spectre-frontend/pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

39 changes: 7 additions & 32 deletions spectre-frontend/src/api/impl/arthas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,42 +43,19 @@ export const joinChannel = (

export type InputStatusResponse = {
type: 'input_status'
fid: number
inputStatus: 'ALLOW_INPUT' | 'DISABLED' | 'ALLOW_INTERRUPT'
jobId: number
}

type ArthasResponse = {
export type PureArthasResponse = {
type: string
jobId: number
}

export type ArthasResponseWithId = ArthasResponse & {
/**
* arthas 实际没用这个值,这个是给前端识别用的
*/
fid: number
}
let fid = Date.now()

function applyIdForArthasResponse(r: unknown): Promise<ArthasResponseWithId[]> {
const resp = r as ArthasResponse[]
const result: ArthasResponseWithId[] = []
for (const arthasRespons of resp) {
result.push({
...arthasRespons,
fid: fid++,
})
}
return Promise.resolve(result)
}

export const pullResults = async (
channelId: string,
): Promise<ArthasResponseWithId[]> =>
axios
.get(`arthas/channel/${channelId}/pull-result`)
.then(applyIdForArthasResponse)
): Promise<PureArthasResponse[]> =>
axios.get(`arthas/channel/${channelId}/pull-result`)

export const executeArthasCommand = (channelId: string, command: string) => {
return axios.post(`arthas/channel/${channelId}/execute`, {
Expand All @@ -89,12 +66,10 @@ export const executeArthasCommand = (channelId: string, command: string) => {
export const executeArthasCommandSync = (
channelId: string,
command: string,
): Promise<ArthasResponseWithId[]> => {
return axios
.post(`arthas/channel/${channelId}/execute-sync`, {
command,
})
.then(applyIdForArthasResponse)
): Promise<PureArthasResponse[]> => {
return axios.post(`arthas/channel/${channelId}/execute-sync`, {
command,
})
}

export const disconnectSession = (channelId: string) => {
Expand Down
1 change: 1 addition & 0 deletions spectre-frontend/src/components/icon/icon.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ const Icon = {
RIGHT_ARROW: 'fc-angle-right',
BOOKMARK: 'fc-bookmark',
LOCK: 'fc-lock',
EXTERNAL: 'fc-arrow-up-right-from-square',
} as const

export type Icons = (typeof Icon)[keyof typeof Icon]
Expand Down
11 changes: 11 additions & 0 deletions spectre-frontend/src/components/icon/svg-symbols.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -337,6 +337,17 @@ const SvgSymbols: React.FC = () => {
d="M256 160L256 224L384 224L384 160C384 124.7 355.3 96 320 96C284.7 96 256 124.7 256 160zM192 224L192 160C192 89.3 249.3 32 320 32C390.7 32 448 89.3 448 160L448 224C483.3 224 512 252.7 512 288L512 512C512 547.3 483.3 576 448 576L192 576C156.7 576 128 547.3 128 512L128 288C128 252.7 156.7 224 192 224z"
/>
</svg>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 640 640"
id={Icon.EXTERNAL}
>
{/*<!--!Font Awesome Free v7.1.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2026 Fonticons, Inc.-->*/}
<path
fill="currentColor"
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"
/>
</svg>
</defs>
</svg>,
document.body,
Expand Down
11 changes: 9 additions & 2 deletions spectre-frontend/src/pages/channel/[channelId]/ChannelLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,21 @@ import ChannelContext, {
} from '@/pages/channel/[channelId]/context.ts'
import type { QuickCommandRef } from '@/pages/channel/[channelId]/_component/QuickCommand'
import useArthasMessageBus from '@/pages/channel/[channelId]/useArthasMessageBus.tsx'
import './_message_view/init.ts'

interface ChannelLayoutProps {
channelId: string
appName: string
}

const ChannelLayout: React.FC<ChannelLayoutProps> = (props) => {
const bus = useArthasMessageBus()
const bus = useArthasMessageBus(props.channelId)
const tabsController = useRef<TabsControllerRef>(null)
const quickCommandRef = useRef<QuickCommandRef>(null)
const contextValue = useMemo<ChannelContextState>(() => {
const contextValue = useMemo<ChannelContextState | null>(() => {
if (!bus) {
return null
}
return {
messageBus: bus,
getTabsController() {
Expand All @@ -32,6 +36,9 @@ const ChannelLayout: React.FC<ChannelLayoutProps> = (props) => {
}
}, [bus])

if (!contextValue) {
return
}
return (
<>
<ChannelSvgSymbols />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ const ChannelIcon = {
DASHBOARD: 'fc-chart-line',
FIELD: 'jb-field',
ARRAY: 'jb-db-array',
ALIGN_LEFT: 'fc-align-left',
}

export default ChannelIcon
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,14 @@ const ChannelSvgSymbols: React.FC = () => {
fill="#6C707E"
/>
</svg>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 640 640"
id={ChannelIcon.ALIGN_LEFT}
>
{/*<!--!Font Awesome Free v7.1.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2026 Fonticons, Inc.-->*/}
<path d="M384 128C384 145.7 369.7 160 352 160L128 160C110.3 160 96 145.7 96 128C96 110.3 110.3 96 128 96L352 96C369.7 96 384 110.3 384 128zM384 384C384 401.7 369.7 416 352 416L128 416C110.3 416 96 401.7 96 384C96 366.3 110.3 352 128 352L352 352C369.7 352 384 366.3 384 384zM96 256C96 238.3 110.3 224 128 224L512 224C529.7 224 544 238.3 544 256C544 273.7 529.7 288 512 288L128 288C110.3 288 96 273.7 96 256zM544 512C544 529.7 529.7 544 512 544L128 544C110.3 544 96 529.7 96 512C96 494.3 110.3 480 128 480L512 480C529.7 480 544 494.3 544 512z" />
</svg>
</defs>
</svg>,
document.body,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,19 +1,22 @@
import type { ArthasResponseWithId } from '@/api/impl/arthas.ts'
import type { PureArthasResponse } from '@/api/impl/arthas.ts'
import React, { useCallback, useMemo, useRef, useState } from 'react'
import { type DetailComponentProps, getArthasMessageView } from './factory.ts'
import type { ArthasMessage } from '@/pages/channel/[channelId]/db.ts'

interface ArthasResponseDetailProps {
message: ArthasResponseWithId
message: ArthasMessage
}
const ArthasResponseDetail: React.FC<ArthasResponseDetailProps> = (props) => {
const Component = useMemo(() => {
return getArthasMessageView(props.message.type)?.detailComponent
}, [props.message.type])
return getArthasMessageView(props.message.value.type)?.detailComponent
}, [props.message.value.type]) as React.FC<
DetailComponentProps<PureArthasResponse>
>
const componentCache = useRef(new Map())
const [dirtyIds, setDirtyIds] = useState(new Set<number>())
const [dirtyIds, setDirtyIds] = useState(new Set<string>())

const handleOnDirty = useCallback(
(id: number) => {
(id: string) => {
if (!dirtyIds.has(id)) {
setDirtyIds((prev) => new Set(prev).add(id))
}
Expand All @@ -23,8 +26,8 @@ const ArthasResponseDetail: React.FC<ArthasResponseDetailProps> = (props) => {

const renderDetail = useCallback(
(
id: number,
Component: React.FC<DetailComponentProps<ArthasResponseWithId>>,
id: string,
Component: React.FC<DetailComponentProps<PureArthasResponse>>,
) => {
// 1. 如果已经在缓存池里,直接返回缓存的实例
if (componentCache.current.has(id)) {
Expand All @@ -34,8 +37,8 @@ const ArthasResponseDetail: React.FC<ArthasResponseDetailProps> = (props) => {
// 2. 如果不在缓存中,创建一个新的实例
const newComponent = (
<Component
key={props.message.fid} // 必须有稳定的 key
msg={props.message}
key={props.message.id} // 必须有稳定的 key
msg={props.message.value}
onDirty={() => handleOnDirty(id)}
/>
)
Expand All @@ -54,15 +57,15 @@ const ArthasResponseDetail: React.FC<ArthasResponseDetailProps> = (props) => {
)
}
if (
dirtyIds.has(props.message.fid) &&
!componentCache.current.has(props.message.fid)
dirtyIds.has(props.message.id) &&
!componentCache.current.has(props.message.id)
) {
componentCache.current.set(
props.message.fid,
renderDetail(props.message.fid, Component),
props.message.id,
renderDetail(props.message.id, Component),
)
}
const currentId = props.message.fid
const currentId = props.message.id
return (
<>
{/* 策略:对于脏组件,我们全部渲染但在 CSS 上隐藏;对于非脏组件,动态切换 */}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,19 +1,21 @@
import type { ArthasResponseWithId } from '@/api/impl/arthas.ts'
import React, { useMemo } from 'react'
import { getArthasMessageView, type PreviewInfo } from './factory.ts'
import clsx from 'clsx'
import { Tooltip } from '@heroui/react'
import type { ArthasMessage } from '@/pages/channel/[channelId]/db.ts'

interface ArthasResponsePreviewProps {
message: ArthasResponseWithId
message: ArthasMessage
}
const ArthasResponsePreview: React.FC<ArthasResponsePreviewProps> = (props) => {
const state: PreviewInfo = useMemo(() => {
return (
getArthasMessageView(props.message.type)?.display(props.message) ?? {
getArthasMessageView(props.message.value.type)?.display(
props.message.value,
) ?? {
name: '<Unknown>',
color: 'default',
tag: props.message.type,
tag: props.message.value.type,
}
)
}, [props.message])
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type React from 'react'
import type { DetailComponentProps } from '@/pages/channel/[channelId]/_tabs/_console/_message_view/factory.ts'
import type { DetailComponentProps } from '../factory.ts'
import {
Table,
TableBody,
Expand All @@ -19,7 +19,6 @@ type Classloader = {
export type ClassLoaderMessage = {
type: 'classloader'
jobId: number
fid: number
classLoaderStats?: Record<
string,
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
import type React from 'react'
import { Code } from '@heroui/react'
import type { DetailComponentProps } from '@/pages/channel/[channelId]/_tabs/_console/_message_view/factory.ts'
import type { DetailComponentProps } from '../factory.ts'

export type CommandMessage = {
type: 'command'
jobId: number
state: string
command: string
message?: string
fid: number
}

const CommandMessageDetail: React.FC<DetailComponentProps<CommandMessage>> = (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,6 @@ export type DashboardMessage = {
runtimeInfo: RuntimeInfo
threads: Thread[]
type: 'dashboard'
fid: number
}

export const DashboardMessageDetail: React.FC = () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Code } from '@heroui/react'
import type { DetailComponentProps } from '@/pages/channel/[channelId]/_tabs/_console/_message_view/factory.ts'
import type { DetailComponentProps } from '../factory.ts'

type EnhancerMessage = {
effect: {
Expand All @@ -10,7 +10,6 @@ type EnhancerMessage = {
}
jobId: number
success: boolean
fid: number
type: 'enhancer'
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React, { useCallback, useContext } from 'react'
import type { DetailComponentProps } from '@/pages/channel/[channelId]/_tabs/_console/_message_view/factory.ts'
import type { DetailComponentProps } from '../factory.ts'
import Code from '@/components/Code.tsx'
import { Link } from '@heroui/react'
import ChannelContext from '@/pages/channel/[channelId]/context.ts'
Expand All @@ -15,7 +15,6 @@ export type JadMessage = {
mappings: Record<string, number>
source: string
type: 'jad'
fid: number
}

const JadMessageDetail: React.FC<DetailComponentProps<JadMessage>> = (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {
TableRow,
} from '@heroui/react'
import React, { useMemo } from 'react'
import type { DetailComponentProps } from '@/pages/channel/[channelId]/_tabs/_console/_message_view/factory.ts'
import type { DetailComponentProps } from '../factory.ts'
import PercentageData from '@/pages/channel/[channelId]/_tabs/_dashboard/PercentageData.tsx'

type MemoryInfo = {
Expand All @@ -21,19 +21,17 @@ type MemoryMessage = {
jobId: number
memoryInfo: Record<string, MemoryInfo[]>
type: 'memory'
fid: number
}
const MemoryMessageDetail: React.FC<DetailComponentProps<MemoryMessage>> = ({
msg,
}) => {
const infos = useMemo(() => {
const infos: MemoryInfo[] = []
console.log(msg.memoryInfo)
for (const entry of Object.entries(msg.memoryInfo)) {
infos.push(...entry[1])
}
return infos
}, [msg.fid])
}, [msg.memoryInfo])
return (
<Table>
<TableHeader>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
import type React from 'react'
import { Code } from '@heroui/react'
import type { DetailComponentProps } from '@/pages/channel/[channelId]/_tabs/_console/_message_view/factory.ts'
import type { DetailComponentProps } from '../factory.ts'

type MessageResponse = {
type: 'message'
jobId: number
message: string
fid: number
}

const MessageDetail: React.FC<DetailComponentProps<MessageResponse>> = (
Expand Down
Loading