= ({ data }) => {
- const arr = useMemo(() => convertBinaryPlanToArray(data), [data])
const columns: IColumn[] = useMemo(() => {
return [
{
- name: 'id',
- key: 'name',
+ name: (
+
+ id hideColumn('id')} />
+
+ ) as any,
+ extra: 'id',
+ key: 'id',
minWidth: 100,
- maxWidth: 300,
+ maxWidth: 600,
onRender: (row: BinaryPlanItem) => {
return (
-
- {row.level > 0 && '└─'}
- {row.name}
-
+
+
+ {row.id}
+
+
)
}
},
{
- name: 'estRows',
+ name: (
+
+ estRows hideColumn('estRows')} />
+
+ ) as any,
+ extra: 'estRows',
key: 'estRows',
minWidth: 100,
maxWidth: 120,
onRender: (row: BinaryPlanItem) => {
- return row.estRows.toFixed(2)
+ return row.estRows
}
},
{
- name: 'estCost',
+ name: (
+
+ estCost hideColumn('estCost')} />
+
+ ) as any,
+ extra: 'estCost',
key: 'estCost',
minWidth: 100,
maxWidth: 120,
onRender: (row: BinaryPlanItem) => {
- return (row.cost ?? 0).toFixed(2)
+ return row.estCost
}
},
{
- name: 'actRows',
+ name: (
+
+ actRows hideColumn('actRows')} />
+
+ ) as any,
+ extra: 'actRows',
key: 'actRows',
minWidth: 100,
maxWidth: 120,
onRender: (row: BinaryPlanItem) => {
- return row.actRows.toFixed(2)
+ return row.actRows
}
},
{
- name: 'task',
- key: 'taskType',
+ name: (
+
+ task hideColumn('task')} />
+
+ ) as any,
+ extra: 'task',
+ key: 'task',
minWidth: 60,
maxWidth: 100,
onRender: (row: BinaryPlanItem) => {
- let task = row.taskType
- if (task !== 'root') {
- task += `[${row.storeType}]`
- }
- return task
+ return row.task
}
},
{
- name: 'access object',
- key: 'accessObjects',
- minWidth: 100,
- maxWidth: 120,
+ name: (
+
+ access object{' '}
+ hideColumn('accessObject')} />
+
+ ) as any,
+ extra: 'access object',
+ key: 'accessObject',
+ minWidth: 120,
+ maxWidth: 140,
onRender: (row: BinaryPlanItem) => {
- const tableName = getTableName(row)
- let content = !!tableName ? `table: ${tableName}` : ''
- return content && {content}
+ return {row.accessObject}
}
},
{
- name: 'execution info',
- key: 'rootGroupExecInfo',
- minWidth: 100,
+ name: (
+
+ execution info{' '}
+ hideColumn('executionInfo')} />
+
+ ) as any,
+ extra: 'execution info',
+ key: 'executionInfo',
+ minWidth: 120,
maxWidth: 300,
onRender: (row: BinaryPlanItem) => {
- const execInfo = getExecutionInfo(row)
return (
-
- {execInfo.map((info, idx) => (
- {info}
- ))}
- >
- }
- >
- {execInfo.join(', ')}
-
+ {row.executionInfo}
)
}
},
{
- name: 'operator info',
+ name: (
+
+ operator info{' '}
+ hideColumn('operatorInfo')} />
+
+ ) as any,
+ extra: 'operator info',
key: 'operatorInfo',
- minWidth: 100,
+ minWidth: 120,
maxWidth: 300,
onRender: (row: BinaryPlanItem) => {
// truncate the string if it's too long
// operation info may be super super long
- const truncateLength = 1000
- let truncatedStr = row.operatorInfo
+ const truncateLength = 100
+ let truncatedStr = row.operatorInfo ?? ''
if (truncatedStr.length > truncateLength) {
truncatedStr = row.operatorInfo.slice(0, truncateLength) + '...'
}
- return {truncatedStr}
+ const truncateTooltipLen = 2000
+ let truncatedTooltipStr = row.operatorInfo ?? ''
+ if (truncatedTooltipStr.length > truncateTooltipLen) {
+ truncatedTooltipStr =
+ row.operatorInfo.slice(0, truncateTooltipLen) +
+ '...(too long to show, copy or download to analyze)'
+ }
+ return {truncatedStr}
}
},
{
- name: 'memory',
- key: 'memoryBytes',
- minWidth: 60,
+ name: (
+
+ memory hideColumn('memory')} />
+
+ ) as any,
+ extra: 'memory',
+ key: 'memory',
+ minWidth: 80,
maxWidth: 100,
onRender: (row: BinaryPlanItem) => {
- return getMemorySize(row)
+ return row.memory
}
},
{
- name: 'disk',
- key: 'diskBytes',
+ name: (
+
+ disk hideColumn('disk')} />
+
+ ) as any,
+ extra: 'disk',
+ key: 'disk',
minWidth: 60,
maxWidth: 100,
onRender: (row: BinaryPlanItem) => {
- return getDiskSize(row)
+ return row.disk
}
}
]
}, [])
- return
+ const [visibleColumnKeys, setVisibleColumnKeys] = React.useState(() => {
+ return COLUM_KEYS.reduce((acc, cur) => {
+ acc[cur] = true
+ return acc
+ }, {})
+ })
+
+ const filteredColumns = useMemo(() => {
+ return columns.filter((c) => visibleColumnKeys[c.key as COLUM_KEYS_UNION])
+ }, [columns, visibleColumnKeys])
+
+ if (arr.length > 0) {
+ return (
+ <>
+
+
+ >
+ )
+ }
+ return (
+
+ Parse plan text failed, original content:
+
{data}
+
+ )
}
diff --git a/ui/packages/tidb-dashboard-lib/src/components/PlanText/index.tsx b/ui/packages/tidb-dashboard-lib/src/components/PlanText/index.tsx
new file mode 100644
index 0000000000..17039cfe81
--- /dev/null
+++ b/ui/packages/tidb-dashboard-lib/src/components/PlanText/index.tsx
@@ -0,0 +1,61 @@
+import React, { useMemo } from 'react'
+import { CopyLink, TxtDownloadLink, Pre } from '@lib/components'
+
+type BinaryPlanTextProps = {
+ data: string
+ downloadFileName: string
+}
+
+// mysql> select tidb_decode_binary_plan("AgQgAQ==");
+// +-------------------------------------+
+// | tidb_decode_binary_plan("AgQgAQ==") |
+// +-------------------------------------+
+// | (plan discarded because too long) |
+// +-------------------------------------+
+// 1 row in set (0.00 sec)
+
+const DISCARDED_TOO_LONG = 'plan discarded because too long'
+
+const MAX_SHOW_LEN = 500 * 1024 // 500KB
+
+export const PlanText: React.FC = ({
+ data,
+ downloadFileName
+}) => {
+ const discardedDueToTooLong = useMemo(() => {
+ return data
+ .slice(0, DISCARDED_TOO_LONG.length + 10)
+ .includes(DISCARDED_TOO_LONG)
+ }, [data])
+
+ const truncatedStr = useMemo(() => {
+ let str = data
+ if (str.length > MAX_SHOW_LEN) {
+ str =
+ str.slice(0, MAX_SHOW_LEN) +
+ '\n...(too long to show, copy or download to analyze)'
+ }
+ // binary_plan_text field starts with '\n' which will show an extra empty line
+ // plan field starts with `\t`
+ if (str.startsWith('\n')) {
+ // remove the first empty line
+ str = str.slice(1)
+ }
+ return str
+ }, [data])
+
+ if (discardedDueToTooLong) {
+ return {data}
+ }
+ return (
+ <>
+
+
+
+
+
+ {truncatedStr}
+
+ >
+ )
+}
diff --git a/ui/packages/tidb-dashboard-lib/src/components/TxtDownloadLink/index.module.less b/ui/packages/tidb-dashboard-lib/src/components/TxtDownloadLink/index.module.less
new file mode 100644
index 0000000000..5d492d7762
--- /dev/null
+++ b/ui/packages/tidb-dashboard-lib/src/components/TxtDownloadLink/index.module.less
@@ -0,0 +1,5 @@
+@import 'antd/es/style/themes/default.less';
+
+.successTxt {
+ color: @success-color;
+}
diff --git a/ui/packages/tidb-dashboard-lib/src/components/TxtDownloadLink/index.tsx b/ui/packages/tidb-dashboard-lib/src/components/TxtDownloadLink/index.tsx
new file mode 100644
index 0000000000..6b9819ebbb
--- /dev/null
+++ b/ui/packages/tidb-dashboard-lib/src/components/TxtDownloadLink/index.tsx
@@ -0,0 +1,72 @@
+import React, { useState } from 'react'
+import { useTranslation } from 'react-i18next'
+import { useTimeoutFn } from 'react-use'
+import { CheckOutlined, DownloadOutlined } from '@ant-design/icons'
+import { addTranslationResource } from '@lib/utils/i18n'
+import { downloadTxt } from '@lib/utils/local-download'
+
+import styles from './index.module.less'
+
+export interface ITxtDownloadLinkProps
+ extends React.DetailedHTMLProps<
+ React.HTMLAttributes,
+ HTMLSpanElement
+ > {
+ data?: string
+ fileName?: string
+}
+
+const translations = {
+ en: {
+ download: 'Download',
+ success: 'Downloaded'
+ },
+ zh: {
+ download: '下载',
+ success: '已下载'
+ }
+}
+
+for (const key in translations) {
+ addTranslationResource(key, {
+ component: {
+ txtDownloadLink: translations[key]
+ }
+ })
+}
+
+function TxtDownloadLink({
+ data,
+ fileName,
+ ...otherProps
+}: ITxtDownloadLinkProps) {
+ const { t } = useTranslation()
+ const [showDownload, setShowDownloaded] = useState(false)
+
+ const reset = useTimeoutFn(() => {
+ setShowDownloaded(false)
+ }, 1500)[2]
+
+ const handleDownload = () => {
+ downloadTxt(data ?? '', fileName ?? 'data.txt')
+ setShowDownloaded(true)
+ reset()
+ }
+
+ return (
+
+ {!showDownload && (
+
+ {t(`component.txtDownloadLink.download`)}
+
+ )}
+ {showDownload && (
+
+ {t('component.txtDownloadLink.success')}
+
+ )}
+
+ )
+}
+
+export default React.memo(TxtDownloadLink)
diff --git a/ui/packages/tidb-dashboard-lib/src/components/index.ts b/ui/packages/tidb-dashboard-lib/src/components/index.ts
index d2187e42a6..26063a5898 100644
--- a/ui/packages/tidb-dashboard-lib/src/components/index.ts
+++ b/ui/packages/tidb-dashboard-lib/src/components/index.ts
@@ -26,6 +26,7 @@ export * from './Expand'
export { default as Expand } from './Expand'
export * from './CopyLink'
export { default as CopyLink } from './CopyLink'
+export { default as TxtDownloadLink } from './TxtDownloadLink'
export * from './ColumnsSelector'
export { default as ColumnsSelector } from './ColumnsSelector'
export * from './Toolbar'
@@ -56,6 +57,7 @@ export { default as DrawerFooter } from './DrawerFooter'
export * from './VisualPlan'
export * from './BinaryPlanTable'
+export * from './PlanText'
export { default as LanguageDropdown } from './LanguageDropdown'
export { default as ParamsPageWrapper } from './ParamsPageWrapper'
diff --git a/ui/packages/tidb-dashboard-lib/src/utils/local-download.ts b/ui/packages/tidb-dashboard-lib/src/utils/local-download.ts
new file mode 100644
index 0000000000..3a197286c3
--- /dev/null
+++ b/ui/packages/tidb-dashboard-lib/src/utils/local-download.ts
@@ -0,0 +1,15 @@
+export function downloadTxt(data: string, fileName: string) {
+ const fileUrl = URL.createObjectURL(
+ new Blob([data], {
+ type: 'text/plain;charset=utf-8;'
+ })
+ )
+ const a = document.createElement('a')
+ document.body.appendChild(a)
+ a.href = fileUrl
+ a.download = fileName
+ a.click()
+ setTimeout(() => {
+ document.body.removeChild(a)
+ }, 0)
+}