mirror of
https://github.com/CodePhiliaX/Chat2DB.git
synced 2025-08-01 18:53:35 +08:00
feat: code migration
This commit is contained in:
169
chat2db-client/mock/sqlResult.json
Normal file
169
chat2db-client/mock/sqlResult.json
Normal file
@ -0,0 +1,169 @@
|
||||
{
|
||||
"success": true,
|
||||
"errorCode": null,
|
||||
"errorMessage": null,
|
||||
"data": [
|
||||
{
|
||||
"sql": "SELECT *\nFROM students\nLIMIT 500",
|
||||
"description": "执行成功",
|
||||
"message": null,
|
||||
"success": true,
|
||||
"headerList": [
|
||||
{
|
||||
"dataType": "NUMERIC",
|
||||
"name": "id"
|
||||
},
|
||||
{
|
||||
"dataType": "STRING",
|
||||
"name": "name"
|
||||
},
|
||||
{
|
||||
"dataType": "STRING",
|
||||
"name": "gender"
|
||||
},
|
||||
{
|
||||
"dataType": "DATETIME",
|
||||
"name": "birthday"
|
||||
},
|
||||
{
|
||||
"dataType": "STRING",
|
||||
"name": "address"
|
||||
},
|
||||
{
|
||||
"dataType": "STRING",
|
||||
"name": "phone"
|
||||
},
|
||||
{
|
||||
"dataType": "STRING",
|
||||
"name": "email"
|
||||
},
|
||||
{
|
||||
"dataType": "DATETIME",
|
||||
"name": "create_time"
|
||||
},
|
||||
{
|
||||
"dataType": "DATETIME",
|
||||
"name": "update_time"
|
||||
}
|
||||
],
|
||||
"dataList": [
|
||||
[
|
||||
"1",
|
||||
"张三",
|
||||
"男",
|
||||
null,
|
||||
"北京市海淀区",
|
||||
"12345678901",
|
||||
"zhangsan@example.com",
|
||||
"2023-05-31 10:41:56.000",
|
||||
"2023-05-31 10:41:56.000"
|
||||
],
|
||||
[
|
||||
"2",
|
||||
"李四",
|
||||
"男",
|
||||
null,
|
||||
"上海市浦东新区",
|
||||
"12345678902",
|
||||
"lisi@example.com",
|
||||
"2023-05-31 10:41:56.000",
|
||||
"2023-05-31 10:41:56.000"
|
||||
],
|
||||
[
|
||||
"3",
|
||||
"王五",
|
||||
"女",
|
||||
null,
|
||||
"广州市天河区",
|
||||
"12345678903",
|
||||
"wangwu@example.com",
|
||||
"2023-05-31 10:41:56.000",
|
||||
"2023-05-31 10:41:56.000"
|
||||
],
|
||||
[
|
||||
"4",
|
||||
"赵六",
|
||||
"男",
|
||||
null,
|
||||
"深圳市南山区",
|
||||
"12345678904",
|
||||
"zhaoliu@example.com",
|
||||
"2023-05-31 10:41:56.000",
|
||||
"2023-05-31 10:41:56.000"
|
||||
],
|
||||
[
|
||||
"5",
|
||||
"陈七",
|
||||
"女",
|
||||
null,
|
||||
"武汉市江汉区",
|
||||
"12345678905",
|
||||
"chenqi@example.com",
|
||||
"2023-05-31 10:41:56.000",
|
||||
"2023-05-31 10:41:56.000"
|
||||
],
|
||||
[
|
||||
"6",
|
||||
"刘八",
|
||||
"男",
|
||||
null,
|
||||
"成都市高新区",
|
||||
"12345678906",
|
||||
"liuba@example.com",
|
||||
"2023-05-31 10:41:56.000",
|
||||
"2023-05-31 10:41:56.000"
|
||||
],
|
||||
[
|
||||
"7",
|
||||
"魏九",
|
||||
"女",
|
||||
null,
|
||||
"重庆市渝北区",
|
||||
"12345678907",
|
||||
"weijiu@example.com",
|
||||
"2023-05-31 10:41:56.000",
|
||||
"2023-05-31 10:41:56.000"
|
||||
],
|
||||
[
|
||||
"8",
|
||||
"孙十",
|
||||
"男",
|
||||
null,
|
||||
"南京市鼓楼区",
|
||||
"12345678908",
|
||||
"sunshi@example.com",
|
||||
"2023-05-31 10:41:56.000",
|
||||
"2023-05-31 10:41:56.000"
|
||||
],
|
||||
[
|
||||
"9",
|
||||
"郑十一",
|
||||
"男",
|
||||
null,
|
||||
"西安市雁塔区",
|
||||
"12345678909",
|
||||
"zhengshiyi@example.com",
|
||||
"2023-05-31 10:41:56.000",
|
||||
"2023-05-31 10:41:56.000"
|
||||
],
|
||||
[
|
||||
"10",
|
||||
"许十二",
|
||||
"女",
|
||||
null,
|
||||
"苏州市姑苏区",
|
||||
"12345678910",
|
||||
"xushier@example.com",
|
||||
"2023-05-31 10:41:56.000",
|
||||
"2023-05-31 10:41:56.000"
|
||||
]
|
||||
],
|
||||
"sqlType": "SELECT",
|
||||
"hasNextPage": false,
|
||||
"pageNo": 1,
|
||||
"pageSize": 500,
|
||||
"duration": 6
|
||||
}
|
||||
],
|
||||
"traceId": null
|
||||
}
|
@ -8,7 +8,7 @@ import { format } from 'sql-formatter';
|
||||
import sqlServer from '@/service/sql';
|
||||
import historyServer from '@/service/history';
|
||||
import MonacoEditor from 'react-monaco-editor';
|
||||
import { useReducerContext } from '@/pages/main/workspace/index'
|
||||
import { useReducerContext } from '@/pages/main/workspace/index';
|
||||
|
||||
import styles from './index.less';
|
||||
import Loading from '../Loading/Loading';
|
||||
@ -43,7 +43,8 @@ interface IProps {
|
||||
consoleId: number;
|
||||
schemaName?: string;
|
||||
consoleName: string;
|
||||
}
|
||||
};
|
||||
onExecuteSQL: (value: any) => void;
|
||||
}
|
||||
|
||||
function Console(props: IProps) {
|
||||
@ -58,7 +59,7 @@ function Console(props: IProps) {
|
||||
|
||||
useEffect(() => {
|
||||
setContext(value);
|
||||
}, [value])
|
||||
}, [value]);
|
||||
|
||||
const onPressChatInput = (value: string) => {
|
||||
const params = formatParams({
|
||||
@ -108,17 +109,21 @@ function Console(props: IProps) {
|
||||
sql: sqlContent,
|
||||
...executeParams,
|
||||
};
|
||||
sqlServer.executeSql(p).then((res) => {
|
||||
console.log(res)
|
||||
let p = {
|
||||
...executeParams,
|
||||
ddl: sqlContent
|
||||
};
|
||||
historyServer.createHistory(p);
|
||||
// setManageResultDataList(res);
|
||||
}).catch((error) => {
|
||||
// setManageResultDataList([]);
|
||||
});
|
||||
sqlServer
|
||||
.executeSql(p)
|
||||
.then((res) => {
|
||||
props.onExecuteSQL && props.onExecuteSQL(res);
|
||||
// console.log(res)
|
||||
let p = {
|
||||
...executeParams,
|
||||
ddl: sqlContent,
|
||||
};
|
||||
historyServer.createHistory(p);
|
||||
// setManageResultDataList(res);
|
||||
})
|
||||
.catch((error) => {
|
||||
// setManageResultDataList([]);
|
||||
});
|
||||
};
|
||||
|
||||
const saveWindowTab = () => {
|
577
chat2db-client/src/components/CreateConnection/index.tsx
Normal file
577
chat2db-client/src/components/CreateConnection/index.tsx
Normal file
@ -0,0 +1,577 @@
|
||||
import React, { memo, useEffect, useMemo, useState, Fragment, useContext, useCallback, useLayoutEffect } from 'react';
|
||||
import { i18n, isEn } from '@/i18n';
|
||||
import styles from './index.less';
|
||||
import classnames from 'classnames';
|
||||
|
||||
import connectionService from '@/service/connection';
|
||||
|
||||
import { DatabaseTypeCode, ConnectionEnvType, databaseMap } from '@/constants/database';
|
||||
import { dataSourceFormConfigs } from './config/dataSource';
|
||||
import { IConnectionConfig, IFormItem, ISelect } from './config/types';
|
||||
import { IConnectionDetails } from '@/typings/connection';
|
||||
import { InputType } from './config/enum';
|
||||
import { deepClone } from '@/utils';
|
||||
import { Select, Form, Input, message, Table, Button, Collapse } from 'antd';
|
||||
import Iconfont from '@/components/Iconfont';
|
||||
import LoadingContent from '@/components/Loading/LoadingContent';
|
||||
import { useTheme } from '@/hooks/useTheme';
|
||||
|
||||
const { Option } = Select;
|
||||
|
||||
type ITabsType = 'ssh' | 'baseInfo';
|
||||
|
||||
export enum submitType {
|
||||
UPDATE = 'update',
|
||||
SAVE = 'save',
|
||||
TEST = 'test',
|
||||
}
|
||||
|
||||
interface IProps {
|
||||
className?: string;
|
||||
closeCreateConnection: () => void;
|
||||
connectionData: IConnectionDetails;
|
||||
submitCallback?: Function;
|
||||
}
|
||||
|
||||
export default function CreateConnection(props: IProps) {
|
||||
const { className, closeCreateConnection, submitCallback } = props;
|
||||
const [baseInfoForm] = Form.useForm();
|
||||
const [sshForm] = Form.useForm();
|
||||
const [backfillData, setBackfillData] = useState<IConnectionDetails>(props.connectionData);
|
||||
const [loadings, setLoading] = useState({
|
||||
confirmButton: false,
|
||||
testButton: false,
|
||||
});
|
||||
// const [connectionData, setConnectionData] = useState<IConnectionDetails>(props.connectionData);
|
||||
// const [currentType, setCurrentType] = useState<DatabaseTypeCode>(createType || DatabaseTypeCode.MYSQL);
|
||||
|
||||
useEffect(() => {
|
||||
setBackfillData(props.connectionData);
|
||||
}, [props.connectionData]);
|
||||
|
||||
useEffect(() => {
|
||||
if (backfillData.id) {
|
||||
getConnectionDetails(backfillData.id);
|
||||
}
|
||||
}, [backfillData.id]);
|
||||
|
||||
function getConnectionDetails(id: number) {
|
||||
connectionService.getDetails({ id }).then((res) => {
|
||||
if (!res) {
|
||||
return;
|
||||
}
|
||||
if (res.user) {
|
||||
res.authentication = 1;
|
||||
} else {
|
||||
res.authentication = 2;
|
||||
}
|
||||
setTimeout(() => {
|
||||
setBackfillData(res);
|
||||
}, 300);
|
||||
});
|
||||
}
|
||||
|
||||
const getItems = () => [
|
||||
{
|
||||
key: 'ssh',
|
||||
label: 'SSH Configuration',
|
||||
children: (
|
||||
<div className={styles.sshBox}>
|
||||
<RenderForm backfillData={backfillData!} form={sshForm} tab="ssh" />
|
||||
<div className={styles.testSSHConnect}>
|
||||
<div onClick={testSSH} className={styles.testSSHConnectText}>
|
||||
{i18n('connection.message.testSshConnection')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'extendInfo',
|
||||
label: 'Advanced Configuration',
|
||||
children: (
|
||||
<div className={styles.extendInfoBox}>
|
||||
<RenderExtendTable backfillData={backfillData!}></RenderExtendTable>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
// 测试、保存、修改连接
|
||||
function saveConnection(type: submitType) {
|
||||
const ssh = sshForm.getFieldsValue();
|
||||
const baseInfo = baseInfoForm.getFieldsValue();
|
||||
const extendInfo: any = [];
|
||||
const loadingsButton = type === submitType.TEST ? 'testButton' : 'confirmButton';
|
||||
extendTableData.map((t: any) => {
|
||||
if (t.label || t.value) {
|
||||
extendInfo.push({
|
||||
key: t.label,
|
||||
value: t.value,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
let p: any = {
|
||||
ssh,
|
||||
...baseInfo,
|
||||
extendInfo,
|
||||
// ...values,
|
||||
ConnectionEnvType: ConnectionEnvType.DAILY,
|
||||
type: backfillData.type,
|
||||
};
|
||||
|
||||
if (type !== submitType.SAVE) {
|
||||
p.id = backfillData.id;
|
||||
}
|
||||
|
||||
const api: any = connectionService[type](p);
|
||||
|
||||
setLoading({
|
||||
...loadings,
|
||||
[loadingsButton]: true,
|
||||
});
|
||||
|
||||
api
|
||||
.then((res: any) => {
|
||||
if (type === submitType.TEST) {
|
||||
message.success(
|
||||
res === false
|
||||
? i18n('connection.message.testConnectResult', i18n('common.text.failure'))
|
||||
: i18n('connection.message.testConnectResult', i18n('common.text.successful')),
|
||||
);
|
||||
} else {
|
||||
submitCallback?.();
|
||||
message.success(
|
||||
type === submitType.UPDATE
|
||||
? i18n('common.message.modifySuccessfully')
|
||||
: i18n('common.message.addedSuccessfully'),
|
||||
);
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
setLoading({
|
||||
...loadings,
|
||||
[loadingsButton]: false,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function onCancel() {
|
||||
closeCreateConnection();
|
||||
// setEditDataSourceData(false)
|
||||
}
|
||||
|
||||
function testSSH() {
|
||||
let p = sshForm.getFieldsValue();
|
||||
connectionService.testSSH(p).then((res) => {
|
||||
message.success(i18n('connection.message.testConnectResult', i18n('common.text.successful')));
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={classnames(styles.box, className)}>
|
||||
<LoadingContent className={styles.loadingContent} data={backfillData}>
|
||||
<div className={styles.connectionBox}>
|
||||
<div className={styles.title}>
|
||||
<Iconfont code={databaseMap[backfillData.type]?.icon}></Iconfont>
|
||||
<div>{databaseMap[backfillData.type]?.name}</div>
|
||||
</div>
|
||||
<div className={styles.baseInfoBox}>
|
||||
<RenderForm backfillData={backfillData!} form={baseInfoForm} tab="baseInfo" />
|
||||
</div>
|
||||
<Collapse items={getItems()} />
|
||||
<div className={styles.formFooter}>
|
||||
<div className={styles.test}>
|
||||
{
|
||||
<Button
|
||||
loading={loadings.testButton}
|
||||
onClick={saveConnection.bind(null, submitType.TEST)}
|
||||
className={styles.test}
|
||||
>
|
||||
{i18n('connection.button.testConnection')}
|
||||
</Button>
|
||||
}
|
||||
</div>
|
||||
<div className={styles.rightButton}>
|
||||
<Button onClick={onCancel} className={styles.cancel}>
|
||||
{i18n('common.button.cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
className={styles.save}
|
||||
type="primary"
|
||||
loading={loadings.confirmButton}
|
||||
onClick={saveConnection.bind(null, backfillData.id ? submitType.UPDATE : submitType.SAVE)}
|
||||
>
|
||||
{i18n('common.button.save')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</LoadingContent>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface IRenderFormProps {
|
||||
tab: ITabsType;
|
||||
form: any;
|
||||
backfillData: IConnectionDetails;
|
||||
}
|
||||
|
||||
function RenderForm(props: IRenderFormProps) {
|
||||
const { tab, form, backfillData } = props;
|
||||
const editId = backfillData.id;
|
||||
const databaseType = backfillData.type;
|
||||
|
||||
let aliasChanged = false;
|
||||
|
||||
const dataSourceFormConfigMemo = useMemo<IConnectionConfig>(() => {
|
||||
return deepClone(dataSourceFormConfigs).find((t: IConnectionConfig) => {
|
||||
return t.type === databaseType;
|
||||
});
|
||||
}, [databaseType]);
|
||||
|
||||
const [dataSourceFormConfig, setDataSourceFormConfig] = useState<IConnectionConfig>(dataSourceFormConfigMemo);
|
||||
|
||||
useEffect(() => {
|
||||
setDataSourceFormConfig(dataSourceFormConfigMemo);
|
||||
}, [databaseType]);
|
||||
|
||||
const initialValuesMemo = useMemo(() => {
|
||||
return initialFormData(dataSourceFormConfigMemo[tab].items);
|
||||
}, []);
|
||||
|
||||
const [initialValues] = useState(initialValuesMemo);
|
||||
|
||||
useEffect(() => {
|
||||
if (!backfillData) {
|
||||
return;
|
||||
}
|
||||
if (tab === 'baseInfo') {
|
||||
// TODO:
|
||||
// selectChange({ name: 'authentication', value: backfillData.user ? 1 : 2 });
|
||||
regEXFormatting({ url: backfillData.url }, backfillData);
|
||||
}
|
||||
|
||||
if (tab === 'ssh') {
|
||||
regEXFormatting({}, backfillData.ssh || {});
|
||||
}
|
||||
}, [backfillData]);
|
||||
|
||||
function initialFormData(dataSourceFormConfig: IFormItem[] | undefined) {
|
||||
let initValue: any = {};
|
||||
dataSourceFormConfig?.map((t) => {
|
||||
initValue[t.name] = t.defaultValue;
|
||||
if (t.selects?.length) {
|
||||
t.selects?.map((item) => {
|
||||
if (item.value === t.defaultValue) {
|
||||
initValue = {
|
||||
...initValue,
|
||||
...initialFormData(item.items),
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
return initValue;
|
||||
}
|
||||
|
||||
function selectChange(t: { name: string; value: any }) {
|
||||
dataSourceFormConfig[tab].items.map((j, i) => {
|
||||
if (j.name === t.name) {
|
||||
j.defaultValue = t.value;
|
||||
}
|
||||
});
|
||||
setDataSourceFormConfig({ ...dataSourceFormConfig });
|
||||
}
|
||||
|
||||
function onFieldsChange(data: any, datas: any) {
|
||||
// 将antd的格式转换为正常的对象格式
|
||||
if (!data.length) {
|
||||
return;
|
||||
}
|
||||
const keyName = data[0].name[0];
|
||||
const keyValue = data[0].value;
|
||||
const variableData = {
|
||||
[keyName]: keyValue,
|
||||
};
|
||||
const dataObj: any = {};
|
||||
datas.map((t: any) => {
|
||||
dataObj[t.name[0]] = t.value;
|
||||
});
|
||||
// 正则拆分url/组建url
|
||||
if (tab === 'baseInfo') {
|
||||
regEXFormatting(variableData, dataObj);
|
||||
}
|
||||
}
|
||||
|
||||
function extractObj(url: any) {
|
||||
const { template, pattern } = dataSourceFormConfig.baseInfo;
|
||||
// 提取关键词对应的内容 value
|
||||
const matches = url.match(pattern)!;
|
||||
// 提取花括号内的关键词 key
|
||||
const reg = /{(.*?)}/g;
|
||||
let match;
|
||||
const arr = [];
|
||||
while ((match = reg.exec(template)) !== null) {
|
||||
arr.push(match[1]);
|
||||
}
|
||||
// key与value一一对应
|
||||
const newExtract: any = {};
|
||||
arr.map((t, i) => {
|
||||
newExtract[t] = t === 'database' ? matches[i + 2] || '' : matches[i + 1];
|
||||
});
|
||||
return newExtract;
|
||||
}
|
||||
|
||||
function regEXFormatting(variableData: { [key: string]: any }, dataObj: { [key: string]: any }) {
|
||||
const { template, pattern } = dataSourceFormConfig.baseInfo;
|
||||
const keyName = Object.keys(variableData)[0];
|
||||
const keyValue = variableData[Object.keys(variableData)[0]];
|
||||
let newData: any = {};
|
||||
if (keyName === 'url') {
|
||||
//先判断url是否符合规定的正则
|
||||
if (pattern.test(keyValue)) {
|
||||
newData = extractObj(keyValue);
|
||||
}
|
||||
} else if (keyName === 'alias') {
|
||||
aliasChanged = true;
|
||||
} else {
|
||||
// 改变上边url动
|
||||
let url = template;
|
||||
Object.keys(dataObj).map((t) => {
|
||||
url = url.replace(`{${t}}`, dataObj[t]);
|
||||
});
|
||||
newData = {
|
||||
url,
|
||||
};
|
||||
}
|
||||
if (keyName === 'host' && !aliasChanged) {
|
||||
newData.alias = '@' + keyValue;
|
||||
}
|
||||
console.log({
|
||||
...dataObj,
|
||||
...newData,
|
||||
});
|
||||
form.setFieldsValue({
|
||||
...dataObj,
|
||||
...newData,
|
||||
});
|
||||
}
|
||||
|
||||
function renderFormItem(t: IFormItem): React.ReactNode {
|
||||
const label = isEn ? t.labelNameEN : t.labelNameCN;
|
||||
const name = t.name;
|
||||
const width = t?.styles?.width || '100%';
|
||||
const labelWidth = isEn ? t?.styles?.labelWidthEN || '100px' : t?.styles?.labelWidthCN || '70px';
|
||||
const labelAlign = t?.styles?.labelAlign || 'left';
|
||||
|
||||
const FormItemTypes: { [key in InputType]: () => React.ReactNode } = {
|
||||
[InputType.INPUT]: () => (
|
||||
<Form.Item
|
||||
label={label}
|
||||
name={name}
|
||||
style={{ '--form-label-width': labelWidth } as any}
|
||||
labelAlign={labelAlign}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
),
|
||||
|
||||
[InputType.SELECT]: () => (
|
||||
<Form.Item
|
||||
label={label}
|
||||
name={name}
|
||||
style={{ '--form-label-width': labelWidth } as any}
|
||||
labelAlign={labelAlign}
|
||||
>
|
||||
<Select
|
||||
value={t.defaultValue}
|
||||
onChange={(e) => {
|
||||
selectChange({ name: name, value: e });
|
||||
}}
|
||||
>
|
||||
{t.selects?.map((t: ISelect) => (
|
||||
<Option key={t.value} value={t.value}>
|
||||
{t.label}
|
||||
</Option>
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
),
|
||||
|
||||
[InputType.PASSWORD]: () => (
|
||||
<Form.Item
|
||||
label={label}
|
||||
name={name}
|
||||
style={{ '--form-label-width': labelWidth } as any}
|
||||
labelAlign={labelAlign}
|
||||
>
|
||||
<Input.Password />
|
||||
</Form.Item>
|
||||
),
|
||||
};
|
||||
|
||||
return (
|
||||
<Fragment key={t.name}>
|
||||
<div
|
||||
key={t.name}
|
||||
className={classnames({ [styles.labelTextAlign]: t.labelTextAlign })}
|
||||
style={{ width: width }}
|
||||
>
|
||||
{FormItemTypes[t.inputType]()}
|
||||
</div>
|
||||
{t.selects?.map((item) => {
|
||||
if (t.defaultValue === item.value) {
|
||||
return item.items?.map((t) => renderFormItem(t));
|
||||
}
|
||||
})}
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Form
|
||||
colon={false}
|
||||
name={tab}
|
||||
form={form}
|
||||
initialValues={initialValues}
|
||||
className={styles.form}
|
||||
autoComplete="off"
|
||||
labelAlign="left"
|
||||
onFieldsChange={onFieldsChange}
|
||||
>
|
||||
{dataSourceFormConfig[tab]!.items.map((t) => renderFormItem(t))}
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
interface IRenderExtendTableProps {
|
||||
backfillData: IConnectionDetails;
|
||||
}
|
||||
|
||||
let extendTableData: any = [];
|
||||
|
||||
function RenderExtendTable(props: IRenderExtendTableProps) {
|
||||
const { backfillData } = props;
|
||||
const databaseType = backfillData.type;
|
||||
const dataSourceFormConfigMemo = useMemo<IConnectionConfig>(() => {
|
||||
return deepClone(dataSourceFormConfigs).find((t: IConnectionConfig) => {
|
||||
return t.type === databaseType;
|
||||
});
|
||||
}, [backfillData.type]);
|
||||
|
||||
const extendInfo =
|
||||
dataSourceFormConfigMemo.extendInfo?.map((t, i) => {
|
||||
return {
|
||||
key: i,
|
||||
label: t.key,
|
||||
value: t.value,
|
||||
};
|
||||
}) || [];
|
||||
|
||||
const [data, setData] = useState([...extendInfo, { key: extendInfo.length, label: '', value: '' }]);
|
||||
|
||||
useEffect(() => {
|
||||
const backfillDataExtendInfo =
|
||||
backfillData?.extendInfo.map((t, i) => {
|
||||
return {
|
||||
key: i,
|
||||
label: t.key,
|
||||
value: t.value,
|
||||
};
|
||||
}) || [];
|
||||
setData([...backfillDataExtendInfo, { key: extendInfo.length, label: '', value: '' }]);
|
||||
}, [backfillData]);
|
||||
|
||||
useEffect(() => {
|
||||
extendTableData = data;
|
||||
}, [data]);
|
||||
|
||||
const columns: any = [
|
||||
{
|
||||
title: i18n('connection.tableHeader.name'),
|
||||
dataIndex: 'label',
|
||||
width: '60%',
|
||||
render: (value: any, row: any, index: number) => {
|
||||
let isCustomLabel = true;
|
||||
|
||||
dataSourceFormConfigMemo.extendInfo?.map((item) => {
|
||||
if (item.key === row.label) {
|
||||
isCustomLabel = false;
|
||||
}
|
||||
});
|
||||
|
||||
function change(e: any) {
|
||||
const newData = [...data];
|
||||
newData[index] = {
|
||||
key: index,
|
||||
label: e.target.value,
|
||||
value: '',
|
||||
};
|
||||
setData(newData);
|
||||
}
|
||||
|
||||
function blur() {
|
||||
const newData = [];
|
||||
data.map((t) => {
|
||||
if (t.label) {
|
||||
newData.push(t);
|
||||
}
|
||||
});
|
||||
if (index === data.length - 1 && row.label) {
|
||||
newData[index] = {
|
||||
key: index,
|
||||
label: row.label,
|
||||
value: '',
|
||||
};
|
||||
}
|
||||
setData([...newData, { key: newData.length, label: '', value: '' }]);
|
||||
}
|
||||
|
||||
if (index === data.length - 1 || isCustomLabel) {
|
||||
return (
|
||||
<Input
|
||||
onBlur={blur}
|
||||
placeholder={index === data.length - 1 ? i18n('common.text.custom') : ''}
|
||||
onChange={change}
|
||||
value={value}
|
||||
></Input>
|
||||
);
|
||||
} else {
|
||||
return <span>{value}</span>;
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
title: i18n('connection.tableHeader.value'),
|
||||
dataIndex: 'value',
|
||||
width: '40%',
|
||||
render: (value: any, row: any, index: number) => {
|
||||
function change(e: any) {
|
||||
const newData = [...data];
|
||||
newData[index] = {
|
||||
key: index,
|
||||
label: row.label,
|
||||
value: e.target.value,
|
||||
};
|
||||
setData(newData);
|
||||
}
|
||||
|
||||
function blur() {}
|
||||
|
||||
if (index === data.length - 1) {
|
||||
return <Input onBlur={blur} disabled placeholder="<value>" onChange={change} value={value}></Input>;
|
||||
} else {
|
||||
return <Input onChange={change} value={value}></Input>;
|
||||
}
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div className={styles.extendTable}>
|
||||
<Table bordered size="small" pagination={false} columns={columns} dataSource={data} />
|
||||
</div>
|
||||
);
|
||||
}
|
120
chat2db-client/src/components/SearchResult/index.less
Normal file
120
chat2db-client/src/components/SearchResult/index.less
Normal file
@ -0,0 +1,120 @@
|
||||
@import '../../styles/var.less';
|
||||
|
||||
.box {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.recordIcon {
|
||||
font-size: 16px;
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.resultHeader {
|
||||
flex-shrink: 0;
|
||||
overflow-x: scroll;
|
||||
padding: 0px 10px;
|
||||
background-color: var(--color-bg-300);
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
& ::after {
|
||||
height: 0px !important;
|
||||
}
|
||||
|
||||
.statusIcon {
|
||||
margin-right: 4px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.successIcon {
|
||||
color: rgb(71, 157, 255);
|
||||
}
|
||||
|
||||
.failIcon {
|
||||
color: red;
|
||||
}
|
||||
}
|
||||
|
||||
.resultContent {
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.tableStatus {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.dot {
|
||||
display: inline-block;
|
||||
margin-right: 5px;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
background-color: #ff4d4f;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.successDot {
|
||||
background-color: #52c41a;
|
||||
}
|
||||
}
|
||||
|
||||
.tableBox {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 0;
|
||||
opacity: 0;
|
||||
overflow: auto;
|
||||
background-color: var(--color-bg-100);
|
||||
}
|
||||
|
||||
.cursorTableBox {
|
||||
z-index: 1;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.tableIndex {
|
||||
width: 50px;
|
||||
}
|
||||
|
||||
.tableHoverBox {
|
||||
display: none;
|
||||
align-items: center;
|
||||
position: absolute;
|
||||
background-color: var(--color-bg-200);
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
|
||||
i {
|
||||
font-size: 15px;
|
||||
margin: 0px 2px;
|
||||
}
|
||||
|
||||
i:hover {
|
||||
color: var(--custom-primary-color);
|
||||
}
|
||||
}
|
||||
|
||||
.monacoEditor {
|
||||
height: 300px;
|
||||
}
|
||||
|
||||
.tableItem {
|
||||
width: 100%;
|
||||
.f-lines(1);
|
||||
cursor: pointer;
|
||||
|
||||
&:hover .tableHoverBox {
|
||||
display: flex;
|
||||
}
|
||||
}
|
199
chat2db-client/src/components/SearchResult/index.tsx
Normal file
199
chat2db-client/src/components/SearchResult/index.tsx
Normal file
@ -0,0 +1,199 @@
|
||||
import React, { memo, useEffect, useState, useRef } from 'react';
|
||||
import classnames from 'classnames';
|
||||
import Tabs from '@/components/Tabs';
|
||||
import Iconfont from '@/components/Iconfont';
|
||||
import StateIndicator from '@/components/StateIndicator';
|
||||
import LoadingContent from '@/components/Loading/LoadingContent';
|
||||
import MonacoEditor from '@/components/Console/MonacoEditor';
|
||||
import { Button, DatePicker, Input, Table, Modal, message } from 'antd';
|
||||
import { StatusType, TableDataType } from '@/constants/table';
|
||||
import { formatDate } from '@/utils/date';
|
||||
import { IManageResultData, ITableHeaderItem } from '@/typings/database';
|
||||
import styles from './index.less';
|
||||
|
||||
interface IProps {
|
||||
className?: string;
|
||||
manageResultDataList: IManageResultData[];
|
||||
}
|
||||
|
||||
interface DataType {
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export default memo<IProps>(function SearchResult({ className, manageResultDataList = [] }) {
|
||||
const [isUnfold, setIsUnfold] = useState(true);
|
||||
const [currentTab, setCurrentTab] = useState('0');
|
||||
|
||||
useEffect(() => {
|
||||
setCurrentTab('0');
|
||||
}, [manageResultDataList]);
|
||||
|
||||
const renderStatus = (text: string) => {
|
||||
return (
|
||||
<div className={styles.tableStatus}>
|
||||
<i className={classnames(styles.dot, { [styles.successDot]: text == StatusType.SUCCESS })}></i>
|
||||
{text == StatusType.SUCCESS ? '成功' : '失败'}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
function onChange(index: string) {
|
||||
setCurrentTab(index);
|
||||
}
|
||||
|
||||
const makerResultHeaderList = () => {
|
||||
const list: any = [];
|
||||
manageResultDataList?.map((item, index) => {
|
||||
list.push({
|
||||
label: (
|
||||
<div key={index}>
|
||||
<Iconfont
|
||||
className={classnames(styles[item.success ? 'successIcon' : 'failIcon'], styles.statusIcon)}
|
||||
code={item.success ? '\ue605' : '\ue87c'}
|
||||
/>
|
||||
执行结果{index + 1}
|
||||
</div>
|
||||
),
|
||||
key: index,
|
||||
});
|
||||
});
|
||||
return list;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={classnames(className, styles.box)}>
|
||||
<div className={styles.resultHeader}>
|
||||
<Tabs onChange={onChange} tabs={makerResultHeaderList()} />
|
||||
</div>
|
||||
<div className={styles.resultContent}>
|
||||
<LoadingContent data={manageResultDataList} handleEmpty>
|
||||
{manageResultDataList.map((item, index) => {
|
||||
if (item.success) {
|
||||
return (
|
||||
<TableBox
|
||||
key={index}
|
||||
className={classnames({ [styles.cursorTableBox]: index + '' == currentTab })}
|
||||
data={item}
|
||||
headerList={item.headerList}
|
||||
dataList={item.dataList}
|
||||
></TableBox>
|
||||
);
|
||||
} else {
|
||||
return <StateIndicator key={index} state="error" text={item.message}></StateIndicator>;
|
||||
}
|
||||
})}
|
||||
</LoadingContent>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
interface ITableProps {
|
||||
headerList: ITableHeaderItem[];
|
||||
dataList: string[][];
|
||||
className?: string;
|
||||
data: IManageResultData;
|
||||
}
|
||||
|
||||
interface IViewTableCellData {
|
||||
name: string;
|
||||
value: any;
|
||||
}
|
||||
|
||||
export function TableBox(props: ITableProps) {
|
||||
const { headerList, dataList, className, data, ...rest } = props;
|
||||
const [columns, setColumns] = useState<any>();
|
||||
const [tableData, setTableData] = useState<any>();
|
||||
const [viewTableCellData, setViewTableCellData] = useState<IViewTableCellData | null>(null);
|
||||
|
||||
function viewTableCell(data: IViewTableCellData) {
|
||||
setViewTableCellData(data);
|
||||
}
|
||||
|
||||
function copyTableCell(data: IViewTableCellData) {
|
||||
navigator.clipboard.writeText(data?.value || viewTableCellData?.value);
|
||||
message.success('复制成功');
|
||||
}
|
||||
|
||||
function handleCancel() {
|
||||
setViewTableCellData(null);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!headerList?.length) {
|
||||
return;
|
||||
}
|
||||
const columns: any = headerList?.map((item: any, index) => {
|
||||
const data = {
|
||||
title: item.name,
|
||||
dataIndex: item.name,
|
||||
key: item.name,
|
||||
type: item.dataType,
|
||||
sorter: (a: any, b: any) => a[item.name] - b[item.name],
|
||||
render: (value: any) => (
|
||||
<div className={styles.tableItem}>
|
||||
<div className={styles.tableHoverBox}>
|
||||
<Iconfont code="" onClick={viewTableCell.bind(null, { name: item.name, value })} />
|
||||
<Iconfont code="" onClick={copyTableCell.bind(null, { name: item.name, value })} />
|
||||
</div>
|
||||
{value}
|
||||
</div>
|
||||
),
|
||||
};
|
||||
return data;
|
||||
});
|
||||
setColumns(columns);
|
||||
}, [headerList]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!columns?.length) return;
|
||||
const tableData = dataList?.map((item, rowIndex) => {
|
||||
const rowData: any = {};
|
||||
item.map((i: string | null, index: number) => {
|
||||
const { dataType: type } = headerList[index] || {};
|
||||
// console.log('headerList[rowIndex]', headerList[rowIndex]);
|
||||
if (type === TableDataType.DATETIME && i) {
|
||||
rowData[columns[index].title] = formatDate(i, 'yyyy-MM-dd hh:mm:ss');
|
||||
} else if (i === null) {
|
||||
rowData[columns[index].title] = '[null]';
|
||||
} else {
|
||||
rowData[columns[index].title] = i;
|
||||
}
|
||||
});
|
||||
rowData.key = rowIndex;
|
||||
return rowData;
|
||||
});
|
||||
|
||||
setTableData(tableData);
|
||||
}, [columns]);
|
||||
|
||||
return (
|
||||
<div {...rest} className={classnames(className, styles.tableBox)}>
|
||||
{dataList !== null ? (
|
||||
<Table bordered pagination={false} columns={columns} dataSource={tableData} size="small" />
|
||||
) : (
|
||||
<StateIndicator state="success" text="执行成功"/>
|
||||
)}
|
||||
<Modal
|
||||
title={viewTableCellData?.name}
|
||||
open={!!viewTableCellData?.name}
|
||||
onCancel={handleCancel}
|
||||
width="60vw"
|
||||
maskClosable={false}
|
||||
footer={
|
||||
<>
|
||||
{
|
||||
<Button onClick={copyTableCell.bind(null, viewTableCellData!)} className={styles.cancel}>
|
||||
复制
|
||||
</Button>
|
||||
}
|
||||
</>
|
||||
}
|
||||
>
|
||||
<div className={styles.monacoEditor}>
|
||||
<MonacoEditor value={viewTableCellData?.value} readOnly={true} id="view_table-Cell_data"></MonacoEditor>
|
||||
</div>
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
}
|
50
chat2db-client/src/components/Tabs/index.less
Normal file
50
chat2db-client/src/components/Tabs/index.less
Normal file
@ -0,0 +1,50 @@
|
||||
// @import '../../var.less';
|
||||
|
||||
.box{
|
||||
display: flex;
|
||||
position: relative;
|
||||
.extra{
|
||||
flex: 1;
|
||||
}
|
||||
&::after{
|
||||
position: absolute;
|
||||
content: '';
|
||||
bottom: 1px;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 1px;
|
||||
background-color: var(--color-border);
|
||||
}
|
||||
:global {
|
||||
.custom-tabs{
|
||||
width: 100%;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.custom-tabs-tab {
|
||||
margin: 0px 10px;
|
||||
font-size: 12px;
|
||||
}
|
||||
.custom-tabs-nav{
|
||||
margin: 0 !important;
|
||||
}
|
||||
.custom-tabs-nav::before{
|
||||
border: 0;
|
||||
border-bottom: 0 !important;
|
||||
}
|
||||
.custom-tabs-tab{
|
||||
user-select: none;
|
||||
padding: 5px 10px;
|
||||
margin: 0px 0px 5px 0px;
|
||||
border-radius: 5px;
|
||||
&:hover{
|
||||
color: var(--color-text-85);
|
||||
background-color: var(--color-bg-hover);
|
||||
}
|
||||
}
|
||||
.custom-tabs-tab-active{
|
||||
&:hover{
|
||||
background-color: transparent;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
37
chat2db-client/src/components/Tabs/index.tsx
Normal file
37
chat2db-client/src/components/Tabs/index.tsx
Normal file
@ -0,0 +1,37 @@
|
||||
import React, { memo, ReactNode, useState } from 'react';
|
||||
import styles from './index.less';
|
||||
import classnames from 'classnames';
|
||||
import { Tabs as AntdTabs } from 'antd';
|
||||
|
||||
export interface ITab {
|
||||
label: ReactNode;
|
||||
key: string;
|
||||
}
|
||||
|
||||
interface IProps {
|
||||
className?: string;
|
||||
tabs: ITab[];
|
||||
currentTab?: string;
|
||||
onChange: (key: string, index: number) => void;
|
||||
extra?: React.ReactNode
|
||||
}
|
||||
|
||||
export default memo(function Tabs({ className, tabs, currentTab, onChange, extra }: IProps) {
|
||||
function myChange(key: string) {
|
||||
const index = tabs.findIndex(t => {
|
||||
return t.key === key
|
||||
})
|
||||
onChange(key, index)
|
||||
}
|
||||
|
||||
return <div className={classnames(className, styles.box)}>
|
||||
<AntdTabs
|
||||
defaultActiveKey={currentTab}
|
||||
onChange={myChange}
|
||||
items={tabs}
|
||||
/>
|
||||
<div className={styles.extra}>
|
||||
{extra}
|
||||
</div>
|
||||
</div>
|
||||
})
|
22
chat2db-client/src/constants/table.ts
Normal file
22
chat2db-client/src/constants/table.ts
Normal file
@ -0,0 +1,22 @@
|
||||
export enum TableDataType {
|
||||
BOOLEAN = 'BOOLEAN',
|
||||
NUMERIC = 'NUMERIC',
|
||||
STRING = 'STRING',
|
||||
DATETIME = 'DATETIME',
|
||||
// 暂时不适配
|
||||
BINARY = 'BINARY',
|
||||
CONTENT = 'CONTENT',
|
||||
STRUCT = 'STRUCT',
|
||||
DOCUMENT = 'DOCUMENT',
|
||||
ARRAY = 'ARRAY',
|
||||
OBJECT = 'OBJECT',
|
||||
REFERENCE = 'REFERENCE',
|
||||
ROWID = 'ROWID',
|
||||
ANY = 'ANY',
|
||||
UNKNOWN = 'UNKNOWN',
|
||||
}
|
||||
|
||||
export enum StatusType {
|
||||
SUCCESS = 'success',
|
||||
FAIL = 'fail',
|
||||
}
|
@ -21,9 +21,10 @@ export default {
|
||||
'common.button.save': 'Save',
|
||||
'common.button.execute': 'Run',
|
||||
'common.message.successfulConfig': 'Successful configuration',
|
||||
'common.text.successful':'successful',
|
||||
'common.text.successful': 'successful',
|
||||
'common.text.failure': 'failure',
|
||||
'common.message.modifySuccessfully':'modify successfully',
|
||||
'common.message.addedSuccessfully':'successfully added',
|
||||
'common.text.custom':'custom',
|
||||
}
|
||||
'common.message.modifySuccessfully': 'modify successfully',
|
||||
'common.message.addedSuccessfully': 'successfully added',
|
||||
'common.text.custom': 'custom',
|
||||
'common.button.delete': 'Delete',
|
||||
};
|
@ -11,7 +11,7 @@ export default {
|
||||
'connection.button.testConnection': 'Test',
|
||||
'connection.label.advancedConfiguration': 'Advanced Configuration',
|
||||
'connection.label.sshConfiguration': 'SSH Configuration',
|
||||
'connection.button.addConnection': 'ADD Connection',
|
||||
'connection.button.addConnection': 'Add Connection',
|
||||
'connection.button.connect': 'Connect',
|
||||
'connection.message.testConnectResult': 'Test connection is {1}',
|
||||
'connection.message.testSshConnection': 'Test the ssh connection',
|
@ -26,4 +26,5 @@ export default {
|
||||
'common.message.modifySuccessfully':'修改成功',
|
||||
'common.message.addedSuccessfully': '添加成功',
|
||||
'common.text.custom':'自定义',
|
||||
'common.button.delete':'删除'
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user