feat(connection): 支持生产连接多项保护策略

- 新增数据编辑、结构编辑、脚本执行和数据导入四类连接级保护配置
- 升级生产连接保护弹窗为多选卡片,并修复选项对齐与勾选态显示
- 按保护类型收口 QueryEditor、DataGrid、表设计、导入与同步目标入口
- 后端统一拦截 SQL 或 Mongo 写操作、结果编辑、结构变更和导入写入
- AI 本地工具与 RPC 执行链路透传连接保护配置并复用后端守卫
- 补充多语言文案、定向测试与需求追踪记录
This commit is contained in:
Syngnat
2026-06-23 17:42:54 +08:00
parent b0a9a995fb
commit adacf0b5c5
35 changed files with 1184 additions and 160 deletions

View File

@@ -459,6 +459,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");

View File

@@ -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 ||

View File

@@ -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';
@@ -533,13 +534,15 @@ 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;
@@ -4176,6 +4179,7 @@ const DataGrid: React.FC<DataGridProps> = ({
copyRowsForPaste,
copyToClipboard,
currentConnConfig,
designerReadOnly,
currentTextRow,
darkMode,
dataContextValue,

View File

@@ -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',
}}
/>

View File

@@ -3116,6 +3116,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');

View File

@@ -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 = [];
@@ -1912,6 +1914,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;
@@ -2101,18 +2123,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 };
}
@@ -2201,7 +2230,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));
@@ -2230,6 +2259,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) {
@@ -2296,9 +2357,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);
}
@@ -2310,7 +2373,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,
@@ -2318,7 +2382,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 同名表

View File

@@ -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;
}

View File

@@ -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 () => {

View File

@@ -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",

View File

@@ -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,