mirror of
https://github.com/CodePhiliaX/Chat2DB.git
synced 2025-07-30 11:12:55 +08:00
Merge branch 'developing' into team
This commit is contained in:
10
CHANGELOG.md
10
CHANGELOG.md
@ -1,3 +1,13 @@
|
||||
# 2.0.7
|
||||
## ⭐ New Features
|
||||
|
||||
- Export query result as file is supported
|
||||
|
||||
## 🐞 Bug Fixes
|
||||
|
||||
- Fixed ai config issues [Issue #346](https://github.com/chat2db/Chat2DB/issues/346)
|
||||
|
||||
|
||||
# 2.0.6
|
||||
|
||||
## 🐞 Bug Fixes
|
||||
|
@ -195,15 +195,12 @@ export default function SettingAI(props: IProps) {
|
||||
</div>
|
||||
)}
|
||||
<div className={styles.bottomButton}>
|
||||
{/* <Button >
|
||||
|
||||
</Button> */}
|
||||
<Button type="primary" onClick={handleApplyAiConfig}>
|
||||
{i18n('setting.button.apply')}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{aiConfig?.aiSqlSource === AiSqlSourceType.CHAT2DBAI && !aiConfig.apiKey && <Popularize source="setting" />}
|
||||
{/* {aiConfig?.aiSqlSource === AiSqlSourceType.CHAT2DBAI && !aiConfig.apiKey && <Popularize source="setting" />} */}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
@ -9,12 +9,11 @@ import ProxySetting from './ProxySetting';
|
||||
import About from './About';
|
||||
import { connect } from 'umi';
|
||||
import { IAIState } from '@/models/ai';
|
||||
import styles from './index.less';
|
||||
import configService from '@/service/config';
|
||||
import { AiSqlSourceType } from '@/typings/ai';
|
||||
import TestVersion from '@/components/TestVersion';
|
||||
import { IAiConfig } from '@/typings';
|
||||
|
||||
import styles from './index.less';
|
||||
|
||||
interface IProps {
|
||||
aiConfig: IAiConfig;
|
||||
className?: string;
|
||||
@ -28,6 +27,12 @@ function Setting(props: IProps) {
|
||||
|
||||
const [currentMenu, setCurrentMenu] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
if (isModalVisible) {
|
||||
getAiSystemConfig();
|
||||
}
|
||||
}, [isModalVisible]);
|
||||
|
||||
useEffect(() => {
|
||||
getAiSystemConfig();
|
||||
}, []);
|
||||
|
@ -111,6 +111,7 @@
|
||||
// top: 0;
|
||||
// right: 0;
|
||||
// bottom: 0;
|
||||
cursor: pointer;
|
||||
|
||||
i {
|
||||
font-size: 15px;
|
||||
@ -139,3 +140,7 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.monacoEditor {
|
||||
height: 60vh;
|
||||
}
|
||||
|
@ -24,7 +24,7 @@ interface ITableProps {
|
||||
config: IResultConfig;
|
||||
onConfigChange: (config: IResultConfig) => void;
|
||||
onSearchTotal: () => Promise<number | undefined>;
|
||||
onExport: (exportType: ExportTypeEnum, exportSize: ExportSizeEnum) => void;
|
||||
onExport: (sql: string, originalSql: string, exportType: ExportTypeEnum, exportSize: ExportSizeEnum) => void;
|
||||
}
|
||||
|
||||
interface IViewTableCellData {
|
||||
@ -54,42 +54,46 @@ export default function TableBox(props: ITableProps) {
|
||||
const [appTheme] = useTheme();
|
||||
const isDarkTheme = useMemo(() => appTheme.backgroundColor === ThemeType.Dark, [appTheme]);
|
||||
|
||||
const handleExport = (exportType: ExportTypeEnum, exportSize: ExportSizeEnum) => {
|
||||
props.onExport && props.onExport(data.sql, data.originalSql, exportType, exportSize);
|
||||
};
|
||||
|
||||
const items: MenuProps['items'] = useMemo(
|
||||
() => [
|
||||
{
|
||||
label: '导出全部数据为csv',
|
||||
key: '1',
|
||||
icon: <UserOutlined />,
|
||||
// icon: <UserOutlined />,
|
||||
onClick: () => {
|
||||
props.onExport && props.onExport(ExportTypeEnum.CSV, ExportSizeEnum.ALL);
|
||||
handleExport(ExportTypeEnum.CSV, ExportSizeEnum.ALL);
|
||||
},
|
||||
},
|
||||
{
|
||||
label: '导出全部数据为插入语句',
|
||||
key: '2',
|
||||
icon: <UserOutlined />,
|
||||
// icon: <UserOutlined />,
|
||||
onClick: () => {
|
||||
props.onExport && props.onExport(ExportTypeEnum.INSERT, ExportSizeEnum.ALL);
|
||||
handleExport(ExportTypeEnum.INSERT, ExportSizeEnum.ALL);
|
||||
},
|
||||
},
|
||||
{
|
||||
label: '导出当前页数据为csv',
|
||||
key: '3',
|
||||
icon: <UserOutlined />,
|
||||
// icon: <UserOutlined />,
|
||||
onClick: () => {
|
||||
props.onExport && props.onExport(ExportTypeEnum.CSV, ExportSizeEnum.CURRENT_PAGE);
|
||||
handleExport(ExportTypeEnum.CSV, ExportSizeEnum.CURRENT_PAGE);
|
||||
},
|
||||
},
|
||||
{
|
||||
label: '导出当前页数据为插入语句',
|
||||
key: '4',
|
||||
icon: <UserOutlined />,
|
||||
// icon: <UserOutlined />,
|
||||
onClick: () => {
|
||||
props.onExport && props.onExport(ExportTypeEnum.INSERT, ExportSizeEnum.CURRENT_PAGE);
|
||||
handleExport(ExportTypeEnum.INSERT, ExportSizeEnum.CURRENT_PAGE);
|
||||
},
|
||||
},
|
||||
],
|
||||
[],
|
||||
[data],
|
||||
);
|
||||
|
||||
const defaultSorts: SortItem[] = useMemo(
|
||||
@ -260,6 +264,7 @@ export default function TableBox(props: ITableProps) {
|
||||
open={!!viewTableCellData?.name}
|
||||
onCancel={handleCancel}
|
||||
width="60vw"
|
||||
height="70vh"
|
||||
maskClosable={false}
|
||||
footer={
|
||||
<>
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React, { memo, useEffect, useState, useMemo, Fragment } from 'react';
|
||||
import React, { memo, useEffect, useState, useMemo, Fragment, useCallback } from 'react';
|
||||
import classnames from 'classnames';
|
||||
import Tabs, { IOption } from '@/components/Tabs';
|
||||
import Iconfont from '@/components/Iconfont';
|
||||
@ -16,7 +16,7 @@ interface IProps {
|
||||
manageResultDataList?: IManageResultData[];
|
||||
resultConfig: IResultConfig[];
|
||||
onExecute: (sql: string, config: IResultConfig, index: number) => void;
|
||||
onExport: (originalSql: string, exportType: ExportTypeEnum, exportSize: ExportSizeEnum) => Promise<void>;
|
||||
onExport: (sql: string, originalSql: string, exportType: ExportTypeEnum, exportSize: ExportSizeEnum) => Promise<void>;
|
||||
onTabEdit: (type: 'add' | 'remove', value?: number | string) => void;
|
||||
onSearchTotal: (index: number) => Promise<number>;
|
||||
isLoading?: boolean;
|
||||
@ -45,7 +45,7 @@ const handleTabs = (result: IManageResultData[]) => {
|
||||
};
|
||||
|
||||
export default memo<IProps>(function SearchResult(props) {
|
||||
const { className, manageResultDataList = [], isLoading, onExecute, onSearchTotal } = props;
|
||||
const { className, manageResultDataList = [], isLoading, onExecute, onExport } = props;
|
||||
const [currentTab, setCurrentTab] = useState<string | number | undefined>();
|
||||
const [resultDataList, setResultDataList] = useState<IManageResultData[]>([]);
|
||||
const [resultConfig, setResultConfig] = useState<IResultConfig[]>([]);
|
||||
@ -64,7 +64,7 @@ export default memo<IProps>(function SearchResult(props) {
|
||||
setCurrentTab(manageResultDataList[0].uuid);
|
||||
}
|
||||
|
||||
setResultDataList(manageResultDataList);
|
||||
setResultDataList([...manageResultDataList]);
|
||||
setTabs(handleTabs(manageResultDataList));
|
||||
}, [manageResultDataList]);
|
||||
|
||||
@ -85,48 +85,50 @@ export default memo<IProps>(function SearchResult(props) {
|
||||
);
|
||||
};
|
||||
|
||||
const renderTable = useMemo(() => {
|
||||
const renderTable = () => {
|
||||
if (!tabs || !tabs.length) {
|
||||
return renderEmpty();
|
||||
}
|
||||
if (!resultDataList || !resultDataList.length) {
|
||||
return renderEmpty();
|
||||
}
|
||||
return (resultDataList || []).map((item, index: number) => {
|
||||
if (item.success) {
|
||||
return (
|
||||
<Fragment key={item.uuid!}>
|
||||
<TableBox
|
||||
className={classnames({ [styles.cursorTableBox]: item.uuid === currentTab })}
|
||||
data={item}
|
||||
config={resultConfig?.[index]}
|
||||
onConfigChange={function (config: IResultConfig) {
|
||||
onExecute && onExecute(item.originalSql, config, index);
|
||||
}}
|
||||
onSearchTotal={async () => {
|
||||
if (props.onSearchTotal) {
|
||||
return await props.onSearchTotal(index);
|
||||
}
|
||||
}}
|
||||
onExport={(exportType: ExportTypeEnum, exportSize: ExportSizeEnum) => {
|
||||
props.onExport && props.onExport(item.originalSql, exportType, exportSize);
|
||||
}}
|
||||
/>
|
||||
</Fragment>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<Fragment key={item.uuid}>
|
||||
<StateIndicator
|
||||
className={classnames(styles.stateIndicator, { [styles.cursorStateIndicator]: item.uuid === currentTab })}
|
||||
state="error"
|
||||
text={item.message}
|
||||
/>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
});
|
||||
}, [currentTab, resultDataList, resultConfig]);
|
||||
|
||||
return renderTableContent;
|
||||
};
|
||||
|
||||
const renderTableContent = (resultDataList || []).map((item, index: number) => {
|
||||
console.log(item.sql, item.originalSql);
|
||||
if (item.success) {
|
||||
return (
|
||||
<TableBox
|
||||
key={item.uuid!}
|
||||
className={classnames({ [styles.cursorTableBox]: item.uuid === currentTab })}
|
||||
data={item}
|
||||
config={resultConfig?.[index]}
|
||||
onConfigChange={(config: IResultConfig) => {
|
||||
onExecute && onExecute(item.originalSql, config, index);
|
||||
}}
|
||||
onSearchTotal={async () => {
|
||||
if (props.onSearchTotal) {
|
||||
return await props.onSearchTotal(index);
|
||||
}
|
||||
}}
|
||||
onExport={(sql: string, originalSql: string, exportType: ExportTypeEnum, exportSize: ExportSizeEnum) => {
|
||||
onExport && onExport(sql, originalSql, exportType, exportSize);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<StateIndicator
|
||||
key={item.uuid}
|
||||
className={classnames(styles.stateIndicator, { [styles.cursorStateIndicator]: item.uuid === currentTab })}
|
||||
state="error"
|
||||
text={item.message}
|
||||
/>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<div className={classnames(className, styles.box)}>
|
||||
@ -144,7 +146,7 @@ export default memo<IProps>(function SearchResult(props) {
|
||||
</div>
|
||||
) : null}
|
||||
<Spin spinning={isLoading} wrapperClassName={styles.resultContentWrapper}>
|
||||
{renderTable}
|
||||
{renderTable()}
|
||||
</Spin>
|
||||
</div>
|
||||
);
|
||||
|
@ -15,6 +15,7 @@ import { v4 as uuidV4 } from 'uuid';
|
||||
import sql from '@/service/sql';
|
||||
import { isNumber } from 'lodash';
|
||||
import { ExportSizeEnum, ExportTypeEnum } from '@/typings/resultTable';
|
||||
import { downloadFile } from '@/utils/common';
|
||||
interface IProps {
|
||||
className?: string;
|
||||
isActive: boolean;
|
||||
@ -139,10 +140,14 @@ const WorkspaceRightItem = memo<IProps>(function (props) {
|
||||
return total;
|
||||
};
|
||||
|
||||
const handleExportSQLResult = async (originalSql: string, exportType: ExportTypeEnum, exportSize: ExportSizeEnum) => {
|
||||
const params: IExportParams = { ...data, originalSql, exportType, exportSize };
|
||||
|
||||
await sqlServer.exportResultTable(params);
|
||||
const handleExportSQLResult = async (
|
||||
sql: string,
|
||||
originalSql: string,
|
||||
exportType: ExportTypeEnum,
|
||||
exportSize: ExportSizeEnum,
|
||||
) => {
|
||||
const params: IExportParams = { ...data, sql, originalSql, exportType, exportSize };
|
||||
downloadFile(window._BaseURL + '/api/rdb/dml/export', params);
|
||||
};
|
||||
|
||||
const handleResultTabEdit = (type: 'add' | 'remove', uuid?: string | number) => {
|
||||
|
@ -123,7 +123,7 @@ export interface IExportParams extends IExecuteSqlParams {
|
||||
/**
|
||||
* 导出-表格
|
||||
*/
|
||||
const exportResultTable = createRequest<IExportParams, any>('/api/rdb/dml/export', { method: 'post' });
|
||||
// const exportResultTable = createRequest<IExportParams, any>('/api/rdb/dml/export', { method: 'post' });
|
||||
|
||||
export default {
|
||||
getList,
|
||||
@ -142,5 +142,5 @@ export default {
|
||||
addTablePin,
|
||||
deleteTablePin,
|
||||
getDMLCount,
|
||||
exportResultTable
|
||||
// exportResultTable
|
||||
};
|
||||
|
@ -14,3 +14,45 @@ export function formatParams(obj: { [key: string]: any }) {
|
||||
});
|
||||
return params.toString();
|
||||
}
|
||||
|
||||
export function downloadFile(url: string, params: any) {
|
||||
// 创建POST请求
|
||||
fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json', // 或者根据服务端的要求设置其他的内容类型
|
||||
},
|
||||
body: JSON.stringify(params), // 将参数转换为JSON字符串
|
||||
})
|
||||
.then((response) => {
|
||||
// 从content-disposition头中获取文件名
|
||||
const contentDisposition = response.headers.get('content-disposition');
|
||||
const filename = contentDisposition ? decodeURIComponent(contentDisposition.split("''")[1]) : 'file.txt';
|
||||
|
||||
// 获取返回的Blob数据
|
||||
return response.blob().then((blob) => ({ blob, filename }));
|
||||
})
|
||||
.then(({ blob, filename }) => {
|
||||
// 创建一个代表Blob对象的URL
|
||||
const blobUrl = URL.createObjectURL(blob);
|
||||
|
||||
// 创建一个隐藏的 <a> 标签,并设置其 href 属性
|
||||
const a = document.createElement('a');
|
||||
a.style.display = 'none';
|
||||
a.href = blobUrl;
|
||||
|
||||
// 使用从响应头解析的文件名
|
||||
a.download = filename;
|
||||
|
||||
// 将 <a> 标签附加到 DOM,并触发点击事件
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
|
||||
// 清理:从 DOM 中移除 <a> 标签,并释放Blob URL
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(blobUrl);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('下载文件失败:', error);
|
||||
});
|
||||
}
|
||||
|
@ -4,7 +4,6 @@ import ai.chat2db.server.tools.common.model.ConfigJson;
|
||||
import ai.chat2db.server.tools.common.util.ConfigUtils;
|
||||
import com.dtflys.forest.springboot.annotation.ForestScan;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.ArrayUtils;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.mybatis.spring.annotation.MapperScan;
|
||||
import org.springframework.boot.SpringApplication;
|
||||
@ -37,7 +36,7 @@ public class Application {
|
||||
// Represents that the current version has been successfully launched
|
||||
if (StringUtils.isNotBlank(currentVersion) && StringUtils.equals(currentVersion, configJson.getLatestStartupSuccessVersion())) {
|
||||
// Flyway doesn't need to start every time to increase startup speed
|
||||
args = ArrayUtils.add(args, "--spring.flyway.enabled=false");
|
||||
//args = ArrayUtils.add(args, "--spring.flyway.enabled=false");
|
||||
log.info("The current version {} has been successfully launched once and will no longer load Flyway.",
|
||||
currentVersion);
|
||||
}
|
||||
|
@ -37,13 +37,13 @@ public class MybatisGeneratorTest extends BaseTest {
|
||||
|
||||
private void doGenerator(List<String> tableList) {
|
||||
|
||||
// 当前项目地址 拿到的是ali-dbhub-server-start地址
|
||||
// 当前项目地址 拿到的是chat2db-server-start地址
|
||||
String outputDir = System.getProperty("user.dir")
|
||||
+ "/../ali-dbhub-server-domain/ali-dbhub-server-domain-repository/src/main"
|
||||
+ "/../chat2db-server-domain/chat2db-server-domain-repository/src/main"
|
||||
+ "/java";
|
||||
String xmlDir = System.getProperty("user.dir")
|
||||
+ "/../ali-dbhub-server-domain/ali-dbhub-server-domain-repository/src/main"
|
||||
+ "/resources/com/alibaba/dbhub/server/domain/repository";
|
||||
+ "/../chat2db-server-domain/chat2db-server-domain-repository/src/main"
|
||||
+ "/resources/ai/chat2db/server/domain/repository";
|
||||
|
||||
// 不要生成service controller
|
||||
Map<OutputFile, String> pathInfo = new HashMap<>();
|
||||
@ -58,7 +58,7 @@ public class MybatisGeneratorTest extends BaseTest {
|
||||
//全局配置
|
||||
.globalConfig(builder -> {
|
||||
// 设置作者
|
||||
builder.author("ali-dbhub")
|
||||
builder.author("chat2db")
|
||||
//执行完毕不打开文件夹
|
||||
.disableOpenDir()
|
||||
// 指定输出目录
|
||||
|
Reference in New Issue
Block a user