mirror of
https://github.com/tencentmusic/supersonic.git
synced 2025-12-15 22:46:49 +00:00
first commit
This commit is contained in:
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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'];
|
||||
@@ -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;
|
||||
@@ -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 });
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user