mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-07-26 08:19:44 +08:00
Merge branch 'dev' into feature/20260602_connection_driver_i18n
This commit is contained in:
@@ -472,6 +472,69 @@ describe("ConnectionModal i18n", () => {
|
||||
},
|
||||
);
|
||||
|
||||
it.each([
|
||||
{
|
||||
language: "zh-CN" as const,
|
||||
sourceLabel: "MySQL",
|
||||
expectations: [
|
||||
"生产连接保护",
|
||||
"按需勾选限制项",
|
||||
"限制数据编辑",
|
||||
"限制结构编辑",
|
||||
"限制脚本执行",
|
||||
"限制数据导入",
|
||||
"当前策略",
|
||||
],
|
||||
},
|
||||
{
|
||||
language: "en-US" as const,
|
||||
sourceLabel: "MySQL",
|
||||
expectations: [
|
||||
"Production guard",
|
||||
"Select only the restrictions you need",
|
||||
"Restrict data edits",
|
||||
"Restrict structure edits",
|
||||
"Restrict script execution",
|
||||
"Restrict data import",
|
||||
"Current policy",
|
||||
],
|
||||
},
|
||||
])(
|
||||
"renders a detailed production-guard card in $language",
|
||||
async ({ language, sourceLabel, expectations }) => {
|
||||
setCurrentLanguage(language);
|
||||
storeState.languagePreference = language;
|
||||
mockFormValues = {
|
||||
type: "mysql",
|
||||
restrictDataEdit: true,
|
||||
restrictStructureEdit: true,
|
||||
restrictScriptExecution: false,
|
||||
restrictDataImport: false,
|
||||
useSSL: true,
|
||||
sslMode: "preferred",
|
||||
timeout: 30,
|
||||
};
|
||||
|
||||
const { default: ConnectionModal } = await import("./ConnectionModal");
|
||||
|
||||
let renderer: ReactTestRenderer;
|
||||
await act(async () => {
|
||||
renderer = create(<ConnectionModal open onClose={vi.fn()} />);
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
findClickableCard(renderer!, sourceLabel).props.onClick();
|
||||
});
|
||||
|
||||
const pageText = textContent(renderer!.toJSON());
|
||||
expectations.forEach((expected) => {
|
||||
expect(pageText).toContain(expected);
|
||||
});
|
||||
expect(pageText).not.toContain("connection.modal.section.undefined.title");
|
||||
expect(pageText).not.toContain("connection.modal.section.undefined.description");
|
||||
},
|
||||
);
|
||||
|
||||
it("renders English topology and authentication copy for legacy mysql, mongodb, and redis sections", async () => {
|
||||
storeState.appearance.uiVersion = "legacy";
|
||||
setCurrentLanguage("en-US");
|
||||
|
||||
@@ -62,6 +62,7 @@ import {
|
||||
import { getCustomConnectionDsnValidationMessage } from "../utils/customConnectionDsn";
|
||||
import { mergeParsedUriValuesForForm } from "../utils/connectionUriMerge";
|
||||
import { buildRpcConnectionConfig } from "../utils/connectionRpcConfig";
|
||||
import { resolveConnectionProtectionConfig } from "../utils/connectionReadOnly";
|
||||
import { getCustomConnectionDriverHelp } from "../utils/driverImportGuidance";
|
||||
import { isBackendCancelledResult } from "../utils/connectionExport";
|
||||
import {
|
||||
@@ -1379,6 +1380,7 @@ const ConnectionModal: React.FC<{
|
||||
: Number(config.timeout || 30);
|
||||
const hasHttpTunnel = !!config.useHttpTunnel;
|
||||
const hasProxy = !hasHttpTunnel && !!config.useProxy;
|
||||
const protection = resolveConnectionProtectionConfig(config);
|
||||
form.setFieldsValue({
|
||||
type: configType,
|
||||
name: initialValues.name,
|
||||
@@ -1387,7 +1389,11 @@ const ConnectionModal: React.FC<{
|
||||
user: config.user,
|
||||
password: config.password,
|
||||
database: config.database,
|
||||
readOnly: config.readOnly === true,
|
||||
restrictDataEdit: protection.restrictDataEdit === true,
|
||||
restrictStructureEdit: protection.restrictStructureEdit === true,
|
||||
restrictScriptExecution:
|
||||
protection.restrictScriptExecution === true,
|
||||
restrictDataImport: protection.restrictDataImport === true,
|
||||
uri: config.uri || "",
|
||||
connectionParams:
|
||||
config.connectionParams ||
|
||||
|
||||
@@ -32,6 +32,7 @@ import { v4 as generateUuid } from 'uuid';
|
||||
import 'react-resizable/css/styles.css';
|
||||
import { buildOrderBySQL, buildPaginatedSelectSQL, buildWhereSQL, escapeLiteral, hasExplicitSort, quoteIdentPart, withSortBufferTuningSQL, type FilterCondition } from '../utils/sql';
|
||||
import { isMacLikePlatform, normalizeOpacityForPlatform, resolveAppearanceValues } from '../utils/appearance';
|
||||
import { isConnectionDataImportRestricted } from '../utils/connectionReadOnly';
|
||||
import { getDataSourceCapabilities, resolveDataSourceType } from '../utils/dataSourceCapabilities';
|
||||
import { buildRpcConnectionConfig } from '../utils/connectionRpcConfig';
|
||||
import { normalizeOceanBaseProtocol } from '../utils/oceanBaseProtocol';
|
||||
@@ -158,7 +159,7 @@ import { useDataGridColumnResize } from './useDataGridColumnResize';
|
||||
import { useDataGridPreviewPanel } from './useDataGridPreviewPanel';
|
||||
import { buildTableExportTab } from '../utils/tableExportTab';
|
||||
import { buildDataGridCssText } from './dataGridStyles';
|
||||
import { formatMongoEditableValue, parseMongoEditedValue } from '../utils/mongodb';
|
||||
import { formatMongoEditableValue, normalizeMongoDocumentForEditing, parseMongoEditedValue } from '../utils/mongodb';
|
||||
|
||||
// --- Error Boundary ---
|
||||
import {
|
||||
@@ -533,25 +534,27 @@ const DataGrid: React.FC<DataGridProps> = ({
|
||||
const prefersManualTotalCount = dataSourceCaps.preferManualTotalCount;
|
||||
const supportsApproximateTableCount = dataSourceCaps.supportsApproximateTableCount;
|
||||
const supportsApproximateTotalPages = dataSourceCaps.supportsApproximateTotalPages;
|
||||
const designerReadOnly = dataSourceCaps.forceReadOnlyStructureDesigner;
|
||||
const importRestricted = isConnectionDataImportRestricted(currentConnConfig);
|
||||
const dbType = dataSourceCaps.type;
|
||||
const isMongoDBConnection = dbType === 'mongodb';
|
||||
const isDuckDBConnection = dataSourceCaps.type === 'duckdb';
|
||||
const supportsCopyInsert = dataSourceCaps.supportsCopyInsert;
|
||||
const supportsSqlQueryExport = dataSourceCaps.supportsSqlQueryExport;
|
||||
const isQueryResultExport = exportScope === 'queryResult';
|
||||
const canImport = exportScope === 'table' && !!tableName && !readOnly;
|
||||
const canImport = exportScope === 'table' && !!tableName && !importRestricted;
|
||||
const canExport = !!connectionId && (isQueryResultExport || !!tableName);
|
||||
const canViewDdl = exportScope === 'table' && !!connectionId && !!tableName;
|
||||
const canOpenObjectDesigner = exportScope === 'table' && objectType === 'table' && !!connectionId && !!tableName;
|
||||
const filteredExportSql = useMemo(() => String(exportSqlWithFilter || '').trim(), [exportSqlWithFilter]);
|
||||
const hasFilteredExportSql = exportScope === 'table' && filteredExportSql.length > 0;
|
||||
|
||||
const mongoAwareEditableText = useCallback((value: any): string => (
|
||||
isMongoDBConnection ? formatMongoEditableValue(value) : toEditableText(value)
|
||||
const mongoAwareEditableText = useCallback((value: any, columnName?: string): string => (
|
||||
isMongoDBConnection ? formatMongoEditableValue(value, columnName) : toEditableText(value)
|
||||
), [isMongoDBConnection]);
|
||||
|
||||
const mongoAwareFormText = useCallback((value: any): string => (
|
||||
isMongoDBConnection ? formatMongoEditableValue(value) : toFormText(value)
|
||||
const mongoAwareFormText = useCallback((value: any, columnName?: string): string => (
|
||||
isMongoDBConnection ? formatMongoEditableValue(value, columnName) : toFormText(value)
|
||||
), [isMongoDBConnection]);
|
||||
|
||||
const normalizeMongoEditedCellValue = useCallback((columnName: string, value: any, currentValue?: any) => (
|
||||
@@ -1494,9 +1497,15 @@ const DataGrid: React.FC<DataGridProps> = ({
|
||||
updateCellSelection,
|
||||
});
|
||||
|
||||
const baseData = useMemo(() => (
|
||||
isMongoDBConnection
|
||||
? data.map((row) => normalizeMongoDocumentForEditing(row))
|
||||
: data
|
||||
), [data, isMongoDBConnection]);
|
||||
|
||||
const displayData = useMemo(() => {
|
||||
return [...data, ...addedRows];
|
||||
}, [data, addedRows]);
|
||||
return [...baseData, ...addedRows];
|
||||
}, [baseData, addedRows]);
|
||||
|
||||
useEffect(() => { displayDataRef.current = displayData; }, [displayData]);
|
||||
|
||||
@@ -1610,7 +1619,7 @@ const DataGrid: React.FC<DataGridProps> = ({
|
||||
}
|
||||
if (deletedRowKeys.has(keyStr)) return;
|
||||
// 查找原始行数据,对比是否真正有值变更
|
||||
const originalRow = data.find(r => r?.[GONAVI_ROW_KEY] === rowKey);
|
||||
const originalRow = baseData.find(r => r?.[GONAVI_ROW_KEY] === rowKey);
|
||||
if (originalRow) {
|
||||
const currentRow = modifiedRows[keyStr] ? { ...originalRow, ...modifiedRows[keyStr] } : originalRow;
|
||||
const normalizedRow = normalizeMongoEditedRow(row, currentRow);
|
||||
@@ -1651,7 +1660,7 @@ const DataGrid: React.FC<DataGridProps> = ({
|
||||
});
|
||||
setModifiedRows(prev => ({ ...prev, [keyStr]: normalizedRow }));
|
||||
}
|
||||
}, [addedRows, data, rowKeyStr, deletedRowKeys, effectiveEditLocator, modifiedRows, normalizeMongoEditedRow]);
|
||||
}, [addedRows, baseData, rowKeyStr, deletedRowKeys, effectiveEditLocator, modifiedRows, normalizeMongoEditedRow]);
|
||||
|
||||
const handleDataPanelSave = useCallback(() => {
|
||||
if (!focusedCellInfo) return;
|
||||
@@ -1710,7 +1719,7 @@ const DataGrid: React.FC<DataGridProps> = ({
|
||||
return;
|
||||
}
|
||||
|
||||
const originalRow = data.find((row) => rowKeyStr(row?.[GONAVI_ROW_KEY]) === keyStr);
|
||||
const originalRow = baseData.find((row) => rowKeyStr(row?.[GONAVI_ROW_KEY]) === keyStr);
|
||||
if (!originalRow) {
|
||||
void message.error(translateDataGrid('data_grid.message.undo_cell_original_missing'));
|
||||
setCellContextMenu(prev => ({ ...prev, visible: false }));
|
||||
@@ -1720,7 +1729,7 @@ const DataGrid: React.FC<DataGridProps> = ({
|
||||
handleCellSave({ ...record, [dataIndex]: originalRow[dataIndex] });
|
||||
setCellContextMenu(prev => ({ ...prev, visible: false }));
|
||||
void message.success(translateDataGrid('data_grid.message.undo_cell_success'));
|
||||
}, [addedRowKeySet, cellContextMenu.dataIndex, cellContextMenu.record, data, handleCellSave, modifiedColumns, rowKeyStr, translateDataGrid]);
|
||||
}, [addedRowKeySet, baseData, cellContextMenu.dataIndex, cellContextMenu.record, handleCellSave, modifiedColumns, rowKeyStr, translateDataGrid]);
|
||||
|
||||
const handleCellEditorSave = useCallback(() => {
|
||||
if (!cellEditorMeta) return;
|
||||
@@ -1770,7 +1779,7 @@ const DataGrid: React.FC<DataGridProps> = ({
|
||||
setCellFieldValue(form, fieldName, parseToDayjs(raw, pickerType));
|
||||
} else {
|
||||
const initialValue = isMongoDBConnection
|
||||
? mongoAwareEditableText(raw)
|
||||
? mongoAwareEditableText(raw, dataIndex)
|
||||
: (typeof raw === 'string' ? normalizeDateTimeString(raw) : raw);
|
||||
setCellFieldValue(form, fieldName, initialValue);
|
||||
}
|
||||
@@ -2043,7 +2052,7 @@ const DataGrid: React.FC<DataGridProps> = ({
|
||||
}
|
||||
|
||||
const baseRow =
|
||||
data.find(r => rowKeyStr(r?.[GONAVI_ROW_KEY]) === keyStr) ||
|
||||
baseData.find(r => rowKeyStr(r?.[GONAVI_ROW_KEY]) === keyStr) ||
|
||||
addedRows.find(r => rowKeyStr(r?.[GONAVI_ROW_KEY]) === keyStr) ||
|
||||
displayRow;
|
||||
|
||||
@@ -2056,7 +2065,7 @@ const DataGrid: React.FC<DataGridProps> = ({
|
||||
const baseVal = (baseRow as any)?.[col];
|
||||
const displayVal = (displayRow as any)?.[col];
|
||||
baseRawMap[col] = baseVal;
|
||||
displayMap[col] = mongoAwareFormText(displayVal);
|
||||
displayMap[col] = mongoAwareFormText(displayVal, col);
|
||||
// 日期时间类型: 将字符串值转为 dayjs 对象供 DatePicker 使用
|
||||
const colMeta = columnMetaMap[col] || columnMetaMapByLowerName[col.toLowerCase()];
|
||||
const rowPickerType = getTemporalPickerType(colMeta?.type, dbType, currentConnConfig);
|
||||
@@ -2064,7 +2073,7 @@ const DataGrid: React.FC<DataGridProps> = ({
|
||||
const dVal = parseToDayjs(displayVal, rowPickerType);
|
||||
formMap[col] = dVal;
|
||||
} else {
|
||||
formMap[col] = displayVal === null || displayVal === undefined ? undefined : mongoAwareFormText(displayVal);
|
||||
formMap[col] = displayVal === null || displayVal === undefined ? undefined : mongoAwareFormText(displayVal, col);
|
||||
}
|
||||
if (baseVal === null || baseVal === undefined) nullCols.add(col);
|
||||
});
|
||||
@@ -2076,7 +2085,7 @@ const DataGrid: React.FC<DataGridProps> = ({
|
||||
nullCols,
|
||||
formValues: formMap,
|
||||
});
|
||||
}, [addedRows, canModifyData, columnMetaMap, columnMetaMapByLowerName, currentConnConfig, data, dbType, mergedDisplayData, mongoAwareFormText, openRowEditor, rowKeyStr, translateDataGrid, visibleColumnNames]);
|
||||
}, [addedRows, baseData, canModifyData, columnMetaMap, columnMetaMapByLowerName, currentConnConfig, dbType, mergedDisplayData, mongoAwareFormText, openRowEditor, rowKeyStr, translateDataGrid, visibleColumnNames]);
|
||||
|
||||
const openCurrentViewRowEditor = useCallback(() => {
|
||||
if (!canModifyData) return;
|
||||
@@ -2140,7 +2149,7 @@ const DataGrid: React.FC<DataGridProps> = ({
|
||||
});
|
||||
|
||||
const originalMap = new Map<string, any>();
|
||||
data.forEach((r) => {
|
||||
baseData.forEach((r) => {
|
||||
const key = r?.[GONAVI_ROW_KEY];
|
||||
if (key === undefined) return;
|
||||
originalMap.set(rowKeyStr(key), r);
|
||||
@@ -2213,7 +2222,7 @@ const DataGrid: React.FC<DataGridProps> = ({
|
||||
|
||||
closeJsonEditor();
|
||||
void message.success(translateDataGrid('data_grid.message.json_applied'));
|
||||
}, [canModifyData, jsonEditorValue, mergedDisplayData, addedRows, rowKeyStr, data, visibleColumnNames, effectiveEditLocator, closeJsonEditor, translateDataGrid]);
|
||||
}, [canModifyData, jsonEditorValue, mergedDisplayData, addedRows, rowKeyStr, baseData, visibleColumnNames, effectiveEditLocator, closeJsonEditor, translateDataGrid]);
|
||||
|
||||
const openRowEditorFieldEditor = useCallback((dataIndex: string) => {
|
||||
if (!dataIndex) return;
|
||||
@@ -2701,7 +2710,7 @@ const DataGrid: React.FC<DataGridProps> = ({
|
||||
addedRows,
|
||||
modifiedRows,
|
||||
deletedRowKeys,
|
||||
data,
|
||||
data: baseData,
|
||||
editLocator: effectiveEditLocator,
|
||||
visibleColumnNames,
|
||||
rowKeyToString: rowKeyStr,
|
||||
@@ -2748,7 +2757,7 @@ const DataGrid: React.FC<DataGridProps> = ({
|
||||
const rawErrorMessage = e?.message || String(e);
|
||||
void message.error(translateDataGrid('data_grid.message.preview_sql_failed_detail', { detail: rawErrorMessage }));
|
||||
}
|
||||
}, [addedRows, modifiedRows, deletedRowKeys, data, effectiveEditLocator,
|
||||
}, [addedRows, modifiedRows, deletedRowKeys, baseData, effectiveEditLocator,
|
||||
visibleColumnNames, rowKeyStr, normalizeCommitCellValue, shouldCommitColumn,
|
||||
connectionId, tableName, connections, rowLocatorMessages, translateDataGrid]);
|
||||
|
||||
@@ -2761,7 +2770,7 @@ const DataGrid: React.FC<DataGridProps> = ({
|
||||
addedRows,
|
||||
modifiedRows,
|
||||
deletedRowKeys,
|
||||
data,
|
||||
data: baseData,
|
||||
editLocator: effectiveEditLocator,
|
||||
visibleColumnNames,
|
||||
rowKeyToString: rowKeyStr,
|
||||
@@ -2845,7 +2854,7 @@ const DataGrid: React.FC<DataGridProps> = ({
|
||||
addedRows,
|
||||
modifiedRows,
|
||||
deletedRowKeys,
|
||||
data,
|
||||
baseData,
|
||||
effectiveEditLocator,
|
||||
visibleColumnNames,
|
||||
rowKeyStr,
|
||||
@@ -4180,6 +4189,7 @@ const DataGrid: React.FC<DataGridProps> = ({
|
||||
copyRowsForPaste,
|
||||
copyToClipboard,
|
||||
currentConnConfig,
|
||||
designerReadOnly,
|
||||
currentTextRow,
|
||||
darkMode,
|
||||
dataContextValue,
|
||||
|
||||
@@ -100,6 +100,7 @@ const DataGridShell: React.FC<DataGridShellProps> = (props) => {
|
||||
copyRowsForPaste,
|
||||
copyToClipboard,
|
||||
currentConnConfig,
|
||||
designerReadOnly,
|
||||
currentTextRow,
|
||||
darkMode,
|
||||
dataContextValue,
|
||||
@@ -734,7 +735,7 @@ const renderDataTableView = () => (
|
||||
dbName,
|
||||
tableName,
|
||||
initialTab: 'columns',
|
||||
readOnly,
|
||||
readOnly: designerReadOnly,
|
||||
objectType: 'table',
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -3129,6 +3129,48 @@ describe('QueryEditor external SQL save', () => {
|
||||
expect(tableSuggestion.detail).not.toContain('Table (analytics)');
|
||||
});
|
||||
|
||||
it('deduplicates Oracle-style database qualified table completion labels when schema matches the qualifier', async () => {
|
||||
storeState.languagePreference = 'zh-CN';
|
||||
setCurrentLanguage('zh-CN');
|
||||
storeState.connections[0].config.type = 'oracle';
|
||||
storeState.connections[0].config.database = 'ORCLPDB1';
|
||||
editorState.value = 'select * from sbdev.AA';
|
||||
autoFetchState.visible = true;
|
||||
backendApp.DBGetDatabases.mockResolvedValueOnce({
|
||||
success: true,
|
||||
data: [{ Database: 'ORCLPDB1' }, { Database: 'sbdev' }],
|
||||
});
|
||||
backendApp.DBGetTables.mockImplementation(async (_config: any, dbName: string) => {
|
||||
if (String(dbName || '').toLowerCase() === 'sbdev') {
|
||||
return { success: true, data: [{ Table: 'SBDEV.AAA3_NJ' }] };
|
||||
}
|
||||
return { success: true, data: [] };
|
||||
});
|
||||
backendApp.DBGetAllColumns.mockResolvedValue({ success: true, data: [] });
|
||||
|
||||
await act(async () => {
|
||||
create(<QueryEditor tab={createTab({ query: editorState.value, dbName: 'ORCLPDB1' })} />);
|
||||
});
|
||||
await act(async () => {
|
||||
await Promise.resolve();
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
const completionProvider = editorState.providers[0];
|
||||
expect(completionProvider).toBeTruthy();
|
||||
|
||||
const completionItems = await completionProvider.provideCompletionItems(
|
||||
editorState.editor.getModel(),
|
||||
{ lineNumber: 1, column: editorState.value.length + 1 },
|
||||
);
|
||||
const tableSuggestion = completionItems?.suggestions?.find((item: any) => item?.label === 'AAA3_NJ');
|
||||
|
||||
expect(tableSuggestion).toBeTruthy();
|
||||
expect(tableSuggestion.insertText).toBe('AAA3_NJ');
|
||||
expect(tableSuggestion.detail).toContain('表 (sbdev)');
|
||||
expect(completionItems?.suggestions?.some((item: any) => item?.label === 'sbdev.SBDEV.AAA3_NJ')).toBe(false);
|
||||
});
|
||||
|
||||
it('localizes schema-qualified table completion detail in zh-CN while preserving the raw database and schema names', async () => {
|
||||
storeState.languagePreference = 'zh-CN';
|
||||
setCurrentLanguage('zh-CN');
|
||||
@@ -4307,6 +4349,47 @@ describe('QueryEditor external SQL save', () => {
|
||||
expect(messageApi.success).toHaveBeenCalledWith('查询已保存。');
|
||||
});
|
||||
|
||||
it('allows Ctrl/Cmd+S to save external SQL files from document-level targets', async () => {
|
||||
const windowListeners: Record<string, ((event?: any) => void)[]> = {};
|
||||
vi.stubGlobal('window', {
|
||||
addEventListener: vi.fn((type: string, listener: (event?: any) => void) => {
|
||||
windowListeners[type] ||= [];
|
||||
windowListeners[type].push(listener);
|
||||
}),
|
||||
removeEventListener: vi.fn(),
|
||||
dispatchEvent: vi.fn(),
|
||||
});
|
||||
|
||||
const filePath = '/Users/me/Documents/gonavi-queries/report.sql';
|
||||
editorState.hasTextFocus = false;
|
||||
|
||||
await act(async () => {
|
||||
create(<QueryEditor tab={createTab({ filePath })} />);
|
||||
});
|
||||
|
||||
editorState.value = 'select 6;';
|
||||
const isMacRuntime = /(Mac|iPhone|iPad|iPod)/i.test(`${navigator.platform || ''} ${navigator.userAgent || ''}`);
|
||||
const event = {
|
||||
ctrlKey: !isMacRuntime,
|
||||
metaKey: isMacRuntime,
|
||||
altKey: false,
|
||||
shiftKey: false,
|
||||
key: 's',
|
||||
target: document.body,
|
||||
preventDefault: vi.fn(),
|
||||
stopPropagation: vi.fn(),
|
||||
};
|
||||
|
||||
await act(async () => {
|
||||
windowListeners.keydown?.forEach((listener) => listener(event));
|
||||
});
|
||||
|
||||
expect(event.preventDefault).toHaveBeenCalled();
|
||||
expect(event.stopPropagation).toHaveBeenCalled();
|
||||
expect(backendApp.WriteSQLFile).toHaveBeenCalledWith(filePath, 'select 6;');
|
||||
expect(messageApi.success).toHaveBeenCalledWith(expect.stringContaining('SQL 文件已保存'));
|
||||
});
|
||||
|
||||
it('does not create saved queries when external SQL file writes fail', async () => {
|
||||
let renderer!: ReactTestRenderer;
|
||||
const filePath = '/Users/me/Documents/gonavi-queries/report.sql';
|
||||
|
||||
@@ -191,6 +191,7 @@ let sharedMaterializedViewsData: CompletionViewMeta[] = [];
|
||||
let sharedTriggersData: CompletionTriggerMeta[] = [];
|
||||
let sharedRoutinesData: CompletionRoutineMeta[] = [];
|
||||
let sharedColumnsCacheData: Record<string, any[]> = {};
|
||||
const QUERY_EDITOR_LAZY_VISIBLE_DB_COMPLETION_LIMIT = 10;
|
||||
const sharedLazyTablesCache: Record<string, CompletionTableMeta[] | undefined> = {};
|
||||
const sharedLazyTablesInFlight: Record<string, Promise<CompletionTableMeta[]> | undefined> = {};
|
||||
const createEmptySqlCompletionResult = () => ({ suggestions: [] as any[] });
|
||||
@@ -204,6 +205,7 @@ const clearRecord = (record: Record<string, unknown>) => {
|
||||
const resetSharedQueryEditorMetadata = () => {
|
||||
sharedTablesData = [];
|
||||
sharedAllColumnsData = [];
|
||||
sharedVisibleDbs = [];
|
||||
sharedViewsData = [];
|
||||
sharedMaterializedViewsData = [];
|
||||
sharedTriggersData = [];
|
||||
@@ -1943,6 +1945,26 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
|
||||
const stripQuotes = stripCompletionIdentifierQuotes;
|
||||
const normalizeQualifiedName = normalizeCompletionQualifiedName;
|
||||
const splitSchemaAndTable = splitCompletionSchemaAndTable;
|
||||
const buildDbQualifiedTableSuggestionMeta = (dbName: string, tableName: string) => {
|
||||
const rawDbName = String(dbName || '').trim();
|
||||
const rawTableName = String(tableName || '').trim();
|
||||
const parsed = splitSchemaAndTable(rawTableName);
|
||||
const schemaMatchesDb = !!parsed.schema
|
||||
&& !!parsed.table
|
||||
&& parsed.schema.toLowerCase() === rawDbName.toLowerCase();
|
||||
const displayName = schemaMatchesDb ? parsed.table : rawTableName;
|
||||
const insertText = schemaMatchesDb
|
||||
? quoteCompletionPart(parsed.table)
|
||||
: quoteCompletionPath(rawTableName);
|
||||
const dbQualifiedLabel = rawDbName
|
||||
? `${rawDbName}.${displayName || rawTableName}`
|
||||
: (displayName || rawTableName);
|
||||
return {
|
||||
displayName: displayName || rawTableName,
|
||||
insertText,
|
||||
dbQualifiedLabel,
|
||||
};
|
||||
};
|
||||
|
||||
const buildConnConfig = () => {
|
||||
const connId = sharedCurrentConnectionId;
|
||||
@@ -2132,18 +2154,25 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
|
||||
}
|
||||
}
|
||||
const filtered = prefix
|
||||
? tables.filter(t => (t.tableName || '').toLowerCase().startsWith(prefix))
|
||||
? tables.filter(t => {
|
||||
const suggestionMeta = buildDbQualifiedTableSuggestionMeta(t.dbName || qualifier, t.tableName || '');
|
||||
return String(suggestionMeta.displayName || '').toLowerCase().startsWith(prefix)
|
||||
|| String(t.tableName || '').toLowerCase().startsWith(prefix);
|
||||
})
|
||||
: tables;
|
||||
|
||||
const suggestions = filtered.map(t => ({
|
||||
label: t.tableName,
|
||||
const suggestions = filtered.map(t => {
|
||||
const suggestionMeta = buildDbQualifiedTableSuggestionMeta(t.dbName || qualifier, t.tableName || '');
|
||||
return {
|
||||
label: suggestionMeta.displayName,
|
||||
kind: monaco.languages.CompletionItemKind.Class,
|
||||
insertText: quoteCompletionPath(t.tableName),
|
||||
insertText: suggestionMeta.insertText,
|
||||
detail: appendCommentToDetail(`${translate('query_editor.object_info.table')} (${t.dbName})`, t.comment),
|
||||
documentation: buildCompletionDocumentation(t.comment),
|
||||
range,
|
||||
sortText: '0' + t.tableName
|
||||
}));
|
||||
sortText: '0' + suggestionMeta.displayName
|
||||
};
|
||||
});
|
||||
return { suggestions };
|
||||
}
|
||||
|
||||
@@ -2232,7 +2261,7 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
|
||||
if (normalized.some((candidate) => candidate.includes(wordPrefix))) return '1';
|
||||
return '9';
|
||||
};
|
||||
const expectsTableName = /\b(?:FROM|JOIN|UPDATE|INTO|DELETE\s+FROM|TABLE|DESCRIBE|DESC|EXPLAIN)\s+[`"]?[\w.]*$/i.test(linePrefix.trim());
|
||||
const expectsTableName = /\b(?:FROM|JOIN|UPDATE|INTO|DELETE\s+FROM|TABLE|DESCRIBE|DESC|EXPLAIN)\s+[`"]?[\w.]*$/i.test(linePrefix);
|
||||
const shouldBoostKeywords = !expectsTableName
|
||||
&& wordPrefix.length > 0
|
||||
&& dialectKeywords.some((keyword) => keyword.toLowerCase().startsWith(wordPrefix));
|
||||
@@ -2261,6 +2290,38 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
|
||||
});
|
||||
}
|
||||
}
|
||||
if (expectsTableName && sharedVisibleDbs.length > 1) {
|
||||
const loadedDbKeys = new Set(
|
||||
completionTables
|
||||
.map((table) => String(table.dbName || '').toLowerCase())
|
||||
.filter(Boolean),
|
||||
);
|
||||
const missingVisibleDbs = sharedVisibleDbs.filter((dbName) => {
|
||||
const normalizedDbName = String(dbName || '').trim();
|
||||
const dbKey = normalizedDbName.toLowerCase();
|
||||
return normalizedDbName
|
||||
&& dbKey !== currentDatabase.toLowerCase()
|
||||
&& !loadedDbKeys.has(dbKey);
|
||||
});
|
||||
if (
|
||||
missingVisibleDbs.length > 0
|
||||
&& missingVisibleDbs.length <= QUERY_EDITOR_LAZY_VISIBLE_DB_COMPLETION_LIMIT
|
||||
) {
|
||||
const lazyTableGroups = await Promise.all(
|
||||
missingVisibleDbs.map((dbName) => getLazyTablesByDB(dbName)),
|
||||
);
|
||||
if (isSqlCompletionRequestCancelled(token)) {
|
||||
return createEmptySqlCompletionResult();
|
||||
}
|
||||
const seenTableKeys = new Set<string>();
|
||||
completionTables = [...completionTables, ...lazyTableGroups.flat()].filter((table) => {
|
||||
const key = `${String(table.dbName || '').toLowerCase()}.${String(table.tableName || '').toLowerCase()}`;
|
||||
if (seenTableKeys.has(key)) return false;
|
||||
seenTableKeys.add(key);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const referencedColumns: CompletionColumnMeta[] = [];
|
||||
if (!expectsTableName) {
|
||||
@@ -2327,9 +2388,11 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
|
||||
const isCurrentDb = (t.dbName || '').toLowerCase() === currentDatabase.toLowerCase();
|
||||
const parsed = splitSchemaAndTable(t.tableName || '');
|
||||
const pureTable = parsed.table || t.tableName || '';
|
||||
if (!isCurrentDb) {
|
||||
// 跨库:用 db.table 格式匹配
|
||||
return includesWordPrefix(`${t.dbName}.${t.tableName}`)
|
||||
if (!isCurrentDb) {
|
||||
const suggestionMeta = buildDbQualifiedTableSuggestionMeta(t.dbName || '', t.tableName || '');
|
||||
const label = suggestionMeta.dbQualifiedLabel;
|
||||
// 跨库:用 db.table 格式匹配
|
||||
return includesWordPrefix(label)
|
||||
|| includesWordPrefix(t.tableName || '')
|
||||
|| includesWordPrefix(pureTable);
|
||||
}
|
||||
@@ -2341,7 +2404,8 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
|
||||
const parsed = splitSchemaAndTable(t.tableName || '');
|
||||
const pureTable = parsed.table || t.tableName || '';
|
||||
if (!isCurrentDb) {
|
||||
const label = `${t.dbName}.${t.tableName}`;
|
||||
const suggestionMeta = buildDbQualifiedTableSuggestionMeta(t.dbName || '', t.tableName || '');
|
||||
const label = suggestionMeta.dbQualifiedLabel;
|
||||
return {
|
||||
label,
|
||||
kind: monaco.languages.CompletionItemKind.Class,
|
||||
@@ -2349,7 +2413,7 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
|
||||
detail: appendCommentToDetail(`${translate('query_editor.object_info.table')} (${t.dbName})`, t.comment),
|
||||
documentation: buildCompletionDocumentation(t.comment),
|
||||
range,
|
||||
sortText: sortGroups.tableOther + getPrefixMatchRank(`${t.dbName}.${t.tableName}`, t.tableName || '', pureTable) + t.tableName,
|
||||
sortText: sortGroups.tableOther + getPrefixMatchRank(label, t.tableName || '', pureTable) + label,
|
||||
};
|
||||
}
|
||||
// 当前库:检查是否有跨 schema 同名表
|
||||
@@ -4117,7 +4181,7 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
|
||||
const targetNode = resolveEventTargetNode(event.target);
|
||||
const editorHasFocus = !!editor?.hasTextFocus?.();
|
||||
const inQueryEditor = !!(targetNode && queryEditorRootRef.current?.contains(targetNode));
|
||||
if (!editorHasFocus && !inQueryEditor) {
|
||||
if (!editorHasFocus && !inQueryEditor && !isDocumentLevelShortcutTarget(targetNode)) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -119,6 +119,7 @@ import { useAutoFetchVisibility } from '../utils/autoFetchVisibility';
|
||||
import FindInDatabaseModal from './FindInDatabaseModal';
|
||||
import { buildRpcConnectionConfig } from '../utils/connectionRpcConfig';
|
||||
import { resolveDataSourceType } from '../utils/dataSourceCapabilities';
|
||||
import { isConnectionStructureEditRestricted } from '../utils/connectionReadOnly';
|
||||
import { noAutoCapInputProps } from '../utils/inputAutoCap';
|
||||
import {
|
||||
resolveSidebarRuntimeDatabase,
|
||||
@@ -1412,7 +1413,10 @@ const Sidebar: React.FC<{
|
||||
|
||||
const openDesign = (node: any, initialTab: string, readOnly: boolean = false) => {
|
||||
const { tableName, dbName, id } = node.dataRef;
|
||||
const forceReadOnly = readOnly || isStructureOnlyDbType(id);
|
||||
const conn = connections.find(c => c.id === id);
|
||||
const forceReadOnly = readOnly
|
||||
|| isStructureOnlyDbType(id)
|
||||
|| isConnectionStructureEditRestricted(conn?.config);
|
||||
addTab({
|
||||
id: `design-${id}-${dbName}-${tableName}`,
|
||||
title: forceReadOnly
|
||||
@@ -1429,7 +1433,8 @@ const Sidebar: React.FC<{
|
||||
|
||||
const openNewTableDesign = (node: any) => {
|
||||
const { dbName, id } = node.dataRef;
|
||||
if (isStructureOnlyDbType(id)) {
|
||||
const conn = connections.find(c => c.id === id);
|
||||
if (isStructureOnlyDbType(id) || isConnectionStructureEditRestricted(conn?.config)) {
|
||||
message.warning(t('sidebar.message.visual_new_table_unsupported'));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ import { isMacLikePlatform } from '../utils/appearance';
|
||||
import { getShortcutPlatform } from '../utils/shortcuts';
|
||||
import { t } from '../i18n';
|
||||
import { buildTableExportTab } from '../utils/tableExportTab';
|
||||
import { getDataSourceCapabilities } from '../utils/dataSourceCapabilities';
|
||||
import { V2TableContextMenuView, type V2TableContextMenuActionKey } from './V2TableContextMenu';
|
||||
import { useExportProgressDialog } from './ExportProgressModal';
|
||||
|
||||
@@ -279,7 +280,7 @@ const TableOverview: React.FC<TableOverviewProps> = ({ tab }) => {
|
||||
[connection?.config?.driver, connection?.config?.oceanBaseProtocol, connection?.config?.type]
|
||||
);
|
||||
const schemaName = String((tab as any).schemaName || '').trim();
|
||||
const supportsDesignWrite = metadataDialect !== 'iotdb';
|
||||
const supportsDesignWrite = !getDataSourceCapabilities(connection?.config).forceReadOnlyStructureDesigner;
|
||||
const autoFetchVisible = useAutoFetchVisibility();
|
||||
|
||||
const loadData = useCallback(async () => {
|
||||
|
||||
@@ -37,7 +37,9 @@ import {
|
||||
import ConnectionModalMongoSections from "../ConnectionModalMongoSections";
|
||||
import ConnectionModalRedisSections from "../ConnectionModalRedisSections";
|
||||
import { t } from "../../i18n";
|
||||
import { supportsConnectionReadOnlyMode } from "../../utils/connectionReadOnly";
|
||||
import {
|
||||
supportsConnectionReadOnlyMode,
|
||||
} from "../../utils/connectionReadOnly";
|
||||
import {
|
||||
getConnectionConfigLayoutKindLabel,
|
||||
getStoredSecretPlaceholder,
|
||||
@@ -198,6 +200,19 @@ const renderStep2 = () => {
|
||||
driver: form.getFieldValue("driver"),
|
||||
oceanBaseProtocol,
|
||||
});
|
||||
const restrictDataEdit = Form.useWatch("restrictDataEdit", form) === true;
|
||||
const restrictStructureEdit =
|
||||
Form.useWatch("restrictStructureEdit", form) === true;
|
||||
const restrictScriptExecution =
|
||||
Form.useWatch("restrictScriptExecution", form) === true;
|
||||
const restrictDataImport =
|
||||
Form.useWatch("restrictDataImport", form) === true;
|
||||
const connectionProtectionEnabledCount = [
|
||||
restrictDataEdit,
|
||||
restrictStructureEdit,
|
||||
restrictScriptExecution,
|
||||
restrictDataImport,
|
||||
].filter(Boolean).length;
|
||||
const baseInfoSection = (
|
||||
<div style={modalInnerSectionStyle}>
|
||||
<div
|
||||
@@ -1349,20 +1364,252 @@ const renderStep2 = () => {
|
||||
renderConfigSectionCard({
|
||||
sectionKey: "readOnly",
|
||||
icon: <SafetyCertificateOutlined />,
|
||||
children: (
|
||||
<Form.Item
|
||||
name="readOnly"
|
||||
label={t("connection.modal.field.readOnly.label")}
|
||||
help={t("connection.modal.field.readOnly.help")}
|
||||
valuePropName="checked"
|
||||
style={{ marginBottom: 0 }}
|
||||
badge: (
|
||||
<Tag
|
||||
color={
|
||||
connectionProtectionEnabledCount > 0 ? "red" : "default"
|
||||
}
|
||||
>
|
||||
<Checkbox
|
||||
onChange={() => clearConnectionTestResultForChoice()}
|
||||
{connectionProtectionEnabledCount > 0
|
||||
? t(
|
||||
"connection.modal.field.readOnly.status.enabledCount",
|
||||
{
|
||||
count: connectionProtectionEnabledCount,
|
||||
},
|
||||
)
|
||||
: t("connection.modal.field.readOnly.status.disabled")}
|
||||
</Tag>
|
||||
),
|
||||
children: (
|
||||
<div style={{ display: "grid", gap: 14 }}>
|
||||
<div
|
||||
style={{
|
||||
padding: 16,
|
||||
borderRadius: 16,
|
||||
border: connectionProtectionEnabledCount > 0
|
||||
? darkMode
|
||||
? "1px solid rgba(255,120,117,0.34)"
|
||||
: "1px solid rgba(245,34,45,0.18)"
|
||||
: darkMode
|
||||
? "1px solid rgba(255,214,102,0.24)"
|
||||
: "1px solid rgba(250,173,20,0.18)",
|
||||
background: connectionProtectionEnabledCount > 0
|
||||
? darkMode
|
||||
? "linear-gradient(180deg, rgba(255,120,117,0.12) 0%, rgba(255,120,117,0.05) 100%)"
|
||||
: "linear-gradient(180deg, rgba(255,245,245,0.96) 0%, rgba(255,240,240,0.92) 100%)"
|
||||
: darkMode
|
||||
? "linear-gradient(180deg, rgba(255,214,102,0.10) 0%, rgba(255,214,102,0.04) 100%)"
|
||||
: "linear-gradient(180deg, rgba(255,251,230,0.98) 0%, rgba(255,247,214,0.94) 100%)",
|
||||
boxShadow: darkMode
|
||||
? "inset 0 1px 0 rgba(255,255,255,0.04)"
|
||||
: "inset 0 1px 0 rgba(255,255,255,0.92)",
|
||||
}}
|
||||
>
|
||||
{t("connection.modal.field.readOnly.checkbox")}
|
||||
</Checkbox>
|
||||
</Form.Item>
|
||||
<div
|
||||
style={{
|
||||
display: "grid",
|
||||
gridTemplateColumns: "minmax(0, 1fr) auto",
|
||||
gap: 16,
|
||||
alignItems: "start",
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
style={{
|
||||
fontSize: 15,
|
||||
fontWeight: 700,
|
||||
color: darkMode ? "#f5f7ff" : "#162033",
|
||||
}}
|
||||
>
|
||||
{t("connection.modal.field.readOnly.label")}
|
||||
</div>
|
||||
<div style={{ ...modalMutedTextStyle, marginTop: 6 }}>
|
||||
{t("connection.modal.field.readOnly.help")}
|
||||
</div>
|
||||
</div>
|
||||
<Text
|
||||
style={{
|
||||
fontSize: 12,
|
||||
color: darkMode ? "#ffd591" : "#ad4e00",
|
||||
whiteSpace: "nowrap",
|
||||
}}
|
||||
>
|
||||
{t("connection.modal.field.readOnly.compatibility")}
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
display: "grid",
|
||||
gridTemplateColumns: "repeat(auto-fit, minmax(220px, 1fr))",
|
||||
gap: 12,
|
||||
}}
|
||||
>
|
||||
{[
|
||||
{
|
||||
field: "restrictDataEdit",
|
||||
checked: restrictDataEdit,
|
||||
label: t(
|
||||
"connection.modal.field.readOnly.option.dataEdit.label",
|
||||
),
|
||||
help: t(
|
||||
"connection.modal.field.readOnly.option.dataEdit.help",
|
||||
),
|
||||
},
|
||||
{
|
||||
field: "restrictStructureEdit",
|
||||
checked: restrictStructureEdit,
|
||||
label: t(
|
||||
"connection.modal.field.readOnly.option.structureEdit.label",
|
||||
),
|
||||
help: t(
|
||||
"connection.modal.field.readOnly.option.structureEdit.help",
|
||||
),
|
||||
},
|
||||
{
|
||||
field: "restrictScriptExecution",
|
||||
checked: restrictScriptExecution,
|
||||
label: t(
|
||||
"connection.modal.field.readOnly.option.scriptExecution.label",
|
||||
),
|
||||
help: t(
|
||||
"connection.modal.field.readOnly.option.scriptExecution.help",
|
||||
),
|
||||
},
|
||||
{
|
||||
field: "restrictDataImport",
|
||||
checked: restrictDataImport,
|
||||
label: t(
|
||||
"connection.modal.field.readOnly.option.dataImport.label",
|
||||
),
|
||||
help: t(
|
||||
"connection.modal.field.readOnly.option.dataImport.help",
|
||||
),
|
||||
},
|
||||
].map((item) => (
|
||||
<div
|
||||
key={item.field}
|
||||
onClick={() =>
|
||||
setChoiceFieldValue(item.field, !item.checked)
|
||||
}
|
||||
style={{
|
||||
padding: 14,
|
||||
borderRadius: 14,
|
||||
border: item.checked
|
||||
? darkMode
|
||||
? "1px solid rgba(255,120,117,0.22)"
|
||||
: "1px solid rgba(245,34,45,0.14)"
|
||||
: darkMode
|
||||
? "1px solid rgba(255,255,255,0.08)"
|
||||
: "1px solid rgba(5,5,5,0.08)",
|
||||
background: item.checked
|
||||
? darkMode
|
||||
? "rgba(255,120,117,0.08)"
|
||||
: "rgba(255,241,240,0.92)"
|
||||
: darkMode
|
||||
? "rgba(255,255,255,0.02)"
|
||||
: "rgba(255,255,255,0.9)",
|
||||
cursor: "pointer",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: "grid",
|
||||
gridTemplateColumns: "auto minmax(0, 1fr)",
|
||||
gap: 12,
|
||||
alignItems: "start",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "flex-start",
|
||||
paddingTop: 2,
|
||||
}}
|
||||
onClick={(event) => event.stopPropagation()}
|
||||
>
|
||||
<Form.Item
|
||||
name={item.field}
|
||||
valuePropName="checked"
|
||||
noStyle
|
||||
>
|
||||
<Checkbox
|
||||
onChange={() =>
|
||||
clearConnectionTestResultForChoice()
|
||||
}
|
||||
style={{
|
||||
marginInlineStart: 0,
|
||||
}}
|
||||
/>
|
||||
</Form.Item>
|
||||
</div>
|
||||
<div>
|
||||
<div
|
||||
style={{
|
||||
fontSize: 13,
|
||||
fontWeight: 700,
|
||||
color: darkMode ? "#f5f7ff" : "#162033",
|
||||
}}
|
||||
>
|
||||
{item.label}
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
...modalMutedTextStyle,
|
||||
marginTop: 6,
|
||||
whiteSpace: "normal",
|
||||
}}
|
||||
>
|
||||
{item.help}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
padding: 14,
|
||||
borderRadius: 14,
|
||||
border: darkMode
|
||||
? "1px solid rgba(82,196,26,0.22)"
|
||||
: "1px solid rgba(82,196,26,0.18)",
|
||||
background: darkMode
|
||||
? "rgba(82,196,26,0.08)"
|
||||
: "rgba(246,255,237,0.92)",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
fontSize: 13,
|
||||
fontWeight: 700,
|
||||
color: darkMode ? "#f5f7ff" : "#162033",
|
||||
}}
|
||||
>
|
||||
{t("connection.modal.field.readOnly.summary.title")}
|
||||
</div>
|
||||
<div style={{ ...modalMutedTextStyle, marginTop: 6 }}>
|
||||
{connectionProtectionEnabledCount > 0
|
||||
? t(
|
||||
"connection.modal.field.readOnly.summary.selected",
|
||||
{ count: connectionProtectionEnabledCount },
|
||||
)
|
||||
: t(
|
||||
"connection.modal.field.readOnly.summary.empty",
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
...modalMutedTextStyle,
|
||||
fontSize: 12,
|
||||
lineHeight: 1.7,
|
||||
padding: "0 4px",
|
||||
}}
|
||||
>
|
||||
{t("connection.modal.field.readOnly.tip")}
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
})}
|
||||
|
||||
@@ -2333,7 +2580,10 @@ const renderStep2 = () => {
|
||||
keepAliveIntervalMinutes: 240,
|
||||
uri: "",
|
||||
connectionParams: "",
|
||||
readOnly: false,
|
||||
restrictDataEdit: false,
|
||||
restrictStructureEdit: false,
|
||||
restrictScriptExecution: false,
|
||||
restrictDataImport: false,
|
||||
oceanBaseProtocol: "mysql",
|
||||
mysqlTopology: "single",
|
||||
rocketmqTopology: "single",
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import type { ConnectionConfig, SavedConnection } from "../../types";
|
||||
import { supportsConnectionReadOnlyMode } from "../../utils/connectionReadOnly";
|
||||
import {
|
||||
deriveLegacyConnectionReadOnlyFlag,
|
||||
normalizeConnectionProtectionConfig,
|
||||
supportsConnectionReadOnlyMode,
|
||||
} from "../../utils/connectionReadOnly";
|
||||
import { resolveConnectionSecretDraft } from "../../utils/connectionSecretDraft";
|
||||
import {
|
||||
getConnectionTypeDefaultPort as getDefaultPortByType,
|
||||
@@ -797,6 +801,19 @@ export const buildConnectionConfig = async ({
|
||||
)
|
||||
: normalizeConnectionParamsText(mergedValues.connectionParams)
|
||||
: "";
|
||||
const supportsProductionGuard = supportsConnectionReadOnlyMode({
|
||||
type,
|
||||
driver: mergedValues.driver,
|
||||
oceanBaseProtocol: selectedOceanBaseProtocol,
|
||||
});
|
||||
const protection = supportsProductionGuard
|
||||
? normalizeConnectionProtectionConfig({
|
||||
restrictDataEdit: mergedValues.restrictDataEdit === true,
|
||||
restrictStructureEdit: mergedValues.restrictStructureEdit === true,
|
||||
restrictScriptExecution: mergedValues.restrictScriptExecution === true,
|
||||
restrictDataImport: mergedValues.restrictDataImport === true,
|
||||
})
|
||||
: undefined;
|
||||
|
||||
return {
|
||||
type: mergedValues.type,
|
||||
@@ -806,12 +823,10 @@ export const buildConnectionConfig = async ({
|
||||
password: keepPassword ? mergedValues.password || "" : "",
|
||||
savePassword: savePassword,
|
||||
database: mergedValues.database || "",
|
||||
readOnly:
|
||||
supportsConnectionReadOnlyMode({
|
||||
type,
|
||||
driver: mergedValues.driver,
|
||||
oceanBaseProtocol: selectedOceanBaseProtocol,
|
||||
}) && mergedValues.readOnly === true,
|
||||
readOnly: protection
|
||||
? deriveLegacyConnectionReadOnlyFlag(protection)
|
||||
: false,
|
||||
protection,
|
||||
useSSL: effectiveUseSSL,
|
||||
sslMode: effectiveUseSSL ? sslMode : "disable",
|
||||
sslCAPath: sslCAPath,
|
||||
|
||||
@@ -18,7 +18,7 @@ interface OpenRowEditorParams {
|
||||
}
|
||||
|
||||
interface UseDataGridModalEditorsParams {
|
||||
toEditableText: (value: any) => string;
|
||||
toEditableText: (value: any, columnName?: string) => string;
|
||||
looksLikeJsonText: (text: string) => boolean;
|
||||
}
|
||||
|
||||
@@ -100,7 +100,7 @@ export const useDataGridModalEditors = ({
|
||||
) => {
|
||||
if (!record || !dataIndex) return;
|
||||
const raw = record?.[dataIndex];
|
||||
const text = toEditableText(raw);
|
||||
const text = toEditableText(raw, dataIndex);
|
||||
const isJson = looksLikeJsonText(text);
|
||||
const titleText = typeof title === 'string'
|
||||
? title
|
||||
|
||||
@@ -9,7 +9,7 @@ export interface DataGridFocusedCellInfo {
|
||||
}
|
||||
|
||||
interface UseDataGridPreviewPanelParams {
|
||||
toEditableText: (value: any) => string;
|
||||
toEditableText: (value: any, columnName?: string) => string;
|
||||
looksLikeJsonText: (text: string) => boolean;
|
||||
normalizeDateTimeString: (value: string) => string;
|
||||
}
|
||||
@@ -44,8 +44,8 @@ export const useDataGridPreviewPanel = ({
|
||||
const updateFocusedCell = React.useCallback((record: GridRecord, dataIndex: string) => {
|
||||
if (!record || !dataIndex) return;
|
||||
const raw = record?.[dataIndex];
|
||||
let text = toEditableText(raw);
|
||||
if (typeof raw === 'string') {
|
||||
let text = toEditableText(raw, dataIndex);
|
||||
if (typeof raw === 'string' && text === raw) {
|
||||
text = normalizeDateTimeString(raw);
|
||||
}
|
||||
const isJson = looksLikeJsonText(text);
|
||||
|
||||
Reference in New Issue
Block a user