mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-07-24 07:09:50 +08:00
✨ feat(query-editor): 收敛 SQL 分析工作台与结果区日志体验
- 新增 SQL 分析工作台,统一承载慢 SQL 和 SQL 诊断视图 - 将 SQL 执行日志收进结果区首个日志标签并在失败时展示错误摘要 - 调整侧边栏入口、标签展示、多语言文案与定向前端测试覆盖
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import { Modal, Spin, Tabs, Typography } from 'antd'
|
||||
import { ApartmentOutlined, CodeOutlined } from '@ant-design/icons'
|
||||
import { Empty, Modal, Segmented, Spin, Typography } from 'antd'
|
||||
import { DiagnoseQuery } from '../../../wailsjs/go/app/App'
|
||||
import { buildRpcConnectionConfig } from '../../utils/connectionRpcConfig'
|
||||
import type { ConnectionConfig } from '../../types'
|
||||
@@ -31,11 +32,20 @@ interface ExplainWorkbenchProps {
|
||||
sql: string
|
||||
}
|
||||
|
||||
export default function ExplainWorkbench({ open, onClose, config, dbName, sql }: ExplainWorkbenchProps) {
|
||||
interface ExplainReportViewProps {
|
||||
config: ConnectionConfig
|
||||
dbName: string
|
||||
sql: string
|
||||
runKey?: string | number | null
|
||||
}
|
||||
|
||||
export function ExplainReportView({ config, dbName, sql, runKey }: ExplainReportViewProps) {
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [report, setReport] = useState<DiagnoseReport | null>(null)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [selectedNodeId, setSelectedNodeId] = useState<string | null>(null)
|
||||
const [activeView, setActiveView] = useState<'plan' | 'raw'>('plan')
|
||||
const hasRequestedRun = runKey !== null && runKey !== undefined && runKey !== ''
|
||||
|
||||
const runDiagnose = useCallback(async () => {
|
||||
if (!sql.trim()) {
|
||||
@@ -62,10 +72,17 @@ export default function ExplainWorkbench({ open, onClose, config, dbName, sql }:
|
||||
}, [config, dbName, sql])
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
void runDiagnose()
|
||||
if (!hasRequestedRun) {
|
||||
return
|
||||
}
|
||||
}, [open, runDiagnose])
|
||||
void runDiagnose()
|
||||
}, [hasRequestedRun, runDiagnose, runKey])
|
||||
|
||||
useEffect(() => {
|
||||
if (report) {
|
||||
setActiveView('plan')
|
||||
}
|
||||
}, [report])
|
||||
|
||||
const selectedNode = useMemo<ExplainNode | undefined>(() => {
|
||||
if (!report || !selectedNodeId) return undefined
|
||||
@@ -78,6 +95,106 @@ export default function ExplainWorkbench({ open, onClose, config, dbName, sql }:
|
||||
}
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div className="gn-explain-report-view">
|
||||
<style>{reportViewStyles}</style>
|
||||
{loading && (
|
||||
<div style={{ textAlign: 'center', padding: '60px 0' }}>
|
||||
<Spin tip="正在执行 EXPLAIN 并解析计划..." />
|
||||
</div>
|
||||
)}
|
||||
{error && (
|
||||
<Paragraph type="danger" style={{ padding: 16 }}>
|
||||
<Text strong>诊断失败:</Text>
|
||||
{error}
|
||||
</Paragraph>
|
||||
)}
|
||||
{!loading && !error && !report && !hasRequestedRun && (
|
||||
<Empty description="输入 SQL 后运行诊断" style={{ padding: '48px 0' }} />
|
||||
)}
|
||||
{!loading && !error && report && (
|
||||
<div className="gn-explain-report-shell">
|
||||
<div className="gn-explain-report-switcher-row">
|
||||
<Segmented
|
||||
value={activeView}
|
||||
onChange={(value) => setActiveView(value as 'plan' | 'raw')}
|
||||
className="gn-explain-report-switcher"
|
||||
options={[
|
||||
{
|
||||
value: 'plan',
|
||||
label: (
|
||||
<span className="gn-explain-report-switcher-label">
|
||||
<ApartmentOutlined />
|
||||
<span>执行计划</span>
|
||||
</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
value: 'raw',
|
||||
label: (
|
||||
<span className="gn-explain-report-switcher-label">
|
||||
<CodeOutlined />
|
||||
<span>原文</span>
|
||||
</span>
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<Text type="secondary" className="gn-explain-report-switcher-meta">
|
||||
{report.plan.nodes.length} 节点
|
||||
<span className="gn-explain-report-switcher-meta-separator">/</span>
|
||||
{report.plan.rawFormat}
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
<div className="gn-explain-report-content">
|
||||
{activeView === 'plan' ? (
|
||||
<div className="gn-explain-plan-view">
|
||||
<div className="gn-explain-plan-graph">
|
||||
<ExplainGraph
|
||||
nodes={report.plan.nodes}
|
||||
edges={report.plan.edges ?? []}
|
||||
selectedNodeId={selectedNodeId ?? undefined}
|
||||
onSelectNode={setSelectedNodeId}
|
||||
/>
|
||||
</div>
|
||||
<div className="gn-explain-plan-sidebar">
|
||||
<ExplainSidebar
|
||||
stats={report.plan.stats}
|
||||
warnings={report.plan.warnings}
|
||||
suggestions={report.suggestions ?? []}
|
||||
selectedNode={selectedNode}
|
||||
onSelectSuggestion={handleSelectSuggestion}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<pre
|
||||
style={{
|
||||
height: '100%',
|
||||
margin: 0,
|
||||
overflow: 'auto',
|
||||
background: 'var(--gn-code-bg, #f1f3f5)',
|
||||
padding: 12,
|
||||
borderRadius: 4,
|
||||
fontSize: 12,
|
||||
fontFamily: 'ui-monospace, Consolas, monospace',
|
||||
whiteSpace: 'pre-wrap',
|
||||
wordBreak: 'break-all',
|
||||
boxSizing: 'border-box',
|
||||
}}
|
||||
>
|
||||
{report.plan.rawPayload || '(无原文)'}
|
||||
</pre>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default function ExplainWorkbench({ open, onClose, config, dbName, sql }: ExplainWorkbenchProps) {
|
||||
return (
|
||||
<Modal
|
||||
open={open}
|
||||
@@ -88,71 +205,96 @@ export default function ExplainWorkbench({ open, onClose, config, dbName, sql }:
|
||||
title={<Title level={5} style={{ margin: 0 }}>SQL 诊断工作台</Title>}
|
||||
destroyOnClose
|
||||
>
|
||||
<div style={{ minHeight: 480 }}>
|
||||
{loading && (
|
||||
<div style={{ textAlign: 'center', padding: '60px 0' }}>
|
||||
<Spin tip="正在执行 EXPLAIN 并解析计划..." />
|
||||
</div>
|
||||
)}
|
||||
{error && (
|
||||
<Paragraph type="danger" style={{ padding: 16 }}>
|
||||
<Text strong>诊断失败:</Text>
|
||||
{error}
|
||||
</Paragraph>
|
||||
)}
|
||||
{!loading && !error && report && (
|
||||
<Tabs
|
||||
items={[
|
||||
{
|
||||
key: 'plan',
|
||||
label: `执行计划(${report.plan.nodes.length} 节点)`,
|
||||
children: (
|
||||
<div style={{ display: 'flex', gap: 12, height: '70vh', minHeight: 400 }}>
|
||||
<div style={{ flex: 1, minWidth: 400, position: 'relative' }}>
|
||||
<ExplainGraph
|
||||
nodes={report.plan.nodes}
|
||||
edges={report.plan.edges ?? []}
|
||||
selectedNodeId={selectedNodeId ?? undefined}
|
||||
onSelectNode={setSelectedNodeId}
|
||||
/>
|
||||
</div>
|
||||
<div style={{ width: 320, flexShrink: 0, overflowY: 'auto', maxHeight: '70vh' }}>
|
||||
<ExplainSidebar
|
||||
stats={report.plan.stats}
|
||||
warnings={report.plan.warnings}
|
||||
suggestions={report.suggestions ?? []}
|
||||
selectedNode={selectedNode}
|
||||
onSelectSuggestion={handleSelectSuggestion}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'raw',
|
||||
label: `原文(${report.plan.rawFormat})`,
|
||||
children: (
|
||||
<pre
|
||||
style={{
|
||||
maxHeight: '60vh',
|
||||
overflow: 'auto',
|
||||
background: 'var(--gn-code-bg, #f1f3f5)',
|
||||
padding: 12,
|
||||
borderRadius: 4,
|
||||
fontSize: 12,
|
||||
fontFamily: 'ui-monospace, Consolas, monospace',
|
||||
whiteSpace: 'pre-wrap',
|
||||
wordBreak: 'break-all',
|
||||
}}
|
||||
>
|
||||
{report.plan.rawPayload || '(无原文)'}
|
||||
</pre>
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
<div style={{ minHeight: 480, height: '70vh' }}>
|
||||
<ExplainReportView
|
||||
config={config}
|
||||
dbName={dbName}
|
||||
sql={sql}
|
||||
runKey={open ? `${dbName}::${sql}` : null}
|
||||
/>
|
||||
</div>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
const reportViewStyles = `
|
||||
.gn-explain-report-view {
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
.gn-explain-report-shell {
|
||||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
.gn-explain-report-switcher-row {
|
||||
flex: 0 0 auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
margin-bottom: 12px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.gn-explain-report-switcher {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
.gn-explain-report-switcher-label {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
min-width: 88px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.gn-explain-report-switcher .ant-segmented-group {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
}
|
||||
.gn-explain-report-switcher .ant-segmented-item {
|
||||
min-height: 30px;
|
||||
}
|
||||
.gn-explain-report-switcher .ant-segmented-item-label {
|
||||
padding: 5px 12px;
|
||||
font-size: 13px;
|
||||
line-height: 20px;
|
||||
}
|
||||
.gn-explain-report-switcher-meta {
|
||||
flex: 0 0 auto;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.gn-explain-report-switcher-meta-separator {
|
||||
display: inline-block;
|
||||
margin: 0 6px;
|
||||
}
|
||||
.gn-explain-report-content {
|
||||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
.gn-explain-plan-view {
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
}
|
||||
.gn-explain-plan-graph {
|
||||
flex: 1 1 auto;
|
||||
min-width: 320px;
|
||||
min-height: 0;
|
||||
position: relative;
|
||||
}
|
||||
.gn-explain-plan-sidebar {
|
||||
width: 320px;
|
||||
flex: 0 0 320px;
|
||||
min-height: 0;
|
||||
overflow-y: auto;
|
||||
}
|
||||
`
|
||||
|
||||
@@ -8,7 +8,7 @@ import { formatMs, formatNumber } from '../../utils/explainTypes'
|
||||
|
||||
// 慢 SQL 历史面板。
|
||||
// 从 GetSlowQueries 加载 TopN,按 duration / rowsRead / recent 切换排序。
|
||||
// 点击条目可触发 onPickQuery 把 SQL 回填到 QueryEditor。
|
||||
// 点击条目可触发 onPickQuery,把 SQL 带到外部工作台或编辑器。
|
||||
//
|
||||
// 设计要点:
|
||||
// - 独立 Modal,不依赖 Sidebar 内部布局(Sidebar.tsx 已经 9000+ 行,避免污染)
|
||||
@@ -40,7 +40,19 @@ interface SlowQueryPanelProps {
|
||||
onPickQuery?: (sql: string) => void
|
||||
}
|
||||
|
||||
export default function SlowQueryPanel({ open, onClose, config, dbName, onPickQuery }: SlowQueryPanelProps) {
|
||||
interface SlowQueryPanelContentProps {
|
||||
config: ConnectionConfig
|
||||
dbName: string
|
||||
onPickQuery?: (sql: string) => void
|
||||
activeToken?: string | number | null
|
||||
}
|
||||
|
||||
export function SlowQueryPanelContent({
|
||||
config,
|
||||
dbName,
|
||||
onPickQuery,
|
||||
activeToken,
|
||||
}: SlowQueryPanelContentProps) {
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [records, setRecords] = useState<SlowQueryRecord[]>([])
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
@@ -65,10 +77,11 @@ export default function SlowQueryPanel({ open, onClose, config, dbName, onPickQu
|
||||
}, [config, dbName, sortBy])
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
void reload()
|
||||
if (activeToken === null || activeToken === undefined || activeToken === '') {
|
||||
return
|
||||
}
|
||||
}, [open, reload])
|
||||
void reload()
|
||||
}, [activeToken, reload])
|
||||
|
||||
const handleClear = useCallback(async () => {
|
||||
const result = await ClearSlowQueries(buildRpcConnectionConfig(config), dbName)
|
||||
@@ -84,32 +97,15 @@ export default function SlowQueryPanel({ open, onClose, config, dbName, onPickQu
|
||||
(record: SlowQueryRecord) => {
|
||||
if (record.sqlPreview && onPickQuery) {
|
||||
onPickQuery(record.sqlPreview)
|
||||
onClose()
|
||||
}
|
||||
},
|
||||
[onPickQuery, onClose],
|
||||
[onPickQuery],
|
||||
)
|
||||
|
||||
const sorted = useMemo(() => records, [records]) // 后端已排序,前端不再排
|
||||
|
||||
return (
|
||||
<Modal
|
||||
open={open}
|
||||
onCancel={onClose}
|
||||
footer={null}
|
||||
width="70%"
|
||||
style={{ top: 40 }}
|
||||
title={
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||
<ThunderboltOutlined style={{ color: '#fa5252' }} />
|
||||
<Title level={5} style={{ margin: 0 }}>慢 SQL 历史</Title>
|
||||
<Text type="secondary" style={{ fontSize: 12 }}>
|
||||
{dbName || '(当前连接)'}
|
||||
</Text>
|
||||
</div>
|
||||
}
|
||||
destroyOnClose
|
||||
>
|
||||
<div style={{ height: '100%', minHeight: 0, display: 'flex', flexDirection: 'column' }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 12 }}>
|
||||
<Segmented
|
||||
value={sortBy}
|
||||
@@ -148,12 +144,46 @@ export default function SlowQueryPanel({ open, onClose, config, dbName, onPickQu
|
||||
)}
|
||||
|
||||
{!loading && !error && sorted.length > 0 && (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 8, maxHeight: '60vh', overflowY: 'auto' }}>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 8, flex: 1, minHeight: 0, overflowY: 'auto' }}>
|
||||
{sorted.map((r, idx) => (
|
||||
<SlowQueryCard key={r.id ?? idx} record={r} onPick={() => handlePick(r)} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default function SlowQueryPanel({ open, onClose, config, dbName, onPickQuery }: SlowQueryPanelProps) {
|
||||
return (
|
||||
<Modal
|
||||
open={open}
|
||||
onCancel={onClose}
|
||||
footer={null}
|
||||
width="70%"
|
||||
style={{ top: 40 }}
|
||||
title={
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||
<ThunderboltOutlined style={{ color: '#fa5252' }} />
|
||||
<Title level={5} style={{ margin: 0 }}>慢 SQL 历史</Title>
|
||||
<Text type="secondary" style={{ fontSize: 12 }}>
|
||||
{dbName || '(当前连接)'}
|
||||
</Text>
|
||||
</div>
|
||||
}
|
||||
destroyOnClose
|
||||
>
|
||||
<div style={{ minHeight: 480, height: '60vh' }}>
|
||||
<SlowQueryPanelContent
|
||||
config={config}
|
||||
dbName={dbName}
|
||||
activeToken={open ? `${dbName}:open` : null}
|
||||
onPickQuery={(sql) => {
|
||||
onPickQuery?.(sql)
|
||||
onClose()
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
255
frontend/src/components/explain/SqlAnalysisWorkbench.tsx
Normal file
255
frontend/src/components/explain/SqlAnalysisWorkbench.tsx
Normal file
@@ -0,0 +1,255 @@
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import { Alert, Button, Input, Segmented, Typography, message } from 'antd'
|
||||
import { HistoryOutlined, SearchOutlined } from '@ant-design/icons'
|
||||
import { useStore } from '../../store'
|
||||
import type { ConnectionConfig, TabData } from '../../types'
|
||||
import { ExplainReportView } from './ExplainWorkbench'
|
||||
import { SlowQueryPanelContent } from './SlowQueryPanel'
|
||||
|
||||
const { Title, Text } = Typography
|
||||
|
||||
type SqlAnalysisViewKey = 'diagnose' | 'slow-query'
|
||||
|
||||
const resolveRequestedView = (tab: TabData): SqlAnalysisViewKey =>
|
||||
tab.sqlAnalysisView === 'slow-query' ? 'slow-query' : 'diagnose'
|
||||
|
||||
const normalizeConnectionConfig = (connection: any): ConnectionConfig => ({
|
||||
...connection.config,
|
||||
port: Number(connection.config.port),
|
||||
password: connection.config.password || '',
|
||||
database: connection.config.database || '',
|
||||
useSSH: connection.config.useSSH || false,
|
||||
ssh: connection.config.ssh || { host: '', port: 22, user: '', password: '', keyPath: '' },
|
||||
})
|
||||
|
||||
export default function SqlAnalysisWorkbench({ tab }: { tab: TabData }) {
|
||||
const connections = useStore((state) => state.connections)
|
||||
const connection = useMemo(
|
||||
() => connections.find((item) => item.id === tab.connectionId) || null,
|
||||
[connections, tab.connectionId],
|
||||
)
|
||||
const connectionConfig = useMemo(
|
||||
() => (connection ? normalizeConnectionConfig(connection) : null),
|
||||
[connection],
|
||||
)
|
||||
const dbName = String(tab.dbName || '').trim()
|
||||
const [activeView, setActiveView] = useState<SqlAnalysisViewKey>(() => resolveRequestedView(tab))
|
||||
const [sqlDraft, setSqlDraft] = useState(() => String(tab.query || ''))
|
||||
const [diagnoseRunKey, setDiagnoseRunKey] = useState(0)
|
||||
|
||||
useEffect(() => {
|
||||
const nextView = resolveRequestedView(tab)
|
||||
const nextSql = String(tab.query || '')
|
||||
setActiveView(nextView)
|
||||
if (nextSql) {
|
||||
setSqlDraft(nextSql)
|
||||
}
|
||||
if (nextView === 'diagnose' && nextSql.trim()) {
|
||||
setDiagnoseRunKey((previous) => previous + 1)
|
||||
}
|
||||
}, [tab.query, tab.sqlAnalysisRequestKey, tab.sqlAnalysisView])
|
||||
|
||||
const triggerDiagnose = useCallback(() => {
|
||||
if (!sqlDraft.trim()) {
|
||||
message.warning('请输入要诊断的 SQL')
|
||||
return
|
||||
}
|
||||
setActiveView('diagnose')
|
||||
setDiagnoseRunKey((previous) => previous + 1)
|
||||
}, [sqlDraft])
|
||||
|
||||
const handlePickSlowQuery = useCallback((sql: string) => {
|
||||
const nextSql = String(sql || '')
|
||||
if (!nextSql.trim()) {
|
||||
return
|
||||
}
|
||||
setSqlDraft(nextSql)
|
||||
setActiveView('diagnose')
|
||||
setDiagnoseRunKey((previous) => previous + 1)
|
||||
}, [])
|
||||
|
||||
const slowQueryLoadKey = useMemo(
|
||||
() =>
|
||||
activeView === 'slow-query' && connectionConfig
|
||||
? `${tab.sqlAnalysisRequestKey || 'slow-query'}:${tab.connectionId}:${dbName}`
|
||||
: null,
|
||||
[activeView, connectionConfig, dbName, tab.connectionId, tab.sqlAnalysisRequestKey],
|
||||
)
|
||||
|
||||
if (!connectionConfig) {
|
||||
return (
|
||||
<div className="gn-sql-analysis-workbench">
|
||||
<style>{workbenchStyles}</style>
|
||||
<Alert
|
||||
type="warning"
|
||||
showIcon
|
||||
message="当前工作台对应的连接已不可用"
|
||||
description="请重新选择一个有效连接后再打开 SQL 分析工作台。"
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="gn-sql-analysis-workbench">
|
||||
<style>{workbenchStyles}</style>
|
||||
<div className="gn-sql-analysis-workbench-header">
|
||||
<div className="gn-sql-analysis-workbench-header-main">
|
||||
<Title level={5} style={{ margin: 0 }}>
|
||||
SQL 分析工作台
|
||||
</Title>
|
||||
<Text type="secondary">
|
||||
{connection?.name || tab.connectionId}
|
||||
{dbName ? ` / ${dbName}` : ''}
|
||||
</Text>
|
||||
</div>
|
||||
<Segmented
|
||||
value={activeView}
|
||||
onChange={(value) => setActiveView(value as SqlAnalysisViewKey)}
|
||||
className="gn-sql-analysis-view-switcher"
|
||||
options={[
|
||||
{
|
||||
value: 'slow-query',
|
||||
label: (
|
||||
<span className="gn-sql-analysis-view-switcher-label">
|
||||
<HistoryOutlined />
|
||||
<span>慢 SQL</span>
|
||||
</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
value: 'diagnose',
|
||||
label: (
|
||||
<span className="gn-sql-analysis-view-switcher-label">
|
||||
<SearchOutlined />
|
||||
<span>SQL 诊断</span>
|
||||
</span>
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="gn-sql-analysis-workbench-body">
|
||||
{activeView === 'slow-query' ? (
|
||||
<div className="gn-sql-analysis-pane">
|
||||
<SlowQueryPanelContent
|
||||
config={connectionConfig}
|
||||
dbName={dbName}
|
||||
onPickQuery={handlePickSlowQuery}
|
||||
activeToken={slowQueryLoadKey}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className="gn-sql-analysis-pane">
|
||||
<div className="gn-sql-analysis-editor-block">
|
||||
<Input.TextArea
|
||||
value={sqlDraft}
|
||||
onChange={(event) => setSqlDraft(event.target.value)}
|
||||
placeholder="输入要诊断的 SQL,或从慢 SQL 列表点击条目带入"
|
||||
autoSize={{ minRows: 5, maxRows: 10 }}
|
||||
/>
|
||||
<div className="gn-sql-analysis-editor-actions">
|
||||
<Text type="secondary">支持从慢 SQL 列表点击条目直接带入</Text>
|
||||
<Button type="primary" icon={<SearchOutlined />} onClick={triggerDiagnose}>
|
||||
运行诊断
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="gn-sql-analysis-report-shell">
|
||||
<ExplainReportView
|
||||
config={connectionConfig}
|
||||
dbName={dbName}
|
||||
sql={sqlDraft}
|
||||
runKey={diagnoseRunKey > 0 ? diagnoseRunKey : null}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const workbenchStyles = `
|
||||
.gn-sql-analysis-workbench {
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
padding: 16px;
|
||||
overflow: hidden;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.gn-sql-analysis-workbench-header {
|
||||
flex: 0 0 auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.gn-sql-analysis-workbench-header-main {
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
.gn-sql-analysis-view-switcher {
|
||||
flex: 0 0 auto;
|
||||
align-self: flex-start;
|
||||
}
|
||||
.gn-sql-analysis-view-switcher-label {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
min-width: 88px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.gn-sql-analysis-view-switcher .ant-segmented-group {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
}
|
||||
.gn-sql-analysis-view-switcher .ant-segmented-item {
|
||||
min-height: 30px;
|
||||
}
|
||||
.gn-sql-analysis-view-switcher .ant-segmented-item-label {
|
||||
padding: 5px 12px;
|
||||
font-size: 13px;
|
||||
line-height: 20px;
|
||||
}
|
||||
.gn-sql-analysis-workbench-body {
|
||||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
.gn-sql-analysis-pane {
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
.gn-sql-analysis-editor-block {
|
||||
flex: 0 0 auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.gn-sql-analysis-editor-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
}
|
||||
.gn-sql-analysis-report-shell {
|
||||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
`
|
||||
Reference in New Issue
Block a user