first commit

This commit is contained in:
jerryjzhang
2023-06-12 18:44:01 +08:00
commit dc4fc69b57
879 changed files with 573090 additions and 0 deletions

View File

@@ -0,0 +1,38 @@
import React from 'react';
import { Form, Input, Spin } from 'antd';
import type { FormInstance } from 'antd/lib/form';
const FormItem = Form.Item;
const { TextArea } = Input;
type Props = {
isEdit?: boolean;
form: FormInstance<any>;
tableLoading?: boolean;
};
const DataSourceBasicForm: React.FC<Props> = ({ isEdit, tableLoading = false }) => {
return (
<Spin spinning={tableLoading}>
<FormItem
name="name"
label="数据源中文名"
rules={[{ required: true, message: '请输入数据源中文名' }]}
>
<Input placeholder="名称不可重复" />
</FormItem>
<FormItem
name="bizName"
label="数据源英文名"
rules={[{ required: true, message: '请输入数据源英文名' }]}
>
<Input placeholder="名称不可重复" disabled={isEdit} />
</FormItem>
<FormItem name="description" label="数据源描述">
<TextArea placeholder="请输入数据源描述" />
</FormItem>
</Spin>
);
};
export default DataSourceBasicForm;

View File

@@ -0,0 +1,291 @@
import React, { useEffect, useRef, useState } from 'react';
import { Form, Button, Modal, Steps, message } from 'antd';
import BasicInfoForm from './DataSourceBasicForm';
import FieldForm from './DataSourceFieldForm';
import { formLayout } from '@/components/FormHelper/utils';
import { EnumDataSourceType } from '../constants';
import type { DataInstanceItem, FieldItem, SaveDataSetForm } from '../data';
import styles from '../style.less';
import { createDatasource, updateDatasource } from '../../service';
export type CreateFormProps = {
createModalVisible: boolean;
sql: string;
domainId: number;
dataSourceItem: DataInstanceItem | any;
onCancel?: () => void;
onSubmit?: (dataSourceInfo: any) => void;
scriptColumns: any[];
};
const { Step } = Steps;
const initFormVal = {
name: '', // 数据源名称
bizName: '', // 数据源英文名
description: '', // 数据源描述
};
const DataSourceCreateForm: React.FC<CreateFormProps> = ({
onCancel,
createModalVisible,
domainId,
scriptColumns,
sql,
onSubmit,
dataSourceItem,
}) => {
const isEdit = !!dataSourceItem?.id;
const [fields, setFields] = useState<FieldItem[]>([]);
const [currentStep, setCurrentStep] = useState(0);
const [saveLoading, setSaveLoading] = useState(false);
const formValRef = useRef(initFormVal as any);
const [form] = Form.useForm();
const updateFormVal = (val: SaveDataSetForm) => {
formValRef.current = val;
};
const forward = () => setCurrentStep(currentStep + 1);
const backward = () => setCurrentStep(currentStep - 1);
const getFieldsClassify = (fieldsList: FieldItem[]) => {
const classify = fieldsList.reduce(
(fieldsClassify, item: FieldItem) => {
const {
type,
bizName,
timeGranularity,
agg,
isCreateDimension,
name,
isCreateMetric,
dateFormat,
} = item;
switch (type) {
case EnumDataSourceType.CATEGORICAL:
fieldsClassify.dimensions.push({
bizName,
type,
isCreateDimension,
name,
});
break;
case EnumDataSourceType.TIME:
fieldsClassify.dimensions.push({
bizName,
type,
isCreateDimension,
name,
dateFormat,
typeParams: {
isPrimary: true,
timeGranularity,
},
});
break;
case EnumDataSourceType.FOREIGN:
case EnumDataSourceType.PRIMARY:
fieldsClassify.identifiers.push({
bizName,
name,
type,
});
break;
case EnumDataSourceType.MEASURES:
fieldsClassify.measures.push({
bizName,
type,
agg,
name,
isCreateMetric,
});
break;
default:
break;
}
return fieldsClassify;
},
{
identifiers: [],
dimensions: [],
measures: [],
} as any,
);
return classify;
};
const handleNext = async () => {
const fieldsValue = await form.validateFields();
const fieldsClassify = getFieldsClassify(fields);
const submitForm = {
...formValRef.current,
...fieldsValue,
...fieldsClassify,
};
updateFormVal(submitForm);
if (currentStep < 1) {
forward();
} else {
setSaveLoading(true);
const queryParams = {
...submitForm,
sqlQuery: sql,
databaseId: dataSourceItem.databaseId,
queryType: 'sql_query',
domainId,
};
const queryDatasource = isEdit ? updateDatasource : createDatasource;
const { code, msg, data } = await queryDatasource(queryParams);
setSaveLoading(false);
if (code === 200) {
message.success('保存数据源成功!');
onSubmit?.({
...queryParams,
...data,
resData: data,
});
return;
}
message.error(msg);
}
};
const initFields = (fieldsClassifyList: any[]) => {
const columnFields: any[] = scriptColumns.map((item: any) => {
const { type, nameEn } = item;
const oldItem = fieldsClassifyList.find((oItem) => oItem.bizName === item.nameEn) || {};
return {
...oldItem,
bizName: nameEn,
// name,
sqlType: type,
};
});
setFields(columnFields || []);
};
const formatterMeasures = (measuresList: any[] = []) => {
return measuresList.map((measures: any) => {
return {
...measures,
type: EnumDataSourceType.MEASURES,
};
});
};
const formatterDimensions = (dimensionsList: any[] = []) => {
return dimensionsList.map((dimension: any) => {
const { typeParams } = dimension;
return {
...dimension,
timeGranularity: typeParams?.timeGranularity || '',
};
});
};
const initData = () => {
const { id, name, bizName, description, datasourceDetail } = dataSourceItem as any;
const initValue = {
id,
name,
bizName,
description,
};
const editInitFormVal = {
...formValRef.current,
...initValue,
};
updateFormVal(editInitFormVal);
form.setFieldsValue(initValue);
const { dimensions, identifiers, measures } = datasourceDetail;
const formatFields = [
...formatterDimensions(dimensions || []),
...(identifiers || []),
...formatterMeasures(measures || []),
];
initFields(formatFields);
};
useEffect(() => {
if (isEdit) {
initData();
} else {
initFields([]);
}
}, [dataSourceItem]);
const handleFieldChange = (fieldName: string, data: any) => {
const result = fields.map((field) => {
if (field.bizName === fieldName) {
return {
...field,
...data,
};
}
return {
...field,
};
});
setFields(result);
};
const renderContent = () => {
if (currentStep === 1) {
return <FieldForm fields={fields} onFieldChange={handleFieldChange} />;
}
return <BasicInfoForm form={form} isEdit={isEdit} />;
};
const renderFooter = () => {
if (currentStep === 1) {
return (
<>
<Button style={{ float: 'left' }} onClick={backward}>
</Button>
<Button onClick={onCancel}></Button>
<Button type="primary" loading={saveLoading} onClick={handleNext}>
</Button>
</>
);
}
return (
<>
<Button onClick={onCancel}></Button>
<Button type="primary" onClick={handleNext}>
</Button>
</>
);
};
return (
<Modal
forceRender
width={1300}
bodyStyle={{ padding: '32px 40px 48px' }}
destroyOnClose
title={`${isEdit ? '编辑' : '新建'}数据源`}
maskClosable={false}
open={createModalVisible}
footer={renderFooter()}
onCancel={onCancel}
>
<Steps style={{ marginBottom: 28 }} size="small" current={currentStep}>
<Step title="基本信息" />
<Step title="字段信息" />
</Steps>
<Form
{...formLayout}
form={form}
initialValues={{
...formValRef.current,
}}
className={styles.form}
>
{renderContent()}
</Form>
</Modal>
);
};
export default DataSourceCreateForm;

View File

@@ -0,0 +1,196 @@
import React from 'react';
import { Table, Select, Checkbox, Input } from 'antd';
import type { FieldItem } from '../data';
import { isUndefined } from 'lodash';
import { TYPE_OPTIONS, DATE_FORMATTER, AGG_OPTIONS, EnumDataSourceType } from '../constants';
type Props = {
fields: FieldItem[];
onFieldChange: (fieldName: string, data: Partial<FieldItem>) => void;
};
const { Option } = Select;
const FieldForm: React.FC<Props> = ({ fields, onFieldChange }) => {
const handleFieldChange = (record: FieldItem, fieldName: string, value: any) => {
onFieldChange(record.bizName, {
...record,
[fieldName]: value,
});
};
const columns = [
{
title: '字段名称',
dataIndex: 'bizName',
width: 100,
},
{
title: '数据类型',
dataIndex: 'sqlType',
width: 80,
},
{
title: '字段类型',
dataIndex: 'type',
width: 100,
render: (_: any, record: FieldItem) => {
const type = fields.find((field) => field.bizName === record.bizName)?.type;
return (
<Select
placeholder="字段类型"
value={type}
onChange={(value) => {
let defaultParams = {};
if (value === EnumDataSourceType.MEASURES) {
defaultParams = {
agg: AGG_OPTIONS[0].value,
};
} else if (value === EnumDataSourceType.TIME) {
defaultParams = {
dateFormat: DATE_FORMATTER[0],
timeGranularity: 'day',
};
} else {
defaultParams = {
agg: undefined,
dateFormat: undefined,
timeGranularity: undefined,
};
}
// handleFieldChange(record, 'type', value);
onFieldChange(record.bizName, {
...record,
type: value,
...defaultParams,
});
}}
style={{ width: '100%' }}
>
{TYPE_OPTIONS.map((item) => (
<Option key={item.label} value={item.value}>
{item.label}
</Option>
))}
</Select>
);
},
},
{
title: '扩展配置',
dataIndex: 'extender',
width: 100,
render: (_: any, record: FieldItem) => {
const { type } = record;
if (type === EnumDataSourceType.MEASURES) {
const agg = fields.find((field) => field.bizName === record.bizName)?.agg;
return (
<Select
placeholder="度量算子"
value={agg}
onChange={(value) => {
handleFieldChange(record, 'agg', value);
}}
defaultValue={AGG_OPTIONS[0].value}
style={{ width: '100%' }}
>
{AGG_OPTIONS.map((item) => (
<Option key={item.value} value={item.value}>
{item.label}
</Option>
))}
</Select>
);
}
if (type === EnumDataSourceType.TIME) {
const dateFormat = fields.find((field) => field.bizName === record.bizName)?.dateFormat;
return (
<Select
placeholder="时间格式"
value={dateFormat}
onChange={(value) => {
handleFieldChange(record, 'dateFormat', value);
}}
defaultValue={DATE_FORMATTER[0]}
style={{ width: '100%' }}
>
{DATE_FORMATTER.map((item) => (
<Option key={item} value={item}>
{item}
</Option>
))}
</Select>
);
}
return <></>;
},
},
{
title: '快速创建',
dataIndex: 'fastCreate',
width: 100,
render: (_: any, record: FieldItem) => {
const { type, name } = record;
if (
[
EnumDataSourceType.PRIMARY,
EnumDataSourceType.FOREIGN,
EnumDataSourceType.CATEGORICAL,
EnumDataSourceType.TIME,
EnumDataSourceType.MEASURES,
].includes(type as EnumDataSourceType)
) {
const isCreateName = [EnumDataSourceType.CATEGORICAL, EnumDataSourceType.TIME].includes(
type as EnumDataSourceType,
)
? 'isCreateDimension'
: 'isCreateMetric';
const editState = !isUndefined(record[isCreateName]) ? !!record[isCreateName] : true;
return (
<Checkbox
checked={editState}
onChange={(e) => {
const value = e.target.checked ? 1 : 0;
if (!value) {
onFieldChange(record.bizName, {
...record,
name: '',
[isCreateName]: value,
});
} else {
handleFieldChange(record, isCreateName, value);
}
}}
>
<Input
value={name}
disabled={!editState}
onChange={(e) => {
const value = e.target.value;
handleFieldChange(record, 'name', value);
}}
placeholder="请输入中文名"
/>
</Checkbox>
);
}
return <></>;
},
},
];
return (
<>
<Table<FieldItem>
dataSource={fields}
columns={columns}
className="fields-table"
rowKey="bizName"
pagination={false}
scroll={{ y: 500 }}
/>
</>
);
};
export default FieldForm;

View File

@@ -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, '&nbsp;&nbsp;&nbsp;&nbsp;'),
}}
/>
</>
);
}
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;

View File

@@ -0,0 +1,126 @@
import React, { useState, useRef, useEffect } from 'react';
import { Tabs } from 'antd';
import SqlDetail from './SqlDetail';
import type { SqlItem } from '../data';
import styles from '../style.less';
type Panes = {
title: string;
key: string;
type: 'add' | 'edit';
scriptId?: number;
sql?: string;
sqlInfo?: SqlItem;
isSave?: boolean; // 暂存提示保存
};
type TableRef = {
current?: {
fetchSqlList: () => void;
upDateActiveItem: (key: any) => void;
};
};
type Props = {
initialValues: any;
domainId: number;
onSubmitSuccess?: (dataSourceInfo: any) => void;
};
const { TabPane } = Tabs;
const LIST_KEY = 'list';
const SqlSide: React.FC<Props> = ({ initialValues, domainId, onSubmitSuccess }) => {
const defaultPanes: Panes[] = [
{
key: '数据源查询',
title: initialValues?.name || '数据源查询',
type: 'add',
isSave: true,
},
];
const [activeKey, setActiveKey] = useState('数据源查询');
const [panes, setPanes] = useState<Panes[]>(defaultPanes);
const tableRef: TableRef = useRef();
const panesRef = useRef<Panes[]>(defaultPanes);
const [dataSourceItem, setDataSourceItem] = useState<any>(initialValues || {});
const updatePane = (list: Panes[]) => {
setPanes(list);
panesRef.current = list;
};
// 更新脚本内容
const updateTabSql = (sql: string, targetKey: string) => {
const newPanes = panesRef.current.slice();
const index = newPanes.findIndex((item) => item.key === targetKey);
const targetItem = newPanes[index];
newPanes.splice(index, 1, {
...targetItem,
sql,
isSave: false,
});
updatePane(newPanes);
};
useEffect(() => {
if (initialValues) {
updateTabSql(initialValues?.datasourceDetail?.sqlQuery || '', '数据源查询');
}
}, [initialValues]);
const onChange = (key: string) => {
setActiveKey(key);
tableRef?.current?.upDateActiveItem(key);
if (key === LIST_KEY) {
tableRef?.current?.fetchSqlList();
}
};
return (
<>
<div className={styles.outside}>
<Tabs
type="editable-card"
hideAdd={true}
activeKey={activeKey}
onChange={onChange}
className={styles.middleArea}
>
{panes.map((pane) => {
return (
<TabPane
tab={<div className={styles.paneName}>{pane.title}</div>}
closable={false}
key={pane.key}
>
<SqlDetail
onSubmitSuccess={onSubmitSuccess}
dataSourceItem={dataSourceItem}
oprType={pane.type}
domainId={domainId}
onUpdateSql={(sql: string) => {
updateTabSql(sql, pane.key);
}}
onJdbcSourceChange={(databaseId) => {
setDataSourceItem({
...dataSourceItem,
databaseId,
});
}}
sql={pane.sql}
/>
</TabPane>
);
})}
</Tabs>
</div>
{/* </SplitPane> */}
</>
);
};
export default SqlSide;

View File

@@ -0,0 +1,67 @@
export const EDITOR_HEIGHT_MAP = new Map([
['small', '250px'],
['middle', '300px'],
['large', '400px'],
]);
export enum EnumDataSourceType {
CATEGORICAL = 'categorical',
TIME = 'time',
MEASURES = 'measures',
PRIMARY = 'primary',
FOREIGN = 'foreign',
}
export const TYPE_OPTIONS = [
{
label: '维度',
value: EnumDataSourceType.CATEGORICAL,
},
{
label: '日期',
value: EnumDataSourceType.TIME,
},
{
label: '度量',
value: EnumDataSourceType.MEASURES,
},
{
label: '主键',
value: EnumDataSourceType.PRIMARY,
},
{
label: '外键',
value: EnumDataSourceType.FOREIGN,
},
];
export const AGG_OPTIONS = [
{
label: 'sum',
value: 'sum',
},
{
label: 'max',
value: 'max',
},
{
label: 'min',
value: 'min',
},
{
label: 'avg',
value: 'avg',
},
{
label: 'count',
value: 'count',
},
{
label: 'count_distinct',
value: 'count_distinct',
},
];
export const DATE_OPTIONS = ['day', 'week', 'month'];
export const DATE_FORMATTER = ['YYYY-MM-DD', 'YYYYMMDD', 'YYYY-MM', 'YYYYMM'];

View File

@@ -0,0 +1,68 @@
import React, { useState, useEffect } from 'react';
import SplitPane from 'react-split-pane';
import SqlSide from './components/SqlSide';
import Pane from 'react-split-pane/lib/Pane';
import styles from './style.less';
import { RightOutlined, LeftOutlined } from '@ant-design/icons';
type Props = {
initialValues: any;
domainId: number;
onSubmitSuccess?: (dataSourceInfo: any) => void;
};
const DEFAULT_RIGHT_SIZE = '300px';
const DataExploreView: React.FC<Props> = ({ initialValues, domainId, onSubmitSuccess }) => {
const [collapsed, setCollapsed] = useState(false);
useEffect(() => {
const exploreRightCollapsed = localStorage.getItem('exploreRightCollapsed');
setCollapsed(exploreRightCollapsed === 'true');
}, []);
const onCollapse = () => {
const collapsedValue = !collapsed;
setCollapsed(collapsedValue);
localStorage.setItem('exploreRightCollapsed', String(collapsedValue));
const exploreRightSize = collapsedValue ? '0px' : localStorage.getItem('exploreRightSize');
const sizeValue = parseInt(exploreRightSize || '0');
if (!collapsedValue && sizeValue <= 10) {
localStorage.setItem('exploreRightSize', DEFAULT_RIGHT_SIZE);
}
};
return (
<div
className={`${styles.pageContainer} ${
window.location.hash.includes('external') ? styles.externalPageContainer : ''
}`}
>
<div className={styles.main}>
<SplitPane
split="vertical"
onChange={(size) => {
localStorage.setItem('exploreRightSize', size[1]);
}}
>
<div className={styles.rightListSide}>
{false && (
<div className={styles.collapseRightBtn} onClick={onCollapse}>
{collapsed ? <LeftOutlined /> : <RightOutlined />}
</div>
)}
<SqlSide
initialValues={initialValues}
domainId={domainId}
onSubmitSuccess={onSubmitSuccess}
/>
</div>
<Pane initialSize={0} />
</SplitPane>
</div>
</div>
);
};
export default DataExploreView;

View File

@@ -0,0 +1,12 @@
import request from 'umi-request';
type ExcuteSqlParams = {
sql: string;
domainId: number;
};
// 执行脚本
export async function excuteSql(params: ExcuteSqlParams) {
const data = { ...params };
return request.post(`${process.env.API_BASE_URL}database/executeSql`, { data });
}

View File

@@ -0,0 +1,759 @@
@borderColor: #eee;
@activeColor: #a0c5e8;
@hoverColor: #dee4e9;
.pageContainer {
position: absolute;
top: 55px;
right: 0;
bottom: 0;
left: 0;
// margin: -24px;
background: #fff;
&.externalPageContainer {
margin: 0 !important;
}
}
.searchBar {
:global {
.ant-form-item-label {
width: 70px;
}
}
}
.main {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
display: flex;
:global {
.ant-tabs {
height: 100% !important;
.ant-tabs-content {
height: 100% !important;
.ant-tabs-tabpane {
height: 100%;
}
}
}
}
}
.rightSide {
position: relative;
z-index: 1;
min-width: 250px;
height: 100%;
margin-left: 4px;
padding: 10px;
overflow: hidden;
:global {
.ant-form-item {
margin-bottom: 6px;
.ant-form-item-label {
width: 70px;
}
.ant-form-item-control {
min-width: 100px;
}
}
}
}
.rightListSide {
position: relative;
z-index: 2;
flex: 1;
height: 100%;
// padding: 10px 10px 0;
background-color: #fff;
// 去掉标签间距
:global {
.ant-tabs-card.ant-tabs-top > .ant-tabs-nav .ant-tabs-tab + .ant-tabs-tab,
.ant-tabs-card.ant-tabs-bottom > .ant-tabs-nav .ant-tabs-tab + .ant-tabs-tab,
.ant-tabs-card.ant-tabs-top > div > .ant-tabs-nav .ant-tabs-tab + .ant-tabs-tab,
.ant-tabs-card.ant-tabs-bottom > div > .ant-tabs-nav .ant-tabs-tab + .ant-tabs-tab {
margin-left: 0;
}
.ant-tabs > .ant-tabs-nav .ant-tabs-nav-add,
.ant-tabs > div > .ant-tabs-nav .ant-tabs-nav-add {
margin-left: 0;
}
}
}
.leftListSide {
position: relative;
z-index: 2;
flex: 1;
height: 100%;
// padding: 10px 10px 0;
background-color: #fff;
}
.tableTotal {
margin: 0 2px;
color: #296df3;
font-weight: bold;
}
.tableDetaildrawer {
:global {
.ant-drawer-header {
padding: 10px 45px 10px 10px;
}
.ant-drawer-close {
padding: 10px;
}
.ant-drawer-body {
padding: 0 10px 10px;
}
.ant-tabs-top > .ant-tabs-nav {
margin-bottom: 8px;
}
}
}
.tableDetailTable {
:global {
.ant-table-cell,
.resultTableRow > td {
padding: 8px;
font-size: 12px;
}
}
}
.sqlEditor {
min-width: 0;
height: 100%;
border: solid 1px @borderColor;
:global {
.ace_editor {
font-family: 'Menlo', 'Monaco', 'Ubuntu Mono', 'Consolas', 'source-code-pro' !important;
}
}
}
.sqlOprBar {
margin-top: -10px;
padding: 5px;
display: flex;
.sqlOprBarLeftBox {
flex: 1 1 200px;
}
.sqlOprBarRightBox {
flex: 0 1 210px;
}
:global {
.ant-btn-round.ant-btn-sm {
font-size: 12px;
}
.ant-btn-primary {
color: #fff;
background: #02a7f0;
border-color: #02a7f0;
}
.ant-segmented-item-selected {
color: #fff;
background: #02a7f0;
border-color: #02a7f0;
}
}
}
.sqlOprIcon {
margin-right: 30px;
color: #02a7f0;
font-size: 22px;
&:hover {
cursor: pointer;
opacity: 0.8;
}
&:active {
opacity: 0.7;
}
}
.sqlOprBtn {
margin-right: 30px;
vertical-align: super !important;
&:hover {
cursor: pointer;
opacity: 0.8;
}
&:active {
opacity: 0.7;
}
}
.sqlOprSwitch {
// vertical-align: super !important;
float: right;
margin-right: 10px !important;
}
:global {
.is-sql-full-select {
background-color: #02a7f0;
}
.cjjWdp:hover {
z-index: 10;
}
}
.sqlMain {
display: flex;
flex-direction: row;
height: 100%;
.sqlEditorWrapper {
flex: 1;
height: 100%;
overflow: hidden;
}
.sqlParams {
width: 20%;
height: 100% !important;
overflow: auto;
}
.hideSqlParams {
width: 0;
height: 100% !important;
overflow: auto;
}
}
.sqlParamsBody {
.header {
display: flex;
padding: 10px;
font-weight: bold;
.title {
flex: 1;
}
.icon {
display: flex;
align-items: center;
margin-right: 10px !important;
cursor: pointer;
}
}
.paramsList {
.paramsItem {
display: flex;
padding: 10px;
:global {
.ant-list-item-action {
margin-left: 5px;
}
}
.name {
flex: 1;
width: 80%;
overflow: hidden;
font-size: 12px;
text-overflow: ellipsis;
&:hover {
cursor: pointer;
}
}
.icon {
// display: none;
margin-left: 10px;
}
}
}
// .paramsItem:hover {
// .icon {
// display: inline-block;
// margin-left: 8px;
// cursor: pointer;
// }
// }
}
.disableIcon {
vertical-align: super !important;
// color: rgba(0, 10, 36, 0.25);
background: #7d7f80 !important;
border-color: #7d7f80 !important;
:global {
.anticon .anticon-play-circle {
color: #fff;
}
}
&:hover {
cursor: not-allowed;
opacity: 1;
}
}
.sqlTaskListWrap {
position: relative;
width: 262px;
border-top: 0 !important;
border-radius: 0;
:global {
.ant-card-head {
min-height: 20px;
}
.ant-card-head-title {
padding: 8px 0;
}
}
}
.sqlTaskList {
position: absolute !important;
top: 42px;
right: 0;
bottom: 0;
left: 0;
overflow: auto;
}
.sqlBottmWrap {
// position: absolute;
// top: 484px;
// right: 0;
// bottom: 0;
// left: 0;
display: flex;
height: 100%;
// padding: 0 10px;
&:global(.small) {
top: 334px;
}
&:global(.middle) {
top: 384px;
}
}
.sqlResultWrap {
position: relative;
display: flex;
flex: 1;
flex-direction: column;
overflow: auto;
border: solid 1px @borderColor;
border-top: 0;
border-left: 0;
}
.sqlToolBar {
display: flex;
flex-direction: row-reverse;
align-items: center;
height: 41px;
padding: 5px 0;
text-align: right;
}
.sqlResultPane {
flex: 1;
border-top: solid 1px @borderColor;
}
.sqlToolBtn {
margin-right: 15px;
}
.runScriptBtn {
margin-right: 15px;
background-color: #e87954;
border-color: #e87954;
&:hover{
border-color: #f89878;
background: #f89878;
}
&:focus {
border-color: #f89878;
background: #f89878;
}
}
.taskFailed {
padding: 20px 20px 0 20px;
}
.sqlResultContent {
position: absolute;
top: 50%;
width: 100%;
color: rgba(0, 0, 0, 0.25);
font-size: 16px;
text-align: center;
}
.sqlResultLog {
padding: 20px;
word-wrap: break-word;
}
.tableList {
position: absolute !important;
top: 160px;
right: 0;
bottom: 26px;
left: 0;
overflow-x: hidden;
overflow-y: auto;
border-bottom: solid 1px @borderColor;
}
.tablePage {
position: absolute !important;
bottom: 0;
left: 0;
z-index: 1;
width: 100%;
min-width: 250px;
overflow: hidden;
}
.tableListItem {
width: 88%;
overflow: hidden;
font-size: 12px;
text-overflow: ellipsis;
&:hover {
cursor: pointer;
}
}
.tableItem {
&:global(.ant-list-item) {
padding: 6px 0 6px 6px;
}
:global(.ant-list-item-action) {
margin-left: 12px !important;
}
&:hover {
background: @hoverColor;
border-bottom: 1px solid #f0f0f0;
}
&:global(.active) {
background: @activeColor;
}
}
.taskIcon {
margin-right: 10px;
color: #1890ff;
font-size: 14px;
}
.taskSuccessIcon {
.taskIcon();
color: #67c23a;
}
.taskFailIcon {
.taskIcon();
color: #f56c6c;
}
.resultFailIcon {
margin-right: 8px;
color: #f56c6c;
}
.taskItem {
padding: 10px 8px !important;
font-size: 12px;
cursor: pointer;
&:global(.ant-list-item) {
justify-content: flex-start;
}
&:hover {
background: @hoverColor;
}
}
.activeTask {
background: @activeColor;
}
.resultTable {
width: 100%;
:global {
.ant-table-body {
width: 100%;
// max-height: none !important;
overflow: auto !important;
}
.ant-table-cell,
.resultTableRow > td {
padding: 8px;
font-size: 12px;
}
}
}
.taskLogWrap {
word-wrap: break-word;
}
.siteTagPlus {
background: #fff;
border-style: dashed;
}
.editTag {
margin-bottom: 5px;
user-select: none;
}
.tagInput {
width: 78px;
margin-right: 8px;
vertical-align: top;
}
.outside {
position: relative;
height: 100%;
}
.collapseRightBtn {
position: absolute;
top: calc(50% + 50px);
right: 0;
z-index: 100;
display: flex;
align-items: center;
height: 70px;
color: #fff;
font-size: 12px;
background-color: rgba(40, 46, 54, 0.2);
border-radius: 24px 0 0 24px;
cursor: pointer;
transition: all 0.3s ease;
}
.collapseLeftBtn {
position: absolute;
top: calc(50% + 45px);
left: 0;
z-index: 100;
display: flex;
align-items: center;
height: 70px;
color: #fff;
font-size: 12px;
background-color: rgba(40, 46, 54, 0.2);
border-radius: 0 24px 24px 0;
cursor: pointer;
transition: all 0.3s ease;
}
.detail {
.titleCollapse {
float: right;
padding-right: 18px;
color: #1890ff;
line-height: 35px;
text-align: right;
cursor: pointer;
}
.tableTitle {
display: inline-block;
width: 85%;
margin-left: 15px;
overflow: hidden;
line-height: 35px;
white-space: nowrap;
text-overflow: ellipsis;
cursor: pointer;
}
:global {
.ant-divider-horizontal {
margin: 0;
}
}
}
.search {
margin-left: 10px;
}
.middleArea {
:global {
.ant-tabs-nav .ant-tabs-tab {
border: none;
// background: #d9d9d96e;
border-right: 1px solid #f0f0f0;
border-radius: 0 !important;
}
.ant-tabs-nav-add {
border-radius: 0 !important;
}
.ant-tabs-tab {
.ant-tabs-tab-remove {
.closeTab {
opacity: 0;
}
.dot {
opacity: 1;
}
}
}
.ant-tabs-tab:hover {
.ant-tabs-tab-remove {
.closeTab {
opacity: 1 !important;
}
.dot {
opacity: 0;
}
}
}
}
}
.menu {
position: relative;
z-index: 1;
height: 100%;
padding: 5px;
overflow: hidden;
overflow-x: hidden;
overflow-y: auto;
:global {
.ant-form {
margin: -2px;
}
}
}
.menuList {
position: absolute !important;
top: 95px;
right: 0;
bottom: 26px;
left: 0;
overflow-x: hidden;
overflow-y: auto;
border-bottom: solid 1px @borderColor;
.menuItem {
&:global(.ant-list-item) {
padding: 6px 0 6px 14px;
}
:global(.ant-list-item-action) {
margin-left: 12px !important;
}
&:hover {
background: @hoverColor;
border-bottom: 1px solid #f0f0f0;
.icon {
display: block;
}
}
&:global(.active) {
background: @activeColor;
}
.menuListItem {
width: 90%;
overflow: hidden;
font-size: 12px;
white-space: nowrap;
text-overflow: ellipsis;
&:hover {
cursor: pointer;
}
}
.icon {
display: none;
margin-right: 15px !important;
cursor: pointer;
}
.menuIcon {
display: flex;
}
}
}
.scriptFile {
width: 100%;
margin: 10px;
overflow: hidden;
font-size: 14px;
white-space: nowrap;
text-overflow: ellipsis;
.icon {
margin-right: 10px;
}
}
.sqlScriptName {
width: 93% !important;
margin: 14px 0 0 14px !important;
}
.fileIcon {
width: 20px !important;
height: 20px !important;
padding-top: 2px !important;
padding-right: 5px !important;
vertical-align: middle;
}
.itemName {
vertical-align: middle;
}
.paneName {
width: 100px;
overflow: hidden;
font-size: 12px !important;
white-space: nowrap;
text-overflow: ellipsis;
}
.titleIcon {
width: 16px !important;
height: 16px !important;
margin: 0 3px 4px;
}