diff --git a/arthas/README.md b/arthas/README.md index 077b281..e61ed70 100644 --- a/arthas/README.md +++ b/arthas/README.md @@ -1,6 +1,6 @@ # Arthas Mission Control Plugin -The Arthas plugin adds JVM diagnostics to Mission Control for Kubernetes workloads. It attaches [Arthas](https://arthas.aliyun.com/) to a Java process running in a selected pod and exposes the Arthas web console, HTTP API, and diagnostic operations from the Mission Control UI. +The Arthas plugin adds JVM diagnostics to Mission Control for Kubernetes workloads. It attaches [Arthas](https://arthas.aliyun.com/) to a Java process running in a selected pod and exposes supported diagnostic operations from the Mission Control UI. ## What it does @@ -15,8 +15,8 @@ The Arthas plugin adds JVM diagnostics to Mission Control for Kubernetes workloa - Resolves the selected workload to a running pod and container. - Copies/installs `arthas-boot.jar` into the target pod when needed. - Attaches Arthas to the JVM in the target container. -- Opens Kubernetes port-forwards to the Arthas HTTP console and optional MCP endpoint. -- Proxies the Arthas UI/API through Mission Control. +- Opens a Kubernetes port-forward to the Arthas HTTP API for plugin-side operation handlers. +- Executes supported Arthas commands through declared Mission Control plugin operations. - Tracks active sessions inside the plugin process. ## Operations diff --git a/arthas/http.go b/arthas/http.go index 2081edc..6180303 100644 --- a/arthas/http.go +++ b/arthas/http.go @@ -1,16 +1,10 @@ package main import ( - "bytes" "context" "encoding/json" - "fmt" "io" - "log" "net/http" - "net/http/httputil" - "net/url" - "os" "strings" "github.com/flanksource/incident-commander/plugin/sdk" @@ -43,206 +37,3 @@ func (p *ArthasPlugin) httpInvoke(operation string, handler func(context.Context } }) } - -func (p *ArthasPlugin) HTTPHandler() http.Handler { - mux := http.NewServeMux() - mux.HandleFunc("/proxy/", p.httpProxyConsole) - mux.HandleFunc("/mcp/", p.httpProxyMCP) - mux.Handle("/version", sdk.VersionHandler(sdk.BuildInfo{ - Name: "arthas", - Version: Version, - BuildDate: BuildDate, - UIChecksum: uiChecksum, - })) - return mux -} - -func (p *ArthasPlugin) httpProxyConsole(w http.ResponseWriter, r *http.Request) { - p.proxyTo(w, r, "/proxy/", func(s sessionPorts) int { return s.HTTP }, true) -} - -func (p *ArthasPlugin) httpProxyMCP(w http.ResponseWriter, r *http.Request) { - p.proxyTo(w, r, "/mcp/", func(s sessionPorts) int { return s.MCP }, false) -} - -type sessionPorts struct { - HTTP int - MCP int -} - -func (p *ArthasPlugin) proxyTo(w http.ResponseWriter, r *http.Request, prefix string, portOf func(sessionPorts) int, rewriteHTML bool) { - rest := strings.TrimPrefix(r.URL.Path, prefix) - id, tail, _ := strings.Cut(rest, "/") - if id == "" { - http.Error(w, "missing session id", http.StatusBadRequest) - return - } - sess, ok := p.sessions.Get(id) - if !ok { - http.Error(w, "session not found", http.StatusNotFound) - return - } - port := portOf(sessionPorts{HTTP: sess.HTTPLocalPort, MCP: sess.MCPLocalPort}) - if port == 0 { - http.Error(w, "session endpoint is not enabled", http.StatusBadRequest) - return - } - target, _ := url.Parse(fmt.Sprintf("http://127.0.0.1:%d", port)) - proxy := httputil.NewSingleHostReverseProxy(target) - proxy.ErrorLog = log.New(os.Stderr, "[WARN] arthas console proxy: ", 0) - if rewriteHTML { - // The browser sees this iframe behind the host's UI proxy, so absolute - // URLs we inject must include the X-Forwarded-Prefix the host set. - hostPrefix := strings.TrimRight(r.Header.Get("X-Forwarded-Prefix"), "/") - basePrefix := fmt.Sprintf("%s%s%s/", hostPrefix, prefix, id) - proxy.ModifyResponse = func(resp *http.Response) error { - return rewriteArthasResponse(resp, basePrefix) - } - } - r.URL.Path = "/" + tail - r.URL.RawPath = "" - proxy.ServeHTTP(w, r) -} - -func rewriteArthasResponse(resp *http.Response, basePrefix string) error { - rewriteLocation(resp, basePrefix) - - ctype := resp.Header.Get("Content-Type") - isHTML := strings.Contains(ctype, "text/html") - isJS := strings.Contains(ctype, "javascript") - isCSS := strings.Contains(ctype, "text/css") - if !isHTML && !isJS && !isCSS { - return nil - } - body, err := io.ReadAll(resp.Body) - if err != nil { - return err - } - _ = resp.Body.Close() - - proxyRoot := strings.TrimSuffix(basePrefix, "/") - if isHTML { - body = rewriteHTMLRootPaths(body, basePrefix) - wsShim := fmt.Appendf(nil, ``, basePrefix, proxyRoot) - idx := bytes.Index(body, []byte("")) - if idx < 0 { - idx = bytes.Index(bytes.ToLower(body), []byte("")) - } - if idx >= 0 { - insertAt := idx + len("") - base := fmt.Appendf(nil, ``, basePrefix) - inject := append(base, wsShim...) - body = append(body[:insertAt], append(inject, body[insertAt:]...)...) - } - } - if isJS { - body = rewriteScriptRootPaths(body, proxyRoot) - } - if isCSS { - body = rewriteCSSRootPaths(body, proxyRoot) - } - resp.Body = io.NopCloser(bytes.NewReader(body)) - resp.ContentLength = int64(len(body)) - resp.Header.Set("Content-Length", fmt.Sprint(len(body))) - return nil -} - -func rewriteLocation(resp *http.Response, basePrefix string) { - loc := resp.Header.Get("Location") - if loc == "" { - return - } - proxyRoot := strings.TrimSuffix(basePrefix, "/") - for _, root := range []string{"/static/", "/api", "/ws"} { - if loc == root || strings.HasPrefix(loc, root+"/") || strings.HasPrefix(loc, root) && strings.HasSuffix(root, "/") { - resp.Header.Set("Location", proxyRoot+loc) - return - } - } -} - -func rewriteHTMLRootPaths(body []byte, basePrefix string) []byte { - proxyRoot := strings.TrimSuffix(basePrefix, "/") - attrs := []string{"src", "href", "action", "poster", "data", "content"} - for _, attr := range attrs { - for _, quote := range []byte{'"', '\''} { - for _, root := range proxiedRoots() { - old := []byte(fmt.Sprintf(`%s=%c%s`, attr, quote, root)) - newValue := proxyRoot + root - body = bytes.ReplaceAll(body, old, []byte(fmt.Sprintf(`%s=%c%s`, attr, quote, newValue))) - } - } - } - body = rewriteCSSRootPaths(body, proxyRoot) - return body -} - -func rewriteScriptRootPaths(body []byte, proxyRoot string) []byte { - for _, quote := range []byte{'"', '\'', '`'} { - for _, root := range proxiedRoots() { - old := []byte{quote} - old = append(old, root...) - newValue := []byte{quote} - newValue = append(newValue, proxyRoot...) - newValue = append(newValue, root...) - body = bytes.ReplaceAll(body, old, newValue) - } - } - return body -} - -func rewriteCSSRootPaths(body []byte, proxyRoot string) []byte { - for _, root := range proxiedRoots() { - body = bytes.ReplaceAll(body, []byte("url("+root), []byte("url("+proxyRoot+root)) - body = bytes.ReplaceAll(body, []byte(`url("`+root), []byte(`url("`+proxyRoot+root)) - body = bytes.ReplaceAll(body, []byte(`url('`+root), []byte(`url('`+proxyRoot+root)) - } - return body -} - -func proxiedRoots() []string { - return []string{"/api", "/ws", "/static/"} -} diff --git a/arthas/http_test.go b/arthas/http_test.go deleted file mode 100644 index 0238a80..0000000 --- a/arthas/http_test.go +++ /dev/null @@ -1,71 +0,0 @@ -package main - -import ( - "io" - "net/http" - "strings" - - ginkgo "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" -) - -var _ = ginkgo.Describe("Arthas proxy rewriting", func() { - ginkgo.It("rewrites root absolute assets in HTML", func() { - resp := &http.Response{ - Header: http.Header{"Content-Type": []string{"text/html"}}, - Body: io.NopCloser(strings.NewReader(``)), - } - Expect(rewriteArthasResponse(resp, "/proxy/s1/")).To(Succeed()) - body, err := io.ReadAll(resp.Body) - Expect(err).ToNot(HaveOccurred()) - Expect(string(body)).To(ContainSubstring(`src="/proxy/s1/static/js/main.js"`)) - Expect(string(body)).To(ContainSubstring("window.WebSocket")) - }) - - ginkgo.It("rewrites assets through the host UI prefix when present", func() { - resp := &http.Response{ - Header: http.Header{"Content-Type": []string{"text/html"}}, - Body: io.NopCloser(strings.NewReader(`
`)), - } - basePrefix := "/api/plugins/arthas/ui/proxy/s1/" - Expect(rewriteArthasResponse(resp, basePrefix)).To(Succeed()) - body, err := io.ReadAll(resp.Body) - Expect(err).ToNot(HaveOccurred()) - got := string(body) - Expect(got).To(ContainSubstring(``)) - Expect(got).To(ContainSubstring(`src="/api/plugins/arthas/ui/proxy/s1/static/js/main.js"`)) - Expect(got).To(ContainSubstring(`href="/api/plugins/arthas/ui/proxy/s1/static/main.css"`)) - Expect(got).To(ContainSubstring(`action="/api/plugins/arthas/ui/proxy/s1/api"`)) - Expect(got).To(ContainSubstring(`proxyBase = "/api/plugins/arthas/ui/proxy/s1/"`)) - }) - - ginkgo.It("rewrites JS root-relative service paths via the host prefix", func() { - resp := &http.Response{ - Header: http.Header{"Content-Type": []string{"application/javascript"}}, - Body: io.NopCloser(strings.NewReader(`fetch("/api"); new WebSocket("/ws"); var u = "/static/js/extra.js"; var v = '/static/img/logo.png';`)), - } - Expect(rewriteArthasResponse(resp, "/api/plugins/arthas/ui/proxy/s1/")).To(Succeed()) - body, err := io.ReadAll(resp.Body) - Expect(err).ToNot(HaveOccurred()) - got := string(body) - Expect(got).To(ContainSubstring(`fetch("/api/plugins/arthas/ui/proxy/s1/api")`)) - Expect(got).To(ContainSubstring(`new WebSocket("/api/plugins/arthas/ui/proxy/s1/ws")`)) - Expect(got).To(ContainSubstring(`"/api/plugins/arthas/ui/proxy/s1/static/js/extra.js"`)) - Expect(got).To(ContainSubstring(`'/api/plugins/arthas/ui/proxy/s1/static/img/logo.png'`)) - }) - - ginkgo.It("rewrites CSS urls and redirect locations", func() { - resp := &http.Response{ - Header: http.Header{ - "Content-Type": []string{"text/css"}, - "Location": []string{"/api"}, - }, - Body: io.NopCloser(strings.NewReader(`.logo{background:url(/static/png/arthas.png)}`)), - } - Expect(rewriteArthasResponse(resp, "/api/plugins/arthas/ui/proxy/s1/")).To(Succeed()) - Expect(resp.Header.Get("Location")).To(Equal("/api/plugins/arthas/ui/proxy/s1/api")) - body, err := io.ReadAll(resp.Body) - Expect(err).ToNot(HaveOccurred()) - Expect(string(body)).To(ContainSubstring(`url(/api/plugins/arthas/ui/proxy/s1/static/png/arthas.png)`)) - }) -}) diff --git a/arthas/internal/arthas/bootstrap.go b/arthas/internal/arthas/bootstrap.go index db5e371..2f499f6 100644 --- a/arthas/internal/arthas/bootstrap.go +++ b/arthas/internal/arthas/bootstrap.go @@ -30,7 +30,6 @@ const ( ArthasJarPath = "/tmp/arthas-boot.jar" ArthasBootURL = "https://arthas.aliyun.com/arthas-boot.jar" DefaultRemoteHTTP = 8563 - DefaultRemoteMCP = 8777 ) // portProbePrelude is a POSIX-sh helper injected at the top of every exec @@ -68,9 +67,7 @@ type StartOptions struct { Pod string Container string LocalHTTP int // 0 = auto-allocate - LocalMCP int // 0 = auto-allocate RemoteHTTP int // defaults to 8563 - RemoteMCP int // defaults to 8777 // SkipJDKInstall bypasses the Java 8 JRE side-load; the attach will fail // loud if tools.jar is missing. Intended for operators who've provisioned // the pod out-of-band. @@ -78,18 +75,14 @@ type StartOptions struct { } // Start runs the full bootstrap: ensure Arthas jar is present, attach it to -// PID 1 with HTTP enabled, start the MCP server plugin, and open port-forwards -// to the caller's workstation. The returned Session carries a Stop closure -// that tears down the port-forwards. Note: the in-pod Arthas process is left -// running; a subsequent Start on the same pod will reuse it. +// PID 1 with HTTP enabled, and open a port-forward to the Arthas HTTP API. +// The returned Session carries a Stop closure that tears down the port-forward. +// Note: the in-pod Arthas process is left running; a subsequent Start on the +// same pod will reuse it. func Start(ctx context.Context, restCfg *rest.Config, opts StartOptions) (*Session, error) { if opts.RemoteHTTP == 0 { opts.RemoteHTTP = DefaultRemoteHTTP } - if opts.RemoteMCP == 0 { - opts.RemoteMCP = DefaultRemoteMCP - } - javaInfo, err := detectJava(ctx, restCfg, opts) if err != nil { return nil, fmt.Errorf("detect java: %w", err) @@ -111,30 +104,11 @@ func Start(ctx context.Context, restCfg *rest.Config, opts StartOptions) (*Sessi if err := attachArthas(ctx, restCfg, opts, javaHome, remoteTelnet); err != nil { return nil, fmt.Errorf("attach arthas: %w", err) } - // MCP plugin is distributed separately from arthas-boot and isn't present - // in upstream arthas 4.1.x. Attempt to start it — if the command doesn't - // exist, fall through: the arthas HTTP /api endpoint on :8563 is itself a - // fully-featured REST surface the UI exposes directly, so MCP is a nice- - // to-have, not a hard requirement. - mcpEnabled := true - if err := enableMCP(ctx, restCfg, opts); err != nil { - mcpEnabled = false - } - _ = mcpEnabled // surfaced to Session below - localHTTP, err := pickLocalPort(opts.LocalHTTP) if err != nil { return nil, err } mappings := []k8s.PortMapping{{LocalPort: localHTTP, RemotePort: opts.RemoteHTTP}} - localMCP := 0 - if mcpEnabled { - localMCP, err = pickLocalPort(opts.LocalMCP) - if err != nil { - return nil, err - } - mappings = append(mappings, k8s.PortMapping{LocalPort: localMCP, RemotePort: opts.RemoteMCP}) - } fwd, ready, err := k8s.StartPortForward(restCfg, opts.Namespace, opts.Pod, mappings, io.Discard, io.Discard) if err != nil { @@ -145,13 +119,12 @@ func Start(ctx context.Context, restCfg *rest.Config, opts StartOptions) (*Sessi return nil, fmt.Errorf("port-forward not ready: %w", err) } - sess := NewSession(opts.Namespace, opts.Kind, opts.Name, opts.Pod, opts.Container, localHTTP, localMCP, func() error { + sess := NewSession(opts.Namespace, opts.Kind, opts.Name, opts.Pod, opts.Container, localHTTP, func() error { return fwd.Close() }) sess.JavaVersion = javaInfo.Major sess.JDKProvisioned = javaHome != "" sess.SideloadedJavaHome = javaHome - sess.MCPEnabled = mcpEnabled return sess, nil } @@ -193,7 +166,7 @@ if probe_port 127.0.0.1 %[1]d; then exit 0; fi # answers it bails with "already listen port ..., skip attach". Some base # images already bind 3658 for other reasons, so we pick a fresh high port # per session (the telnet listener stays loopback-only anyway — we only -# port-forward HTTP + MCP to the caller). +# port-forward HTTP to the plugin process). if probe_port 127.0.0.1 %[4]d; then echo "chosen telnet port %[4]d is already in use in the pod; retry to pick a different port" >&2 exit 1 @@ -245,29 +218,6 @@ exit 1`, opts.RemoteHTTP, ArthasJarPath, javaHomeExport, remoteTelnet) return execSh(ctx, restCfg, opts, script) } -func enableMCP(ctx context.Context, restCfg *rest.Config, opts StartOptions) error { - // Use Arthas HTTP API to start the mcp-server plugin. - // https://arthas.aliyun.com/en/doc/http-api.html - script := fmt.Sprintf(`set -e -`+portProbePrelude+` -if probe_port 127.0.0.1 %[2]d; then exit 0; fi -RESP=$(curl -sS -XPOST "http://127.0.0.1:%[1]d/api" \ - -H 'Content-Type: application/json' \ - -d '{"action":"exec","command":"mcp-server start --port %[2]d"}') -echo "$RESP" -echo "$RESP" | grep -q '"state":"SUCCEEDED"' || { - echo "mcp-server plugin did not start (needs Arthas with MCP support)" >&2 - exit 1 -} -for i in $(seq 1 20); do - if probe_port 127.0.0.1 %[2]d; then exit 0; fi - sleep 0.5 -done -echo "mcp port %[2]d did not come up" >&2 -exit 1`, opts.RemoteHTTP, opts.RemoteMCP) - return execSh(ctx, restCfg, opts, script) -} - func execSh(ctx context.Context, restCfg *rest.Config, opts StartOptions, script string) error { var stdout, stderr bytes.Buffer err := k8s.ExecInPod(ctx, restCfg, k8s.ExecOptions{ diff --git a/arthas/internal/arthas/session.go b/arthas/internal/arthas/session.go index 4c47535..28310dd 100644 --- a/arthas/internal/arthas/session.go +++ b/arthas/internal/arthas/session.go @@ -20,7 +20,6 @@ type Session struct { Pod string `json:"pod"` Container string `json:"container"` HTTPLocalPort int `json:"httpLocalPort"` - MCPLocalPort int `json:"mcpLocalPort"` StartedAt time.Time `json:"startedAt"` // JavaVersion is the major version detected via `java -version` in the @@ -32,10 +31,6 @@ type Session struct { // SideloadedJavaHome is the path inside the pod where the downloaded JDK // lives, when JDKProvisioned is true. SideloadedJavaHome string `json:"sideloadedJavaHome,omitempty"` - // MCPEnabled is true when the arthas mcp-server plugin was successfully - // started. Upstream arthas 4.1.x doesn't bundle MCP, so this is often - // false; clients should fall back to arthas' native HTTP /api endpoint. - MCPEnabled bool `json:"mcpEnabled"` stop func() error stopOnce sync.Once @@ -118,7 +113,7 @@ func (r *SessionRegistry) StopAll() { } // NewSession builds a Session with a random ID and installs the Stop closure. -func NewSession(namespace, kind, name, pod, container string, httpPort, mcpPort int, stop func() error) *Session { +func NewSession(namespace, kind, name, pod, container string, httpPort int, stop func() error) *Session { return &Session{ ID: newID(), Namespace: namespace, @@ -127,7 +122,6 @@ func NewSession(namespace, kind, name, pod, container string, httpPort, mcpPort Pod: pod, Container: container, HTTPLocalPort: httpPort, - MCPLocalPort: mcpPort, StartedAt: time.Now().UTC(), stop: stop, } diff --git a/arthas/ops.go b/arthas/ops.go index f2b65df..b175ff1 100644 --- a/arthas/ops.go +++ b/arthas/ops.go @@ -42,9 +42,7 @@ type SessionCreateParams struct { Pod string `json:"pod,omitempty"` Container string `json:"container,omitempty"` LocalHTTP int `json:"localHttp,omitempty"` - LocalMCP int `json:"localMcp,omitempty"` RemoteHTTP int `json:"remoteHttp,omitempty"` - RemoteMCP int `json:"remoteMcp,omitempty"` SkipJDKInstall bool `json:"skipJdkInstall,omitempty"` } @@ -123,9 +121,7 @@ func (p *ArthasPlugin) sessionCreate(ctx context.Context, req sdk.InvokeCtx) (an Pod: pod, Container: container, LocalHTTP: params.LocalHTTP, - LocalMCP: params.LocalMCP, RemoteHTTP: params.RemoteHTTP, - RemoteMCP: params.RemoteMCP, SkipJDKInstall: params.SkipJDKInstall, }) if err != nil { diff --git a/arthas/ui-src/src/lib/api.ts b/arthas/ui-src/src/lib/api.ts index 745fd25..b3f80dc 100644 --- a/arthas/ui-src/src/lib/api.ts +++ b/arthas/ui-src/src/lib/api.ts @@ -43,7 +43,3 @@ export async function callOp( export function configIDFromURL(): string { return new URLSearchParams(window.location.search).get("config_id") ?? ""; } - -export function pluginURL(path: string): string { - return new URL(path.replace(/^\//, ""), window.location.href).toString(); -} diff --git a/arthas/ui-src/src/pages/ArthasDashboardTab.tsx b/arthas/ui-src/src/pages/ArthasDashboardTab.tsx index 89eef56..51d140c 100644 --- a/arthas/ui-src/src/pages/ArthasDashboardTab.tsx +++ b/arthas/ui-src/src/pages/ArthasDashboardTab.tsx @@ -9,7 +9,7 @@ import { type ProcessNode, type ProgressSegment, } from "@flanksource/clicky-ui"; -import { pluginURL } from "@/lib/api"; +import { callOp } from "@/lib/api"; import { Input } from "@/components/ui/input"; import { Spinner } from "@/components/ui/spinner"; @@ -61,19 +61,8 @@ async function execArthas( sessionId: string, command: string, ): Promise<{ results: unknown[]; state: string }> { - const res = await fetch(pluginURL(`proxy/${sessionId}/api`), { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ action: "exec", command }), - }); - if (!res.ok) throw new Error(`arthas exec (${command}): ${res.status}`); - const body = await res.json(); - const state = body.state ?? body?.body?.state ?? "UNKNOWN"; - const results = body?.body?.results ?? []; - if (state !== "SUCCEEDED") { - throw new Error(`arthas command "${command}" failed (${state}): ${body?.body?.message ?? ""}`); - } - return { results, state }; + const body = await callOp<{ results?: unknown[]; state?: string }>("exec", { sessionId, command }); + return { results: body.results ?? [], state: body.state ?? "UNKNOWN" }; } function fmtBytes(n: number | undefined): string { diff --git a/arthas/ui-src/src/pages/ArthasPage.tsx b/arthas/ui-src/src/pages/ArthasPage.tsx index f635817..c1375ef 100644 --- a/arthas/ui-src/src/pages/ArthasPage.tsx +++ b/arthas/ui-src/src/pages/ArthasPage.tsx @@ -1,11 +1,11 @@ import { useEffect, useMemo, useRef, useState } from "react"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; -import { Bug, Check, ChevronDown, Copy, Plus, Trash2 } from "lucide-react"; +import { Bug, ChevronDown, Plus, Trash2 } from "lucide-react"; import { Button } from "@/components/ui/button"; import { Spinner } from "@/components/ui/spinner"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { toastManager } from "@/components/ui/toast"; -import { callOp, configIDFromURL, pluginURL } from "@/lib/api"; +import { callOp, configIDFromURL } from "@/lib/api"; import { ArthasDashboardTab } from "./ArthasDashboardTab"; import { ArthasMBeanTab } from "./ArthasMBeanTab"; import { ArthasOgnlTab } from "./ArthasOgnlTab"; @@ -19,12 +19,10 @@ interface ArthasSession { pod: string; container: string; httpLocalPort: number; - mcpLocalPort: number; startedAt: string; javaVersion?: number; jdkProvisioned?: boolean; sideloadedJavaHome?: string; - mcpEnabled?: boolean; } interface RunningPod { @@ -317,26 +315,20 @@ function SessionDetail({ }: { session: ArthasSession; } & SessionMenuProps) { - const [tab, setTab] = useState("console"); - const consoleURL = pluginURL(`proxy/${session.id}/`); + const [tab, setTab] = useState("dashboard"); return (
- Web Console - OGNL Dashboard + OGNL MBeans Profiler - {session.mcpEnabled ? MCP : HTTP API} Info
- - - @@ -349,12 +341,6 @@ function SessionDetail({ - - - - - -
Session ID
@@ -376,79 +362,3 @@ function SessionDetail({ ); } - -function ConsoleFrame({ src, pod }: { src: string; pod: string }) { - const [loaded, setLoaded] = useState(false); - const ref = useRef(null); - return ( -
- {!loaded && ( -
- - Connecting to Arthas web console… -
- )} -