mirror of
https://github.com/tencentmusic/supersonic.git
synced 2025-12-12 20:51:48 +00:00
first commit
This commit is contained in:
@@ -0,0 +1,530 @@
|
||||
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;
|
||||
Reference in New Issue
Block a user