mirror of
https://github.com/tencentmusic/supersonic.git
synced 2025-12-13 13:07:32 +00:00
531 lines
16 KiB
TypeScript
531 lines
16 KiB
TypeScript
import React, { useState, useEffect, useRef } from 'react';
|
||
import { Button, Table, message, Tooltip, Space, Dropdown } from 'antd';
|
||
import SplitPane from 'react-split-pane';
|
||
import Pane from 'react-split-pane/lib/Pane';
|
||
import sqlFormatter from 'sql-formatter';
|
||
import {
|
||
FullscreenOutlined,
|
||
WarningOutlined,
|
||
EditOutlined,
|
||
PlayCircleTwoTone,
|
||
SwapOutlined,
|
||
PlayCircleOutlined,
|
||
CloudServerOutlined,
|
||
} from '@ant-design/icons';
|
||
import { isFunction } from 'lodash';
|
||
import FullScreen from '@/components/FullScreen';
|
||
import SqlEditor from '@/components/SqlEditor';
|
||
import type { TaskResultParams, TaskResultItem, DataInstanceItem, TaskResultColumn } from '../data';
|
||
import { excuteSql } from '../service';
|
||
import { getDatabaseByDomainId } from '../../service';
|
||
import DataSourceCreateForm from './DataSourceCreateForm';
|
||
import styles from '../style.less';
|
||
|
||
import 'ace-builds/src-min-noconflict/ext-searchbox';
|
||
import 'ace-builds/src-min-noconflict/theme-sqlserver';
|
||
import 'ace-builds/src-min-noconflict/theme-monokai';
|
||
import 'ace-builds/src-min-noconflict/mode-sql';
|
||
|
||
type IProps = {
|
||
oprType: 'add' | 'edit';
|
||
dataSourceItem: DataInstanceItem;
|
||
domainId: number;
|
||
onUpdateSql?: (sql: string) => void;
|
||
sql?: string;
|
||
onSubmitSuccess?: (dataSourceInfo: any) => void;
|
||
onJdbcSourceChange?: (jdbcId: number) => void;
|
||
};
|
||
|
||
type ResultTableItem = Record<string, any>;
|
||
|
||
type ResultColItem = {
|
||
key: string;
|
||
title: string;
|
||
dataIndex: string;
|
||
};
|
||
|
||
type ScreenSize = 'small' | 'middle' | 'large';
|
||
|
||
type JdbcSourceItems = {
|
||
label: string;
|
||
key: number;
|
||
};
|
||
|
||
const SqlDetail: React.FC<IProps> = ({
|
||
dataSourceItem,
|
||
onSubmitSuccess,
|
||
domainId,
|
||
sql = '',
|
||
onUpdateSql,
|
||
onJdbcSourceChange,
|
||
}) => {
|
||
const [resultTable, setResultTable] = useState<ResultTableItem[]>([]);
|
||
const [resultTableLoading, setResultTableLoading] = useState(false);
|
||
const [resultCols, setResultCols] = useState<ResultColItem[]>([]);
|
||
const [pagination, setPagination] = useState({
|
||
current: 1,
|
||
pageSize: 20,
|
||
total: 0,
|
||
});
|
||
const [jdbcSourceItems, setJdbcSourceItems] = useState<JdbcSourceItems[]>([]);
|
||
const [dataSourceModalVisible, setDataSourceModalVisible] = useState(false);
|
||
|
||
const [tableScroll, setTableScroll] = useState({
|
||
scrollToFirstRowOnChange: true,
|
||
x: '100%',
|
||
y: 200,
|
||
});
|
||
|
||
// const [dataSourceResult, setDataSourceResult] = useState<any>({});
|
||
|
||
const [runState, setRunState] = useState<boolean | undefined>();
|
||
|
||
const [taskLog, setTaskLog] = useState('');
|
||
const [isSqlExcLocked, setIsSqlExcLocked] = useState(false);
|
||
const [screenSize, setScreenSize] = useState<ScreenSize>('middle');
|
||
|
||
const [isSqlIdeFullScreen, setIsSqlIdeFullScreen] = useState<boolean>(false);
|
||
const [isSqlResFullScreen, setIsSqlResFullScreen] = useState<boolean>(false);
|
||
|
||
// const [sqlParams, setSqlParams] = useState<SqlParamsItem[]>([]);
|
||
const resultInnerWrap = useRef<HTMLDivElement>();
|
||
|
||
const [editorSize, setEditorSize] = useState<number>(0);
|
||
const DEFAULT_FULLSCREEN_TOP = 0;
|
||
|
||
const [partialSql, setPartialSql] = useState('');
|
||
const [isPartial, setIsPartial] = useState(false);
|
||
const [isRight, setIsRight] = useState(false);
|
||
|
||
const [scriptColumns, setScriptColumns] = useState<any[]>([]);
|
||
// const [jdbcSourceName, setJdbcSourceName] = useState<string>(() => {
|
||
// const sourceId = dataSourceItem.databaseId;
|
||
// if (sourceId) {
|
||
// const target: any = jdbcSourceItems.filter((item: any) => {
|
||
// return item.key === Number(sourceId);
|
||
// })[0];
|
||
// if (target) {
|
||
// return target.label;
|
||
// }
|
||
// }
|
||
// return 'ClickHouse';
|
||
// });
|
||
|
||
const queryDatabaseConfig = async () => {
|
||
const { code, data } = await getDatabaseByDomainId(domainId);
|
||
if (code === 200) {
|
||
setJdbcSourceItems([
|
||
{
|
||
label: data?.name,
|
||
key: data?.id,
|
||
},
|
||
]);
|
||
onJdbcSourceChange?.(data?.id && Number(data?.id));
|
||
return;
|
||
}
|
||
message.error('数据库配置获取错误');
|
||
};
|
||
|
||
function creatCalcItem(key: string, data: string) {
|
||
const line = document.createElement('div'); // 需要每条数据一行,这样避免数据换行的时候获得的宽度不准确
|
||
const child = document.createElement('span');
|
||
child.classList.add(`resultCalcItem_${key}`);
|
||
child.innerText = data;
|
||
line.appendChild(child);
|
||
return line;
|
||
}
|
||
|
||
// 计算每列的宽度,通过容器插入文档中动态得到该列数据(包括表头)的最长宽度,设为列宽度,保证每列的数据都能一行展示完
|
||
function getKeyWidthMap(list: TaskResultItem[]): TaskResultItem {
|
||
const widthMap = {};
|
||
const container = document.createElement('div');
|
||
container.id = 'resultCalcWrap';
|
||
container.style.position = 'fixed';
|
||
container.style.left = '-99999px';
|
||
container.style.top = '-99999px';
|
||
container.style.width = '19999px';
|
||
container.style.fontSize = '12px';
|
||
list.forEach((item, index) => {
|
||
if (index === 0) {
|
||
Object.keys(item).forEach((key, keyIndex) => {
|
||
// 因为key可能存在一些特殊字符,导致querySelectorAll获取的时候报错,所以用keyIndex(而不用key)拼接className
|
||
container.appendChild(creatCalcItem(`${keyIndex}`, key));
|
||
container.appendChild(creatCalcItem(`${keyIndex}`, `${item[key]}`));
|
||
});
|
||
} else {
|
||
Object.keys(item).forEach((key, keyIndex) => {
|
||
container.appendChild(creatCalcItem(`${keyIndex}`, `${item[key]}`));
|
||
});
|
||
}
|
||
});
|
||
document.body.appendChild(container);
|
||
Object.keys(list[0]).forEach((key, keyIndex) => {
|
||
// 因为key可能存在一些特殊字符,导致querySelectorAll获取的时候报错,所以用keyIndex(而不用key)拼接className
|
||
const widthArr = Array.from(container.querySelectorAll(`.resultCalcItem_${keyIndex}`)).map(
|
||
(node: any) => node.offsetWidth,
|
||
);
|
||
widthMap[key] = Math.max(...widthArr);
|
||
});
|
||
document.body.removeChild(container);
|
||
return widthMap;
|
||
}
|
||
|
||
const updateResultCols = (list: TaskResultItem[], columns: TaskResultColumn[]) => {
|
||
if (list.length) {
|
||
const widthMap = getKeyWidthMap(list);
|
||
const cols = columns.map(({ nameEn }) => {
|
||
return {
|
||
key: nameEn,
|
||
title: nameEn,
|
||
dataIndex: nameEn,
|
||
width: `${(widthMap[nameEn] as number) + 22}px`, // 字宽度 + 20px(比左右padding宽几像素,作为一个buffer值)
|
||
};
|
||
});
|
||
setResultCols(cols);
|
||
}
|
||
};
|
||
|
||
const fetchTaskResult = (params: TaskResultParams) => {
|
||
setResultTable(
|
||
params.resultList.map((item, index) => {
|
||
return {
|
||
...item,
|
||
index,
|
||
};
|
||
}),
|
||
);
|
||
setPagination({
|
||
current: 1,
|
||
pageSize: 20,
|
||
total: params.resultList.length,
|
||
});
|
||
setScriptColumns(params.columns);
|
||
updateResultCols(params.resultList, params.columns);
|
||
};
|
||
|
||
const changePaging = (paging: Pagination) => {
|
||
setPagination({
|
||
...pagination,
|
||
...paging,
|
||
});
|
||
};
|
||
|
||
const onSqlChange = (sqlString: string) => {
|
||
if (onUpdateSql && isFunction(onUpdateSql)) {
|
||
onUpdateSql(sqlString);
|
||
}
|
||
};
|
||
|
||
const formatSQL = () => {
|
||
const sqlvalue = sqlFormatter.format(sql);
|
||
if (onUpdateSql && isFunction(onUpdateSql)) {
|
||
onUpdateSql(sqlvalue);
|
||
}
|
||
// eslint-disable-next-line no-param-reassign
|
||
sql = sqlvalue;
|
||
};
|
||
|
||
const separateSql = async (value: string) => {
|
||
setResultTableLoading(true);
|
||
const { code, data, msg } = await excuteSql({
|
||
sql: value,
|
||
domainId,
|
||
});
|
||
setResultTableLoading(false);
|
||
if (code === 200) {
|
||
// setDataSourceResult(data);
|
||
fetchTaskResult(data);
|
||
setRunState(true);
|
||
} else {
|
||
setRunState(false);
|
||
setTaskLog(msg);
|
||
}
|
||
};
|
||
|
||
const onSelect = (value: string) => {
|
||
if (value) {
|
||
setIsPartial(true);
|
||
setPartialSql(value);
|
||
} else {
|
||
setIsPartial(false);
|
||
}
|
||
};
|
||
|
||
const excuteScript = () => {
|
||
if (!sql) {
|
||
return message.error('SQL查询语句不可以为空!');
|
||
}
|
||
if (isSqlExcLocked) {
|
||
return message.warning('请间隔5s再重新执行!');
|
||
}
|
||
const waitTime = 5000;
|
||
setIsSqlExcLocked(true); // 加锁,5s后再解锁
|
||
setTimeout(() => {
|
||
setIsSqlExcLocked(false);
|
||
}, waitTime);
|
||
|
||
return isPartial ? separateSql(partialSql) : separateSql(sql);
|
||
};
|
||
|
||
const showDataSetModal = () => {
|
||
setDataSourceModalVisible(true);
|
||
};
|
||
|
||
const startCreatDataSource = async () => {
|
||
showDataSetModal();
|
||
};
|
||
|
||
const updateNormalResScroll = () => {
|
||
const node = resultInnerWrap?.current;
|
||
if (node) {
|
||
setTableScroll({
|
||
scrollToFirstRowOnChange: true,
|
||
x: '100%',
|
||
y: node.clientHeight - 120,
|
||
});
|
||
}
|
||
};
|
||
|
||
const updateFullScreenResScroll = () => {
|
||
const windowHeight = window.innerHeight;
|
||
const paginationHeight = 96;
|
||
setTableScroll({
|
||
scrollToFirstRowOnChange: true,
|
||
x: '100%',
|
||
y: windowHeight - DEFAULT_FULLSCREEN_TOP - paginationHeight - 30, // 30为退出全屏按钮的高度
|
||
});
|
||
};
|
||
|
||
const handleFullScreenSqlIde = () => {
|
||
setIsSqlIdeFullScreen(true);
|
||
};
|
||
|
||
const handleNormalScreenSqlIde = () => {
|
||
setIsSqlIdeFullScreen(false);
|
||
};
|
||
|
||
const handleFullScreenSqlResult = () => {
|
||
setIsSqlResFullScreen(true);
|
||
};
|
||
|
||
const handleNormalScreenSqlResult = () => {
|
||
setIsSqlResFullScreen(false);
|
||
};
|
||
|
||
const handleThemeChange = () => {
|
||
setIsRight(!isRight);
|
||
};
|
||
|
||
const renderResult = () => {
|
||
if (runState === false) {
|
||
return (
|
||
<>
|
||
{
|
||
<div className={styles.taskFailed}>
|
||
<WarningOutlined className={styles.resultFailIcon} />
|
||
任务执行失败
|
||
</div>
|
||
}
|
||
<div
|
||
className={styles.sqlResultLog}
|
||
dangerouslySetInnerHTML={{
|
||
__html: taskLog.replace(/\r\n/g, '<br/>').replace(/\t/g, ' '),
|
||
}}
|
||
/>
|
||
</>
|
||
);
|
||
}
|
||
|
||
if (runState) {
|
||
return (
|
||
<>
|
||
<div className={styles.detail} />
|
||
<Table<TaskResultItem>
|
||
loading={resultTableLoading}
|
||
dataSource={resultTable}
|
||
columns={resultCols}
|
||
onChange={changePaging}
|
||
pagination={pagination}
|
||
scroll={tableScroll}
|
||
className={styles.resultTable}
|
||
rowClassName="resultTableRow"
|
||
rowKey="index"
|
||
/>
|
||
</>
|
||
);
|
||
}
|
||
return <div className={styles.sqlResultContent}>请点击左侧任务列表查看执行详情</div>;
|
||
};
|
||
|
||
// 更新任务结果列表的高度,使其撑满容器
|
||
useEffect(() => {
|
||
if (isSqlResFullScreen) {
|
||
updateFullScreenResScroll();
|
||
} else {
|
||
updateNormalResScroll();
|
||
}
|
||
}, [resultTable, isSqlResFullScreen]);
|
||
|
||
useEffect(() => {
|
||
queryDatabaseConfig();
|
||
const windowHeight = window.innerHeight;
|
||
let size: ScreenSize = 'small';
|
||
if (windowHeight > 1100) {
|
||
size = 'large';
|
||
} else if (windowHeight > 850) {
|
||
size = 'middle';
|
||
}
|
||
setScreenSize(size);
|
||
}, []);
|
||
|
||
const exploreEditorSize = localStorage.getItem('exploreEditorSize');
|
||
|
||
return (
|
||
<>
|
||
<div className={styles.sqlOprBar}>
|
||
<div className={styles.sqlOprBarLeftBox}>
|
||
<Tooltip title="数据类型">
|
||
<Dropdown
|
||
menu={{
|
||
items: jdbcSourceItems,
|
||
onClick: (e) => {
|
||
const value = e.key;
|
||
const target: any = jdbcSourceItems.filter((item: any) => {
|
||
return item.key === Number(value);
|
||
})[0];
|
||
if (target) {
|
||
// setJdbcSourceName(target.label);
|
||
onJdbcSourceChange?.(Number(value));
|
||
}
|
||
},
|
||
}}
|
||
placement="bottom"
|
||
>
|
||
<Button style={{ marginRight: '15px', minWidth: '140px' }}>
|
||
<Space>
|
||
<CloudServerOutlined className={styles.sqlOprIcon} style={{ marginRight: 0 }} />
|
||
<span>{jdbcSourceItems[0]?.label}</span>
|
||
</Space>
|
||
</Button>
|
||
</Dropdown>
|
||
</Tooltip>
|
||
<Tooltip title="全屏">
|
||
<FullscreenOutlined className={styles.sqlOprIcon} onClick={handleFullScreenSqlIde} />
|
||
</Tooltip>
|
||
<Tooltip title="格式化SQL语句">
|
||
<EditOutlined className={styles.sqlOprIcon} onClick={formatSQL} />
|
||
</Tooltip>
|
||
<Tooltip title="改变主题">
|
||
<SwapOutlined className={styles.sqlOprIcon} onClick={handleThemeChange} />
|
||
</Tooltip>
|
||
<Tooltip title="执行脚本">
|
||
<Button
|
||
style={{
|
||
lineHeight: '24px',
|
||
top: '3px',
|
||
position: 'relative',
|
||
}}
|
||
type="primary"
|
||
shape="round"
|
||
icon={
|
||
isPartial ? '' : isSqlExcLocked ? <PlayCircleOutlined /> : <PlayCircleTwoTone />
|
||
}
|
||
size={'small'}
|
||
className={
|
||
isSqlExcLocked ? `${styles.disableIcon} ${styles.sqlOprIcon}` : styles.sqlOprBtn
|
||
}
|
||
onClick={excuteScript}
|
||
>
|
||
{isPartial ? '部分运行' : '运行'}
|
||
</Button>
|
||
</Tooltip>
|
||
</div>
|
||
</div>
|
||
<SplitPane
|
||
split="horizontal"
|
||
onChange={(size) => {
|
||
setEditorSize(size);
|
||
localStorage.setItem('exploreEditorSize', size[0]);
|
||
}}
|
||
>
|
||
<Pane initialSize={exploreEditorSize || '500px'}>
|
||
<div className={styles.sqlMain}>
|
||
<div className={styles.sqlEditorWrapper}>
|
||
<FullScreen
|
||
isFullScreen={isSqlIdeFullScreen}
|
||
top={`${DEFAULT_FULLSCREEN_TOP}px`}
|
||
triggerBackToNormal={handleNormalScreenSqlIde}
|
||
>
|
||
<SqlEditor
|
||
value={sql}
|
||
// height={sqlEditorHeight}
|
||
// theme="monokai"
|
||
isRightTheme={isRight}
|
||
sizeChanged={editorSize}
|
||
onSqlChange={onSqlChange}
|
||
onSelect={onSelect}
|
||
/>
|
||
</FullScreen>
|
||
</div>
|
||
</div>
|
||
</Pane>
|
||
<div className={`${styles.sqlBottmWrap} ${screenSize}`}>
|
||
<div className={styles.sqlResultWrap}>
|
||
<div className={styles.sqlToolBar}>
|
||
{
|
||
<Button
|
||
className={styles.sqlToolBtn}
|
||
type="primary"
|
||
onClick={startCreatDataSource}
|
||
disabled={!runState}
|
||
>
|
||
生成数据源
|
||
</Button>
|
||
}
|
||
<Button
|
||
className={styles.sqlToolBtn}
|
||
type="primary"
|
||
onClick={handleFullScreenSqlResult}
|
||
disabled={!runState}
|
||
>
|
||
全屏查看
|
||
</Button>
|
||
</div>
|
||
<div
|
||
className={styles.sqlResultPane}
|
||
ref={resultInnerWrap as React.MutableRefObject<HTMLDivElement | null>}
|
||
>
|
||
<FullScreen
|
||
isFullScreen={isSqlResFullScreen}
|
||
top={`${DEFAULT_FULLSCREEN_TOP}px`}
|
||
triggerBackToNormal={handleNormalScreenSqlResult}
|
||
>
|
||
{renderResult()}
|
||
</FullScreen>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</SplitPane>
|
||
|
||
{dataSourceModalVisible && (
|
||
<DataSourceCreateForm
|
||
sql={sql}
|
||
domainId={domainId}
|
||
dataSourceItem={dataSourceItem}
|
||
scriptColumns={scriptColumns}
|
||
onCancel={() => {
|
||
setDataSourceModalVisible(false);
|
||
}}
|
||
onSubmit={(dataSourceInfo: any) => {
|
||
setDataSourceModalVisible(false);
|
||
onSubmitSuccess?.(dataSourceInfo);
|
||
}}
|
||
createModalVisible={dataSourceModalVisible}
|
||
/>
|
||
)}
|
||
</>
|
||
);
|
||
};
|
||
|
||
export default SqlDetail;
|