mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-07-09 22:42:55 +08:00
Compare commits
14 Commits
fix/ob-ora
...
fix/ob-ora
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
93a3240862 | ||
|
|
1116a3736b | ||
|
|
19af57e46b | ||
|
|
401c33c505 | ||
|
|
8c968c1a87 | ||
|
|
4fee58999d | ||
|
|
bd0752df96 | ||
|
|
47a15a6674 | ||
|
|
2a64bad76e | ||
|
|
7ee8698165 | ||
|
|
9ae35e5fec | ||
|
|
b618927454 | ||
|
|
e8ad34d2f0 | ||
|
|
007fdc9a5d |
@@ -1,6 +1,6 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Button, InputNumber, Pagination, Select } from 'antd';
|
import { Button, InputNumber, Pagination, Select } from 'antd';
|
||||||
import { LeftOutlined, RightOutlined } from '@ant-design/icons';
|
import { CloseOutlined, LeftOutlined, RightOutlined, VerticalAlignBottomOutlined } from '@ant-design/icons';
|
||||||
import { t as defaultTranslate, type I18nParams } from '../i18n';
|
import { t as defaultTranslate, type I18nParams } from '../i18n';
|
||||||
|
|
||||||
interface DataGridPaginationState {
|
interface DataGridPaginationState {
|
||||||
@@ -26,12 +26,31 @@ export interface DataGridPaginationBarProps {
|
|||||||
paginationPageText: string;
|
paginationPageText: string;
|
||||||
paginationPageSizeOptions: string[];
|
paginationPageSizeOptions: string[];
|
||||||
showKnownPageCount: boolean;
|
showKnownPageCount: boolean;
|
||||||
|
manualTotalCountAvailable?: boolean;
|
||||||
|
totalCountLoading?: boolean;
|
||||||
onPageChange?: (page: number, size: number) => void;
|
onPageChange?: (page: number, size: number) => void;
|
||||||
onPageSizeChange: (value: string) => void;
|
onPageSizeChange: (value: string) => void;
|
||||||
onV2PageStep: (direction: 'previous' | 'next') => void;
|
onV2PageStep: (direction: 'previous' | 'next') => void;
|
||||||
|
onToggleTotalCount?: () => void;
|
||||||
translate?: DataGridPaginationTranslate;
|
translate?: DataGridPaginationTranslate;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const findToolbarTotalCountButton = (
|
||||||
|
trigger: HTMLElement,
|
||||||
|
labels: string[],
|
||||||
|
): HTMLButtonElement | null => {
|
||||||
|
const root = trigger.closest('.data-grid-root') || trigger.ownerDocument?.body;
|
||||||
|
if (!root) return null;
|
||||||
|
const normalizedLabels = labels.map((label) => String(label || '').trim()).filter(Boolean);
|
||||||
|
const buttons = Array.from(root.querySelectorAll('button')) as HTMLButtonElement[];
|
||||||
|
return buttons.find((button) => {
|
||||||
|
if (button === trigger) return false;
|
||||||
|
if (button.disabled) return false;
|
||||||
|
const text = String(button.textContent || '').replace(/\s+/g, ' ').trim();
|
||||||
|
return normalizedLabels.some((label) => text === label || text.includes(label));
|
||||||
|
}) || null;
|
||||||
|
};
|
||||||
|
|
||||||
const DataGridPaginationBar: React.FC<DataGridPaginationBarProps> = ({
|
const DataGridPaginationBar: React.FC<DataGridPaginationBarProps> = ({
|
||||||
isV2Ui,
|
isV2Ui,
|
||||||
pagination,
|
pagination,
|
||||||
@@ -42,9 +61,12 @@ const DataGridPaginationBar: React.FC<DataGridPaginationBarProps> = ({
|
|||||||
paginationPageText,
|
paginationPageText,
|
||||||
paginationPageSizeOptions,
|
paginationPageSizeOptions,
|
||||||
showKnownPageCount,
|
showKnownPageCount,
|
||||||
|
manualTotalCountAvailable = false,
|
||||||
|
totalCountLoading = false,
|
||||||
onPageChange,
|
onPageChange,
|
||||||
onPageSizeChange,
|
onPageSizeChange,
|
||||||
onV2PageStep,
|
onV2PageStep,
|
||||||
|
onToggleTotalCount,
|
||||||
translate = defaultTranslate,
|
translate = defaultTranslate,
|
||||||
}) => {
|
}) => {
|
||||||
const [jumpPage, setJumpPage] = React.useState<number | null>(pagination?.current ?? null);
|
const [jumpPage, setJumpPage] = React.useState<number | null>(pagination?.current ?? null);
|
||||||
@@ -58,6 +80,37 @@ const DataGridPaginationBar: React.FC<DataGridPaginationBarProps> = ({
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const countTotalLabel = translate('data_grid.toolbar.count_total');
|
||||||
|
const cancelCountLabel = translate('data_grid.toolbar.cancel_count');
|
||||||
|
const effectiveTotalCountLoading = totalCountLoading || Boolean(pagination.totalCountLoading);
|
||||||
|
const shouldShowTotalCountButton = Boolean(
|
||||||
|
onToggleTotalCount
|
||||||
|
|| manualTotalCountAvailable
|
||||||
|
|| pagination.totalCountLoading
|
||||||
|
|| pagination.totalKnown === false,
|
||||||
|
);
|
||||||
|
const handleToggleTotalCount = (event: React.MouseEvent<HTMLButtonElement>) => {
|
||||||
|
if (onToggleTotalCount) {
|
||||||
|
onToggleTotalCount();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Backward-compatible bridge for existing DataGridShell callers: the top toolbar already owns
|
||||||
|
// the total-count handler, but it can be horizontally scrolled out of view on large toolbars.
|
||||||
|
// Trigger that existing button so the pagination bar can expose the action without duplicating data-flow state.
|
||||||
|
const toolbarButton = findToolbarTotalCountButton(event.currentTarget, [countTotalLabel, cancelCountLabel]);
|
||||||
|
toolbarButton?.click();
|
||||||
|
};
|
||||||
|
const totalCountButton = shouldShowTotalCountButton ? (
|
||||||
|
<Button
|
||||||
|
data-grid-pagination-total-count="true"
|
||||||
|
size="small"
|
||||||
|
icon={effectiveTotalCountLoading ? <CloseOutlined /> : <VerticalAlignBottomOutlined />}
|
||||||
|
onClick={handleToggleTotalCount}
|
||||||
|
>
|
||||||
|
{effectiveTotalCountLoading ? cancelCountLabel : countTotalLabel}
|
||||||
|
</Button>
|
||||||
|
) : null;
|
||||||
|
|
||||||
const maxJumpPage = showKnownPageCount ? Math.max(1, paginationTotalPages) : null;
|
const maxJumpPage = showKnownPageCount ? Math.max(1, paginationTotalPages) : null;
|
||||||
const normalizedJumpPage = Number.isFinite(Number(jumpPage)) && Number(jumpPage) > 0
|
const normalizedJumpPage = Number.isFinite(Number(jumpPage)) && Number(jumpPage) > 0
|
||||||
? (maxJumpPage !== null
|
? (maxJumpPage !== null
|
||||||
@@ -132,6 +185,7 @@ const DataGridPaginationBar: React.FC<DataGridPaginationBarProps> = ({
|
|||||||
<div className="data-grid-pagination-summary" aria-live="polite">
|
<div className="data-grid-pagination-summary" aria-live="polite">
|
||||||
<span className="data-grid-pagination-summary-value">{paginationV2SummaryText}</span>
|
<span className="data-grid-pagination-summary-value">{paginationV2SummaryText}</span>
|
||||||
</div>
|
</div>
|
||||||
|
{totalCountButton}
|
||||||
<Button
|
<Button
|
||||||
data-grid-v2-pagination-prev="true"
|
data-grid-v2-pagination-prev="true"
|
||||||
size="small"
|
size="small"
|
||||||
@@ -174,6 +228,7 @@ const DataGridPaginationBar: React.FC<DataGridPaginationBarProps> = ({
|
|||||||
<span className="data-grid-pagination-kicker">{translate('data_grid.pagination.result_set')}</span>
|
<span className="data-grid-pagination-kicker">{translate('data_grid.pagination.result_set')}</span>
|
||||||
<span className="data-grid-pagination-summary-value">{paginationSummaryText}</span>
|
<span className="data-grid-pagination-summary-value">{paginationSummaryText}</span>
|
||||||
</div>
|
</div>
|
||||||
|
{totalCountButton}
|
||||||
{showSequentialPagination ? sequentialPaginationControl : (
|
{showSequentialPagination ? sequentialPaginationControl : (
|
||||||
<Pagination
|
<Pagination
|
||||||
current={pagination.current}
|
current={pagination.current}
|
||||||
|
|||||||
@@ -24,6 +24,140 @@ const sameEditorPosition = (left: any, right: any): boolean => (
|
|||||||
&& Number(left?.column) === Number(right?.column)
|
&& Number(left?.column) === Number(right?.column)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const stripSqlIdentifierQuotes = (value: string): string => {
|
||||||
|
const text = String(value || '').trim();
|
||||||
|
if (!text) return '';
|
||||||
|
if ((text.startsWith('`') && text.endsWith('`'))
|
||||||
|
|| (text.startsWith('"') && text.endsWith('"'))
|
||||||
|
|| (text.startsWith('[') && text.endsWith(']'))) {
|
||||||
|
return text.slice(1, -1).trim();
|
||||||
|
}
|
||||||
|
return text;
|
||||||
|
};
|
||||||
|
|
||||||
|
const splitSqlIdentifierPath = (raw: string): string[] => (
|
||||||
|
String(raw || '')
|
||||||
|
.split('.')
|
||||||
|
.map(stripSqlIdentifierQuotes)
|
||||||
|
.map((part) => part.trim())
|
||||||
|
.filter(Boolean)
|
||||||
|
);
|
||||||
|
|
||||||
|
const resolveIdentifierWindowAtColumn = (
|
||||||
|
lineContent: string,
|
||||||
|
column: number,
|
||||||
|
): { start: number; end: number; text: string } | null => {
|
||||||
|
const text = String(lineContent || '');
|
||||||
|
if (!text) return null;
|
||||||
|
const isIdentChar = (ch: string) => /[A-Za-z0-9_$`"\[\].]/.test(ch || '');
|
||||||
|
let offset = Math.max(0, Math.min(text.length - 1, Number(column || 1) - 2));
|
||||||
|
if (!isIdentChar(text[offset] || '')) {
|
||||||
|
if (offset > 0 && isIdentChar(text[offset - 1] || '')) {
|
||||||
|
offset -= 1;
|
||||||
|
} else if (offset + 1 < text.length && isIdentChar(text[offset + 1] || '')) {
|
||||||
|
offset += 1;
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let start = offset;
|
||||||
|
while (start > 0 && isIdentChar(text[start - 1] || '')) start -= 1;
|
||||||
|
let end = offset + 1;
|
||||||
|
while (end < text.length && isIdentChar(text[end] || '')) end += 1;
|
||||||
|
return start < end ? { start, end, text: text.slice(start, end).trim() } : null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const isLikelyTableReferenceIdentifier = (
|
||||||
|
lineContent: string,
|
||||||
|
identifierStart: number,
|
||||||
|
): boolean => {
|
||||||
|
const beforeIdentifier = String(lineContent || '').slice(0, Math.max(0, identifierStart));
|
||||||
|
return /\b(?:from|join|update|into|delete\s+from|alter\s+table|drop\s+table|truncate\s+table)\s*$/i.test(beforeIdentifier);
|
||||||
|
};
|
||||||
|
|
||||||
|
const isOceanBaseOracleConnection = (connection: any): boolean => {
|
||||||
|
const config = connection?.config || {};
|
||||||
|
return String(config.type || '').trim().toLowerCase() === 'oceanbase'
|
||||||
|
&& String(config.oceanBaseProtocol || '').trim().toLowerCase() === 'oracle';
|
||||||
|
};
|
||||||
|
|
||||||
|
const installOceanBaseOracleNavigationFallback = (editor: any) => {
|
||||||
|
const editorDomNode = editor?.getDomNode?.();
|
||||||
|
if (!editorDomNode || editor.__gonaviObOracleNavigationFallbackInstalled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Object.defineProperty(editor, '__gonaviObOracleNavigationFallbackInstalled', {
|
||||||
|
value: true,
|
||||||
|
configurable: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleMouseDownCapture = (event: MouseEvent) => {
|
||||||
|
if (event.button !== 0 || !(event.ctrlKey || event.metaKey) || event.altKey) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const store = useStore.getState();
|
||||||
|
const activeTab = (store.tabs || []).find((tab: any) => tab.id === store.activeTabId);
|
||||||
|
if (!activeTab || activeTab.type !== 'query') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const connectionId = String(activeTab.connectionId || store.activeContext?.connectionId || '').trim();
|
||||||
|
if (!connectionId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const connection = (store.connections || []).find((item: any) => item.id === connectionId);
|
||||||
|
if (!isOceanBaseOracleConnection(connection)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const target = editor.getTargetAtClientPoint?.(event.clientX, event.clientY);
|
||||||
|
const position = target?.position;
|
||||||
|
if (!position) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const model = editor.getModel?.();
|
||||||
|
const lineContent = String(model?.getLineContent?.(position.lineNumber) || '');
|
||||||
|
const identifier = resolveIdentifierWindowAtColumn(lineContent, position.column);
|
||||||
|
if (!identifier || !identifier.text.includes('.')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!isLikelyTableReferenceIdentifier(lineContent, identifier.start)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const parts = splitSqlIdentifierPath(identifier.text);
|
||||||
|
if (parts.length !== 2) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const [schemaName, tableName] = parts;
|
||||||
|
if (!schemaName || !tableName) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
event.stopImmediatePropagation?.();
|
||||||
|
store.setActiveContext?.({ connectionId, dbName: schemaName });
|
||||||
|
store.addTab?.({
|
||||||
|
id: `${connectionId}-${schemaName}-table-${tableName}`,
|
||||||
|
title: tableName,
|
||||||
|
type: 'table',
|
||||||
|
connectionId,
|
||||||
|
dbName: schemaName,
|
||||||
|
tableName,
|
||||||
|
initialViewMode: 'fields',
|
||||||
|
initialViewModeRequestId: String(Date.now()),
|
||||||
|
objectType: 'table',
|
||||||
|
returnToTabId: activeTab.id || undefined,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
editorDomNode.addEventListener('mousedown', handleMouseDownCapture, true);
|
||||||
|
editor.onDidDispose?.(() => {
|
||||||
|
editorDomNode.removeEventListener('mousedown', handleMouseDownCapture, true);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const patchQueryEditorAiInlineRightArrowFallback = (editor: any, monaco: any) => {
|
const patchQueryEditorAiInlineRightArrowFallback = (editor: any, monaco: any) => {
|
||||||
const originalAddCommand = editor?.addCommand?.bind?.(editor);
|
const originalAddCommand = editor?.addCommand?.bind?.(editor);
|
||||||
if (!originalAddCommand || !monaco?.KeyCode?.RightArrow) {
|
if (!originalAddCommand || !monaco?.KeyCode?.RightArrow) {
|
||||||
@@ -154,6 +288,7 @@ const MonacoEditor: React.FC<MonacoEditorProps> = ({
|
|||||||
}, [beforeMount]);
|
}, [beforeMount]);
|
||||||
|
|
||||||
const handleMount: OnMount = useCallback((editor, monaco) => {
|
const handleMount: OnMount = useCallback((editor, monaco) => {
|
||||||
|
installOceanBaseOracleNavigationFallback(editor);
|
||||||
patchQueryEditorAiInlineRightArrowFallback(editor, monaco);
|
patchQueryEditorAiInlineRightArrowFallback(editor, monaco);
|
||||||
onMount?.(editor, monaco);
|
onMount?.(editor, monaco);
|
||||||
}, [onMount]);
|
}, [onMount]);
|
||||||
|
|||||||
@@ -37,6 +37,13 @@ describe('formatSqlExecutionError', () => {
|
|||||||
expect(formatted).toContain('Raw error: driver returned unexpected status 123');
|
expect(formatted).toContain('Raw error: driver returned unexpected status 123');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('recognizes driver bad connection during SQL execution as timeout semantics', () => {
|
||||||
|
const formatted = formatSqlExecutionError('第 1 条语句执行失败:driver: bad connection');
|
||||||
|
|
||||||
|
expect(formatted).toContain('Semantic meaning: Query timed out or was canceled');
|
||||||
|
expect(formatted).toContain('Raw error: 第 1 条语句执行失败:driver: bad connection');
|
||||||
|
});
|
||||||
|
|
||||||
it('recognizes localized connection-timeout wrappers as timeout semantics', () => {
|
it('recognizes localized connection-timeout wrappers as timeout semantics', () => {
|
||||||
const translate = (key: string, params?: Record<string, unknown>) => {
|
const translate = (key: string, params?: Record<string, unknown>) => {
|
||||||
if (key === 'query_editor.sql_error.wrapper.semantic_line') {
|
if (key === 'query_editor.sql_error.wrapper.semantic_line') {
|
||||||
|
|||||||
@@ -135,6 +135,7 @@ const SQL_ERROR_RULES: SqlErrorSemanticRule[] = [
|
|||||||
/context cancelled/i,
|
/context cancelled/i,
|
||||||
/timeout/i,
|
/timeout/i,
|
||||||
/timed out/i,
|
/timed out/i,
|
||||||
|
/driver:\s*bad connection/i,
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -20,6 +20,9 @@ func TestNormalizeRunConfig_OceanBaseOracleAddsCurrentSchemaInit(t *testing.T) {
|
|||||||
if runConfig.Database != "OBORCL" {
|
if runConfig.Database != "OBORCL" {
|
||||||
t.Fatalf("expected OceanBase Oracle service name to stay OBORCL, got %q", runConfig.Database)
|
t.Fatalf("expected OceanBase Oracle service name to stay OBORCL, got %q", runConfig.Database)
|
||||||
}
|
}
|
||||||
|
if runConfig.Timeout != 0 {
|
||||||
|
t.Fatalf("expected default OceanBase Oracle timeout to stay unset, got %d", runConfig.Timeout)
|
||||||
|
}
|
||||||
values, err := url.ParseQuery(runConfig.ConnectionParams)
|
values, err := url.ParseQuery(runConfig.ConnectionParams)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("unexpected connection params parse error: %v", err)
|
t.Fatalf("unexpected connection params parse error: %v", err)
|
||||||
@@ -30,17 +33,21 @@ func TestNormalizeRunConfig_OceanBaseOracleAddsCurrentSchemaInit(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestNormalizeRunConfig_OceanBaseOraclePreservesExistingInit(t *testing.T) {
|
func TestNormalizeRunConfig_OceanBaseOraclePreservesExplicitTimeoutAndExistingInit(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
config := connection.ConnectionConfig{
|
config := connection.ConnectionConfig{
|
||||||
Type: "oceanbase",
|
Type: "oceanbase",
|
||||||
Database: "OBORCL",
|
Database: "OBORCL",
|
||||||
OceanBaseProtocol: "oracle",
|
OceanBaseProtocol: "oracle",
|
||||||
|
Timeout: 45,
|
||||||
ConnectionParams: "init=ALTER+SESSION+SET+NLS_DATE_FORMAT%3D%27YYYY-MM-DD%27&timeout=10s",
|
ConnectionParams: "init=ALTER+SESSION+SET+NLS_DATE_FORMAT%3D%27YYYY-MM-DD%27&timeout=10s",
|
||||||
}
|
}
|
||||||
runConfig := normalizeRunConfig(config, "sbdev")
|
runConfig := normalizeRunConfig(config, "sbdev")
|
||||||
|
|
||||||
|
if runConfig.Timeout != 45 {
|
||||||
|
t.Fatalf("expected explicit timeout to be preserved, got %d", runConfig.Timeout)
|
||||||
|
}
|
||||||
values, err := url.ParseQuery(runConfig.ConnectionParams)
|
values, err := url.ParseQuery(runConfig.ConnectionParams)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("unexpected connection params parse error: %v", err)
|
t.Fatalf("unexpected connection params parse error: %v", err)
|
||||||
|
|||||||
@@ -19,6 +19,12 @@ func configureSQLConnectionPool(db *sql.DB, dbType string) {
|
|||||||
switch strings.ToLower(strings.TrimSpace(dbType)) {
|
switch strings.ToLower(strings.TrimSpace(dbType)) {
|
||||||
case "sqlite", "duckdb":
|
case "sqlite", "duckdb":
|
||||||
return
|
return
|
||||||
|
case "oracle", "oceanbase":
|
||||||
|
db.SetMaxOpenConns(defaultSQLMaxOpenConns)
|
||||||
|
db.SetMaxIdleConns(1)
|
||||||
|
db.SetConnMaxIdleTime(defaultSQLConnMaxIdleTime)
|
||||||
|
db.SetConnMaxLifetime(defaultSQLConnMaxLifetime)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
db.SetMaxOpenConns(defaultSQLMaxOpenConns)
|
db.SetMaxOpenConns(defaultSQLMaxOpenConns)
|
||||||
db.SetMaxIdleConns(0)
|
db.SetMaxIdleConns(0)
|
||||||
|
|||||||
Reference in New Issue
Block a user