mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-08-21 16:34:21 +08:00
Merge remote-tracking branch 'origin/dev' into dev
This commit is contained in:
@@ -504,6 +504,52 @@ describe('DataGrid layout', () => {
|
||||
expect(css).not.toContain('modified-hover');
|
||||
});
|
||||
|
||||
it('keeps fixed row controls opaque when their row is hovered or selected', () => {
|
||||
const css = buildDataGridCssText({
|
||||
darkMode: false,
|
||||
densityParams: { dataFontSize: 12 },
|
||||
gridId: 'row-number-state-grid',
|
||||
bgContent: '#ffffff',
|
||||
});
|
||||
|
||||
const transparentHover = css.indexOf(
|
||||
'.row-number-state-grid.data-grid-root .ant-table-tbody .ant-table-row:hover > .ant-table-cell',
|
||||
);
|
||||
expect(transparentHover).toBeGreaterThanOrEqual(0);
|
||||
|
||||
const fixedRowControl = ':is(.data-grid-row-number-cell, .ant-table-selection-column)';
|
||||
const fixedRowControlHoverSelectors = [
|
||||
`.row-number-state-grid.data-grid-root .ant-table-tbody-virtual-holder .ant-table-row:hover > .ant-table-cell${fixedRowControl}`,
|
||||
`.row-number-state-grid.data-grid-root .ant-table-tbody-virtual .ant-table-row:hover > .ant-table-cell${fixedRowControl}`,
|
||||
`.row-number-state-grid.data-grid-root .ant-table-tbody-virtual-holder-inner .ant-table-row:hover > .ant-table-cell${fixedRowControl}`,
|
||||
`.row-number-state-grid.data-grid-root .ant-table-tbody > tr:hover > td${fixedRowControl}`,
|
||||
];
|
||||
fixedRowControlHoverSelectors.forEach((selector) => {
|
||||
const ruleStart = css.indexOf(selector);
|
||||
expect(ruleStart).toBeGreaterThan(transparentHover);
|
||||
const ruleEnd = css.indexOf('}', ruleStart);
|
||||
const rule = css.slice(ruleStart, ruleEnd + 1);
|
||||
expect(rule).toContain('background: var(--gn-bg-panel, #ffffff) !important;');
|
||||
expect(rule).toContain('background-image: none !important;');
|
||||
});
|
||||
|
||||
const fixedRowControlSelectedSelectors = [
|
||||
`.row-number-state-grid.data-grid-root .ant-table-tbody-virtual-holder .ant-table-row:is(.ant-table-row-selected, .ant-table-row-selected:hover) > .ant-table-cell${fixedRowControl}`,
|
||||
`.row-number-state-grid.data-grid-root .ant-table-tbody-virtual .ant-table-row:is(.ant-table-row-selected, .ant-table-row-selected:hover) > .ant-table-cell${fixedRowControl}`,
|
||||
`.row-number-state-grid.data-grid-root .ant-table-tbody-virtual-holder-inner .ant-table-row:is(.ant-table-row-selected, .ant-table-row-selected:hover) > .ant-table-cell${fixedRowControl}`,
|
||||
`.row-number-state-grid.data-grid-root .ant-table-tbody > tr:is(.ant-table-row-selected, .ant-table-row-selected:hover) > td${fixedRowControl}`,
|
||||
];
|
||||
fixedRowControlSelectedSelectors.forEach((selector) => {
|
||||
const ruleStart = css.indexOf(selector);
|
||||
expect(ruleStart).toBeGreaterThan(transparentHover);
|
||||
const ruleEnd = css.indexOf('}', ruleStart);
|
||||
const rule = css.slice(ruleStart, ruleEnd + 1);
|
||||
expect(rule).toContain('background-color: var(--gn-bg-panel, #ffffff) !important;');
|
||||
expect(rule).toContain('background-image: linear-gradient(');
|
||||
expect(rule).toContain('var(--gn-bg-selected, rgba(34, 197, 94, 0.14))');
|
||||
});
|
||||
});
|
||||
|
||||
it('uses the table cell as the only V2 inline edit frame', () => {
|
||||
const css = readV2ThemeCss();
|
||||
const inlineEditorCss = css.slice(
|
||||
|
||||
@@ -1186,6 +1186,8 @@ describe('Sidebar locate toolbar', () => {
|
||||
expect(source).toContain('onWheelCapture={handleTreeWheel}');
|
||||
expect(source).toContain('onTouchMoveCapture={markTreeScrollActivity}');
|
||||
expect(source).toContain('setIsTreeScrolling(false)');
|
||||
expect(source).toContain('SIDEBAR_TREE_SCROLL_IDLE_DELAY_MS = 2000');
|
||||
expect(source).toContain('}, SIDEBAR_TREE_SCROLL_IDLE_DELAY_MS);');
|
||||
|
||||
const idleScrollbarCss = readCssRuleBlock(
|
||||
css,
|
||||
|
||||
@@ -335,6 +335,7 @@ const SIDEBAR_LOCATE_LOAD_WAIT_ATTEMPTS = 160;
|
||||
const SIDEBAR_CACHED_DATABASE_TREE_LIMIT = 12;
|
||||
const NACOS_SERVICES_CHANGED_EVENT = 'gonavi:nacos-services-changed';
|
||||
const SIDEBAR_GROUP_HOVER_EXPAND_DELAY_MS = 500;
|
||||
const SIDEBAR_TREE_SCROLL_IDLE_DELAY_MS = 2000;
|
||||
|
||||
type SidebarTreeDragEventLike = {
|
||||
dataTransfer?: DataTransfer | null;
|
||||
@@ -1048,7 +1049,7 @@ const Sidebar: React.FC<{
|
||||
treeScrollIdleTimerRef.current = window.setTimeout(() => {
|
||||
treeScrollIdleTimerRef.current = null;
|
||||
setIsTreeScrolling(false);
|
||||
}, 500);
|
||||
}, SIDEBAR_TREE_SCROLL_IDLE_DELAY_MS);
|
||||
}, [isV2Ui]);
|
||||
|
||||
const handleTreeWheel = useCallback((event: React.WheelEvent<HTMLDivElement>) => {
|
||||
@@ -1469,7 +1470,7 @@ const Sidebar: React.FC<{
|
||||
case 'external-sql-root':
|
||||
return <FolderOpenOutlined />;
|
||||
case 'external-sql-directory':
|
||||
return <HddOutlined />;
|
||||
return node.dataRef.directoryStatus === 'missing' ? <WarningOutlined /> : <HddOutlined />;
|
||||
case 'external-sql-folder':
|
||||
return <FolderOutlined />;
|
||||
default:
|
||||
@@ -1487,9 +1488,14 @@ const Sidebar: React.FC<{
|
||||
const buildExternalSQLRootTreeNode = useCallback((
|
||||
directories: ExternalSQLDirectory[] = externalSQLDirectories,
|
||||
directoryTrees: Record<string, ExternalSQLTreeEntry[]> = externalSQLDirectoryTreesRef.current,
|
||||
directoryStatuses: Record<string, 'missing'> = {},
|
||||
): TreeNode => decorateExternalSQLTreeNode(buildExternalSQLRootNode({
|
||||
directories,
|
||||
directoryTrees,
|
||||
directoryStatuses,
|
||||
labels: {
|
||||
missingDirectory: t('sidebar.message.external_sql_directory_not_found'),
|
||||
},
|
||||
})), [externalSQLDirectories]);
|
||||
|
||||
const refreshGlobalExternalSQLRootNode = useCallback(async (
|
||||
@@ -1498,16 +1504,22 @@ const Sidebar: React.FC<{
|
||||
) => {
|
||||
const targetDirectories = directoriesOverride || externalSQLDirectories;
|
||||
const directoryTrees: Record<string, ExternalSQLTreeEntry[]> = {};
|
||||
const directoryStatuses: Record<string, 'missing'> = {};
|
||||
await Promise.all(targetDirectories.map(async (directory) => {
|
||||
const directoryRes = await ListSQLDirectory(directory.path);
|
||||
if (!directoryRes.success) {
|
||||
message.warning({
|
||||
key: `external-sql-${directory.id}`,
|
||||
content: t('sidebar.message.external_sql_directory_read_failed', {
|
||||
name: directory.name,
|
||||
error: directoryRes.message,
|
||||
}),
|
||||
});
|
||||
const errorCode = String((directoryRes.data as Record<string, unknown> | undefined)?.errorCode || '').trim();
|
||||
if (errorCode === 'directory_not_found') {
|
||||
directoryStatuses[directory.id] = 'missing';
|
||||
} else {
|
||||
message.warning({
|
||||
key: `external-sql-${directory.id}`,
|
||||
content: t('sidebar.message.external_sql_directory_read_failed', {
|
||||
name: directory.name,
|
||||
error: directoryRes.message,
|
||||
}),
|
||||
});
|
||||
}
|
||||
directoryTrees[directory.id] = [];
|
||||
return;
|
||||
}
|
||||
@@ -1516,7 +1528,7 @@ const Sidebar: React.FC<{
|
||||
: [];
|
||||
}));
|
||||
externalSQLDirectoryTreesRef.current = directoryTrees;
|
||||
const rootNode = buildExternalSQLRootTreeNode(targetDirectories, directoryTrees);
|
||||
const rootNode = buildExternalSQLRootTreeNode(targetDirectories, directoryTrees, directoryStatuses);
|
||||
setTreeData((prev) => {
|
||||
const withoutExternalRoot = prev.filter((node) => node.type !== 'external-sql-root');
|
||||
const nextTreeData = [...withoutExternalRoot, rootNode];
|
||||
|
||||
@@ -435,6 +435,79 @@ describe('TableOverview metadata compatibility', () => {
|
||||
expect(renderedText).toContain('embeddings');
|
||||
});
|
||||
|
||||
it.each(['postgres', 'kingbase'])('shows bare table names for %s while preserving qualified operation targets', async (type) => {
|
||||
storeState.appearance = { uiVersion: 'v2', tableDoubleClickAction: 'open-data' };
|
||||
storeState.connections = [
|
||||
{
|
||||
id: 'conn-1',
|
||||
config: {
|
||||
type,
|
||||
host: '127.0.0.1',
|
||||
port: 20035,
|
||||
user: 'postgres',
|
||||
password: 'secret',
|
||||
database: 'dbx_test',
|
||||
useSSH: false,
|
||||
ssh: { host: '', port: 22, user: '', password: '', keyPath: '' },
|
||||
},
|
||||
},
|
||||
];
|
||||
backendApp.DBQuery.mockResolvedValue({
|
||||
success: true,
|
||||
data: [
|
||||
{
|
||||
table_name: 'reporting.orders',
|
||||
table_comment: 'Orders',
|
||||
table_rows: 12,
|
||||
data_length: 4096,
|
||||
index_length: 1024,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
let renderer: ReactTestRenderer;
|
||||
await act(async () => {
|
||||
renderer = create(<TableOverview tab={{
|
||||
id: 'tab-1',
|
||||
title: '表概览 - dbx_test',
|
||||
type: 'table-overview',
|
||||
connectionId: 'conn-1',
|
||||
dbName: 'dbx_test',
|
||||
schemaName: 'reporting',
|
||||
} as any} />);
|
||||
});
|
||||
await flushPromises();
|
||||
|
||||
expect(backendApp.DBQuery).toHaveBeenCalledOnce();
|
||||
expect(String(backendApp.DBQuery.mock.calls[0]?.[2] || '')).toContain("n.nspname = 'reporting'");
|
||||
|
||||
const assertBareTableName = () => {
|
||||
const renderedText = collectText(renderer!.toJSON());
|
||||
expect(renderedText).toContain('dbx_test · reporting');
|
||||
expect(renderedText).toContain('orders');
|
||||
expect(renderedText).not.toContain('reporting.orders');
|
||||
};
|
||||
|
||||
assertBareTableName();
|
||||
await act(async () => {
|
||||
renderer!.root.findByProps({ 'data-table-overview-view-mode': 'list' }).props.onClick();
|
||||
});
|
||||
assertBareTableName();
|
||||
await act(async () => {
|
||||
renderer!.root.findByProps({ 'data-table-overview-view-mode': 'table' }).props.onClick();
|
||||
});
|
||||
assertBareTableName();
|
||||
|
||||
storeState.addTab.mockClear();
|
||||
await act(async () => {
|
||||
renderer!.root.findByProps({ 'data-table-overview-row': 'reporting.orders' }).props.onDoubleClick();
|
||||
});
|
||||
expect(storeState.addTab).toHaveBeenCalledWith(expect.objectContaining({
|
||||
type: 'table',
|
||||
tableName: 'reporting.orders',
|
||||
}));
|
||||
});
|
||||
|
||||
it.each([
|
||||
{ type: 'oracle', dbName: 'APP' },
|
||||
{ type: 'trino', dbName: 'catalog' },
|
||||
|
||||
@@ -34,6 +34,7 @@ import { confirmCopyTable } from './tableCopyAction';
|
||||
import { APP_POPUP_Z_INDEX } from '../utils/overlayZIndex';
|
||||
import { formatSidebarTableTimestamp } from './sidebar/sidebarHelpers';
|
||||
import { confirmProductionMutation } from '../utils/productionRiskConfirm';
|
||||
import { stripSchemaFromTabObjectLabel } from '../utils/tabDisplay';
|
||||
|
||||
interface TableOverviewProps {
|
||||
tab: TabData;
|
||||
@@ -142,6 +143,21 @@ const getMetadataDialect = (connType: string, driver?: string, oceanBaseProtocol
|
||||
return type;
|
||||
};
|
||||
|
||||
const isSchemaScopedTableOverviewDialect = (dialect: string): boolean => [
|
||||
'postgres',
|
||||
'kingbase',
|
||||
'vastbase',
|
||||
'highgo',
|
||||
'opengauss',
|
||||
'gaussdb',
|
||||
].includes(dialect);
|
||||
|
||||
const getTableOverviewDisplayName = (dialect: string, tableName: string): string => {
|
||||
const rawName = String(tableName || '').trim();
|
||||
if (!isSchemaScopedTableOverviewDialect(dialect)) return rawName;
|
||||
return stripSchemaFromTabObjectLabel(rawName) || rawName;
|
||||
};
|
||||
|
||||
const buildTableStatusSQL = (dialect: string, dbName: string, schemaName?: string): string => {
|
||||
const escapeLiteral = (s: string) => s.replace(/'/g, "''");
|
||||
const iotdbDevicePattern = (name: string) => {
|
||||
@@ -291,6 +307,9 @@ const TableOverview: React.FC<TableOverviewProps> = ({ tab }) => {
|
||||
[connection?.config?.driver, connection?.config?.oceanBaseProtocol, connection?.config?.type]
|
||||
);
|
||||
const schemaName = String((tab as any).schemaName || '').trim();
|
||||
const overviewSchemaName = isSchemaScopedTableOverviewDialect(metadataDialect)
|
||||
? (schemaName || 'public')
|
||||
: '';
|
||||
const supportsDesignWrite = !getDataSourceCapabilities(connection?.config).forceReadOnlyStructureDesigner;
|
||||
const supportsCopyTable = getDataSourceCapabilities(connection?.config).supportsCopyTable;
|
||||
const autoFetchVisible = useAutoFetchVisibility();
|
||||
@@ -1186,9 +1205,9 @@ const TableOverview: React.FC<TableOverviewProps> = ({ tab }) => {
|
||||
>
|
||||
<div className={isV2Ui ? 'gn-v2-table-card-name' : undefined} style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 8 }}>
|
||||
<TableOutlined style={{ fontSize: 14, color: accentColor }} />
|
||||
<Tooltip title={table.name} mouseEnterDelay={0.4}>
|
||||
<Tooltip title={getTableOverviewDisplayName(metadataDialect, table.name)} mouseEnterDelay={0.4}>
|
||||
<span style={{ fontSize: 13, fontWeight: 600, color: textPrimary, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', flex: 1, display: 'block' }}>
|
||||
{table.name}
|
||||
{getTableOverviewDisplayName(metadataDialect, table.name)}
|
||||
</span>
|
||||
</Tooltip>
|
||||
</div>
|
||||
@@ -1230,6 +1249,7 @@ const TableOverview: React.FC<TableOverviewProps> = ({ tab }) => {
|
||||
|
||||
const renderListTable = (table: TableStatRow) => {
|
||||
const combinedSize = getCombinedTableSize(table);
|
||||
const displayName = getTableOverviewDisplayName(metadataDialect, table.name);
|
||||
const sizeRatio = maxCombinedSize > 0 && hasKnownTableSize(table) ? combinedSize / maxCombinedSize : 0;
|
||||
const fillWidth = maxCombinedSize > 0 && hasKnownTableSize(table) ? `${Math.max(10, Math.round(sizeRatio * 100))}%` : '0%';
|
||||
const fillColor = isV2Ui
|
||||
@@ -1285,9 +1305,9 @@ const TableOverview: React.FC<TableOverviewProps> = ({ tab }) => {
|
||||
<div style={{ minWidth: 0, flex: '1 1 320px' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8, minWidth: 0 }}>
|
||||
<TableOutlined style={{ fontSize: 13, color: accentColor, flexShrink: 0 }} />
|
||||
<Tooltip title={table.name} mouseEnterDelay={0.4}>
|
||||
<Tooltip title={displayName} mouseEnterDelay={0.4}>
|
||||
<span style={{ color: textPrimary, fontWeight: 600, fontSize: 13, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
|
||||
{table.name}
|
||||
{displayName}
|
||||
</span>
|
||||
</Tooltip>
|
||||
{table.engine && (
|
||||
@@ -1423,6 +1443,7 @@ const TableOverview: React.FC<TableOverviewProps> = ({ tab }) => {
|
||||
);
|
||||
|
||||
const renderCompactTableRow = (table: TableStatRow) => {
|
||||
const displayName = getTableOverviewDisplayName(metadataDialect, table.name);
|
||||
const content = (
|
||||
<div
|
||||
className="gn-table-overview-compact-row"
|
||||
@@ -1447,9 +1468,9 @@ const TableOverview: React.FC<TableOverviewProps> = ({ tab }) => {
|
||||
}),
|
||||
}}
|
||||
>
|
||||
<div className="gn-table-overview-compact-name" role="cell" title={table.name}>
|
||||
<div className="gn-table-overview-compact-name" role="cell" title={displayName}>
|
||||
<TableOutlined aria-hidden="true" />
|
||||
<span>{table.name}</span>
|
||||
<span>{displayName}</span>
|
||||
</div>
|
||||
<div className="gn-table-overview-compact-cell" role="cell" title={table.comment || undefined}>{table.comment || '—'}</div>
|
||||
<div className="gn-table-overview-compact-cell gn-table-overview-compact-number" role="cell" title={table.rows >= 0 ? String(table.rows) : undefined}>{formatRows(table.rows)}</div>
|
||||
@@ -1508,7 +1529,9 @@ const TableOverview: React.FC<TableOverviewProps> = ({ tab }) => {
|
||||
<span className={isV2Ui ? 'gn-v2-table-overview-icon' : undefined}>
|
||||
<DatabaseOutlined style={{ fontSize: 16, color: isV2Ui ? undefined : accentColor }} />
|
||||
</span>
|
||||
<span className={isV2Ui ? 'gn-v2-table-overview-title' : undefined} style={{ fontSize: 14, fontWeight: 600, color: textPrimary }}>{tab.dbName}</span>
|
||||
<span className={isV2Ui ? 'gn-v2-table-overview-title' : undefined} style={{ fontSize: 14, fontWeight: 600, color: textPrimary }}>
|
||||
{[tab.dbName, overviewSchemaName].filter(Boolean).join(' · ')}
|
||||
</span>
|
||||
<span className={isV2Ui ? 'gn-table-overview-summary gn-v2-table-overview-summary' : 'gn-table-overview-summary'} style={{ fontSize: 12, color: textMuted }}>
|
||||
{renderToolbarSummary()}
|
||||
</span>
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import {
|
||||
ATLAS_CLOUD_BASE_URL,
|
||||
ATLAS_CLOUD_DEFAULT_MODEL,
|
||||
QWEN_CODING_PLAN_ANTHROPIC_BASE_URL,
|
||||
resolvePresetBaseURL,
|
||||
resolvePresetTransport,
|
||||
@@ -53,6 +55,21 @@ describe('aiSettingsModalConfig', () => {
|
||||
expect(preset.key).toBe('cursor');
|
||||
});
|
||||
|
||||
it('exposes and recognizes the Atlas Cloud preset', () => {
|
||||
const preset = findPreset('atlascloud');
|
||||
|
||||
expect(preset).toMatchObject({
|
||||
label: 'Atlas Cloud',
|
||||
backendType: 'openai',
|
||||
defaultBaseUrl: ATLAS_CLOUD_BASE_URL,
|
||||
defaultModel: ATLAS_CLOUD_DEFAULT_MODEL,
|
||||
});
|
||||
expect(matchProviderPreset({
|
||||
type: 'openai',
|
||||
baseUrl: ATLAS_CLOUD_BASE_URL,
|
||||
}).key).toBe('atlascloud');
|
||||
});
|
||||
|
||||
it('supports every configured MiniMax region and protocol endpoint', () => {
|
||||
const preset = findPreset('minimax');
|
||||
|
||||
@@ -125,6 +142,7 @@ describe('aiSettingsModalConfig', () => {
|
||||
});
|
||||
|
||||
it('keeps the provider preset list available for the settings modal', () => {
|
||||
expect(PROVIDER_PRESETS.some((item) => item.key === 'atlascloud')).toBe(true);
|
||||
expect(PROVIDER_PRESETS.some((item) => item.key === 'codex')).toBe(true);
|
||||
expect(PROVIDER_PRESETS.some((item) => item.key === 'claude-subscription')).toBe(true);
|
||||
expect(PROVIDER_PRESETS.some((item) => item.key === 'codebuddy')).toBe(true);
|
||||
|
||||
@@ -16,6 +16,8 @@ import type {
|
||||
AIUserPromptSettings,
|
||||
} from '../../types';
|
||||
import {
|
||||
ATLAS_CLOUD_BASE_URL,
|
||||
ATLAS_CLOUD_DEFAULT_MODEL,
|
||||
QWEN_BAILIAN_ANTHROPIC_BASE_URL,
|
||||
QWEN_CODING_PLAN_ANTHROPIC_BASE_URL,
|
||||
QWEN_CODING_PLAN_MODELS,
|
||||
@@ -50,6 +52,7 @@ export const MINIMAX_ENDPOINTS: ProviderPresetEndpoint[] = [
|
||||
|
||||
export const PROVIDER_PRESETS: ProviderPreset[] = [
|
||||
{ key: 'openai', label: 'OpenAI', labelKey: 'ai_settings.provider_preset.openai.label', icon: <ApiOutlined />, desc: 'GPT-5.4 / 5.3 series', descKey: 'ai_settings.provider_preset.openai.desc', color: '#10b981', backendType: 'openai', defaultBaseUrl: 'https://api.openai.com/v1', defaultModel: 'gpt-4o', models: [] },
|
||||
{ key: 'atlascloud', label: 'Atlas Cloud', labelKey: 'ai_settings.provider_preset.atlascloud.label', icon: <CloudOutlined />, desc: 'Qwen3.8 Max / OpenAI-compatible', descKey: 'ai_settings.provider_preset.atlascloud.desc', color: '#0891b2', backendType: 'openai', defaultBaseUrl: ATLAS_CLOUD_BASE_URL, defaultModel: ATLAS_CLOUD_DEFAULT_MODEL, models: [] },
|
||||
{ key: 'codex', label: 'Codex Subscription', labelKey: 'ai_settings.provider_preset.codex.label', icon: <ApiOutlined />, desc: 'Local Codex CLI / ChatGPT subscription login', descKey: 'ai_settings.provider_preset.codex.desc', color: '#111827', backendType: 'custom', fixedApiFormat: 'codex-cli', authMode: 'local-cli', defaultBaseUrl: '', defaultModel: '', models: [] },
|
||||
{ key: 'deepseek', label: 'DeepSeek', labelKey: 'ai_settings.provider_preset.deepseek.label', icon: <ThunderboltOutlined />, desc: 'DeepSeek-V4 / R1', descKey: 'ai_settings.provider_preset.deepseek.desc', color: '#3b82f6', backendType: 'openai', defaultBaseUrl: 'https://api.deepseek.com/v1', defaultModel: 'deepseek-chat', models: [] },
|
||||
{ key: 'qwen-bailian', label: 'Qwen (Bailian General)', labelKey: 'ai_settings.provider_preset.qwen_bailian.label', icon: <CloudOutlined />, desc: 'Bailian Anthropic-compatible endpoint / remote model list', descKey: 'ai_settings.provider_preset.qwen_bailian.desc', color: '#6366f1', backendType: 'anthropic', defaultBaseUrl: QWEN_BAILIAN_ANTHROPIC_BASE_URL, defaultModel: '', models: [] },
|
||||
|
||||
@@ -426,6 +426,15 @@ export const buildDataGridCssText = ({
|
||||
|
||||
.${gridId}.data-grid-root .ant-table-tbody .ant-table-row:hover > .ant-table-cell { background-color: transparent !important; }
|
||||
|
||||
/* 固定行控制列(序号/单选/多选)在整行 hover 透明时仍保持实心底。 */
|
||||
.${gridId}.data-grid-root .ant-table-tbody-virtual-holder .ant-table-row:hover > .ant-table-cell:is(.data-grid-row-number-cell, .ant-table-selection-column),
|
||||
.${gridId}.data-grid-root .ant-table-tbody-virtual .ant-table-row:hover > .ant-table-cell:is(.data-grid-row-number-cell, .ant-table-selection-column),
|
||||
.${gridId}.data-grid-root .ant-table-tbody-virtual-holder-inner .ant-table-row:hover > .ant-table-cell:is(.data-grid-row-number-cell, .ant-table-selection-column),
|
||||
.${gridId}.data-grid-root .ant-table-tbody > tr:hover > td:is(.data-grid-row-number-cell, .ant-table-selection-column) {
|
||||
background: var(--gn-bg-panel, ${bgContent}) !important;
|
||||
background-image: none !important;
|
||||
}
|
||||
|
||||
/*
|
||||
* 行选中:整行统一绿色。
|
||||
* 关键:virtual-holder 下有
|
||||
@@ -457,6 +466,18 @@ export const buildDataGridCssText = ({
|
||||
background-image: none !important;
|
||||
}
|
||||
|
||||
/* 选中行同样覆盖固定行控制列,保持与整行一致的选中底色。 */
|
||||
.${gridId}.data-grid-root .ant-table-tbody-virtual-holder .ant-table-row:is(.ant-table-row-selected, .ant-table-row-selected:hover) > .ant-table-cell:is(.data-grid-row-number-cell, .ant-table-selection-column),
|
||||
.${gridId}.data-grid-root .ant-table-tbody-virtual .ant-table-row:is(.ant-table-row-selected, .ant-table-row-selected:hover) > .ant-table-cell:is(.data-grid-row-number-cell, .ant-table-selection-column),
|
||||
.${gridId}.data-grid-root .ant-table-tbody-virtual-holder-inner .ant-table-row:is(.ant-table-row-selected, .ant-table-row-selected:hover) > .ant-table-cell:is(.data-grid-row-number-cell, .ant-table-selection-column),
|
||||
.${gridId}.data-grid-root .ant-table-tbody > tr:is(.ant-table-row-selected, .ant-table-row-selected:hover) > td:is(.data-grid-row-number-cell, .ant-table-selection-column) {
|
||||
background-color: var(--gn-bg-panel, ${bgContent}) !important;
|
||||
background-image: linear-gradient(
|
||||
var(--gn-bg-selected, rgba(34, 197, 94, 0.14)),
|
||||
var(--gn-bg-selected, rgba(34, 197, 94, 0.14))
|
||||
) !important;
|
||||
}
|
||||
|
||||
.${gridId} .row-added td,
|
||||
|
||||
.${gridId} .row-added > .ant-table-cell { background-color: ${rowAddedBg} !important; color: ${darkMode ? '#e6fffb' : 'inherit'}; }
|
||||
|
||||
@@ -7,6 +7,7 @@ import { noAutoCapInputProps } from '../../utils/inputAutoCap';
|
||||
import {
|
||||
buildExternalSQLDirectoryId,
|
||||
buildExternalSQLTabId,
|
||||
findExternalSQLDirectoriesByPath,
|
||||
moveExternalSQLFileBindings,
|
||||
normalizeExternalSQLPath,
|
||||
removeExternalSQLFileBindings,
|
||||
@@ -913,9 +914,9 @@ export const useSidebarExternalSqlWorkflow = ({
|
||||
}
|
||||
if (externalSQLFileTarget?.type === 'external-sql-directory') {
|
||||
const nextName = String(payload.name || name).trim();
|
||||
const previousDirectoryPath = normalizeExternalSQLPath(directoryPath);
|
||||
const matchingDirectories = externalSQLDirectories.filter(
|
||||
(directory) => normalizeExternalSQLPath(directory.path) === previousDirectoryPath,
|
||||
const matchingDirectories = findExternalSQLDirectoriesByPath(
|
||||
externalSQLDirectories,
|
||||
directoryPath,
|
||||
);
|
||||
if (!nextPath || matchingDirectories.length === 0) {
|
||||
message.error(t('sidebar.message.external_sql_directory_rename_sync_failed'));
|
||||
@@ -1020,9 +1021,9 @@ export const useSidebarExternalSqlWorkflow = ({
|
||||
removeRecentSQLFilesByDirectory(directoryPath);
|
||||
|
||||
if (node?.type === 'external-sql-directory') {
|
||||
const normalizedDirectoryPath = normalizeExternalSQLPath(directoryPath);
|
||||
const matchingDirectories = externalSQLDirectories.filter(
|
||||
(directory) => normalizeExternalSQLPath(directory.path) === normalizedDirectoryPath,
|
||||
const matchingDirectories = findExternalSQLDirectoriesByPath(
|
||||
externalSQLDirectories,
|
||||
directoryPath,
|
||||
);
|
||||
if (matchingDirectories.length > 0) {
|
||||
const matchingDirectoryIds = new Set(matchingDirectories.map((directory) => directory.id));
|
||||
@@ -1087,13 +1088,18 @@ export const useSidebarExternalSqlWorkflow = ({
|
||||
};
|
||||
|
||||
const handleRemoveExternalSQLDirectory = async (node: any) => {
|
||||
const directoryId = String(node?.dataRef?.id || '').trim();
|
||||
if (!directoryId) {
|
||||
const directoryPath = String(node?.dataRef?.path || '').trim();
|
||||
if (!directoryPath) {
|
||||
message.error(t('sidebar.message.external_sql_directory_not_found'));
|
||||
return;
|
||||
}
|
||||
deleteExternalSQLDirectory(directoryId);
|
||||
const nextDirectories = externalSQLDirectories.filter((item) => item.id !== directoryId);
|
||||
const matchingDirectories = findExternalSQLDirectoriesByPath(
|
||||
externalSQLDirectories,
|
||||
directoryPath,
|
||||
);
|
||||
matchingDirectories.forEach((directory) => deleteExternalSQLDirectory(directory.id));
|
||||
const matchingDirectoryIds = new Set(matchingDirectories.map((directory) => directory.id));
|
||||
const nextDirectories = externalSQLDirectories.filter((item) => !matchingDirectoryIds.has(item.id));
|
||||
await refreshGlobalExternalSQLRootNode(false, nextDirectories);
|
||||
message.success(t('sidebar.message.external_sql_directory_removed'));
|
||||
};
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import type { AIProviderAuthMode, AIProviderType } from '../types';
|
||||
import {
|
||||
ATLAS_CLOUD_BASE_URL,
|
||||
LEGACY_QWEN_CODING_PLAN_OPENAI_BASE_URL,
|
||||
QWEN_BAILIAN_ANTHROPIC_BASE_URL,
|
||||
QWEN_BAILIAN_MODELS_BASE_URL,
|
||||
@@ -24,6 +25,7 @@ type PresetMatcher = {
|
||||
|
||||
const PRESETS: PresetMatcher[] = [
|
||||
{ key: 'openai', backendType: 'openai', defaultBaseUrl: 'https://api.openai.com/v1' },
|
||||
{ key: 'atlascloud', backendType: 'openai', defaultBaseUrl: ATLAS_CLOUD_BASE_URL },
|
||||
{ key: 'qwen-bailian', backendType: 'anthropic', defaultBaseUrl: QWEN_BAILIAN_ANTHROPIC_BASE_URL },
|
||||
{
|
||||
key: 'qwen-coding-plan',
|
||||
@@ -212,6 +214,13 @@ describe('ai provider preset helpers', () => {
|
||||
});
|
||||
|
||||
describe('resolveProviderPresetKey', () => {
|
||||
it('recognizes Atlas Cloud by its OpenAI-compatible endpoint', () => {
|
||||
expect(resolveProviderPresetKey({
|
||||
type: 'openai',
|
||||
baseUrl: `${ATLAS_CLOUD_BASE_URL}/`,
|
||||
}, PRESETS, 'custom')).toBe('atlascloud');
|
||||
});
|
||||
|
||||
it('不会把自定义 OpenAI 端点误识别成千问 Coding Plan', () => {
|
||||
const key = resolveProviderPresetKey(
|
||||
{
|
||||
|
||||
@@ -5,6 +5,8 @@ export const LEGACY_QWEN_CODING_PLAN_OPENAI_BASE_URL = 'https://coding.dashscope
|
||||
export const QWEN_BAILIAN_ANTHROPIC_BASE_URL = 'https://dashscope.aliyuncs.com/apps/anthropic';
|
||||
export const QWEN_CODING_PLAN_ANTHROPIC_BASE_URL = 'https://coding.dashscope.aliyuncs.com/apps/anthropic';
|
||||
export const QWEN_BAILIAN_MODELS_BASE_URL = LEGACY_QWEN_BAILIAN_OPENAI_BASE_URL;
|
||||
export const ATLAS_CLOUD_BASE_URL = 'https://api.atlascloud.ai/v1';
|
||||
export const ATLAS_CLOUD_DEFAULT_MODEL = 'qwen/qwen3.8-max';
|
||||
|
||||
export const QWEN_CODING_PLAN_MODELS = [
|
||||
'qwen3.5-plus',
|
||||
|
||||
@@ -4,6 +4,7 @@ import type { ExternalSQLDirectory, ExternalSQLTreeEntry } from '../types';
|
||||
import {
|
||||
buildExternalSQLRootNode,
|
||||
buildExternalSQLTabId,
|
||||
findExternalSQLDirectoriesByPath,
|
||||
moveExternalSQLFileBindings,
|
||||
removeExternalSQLFileBindings,
|
||||
resolveExternalSQLFileBinding,
|
||||
@@ -60,6 +61,32 @@ describe('externalSqlTree helpers', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('marks a missing directory instead of presenting it as empty', () => {
|
||||
const node = buildExternalSQLRootNode({
|
||||
directories: [{
|
||||
id: 'dir-missing',
|
||||
name: 'archived scripts',
|
||||
path: 'D:/sql/missing',
|
||||
createdAt: 1,
|
||||
}],
|
||||
directoryTrees: {},
|
||||
directoryStatuses: {
|
||||
'dir-missing': 'missing',
|
||||
},
|
||||
labels: {
|
||||
missingDirectory: 'Missing',
|
||||
},
|
||||
});
|
||||
|
||||
expect(node.children?.[0]).toMatchObject({
|
||||
title: 'archived scripts (Missing)',
|
||||
isLeaf: true,
|
||||
dataRef: {
|
||||
directoryStatus: 'missing',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('uses localized root and directory fallback labels while preserving explicit and path segment names', () => {
|
||||
const directories: ExternalSQLDirectory[] = [
|
||||
{
|
||||
@@ -237,6 +264,38 @@ describe('externalSqlTree helpers', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('finds every binding for the same directory path', () => {
|
||||
const matching = findExternalSQLDirectoriesByPath([
|
||||
{
|
||||
id: 'dir-orders',
|
||||
name: 'scripts',
|
||||
path: 'D:/sql/shared',
|
||||
connectionId: 'connection-1',
|
||||
dbName: 'orders',
|
||||
createdAt: 1,
|
||||
},
|
||||
{
|
||||
id: 'dir-reporting',
|
||||
name: 'scripts',
|
||||
path: 'D:\\sql\\shared',
|
||||
connectionId: 'connection-2',
|
||||
dbName: 'reporting',
|
||||
createdAt: 2,
|
||||
},
|
||||
{
|
||||
id: 'dir-other',
|
||||
name: 'other',
|
||||
path: 'D:/sql/other',
|
||||
createdAt: 3,
|
||||
},
|
||||
], 'D:/sql/shared');
|
||||
|
||||
expect(matching.map((directory) => directory.id)).toEqual([
|
||||
'dir-orders',
|
||||
'dir-reporting',
|
||||
]);
|
||||
});
|
||||
|
||||
it('updates file bindings when a file or containing folder moves and removes deleted subtrees', () => {
|
||||
const directory: ExternalSQLDirectory = {
|
||||
id: 'dir-1',
|
||||
|
||||
@@ -25,12 +25,14 @@ type BuildExternalSQLRootNodeParams = {
|
||||
dbName?: string;
|
||||
directories: ExternalSQLDirectory[];
|
||||
directoryTrees: Record<string, ExternalSQLTreeEntry[]>;
|
||||
directoryStatuses?: Record<string, 'missing'>;
|
||||
labels?: Partial<ExternalSQLTreeLabels>;
|
||||
};
|
||||
|
||||
export type ExternalSQLTreeLabels = {
|
||||
root: string;
|
||||
directoryFallback: string;
|
||||
missingDirectory: string;
|
||||
};
|
||||
|
||||
export const normalizeExternalSQLPath = (value: string): string =>
|
||||
@@ -98,6 +100,17 @@ export const resolveExternalSQLFileBinding = (
|
||||
return undefined;
|
||||
};
|
||||
|
||||
export const findExternalSQLDirectoriesByPath = (
|
||||
directories: ExternalSQLDirectory[],
|
||||
directoryPath: string,
|
||||
): ExternalSQLDirectory[] => {
|
||||
const normalizedDirectoryPath = normalizeExternalSQLPath(directoryPath);
|
||||
if (!normalizedDirectoryPath) return [];
|
||||
return directories.filter(
|
||||
(directory) => normalizeExternalSQLPath(directory.path) === normalizedDirectoryPath,
|
||||
);
|
||||
};
|
||||
|
||||
export const setExternalSQLFileBinding = (
|
||||
directory: ExternalSQLDirectory,
|
||||
filePath: string,
|
||||
@@ -174,12 +187,15 @@ export const removeExternalSQLFileBindings = (
|
||||
const DEFAULT_EXTERNAL_SQL_TREE_LABELS: ExternalSQLTreeLabels = {
|
||||
root: 'External SQL files',
|
||||
directoryFallback: 'SQL directory',
|
||||
missingDirectory: 'Missing',
|
||||
};
|
||||
|
||||
const resolveExternalSQLTreeLabels = (labels?: Partial<ExternalSQLTreeLabels>): ExternalSQLTreeLabels => ({
|
||||
root: String(labels?.root || '').trim() || DEFAULT_EXTERNAL_SQL_TREE_LABELS.root,
|
||||
directoryFallback:
|
||||
String(labels?.directoryFallback || '').trim() || DEFAULT_EXTERNAL_SQL_TREE_LABELS.directoryFallback,
|
||||
missingDirectory:
|
||||
String(labels?.missingDirectory || '').trim() || DEFAULT_EXTERNAL_SQL_TREE_LABELS.missingDirectory,
|
||||
});
|
||||
|
||||
const resolveDirectoryDisplayName = (
|
||||
@@ -289,6 +305,7 @@ export const buildExternalSQLRootNode = ({
|
||||
dbName = '',
|
||||
directories,
|
||||
directoryTrees,
|
||||
directoryStatuses = {},
|
||||
labels,
|
||||
}: BuildExternalSQLRootNodeParams): ExternalSQLTreeNode => {
|
||||
const resolvedLabels = resolveExternalSQLTreeLabels(labels);
|
||||
@@ -303,6 +320,7 @@ export const buildExternalSQLRootNode = ({
|
||||
// must retain its own target so every nested SQL file opens against that DB.
|
||||
const directoryConnectionId = String(directory.connectionId || '').trim() || connectionId;
|
||||
const directoryDbName = String(directory.dbName || '').trim() || dbName;
|
||||
const directoryStatus = directoryStatuses[directory.id];
|
||||
const directoryChildren = mapExternalSQLTreeEntries(directoryTrees[directory.id] || [], {
|
||||
connectionId: directoryConnectionId,
|
||||
dbName: directoryDbName,
|
||||
@@ -310,14 +328,18 @@ export const buildExternalSQLRootNode = ({
|
||||
directoryId: directory.id,
|
||||
fileBindings: directory.fileBindings,
|
||||
});
|
||||
const directoryTitle = resolveDirectoryDisplayName(directory, resolvedLabels);
|
||||
return {
|
||||
title: resolveDirectoryDisplayName(directory, resolvedLabels),
|
||||
title: directoryStatus === 'missing'
|
||||
? `${directoryTitle} (${resolvedLabels.missingDirectory})`
|
||||
: directoryTitle,
|
||||
key: buildExternalSQLNodeKey('external-sql-directory', directory.id),
|
||||
type: 'external-sql-directory' as const,
|
||||
isLeaf: directoryChildren.length === 0,
|
||||
children: directoryChildren.length > 0 ? directoryChildren : undefined,
|
||||
dataRef: {
|
||||
...directory,
|
||||
...(directoryStatus ? { directoryStatus } : {}),
|
||||
connectionId: directoryConnectionId,
|
||||
dbName: directoryDbName,
|
||||
dbNodeKey,
|
||||
|
||||
@@ -248,7 +248,7 @@ describe('tabDisplay', () => {
|
||||
layout: 'single',
|
||||
primaryElements: ['object', 'schema', 'host'],
|
||||
secondaryElements: [],
|
||||
})).toBe('andon_events SCHEMA:ldf_server 192.168.10.8');
|
||||
})).toBe('andon_events ldf_server 192.168.10.8');
|
||||
});
|
||||
|
||||
it('builds the default configurable model with the object on the primary line', () => {
|
||||
@@ -404,8 +404,8 @@ describe('tabDisplay', () => {
|
||||
|
||||
expect(model.layout).toBe('double');
|
||||
expect(model.primaryText).toBe('TABLE events');
|
||||
expect(model.secondaryText).toBe('[PROD]·analytics·SCHEMA:reporting·10.0.0.9');
|
||||
expect(model.fullTitle).toBe('TABLE events · [PROD]·analytics·SCHEMA:reporting·10.0.0.9');
|
||||
expect(model.secondaryText).toBe('[PROD]·analytics·reporting·10.0.0.9');
|
||||
expect(model.fullTitle).toBe('TABLE events · [PROD]·analytics·reporting·10.0.0.9');
|
||||
});
|
||||
|
||||
it('uses explicit schema metadata for unqualified table names', () => {
|
||||
@@ -426,7 +426,7 @@ describe('tabDisplay', () => {
|
||||
});
|
||||
|
||||
expect(model.primaryText).toBe('events');
|
||||
expect(model.secondaryText).toBe('SCHEMA:reporting');
|
||||
expect(model.secondaryText).toBe('reporting');
|
||||
});
|
||||
|
||||
it('sanitizes tab display settings with fallback defaults', () => {
|
||||
|
||||
@@ -600,7 +600,6 @@ const getTabDisplayElementValue = (
|
||||
const formatTabDisplayPartValue = (key: TabDisplayElementKey, value: string): string => {
|
||||
if (!value) return '';
|
||||
if (key === 'connection') return `[${value}]`;
|
||||
if (key === 'schema') return `SCHEMA:${value}`;
|
||||
return value;
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user