🐛 fix(sql-editor): 修复结果消息展示与数据目录迁移稳定性

This commit is contained in:
Syngnat
2026-06-04 07:09:42 +08:00
parent 23ac30086f
commit f5166ac3fc
21 changed files with 1608 additions and 153 deletions

View File

@@ -19,14 +19,20 @@ describe('ConnectionModal edit password behavior', () => {
describe('ConnectionModal data source registry', () => {
it('exposes Elasticsearch in the create-connection picker with HTTP defaults', () => {
expect(source).toContain('case "elasticsearch":\n return 9200;');
expect(source).toContain('case "elasticsearch":');
expect(source).toContain('return 9200;');
expect(source).toContain('elasticsearch: ["http", "https"]');
expect(source).toContain('key: "elasticsearch"');
expect(source).toContain('name: "Elasticsearch"');
expect(source).toContain('getDbIcon("elasticsearch", undefined, 36)');
expect(source).toContain('type === "elasticsearch"');
expect(source).toContain('"http://elastic:pass@127.0.0.1:9200/logs-*"');
expect(source).toContain('label="默认索引(可选)"');
expect(source).toContain('"显示索引 (留空显示全部)"');
expect(source).toContain('return "支持索引浏览、Mapping 检查、JSON DSL 和 query_string 查询";');
expect(source).toContain(
'type === "clickhouse" ? "default" : (type === "redis" || type === "elasticsearch") ? "" : "root";',
);
expect(source).toContain(
'placeholder={dbType === "elasticsearch" ? "未开启认证可留空" : undefined}',
);
expect(source).toContain('label="显示数据库 (留空显示全部)"');
});
});

View File

@@ -234,9 +234,31 @@ vi.mock('antd', () => {
</section>
) : null
);
Modal.useModal = () => [{ info: vi.fn(() => ({ destroy: vi.fn() })) }, null];
Modal.useModal = () => {
const [infoConfig, setInfoConfig] = React.useState<any>(null);
return [{
info: vi.fn((config: any) => {
setInfoConfig(config);
return {
destroy: vi.fn(() => {
setInfoConfig(null);
}),
};
}),
}, infoConfig ? <section data-modal-use-holder="true">{infoConfig.content}</section> : null];
};
const passthrough = ({ children }: any) => <>{children}</>;
const Dropdown = ({ children, menu, disabled }: any) => (
<>
{children}
{!disabled && menu?.items?.map((item: any) => (
item?.type === 'divider'
? null
: <button key={item.key} type="button" disabled={item.disabled} onClick={item.onClick}>{item.label}</button>
))}
</>
);
const Space = ({ children }: any) => <div>{children}</div>;
const Tabs = ({ items = [], activeKey, onChange }: any) => {
const resolvedActiveKey = activeKey ?? items[0]?.key;
@@ -289,7 +311,7 @@ vi.mock('antd', () => {
message: messageApi,
Input,
Button,
Dropdown: passthrough,
Dropdown,
Form,
Pagination: () => null,
Select: ({ children }: any) => <div>{children}</div>,
@@ -806,6 +828,49 @@ describe('DataGrid DDL interactions', () => {
renderer!.unmount();
});
it('exports query-result rows from in-memory data without rerunning ExportQuery', async () => {
backendApp.ExportData.mockResolvedValue({ success: true });
backendApp.ExportQuery.mockResolvedValue({ success: true });
let renderer: ReactTestRenderer;
await act(async () => {
renderer = create(
<DataGrid
data={[
{ __gonavi_row_key__: 'row-1', owner: 'sa' },
{ __gonavi_row_key__: 'row-2', owner: 'dbo' },
]}
columnNames={['owner']}
loading={false}
exportScope="queryResult"
resultSql="EXEC sp_helpdb"
dbName="master"
connectionId="conn-1"
/>,
);
});
await waitForEffects();
await act(async () => {
await findButton(renderer!, 'HTML').props.onClick();
});
const exportAllButton = findButton(renderer!, '全部导出');
await act(async () => {
await exportAllButton.props.onClick();
});
await waitForEffects();
expect(backendApp.ExportData).toHaveBeenCalledTimes(1);
expect(backendApp.ExportData).toHaveBeenCalledWith(
[{ owner: 'sa' }, { owner: 'dbo' }],
['owner'],
'export',
'html',
);
expect(backendApp.ExportQuery).not.toHaveBeenCalled();
});
it('copies loaded column data from the v2 column header context menu', async () => {
storeState.appearance.uiVersion = 'v2';

View File

@@ -13,7 +13,9 @@ describe('DatabaseIcons', () => {
it('includes Elasticsearch in the selectable database icons', () => {
expect(DB_ICON_TYPES).toContain('elasticsearch');
expect(getDbIconLabel('elasticsearch')).toBe('Elasticsearch');
expect(renderToStaticMarkup(<>{getDbIcon('elasticsearch', undefined, 22)}</>)).toContain('ES');
const markup = renderToStaticMarkup(<>{getDbIcon('elasticsearch', undefined, 22)}</>);
expect(markup).toContain('elasticsearch.svg');
expect(markup).toContain('alt="elasticsearch"');
});
it('wraps database icons in a consistent frame for sidebar sizing', () => {

View File

@@ -28,6 +28,13 @@ const baseState = {
],
jvmDiagnosticDrafts: {},
jvmDiagnosticOutputs: {},
fontSize: 14,
appearance: {
uiVersion: "legacy",
dataTableFontSize: 14,
dataTableFontSizeFollowGlobal: true,
customMonoFontFamily: "",
},
setJVMDiagnosticDraft: vi.fn(),
appendJVMDiagnosticOutput: vi.fn(),
clearJVMDiagnosticOutput: vi.fn(),
@@ -62,6 +69,7 @@ const mockMonaco = {
KeyMod: { CtrlCmd: 2048 },
KeyCode: { Enter: 3 },
editor: {
defineTheme: vi.fn(),
setTheme: vi.fn(),
},
languages: {
@@ -193,6 +201,7 @@ describe("JVMDiagnosticConsole", () => {
removeEventListener: vi.fn(),
};
mockMonaco.editor.setTheme.mockClear();
mockMonaco.editor.defineTheme.mockClear();
mockMonaco.languages.register.mockClear();
mockMonaco.languages.registerCompletionItemProvider.mockClear();
mockEditor.addCommand.mockClear();

View File

@@ -29,6 +29,13 @@ const storeState = vi.hoisted(() => ({
aiPanelVisible: false,
setAIPanelVisible: vi.fn(),
theme: "light",
fontSize: 14,
appearance: {
uiVersion: "legacy",
dataTableFontSize: 14,
dataTableFontSizeFollowGlobal: true,
customMonoFontFamily: "",
},
}));
const backendApp = vi.hoisted(() => ({

View File

@@ -80,6 +80,10 @@ const dataGridState = vi.hoisted(() => ({
latestProps: null as any,
}));
const tabsState = vi.hoisted(() => ({
activeKey: undefined as string | undefined,
}));
const autoFetchState = vi.hoisted(() => ({
visible: false,
}));
@@ -345,11 +349,24 @@ vi.mock('antd', () => {
),
Tooltip: ({ children }: any) => <>{children}</>,
Select: () => null,
Tabs: ({ activeKey, items }: any) => {
const activeItem = items?.find((item: any) => item.key === activeKey) || items?.[0];
Tabs: ({ activeKey, items, onChange }: any) => {
const resolvedActiveKey = tabsState.activeKey ?? activeKey ?? items?.[0]?.key;
const activeItem = items?.find((item: any) => item.key === resolvedActiveKey) || items?.[0];
return (
<div>
<div>{items?.map((item: any) => <span key={item.key}>{item.label}</span>)}</div>
<div>{items?.map((item: any) => (
<button
key={item.key}
type="button"
data-tab-key={item.key}
onClick={() => {
tabsState.activeKey = item.key;
onChange?.(item.key);
}}
>
{item.label}
</button>
))}</div>
<div>{activeItem?.children}</div>
</div>
);
@@ -423,6 +440,7 @@ describe('QueryEditor external SQL save', () => {
storeState.appearance.uiVersion = 'legacy';
autoFetchState.visible = false;
dataGridState.latestProps = null;
tabsState.activeKey = undefined;
editorState.value = '';
editorState.position = { lineNumber: 1, column: 1 };
editorState.selection = null;
@@ -1788,6 +1806,198 @@ describe('QueryEditor external SQL save', () => {
renderer?.unmount();
});
it('renders result grid for sqlserver exec statements that return rows', async () => {
storeState.connections[0].config.type = 'sqlserver';
storeState.connections[0].config.database = 'master';
backendApp.DBQueryMulti.mockResolvedValueOnce({
success: true,
data: [{ columns: ['SPID', 'STATUS'], rows: [{ SPID: 52, STATUS: 'RUNNABLE' }] }],
});
let renderer!: ReactTestRenderer;
await act(async () => {
renderer = create(<QueryEditor tab={createTab({ dbName: 'master', query: 'EXEC sp_who2' })} />);
});
await act(async () => {
await findButton(renderer!, '运行').props.onClick();
});
await act(async () => {
await Promise.resolve();
await Promise.resolve();
});
expect(textContent(renderer!.toJSON())).toContain('结果 1');
expect(textContent(renderer!.toJSON())).not.toContain('影响行数:');
expect(dataGridState.latestProps?.columnNames).toEqual(['SPID', 'STATUS']);
expect(Array.isArray(dataGridState.latestProps?.data)).toBe(true);
expect(dataGridState.latestProps?.data?.[0]).toMatchObject({ SPID: 52, STATUS: 'RUNNABLE' });
});
it('renders standalone message result for sqlserver statistics statements', async () => {
storeState.connections[0].config.type = 'sqlserver';
storeState.connections[0].config.database = 'master';
backendApp.DBQueryMulti.mockResolvedValueOnce({
success: true,
data: [{
columns: [],
rows: [],
messages: ["Table 'users'. Scan count 1, logical reads 3."],
}],
});
let renderer!: ReactTestRenderer;
await act(async () => {
renderer = create(<QueryEditor tab={createTab({ dbName: 'master', query: 'SET STATISTICS IO ON;' })} />);
});
await act(async () => {
await findButton(renderer!, '运行').props.onClick();
});
await act(async () => {
await Promise.resolve();
await Promise.resolve();
});
expect(textContent(renderer!.toJSON())).toContain('消息 1');
expect(textContent(renderer!.toJSON())).toContain("Table 'users'. Scan count 1, logical reads 3.");
expect(dataGridState.latestProps?.columnNames).not.toEqual([]);
});
it('keeps multiple result sets from a single sqlserver statement', async () => {
storeState.connections[0].config.type = 'sqlserver';
storeState.connections[0].config.database = 'master';
backendApp.DBQueryMulti.mockResolvedValueOnce({
success: true,
data: [
{ statementIndex: 1, columns: ['name'], rows: [{ name: 'master' }] },
{ statementIndex: 1, columns: ['owner'], rows: [{ owner: 'sa' }] },
],
});
let renderer!: ReactTestRenderer;
await act(async () => {
renderer = create(<QueryEditor tab={createTab({ dbName: 'master', query: 'EXEC sp_helpdb' })} />);
});
await act(async () => {
await findButton(renderer!, '运行').props.onClick();
});
await act(async () => {
await Promise.resolve();
await Promise.resolve();
});
expect(textContent(renderer!.toJSON())).toContain('结果 1');
expect(textContent(renderer!.toJSON())).toContain('结果 2');
expect(dataGridState.latestProps?.columnNames).toEqual(['name']);
});
it('keeps both tabs when rerunning the same single sqlserver statement with multiple result sets', async () => {
storeState.connections[0].config.type = 'sqlserver';
storeState.connections[0].config.database = 'master';
backendApp.DBQueryMulti
.mockResolvedValueOnce({
success: true,
data: [
{ statementIndex: 1, columns: ['name'], rows: [{ name: 'master' }] },
{ statementIndex: 1, columns: ['owner'], rows: [{ owner: 'sa' }] },
],
})
.mockResolvedValueOnce({
success: true,
data: [
{ statementIndex: 1, columns: ['name'], rows: [{ name: 'tempdb' }] },
{ statementIndex: 1, columns: ['owner'], rows: [{ owner: 'dbo' }] },
],
});
let renderer!: ReactTestRenderer;
await act(async () => {
renderer = create(<QueryEditor tab={createTab({ dbName: 'master', query: 'EXEC sp_helpdb' })} />);
});
await act(async () => {
await findButton(renderer!, '运行').props.onClick();
});
await act(async () => {
await Promise.resolve();
await Promise.resolve();
});
await act(async () => {
await findButton(renderer!, '运行').props.onClick();
});
await act(async () => {
await Promise.resolve();
await Promise.resolve();
});
const tabLabels = renderer!.root.findAll((node) => {
const className = String(node.props?.className || '');
return className.includes('query-result-tab-label');
});
expect(tabLabels).toHaveLength(2);
expect(dataGridState.latestProps?.columnNames).toEqual(['name']);
expect(dataGridState.latestProps?.data?.[0]).toMatchObject({ name: 'tempdb' });
});
it('reloads the active secondary result set for a single sqlserver statement', async () => {
storeState.connections[0].config.type = 'sqlserver';
storeState.connections[0].config.database = 'master';
backendApp.DBQueryMulti
.mockResolvedValueOnce({
success: true,
data: [
{ statementIndex: 1, columns: ['name'], rows: [{ name: 'master' }] },
{ statementIndex: 1, columns: ['owner'], rows: [{ owner: 'sa' }] },
],
})
.mockResolvedValueOnce({
success: true,
data: [
{ statementIndex: 1, columns: ['name'], rows: [{ name: 'master' }] },
{ statementIndex: 1, columns: ['owner'], rows: [{ owner: 'dbo' }] },
],
});
let renderer!: ReactTestRenderer;
await act(async () => {
renderer = create(<QueryEditor tab={createTab({ dbName: 'master', query: 'EXEC sp_helpdb' })} />);
});
await act(async () => {
await findButton(renderer!, '运行').props.onClick();
});
await act(async () => {
await Promise.resolve();
await Promise.resolve();
});
const resultTabButtons = renderer!.root.findAll((node) => node.type === 'button' && node.props['data-tab-key']);
expect(resultTabButtons).toHaveLength(2);
await act(async () => {
resultTabButtons[1].props.onClick();
});
expect(dataGridState.latestProps?.columnNames).toEqual(['owner']);
expect(dataGridState.latestProps?.data?.[0]).toMatchObject({ owner: 'sa' });
await act(async () => {
await dataGridState.latestProps?.onReload?.();
});
await act(async () => {
await Promise.resolve();
await Promise.resolve();
});
expect(backendApp.DBQueryMulti).toHaveBeenCalledTimes(2);
expect(dataGridState.latestProps?.columnNames).toEqual(['owner']);
expect(dataGridState.latestProps?.data?.[0]).toMatchObject({ owner: 'dbo' });
expect(dataGridState.latestProps?.data).not.toEqual(expect.arrayContaining([expect.objectContaining({ name: 'master' })]));
});
it('keeps non-Oracle query results read-only when no safe locator exists', async () => {
backendApp.DBQueryMulti.mockResolvedValueOnce({
success: true,

View File

@@ -1833,8 +1833,12 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
key: string;
sql: string;
exportSql?: string;
sourceStatementIndex?: number;
statementResultIndex?: number;
rows: any[];
columns: string[];
messages?: string[];
resultType?: 'grid' | 'message';
tableName?: string;
pkColumns: string[];
editLocator?: EditRowLocator;
@@ -3924,6 +3928,13 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
return selected;
};
const buildResultSetMergeKey = (result: ResultSet): string => {
const sqlKey = normalizeExecutedSqlKey(result.exportSql || result.sql);
const sourceStatementIndex = Number(result.sourceStatementIndex || 1);
const statementResultIndex = Number(result.statementResultIndex || 1);
return `${sqlKey}::${sourceStatementIndex}::${statementResultIndex}`;
};
const mergeResultSets = (previous: ResultSet[], next: ResultSet[], replaceAll: boolean): ResultSet[] => {
if (replaceAll || previous.length === 0) {
return next.map((result, index) => ({ ...result, key: `result-${index + 1}` }));
@@ -3931,8 +3942,8 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
const merged = [...previous];
next.forEach((result) => {
const incomingKey = normalizeExecutedSqlKey(result.exportSql || result.sql);
const existingIndex = merged.findIndex((item) => normalizeExecutedSqlKey(item.exportSql || item.sql) === incomingKey);
const incomingKey = buildResultSetMergeKey(result);
const existingIndex = merged.findIndex((item) => buildResultSetMergeKey(item) === incomingKey);
if (existingIndex >= 0) {
merged[existingIndex] = { ...result, key: merged[existingIndex].key };
return;
@@ -3947,8 +3958,8 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
if (!firstExecutedResult) {
return '';
}
const executedSqlKey = normalizeExecutedSqlKey(firstExecutedResult.exportSql || firstExecutedResult.sql);
return merged.find((item) => normalizeExecutedSqlKey(item.exportSql || item.sql) === executedSqlKey)?.key
const executedSqlKey = buildResultSetMergeKey(firstExecutedResult);
return merged.find((item) => buildResultSetMergeKey(item) === executedSqlKey)?.key
|| firstExecutedResult.key
|| merged[0]?.key
|| '';
@@ -4024,6 +4035,8 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
if (!sql?.trim() || !currentDb) return;
const conn = connections.find(c => c.id === currentConnectionId);
if (!conn) return;
const currentResult = resultSets.find((item) => item.key === resultKey);
const statementResultIndex = Math.max(1, Number(currentResult?.statementResultIndex || 1));
const config = {
...conn.config,
@@ -4049,10 +4062,9 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
return;
}
// 取第一个结果集(单条 SQL 只有一个结果集)
const resultSetDataArray = Array.isArray(res.data) ? (res.data as any[]) : [];
if (resultSetDataArray.length === 0) return;
const rsData = resultSetDataArray[0];
const rsData = resultSetDataArray[Math.max(0, statementResultIndex - 1)];
if (!rsData) return;
const isAffectedResult = Array.isArray(rsData.rows) && rsData.rows.length === 1
&& rsData.columns && rsData.columns.length === 1
&& rsData.columns[0] === 'affectedRows';
@@ -4075,7 +4087,16 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
// 只更新匹配的结果集的 rows 和 columns保留 tableName/pkColumns/readOnly 等元数据
setResultSets(prev => prev.map(rs =>
rs.key === resultKey
? { ...rs, rows, columns: cols, truncated }
? {
...rs,
rows,
columns: cols,
messages: Array.isArray(rsData.messages) ? rsData.messages : [],
resultType: ((!Array.isArray(rsData.rows) || rsData.rows.length === 0) && (!Array.isArray(rsData.columns) || rsData.columns.length === 0) && Array.isArray(rsData.messages) && rsData.messages.length > 0)
? 'message'
: 'grid',
truncated,
}
: rs
));
} catch (err: any) {
@@ -4240,12 +4261,29 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
key: `result-${idx + 1}`,
sql: rawStatement,
exportSql: rawStatement,
sourceStatementIndex: idx + 1,
statementResultIndex: 1,
rows,
columns: cols,
messages: Array.isArray(res.messages) ? res.messages : [],
pkColumns: [],
readOnly: true,
truncated
});
} else if (Array.isArray(res.messages) && res.messages.length > 0) {
nextResultSets.push({
key: `result-${idx + 1}`,
sql: rawStatement,
exportSql: rawStatement,
sourceStatementIndex: idx + 1,
statementResultIndex: 1,
rows: [],
columns: [],
messages: res.messages,
resultType: 'message',
pkColumns: [],
readOnly: true,
});
} else {
const affected = Number((res.data as any)?.affectedRows);
if (Number.isFinite(affected)) {
@@ -4255,8 +4293,11 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
key: `result-${idx + 1}`,
sql: rawStatement,
exportSql: rawStatement,
sourceStatementIndex: idx + 1,
statementResultIndex: 1,
rows: [row],
columns: ['affectedRows'],
messages: Array.isArray(res.messages) ? res.messages : [],
pkColumns: [],
readOnly: true
});
@@ -4373,12 +4414,17 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
const nextResultSets: ResultSet[] = [];
const maxRows = Number(queryOptions?.maxRows) || 0;
let anyTruncated = false;
const statementResultCounts = new Map<number, number>();
for (let idx = 0; idx < resultSetDataArray.length; idx++) {
const rsData = resultSetDataArray[idx];
const plan = executablePlans[idx];
const sourceStatementIndex = Number(rsData?.statementIndex || idx + 1);
const statementResultIndex = (statementResultCounts.get(sourceStatementIndex) || 0) + 1;
statementResultCounts.set(sourceStatementIndex, statementResultIndex);
const plan = executablePlans[Math.max(0, sourceStatementIndex - 1)];
const originalSql = plan?.originalSql || '';
const executedSql = plan?.executedSql || originalSql;
const resultMessages = Array.isArray(rsData?.messages) ? rsData.messages : [];
// 检查是否为 affectedRows 类结果集
const isAffectedResult = Array.isArray(rsData.rows) && rsData.rows.length === 1
@@ -4393,11 +4439,28 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
key: `result-${idx + 1}`,
sql: executedSql,
exportSql: originalSql,
sourceStatementIndex,
statementResultIndex,
rows: [row],
columns: ['affectedRows'],
messages: resultMessages,
pkColumns: [],
readOnly: true
});
} else if ((!Array.isArray(rsData.rows) || rsData.rows.length === 0) && (!Array.isArray(rsData.columns) || rsData.columns.length === 0) && resultMessages.length > 0) {
nextResultSets.push({
key: `result-${idx + 1}`,
sql: executedSql,
exportSql: originalSql,
sourceStatementIndex,
statementResultIndex,
rows: [],
columns: [],
messages: resultMessages,
resultType: 'message',
pkColumns: [],
readOnly: true,
});
} else {
let rows = Array.isArray(rsData.rows) ? rsData.rows : [];
let truncated = false;
@@ -4421,8 +4484,11 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
key: `result-${idx + 1}`,
sql: executedSql,
exportSql: originalSql,
sourceStatementIndex,
statementResultIndex,
rows,
columns: cols,
messages: resultMessages,
tableName: tableRef?.tableName,
pkColumns: plan?.pkColumns || [],
editLocator,
@@ -4432,6 +4498,22 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
}
}
if (resultSetDataArray.length === 0 && Array.isArray(res.messages) && res.messages.length > 0) {
nextResultSets.push({
key: 'result-1',
sql: fullSQL,
exportSql: sourceStatements.join(';\n'),
sourceStatementIndex: 1,
statementResultIndex: 1,
rows: [],
columns: [],
messages: res.messages,
resultType: 'message',
pkColumns: [],
readOnly: true,
});
}
const shouldReplaceAllResults = didExecuteWholeEditor;
setResultSets(prev => {
const merged = mergeResultSets(prev, nextResultSets, shouldReplaceAllResults);
@@ -5316,9 +5398,12 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
}}
>
<Tooltip title={rs.sql}>
<span className="query-result-tab-text"> {idx + 1}</span>
<span className="query-result-tab-text">{rs.resultType === 'message' ? `消息 ${idx + 1}` : `结果 ${idx + 1}`}</span>
</Tooltip>
{(() => {
if (rs.resultType === 'message') {
return <span className="query-result-tab-count">i</span>;
}
const isAffected = rs.columns.length === 1 && rs.columns[0] === 'affectedRows';
if (isAffected) {
return <span className="query-result-tab-count"></span>;
@@ -5344,6 +5429,29 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
</Dropdown>
),
children: (() => {
if (rs.resultType === 'message') {
return (
<div className={isV2Ui ? 'gn-v2-query-success' : undefined} style={{
flex: 1, minHeight: 0, display: 'flex', justifyContent: 'center',
flexDirection: 'column', gap: 12, padding: 24, color: '#666', userSelect: 'text',
overflow: 'auto',
}}>
<span style={{ fontSize: 14, fontWeight: 600 }}></span>
<div style={{
padding: 16,
borderRadius: 8,
border: darkMode ? '1px solid rgba(255,255,255,0.12)' : '1px solid rgba(0,0,0,0.08)',
background: darkMode ? 'rgba(255,255,255,0.03)' : '#fff',
whiteSpace: 'pre-wrap',
wordBreak: 'break-word',
fontFamily: 'var(--gn-font-mono)',
fontSize: 'var(--gn-font-size-mono, 13px)',
}}>
{(rs.messages || []).join('\n')}
</div>
</div>
);
}
// affectedRows 类型结果集UPDATE/INSERT/DELETE简洁提示
const isAffectedResult = rs.columns.length === 1 && rs.columns[0] === 'affectedRows';
if (isAffectedResult) {
@@ -5356,11 +5464,44 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
<span style={{ fontSize: 36, color: '#52c41a' }}></span>
<span style={{ fontSize: 14, fontWeight: 500 }}></span>
<span style={{ fontSize: 13, color: '#999' }}>{affected}</span>
{Array.isArray(rs.messages) && rs.messages.length > 0 && (
<div style={{
marginTop: 8,
maxWidth: 720,
padding: 12,
borderRadius: 8,
border: darkMode ? '1px solid rgba(255,255,255,0.12)' : '1px solid rgba(0,0,0,0.08)',
background: darkMode ? 'rgba(255,255,255,0.03)' : '#fff',
whiteSpace: 'pre-wrap',
wordBreak: 'break-word',
fontFamily: 'var(--gn-font-mono)',
fontSize: 'var(--gn-font-size-mono, 12px)',
}}>
{rs.messages.join('\n')}
</div>
)}
</div>
);
}
return (
<div style={{ flex: 1, minHeight: 0, overflow: 'hidden', display: 'flex', flexDirection: 'column' }}>
{Array.isArray(rs.messages) && rs.messages.length > 0 && (
<div style={{
flex: '0 0 auto',
margin: '8px 8px 0',
padding: '10px 12px',
borderRadius: 8,
border: darkMode ? '1px solid rgba(255,255,255,0.12)' : '1px solid rgba(0,0,0,0.08)',
background: darkMode ? 'rgba(255,255,255,0.03)' : '#fff',
whiteSpace: 'pre-wrap',
wordBreak: 'break-word',
fontFamily: 'var(--gn-font-mono)',
fontSize: 'var(--gn-font-size-mono, 12px)',
color: darkMode ? '#d4d4d4' : '#666',
}}>
{rs.messages.join('\n')}
</div>
)}
<DataGrid
data={rs.rows}
columnNames={rs.columns}

View File

@@ -1,7 +1,6 @@
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App'
import PerfDataGridHarness from './dev/PerfDataGridHarness'
// import './index.css' // Optional global styles
// 全局配置 dayjs 使用中文 locale使 Ant Design 的 DatePicker/TimePicker 等组件
@@ -299,12 +298,18 @@ if (typeof window !== 'undefined' && !(window as any).go) {
}
const rootNode = document.getElementById('root')!;
const devHarnessMode = import.meta.env.DEV ? resolveDevHarnessMode() : '';
const rootComponent = devHarnessMode === 'datagrid-perf'
? <PerfDataGridHarness />
: <App />;
const renderRoot = async () => {
let rootComponent = <App />;
if (devHarnessMode === 'datagrid-perf') {
const { default: PerfDataGridHarness } = await import('./dev/PerfDataGridHarness');
rootComponent = <PerfDataGridHarness />;
}
ReactDOM.createRoot(rootNode).render(
<React.StrictMode>
{rootComponent}
</React.StrictMode>,
)
ReactDOM.createRoot(rootNode).render(
<React.StrictMode>
{rootComponent}
</React.StrictMode>,
);
};
void renderRoot();

View File

@@ -63,12 +63,12 @@ describe('dataSourceCapabilities', () => {
supportsCreateDatabase: false,
supportsRenameDatabase: false,
supportsDropDatabase: false,
forceReadOnlyQueryResult: true,
forceReadOnlyQueryResult: false,
});
expect(getDataSourceCapabilities({ type: 'custom', driver: 'elastic' })).toMatchObject({
type: 'elasticsearch',
supportsQueryEditor: true,
forceReadOnlyQueryResult: true,
forceReadOnlyQueryResult: false,
});
});