mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-07-02 09:41:24 +08:00
🐛 fix(sidebar): 恢复连接加载中的转圈状态
- 扩展连接状态为 loading/success/error - 在连接根节点加载开始时显示 pending,成功后才置绿 - 同步 V2 active host 与 legacy Badge 状态显示并补测试
This commit is contained in:
@@ -1009,6 +1009,23 @@ describe('Sidebar locate toolbar', () => {
|
||||
expect(css).toMatch(/\.gn-v2-explorer-filter-tabs button \{[^}]*flex: 0 0 auto;[^}]*white-space: nowrap;/s);
|
||||
});
|
||||
|
||||
it('shows a pending state while a connection root is loading', () => {
|
||||
const css = readV2ThemeCss();
|
||||
const source = readSidebarSource();
|
||||
const treeLoaderSource = readSourceFile('./sidebar/useSidebarTreeLoaders.tsx');
|
||||
const titleRenderSource = readSourceFile('./sidebar/useSidebarTitleRender.tsx');
|
||||
const v2ContextMenuSource = readSourceFile('./sidebar/useSidebarV2ContextMenu.tsx');
|
||||
|
||||
expect(source).toContain("export type SidebarConnectionState = 'loading' | 'success' | 'error';");
|
||||
expect(treeLoaderSource).toContain("setConnectionStates(prev => ({ ...prev, [conn.id]: 'loading' }));");
|
||||
expect(titleRenderSource).toContain("let status: 'loading' | 'success' | 'error' | 'default' = 'default';");
|
||||
expect(titleRenderSource).toContain("if (connectionStates[node.key] === 'loading') status = 'loading';");
|
||||
expect(v2ContextMenuSource).toContain("const statusMap = new Map<string, 'loading' | 'live' | 'error' | 'idle'>();");
|
||||
expect(v2ContextMenuSource).toContain("value === 'loading' ? 'loading'");
|
||||
expect(css).toMatch(/\.gn-v2-tree-status\.is-loading::before \{[^}]*border: 2px solid rgba\(37, 99, 235, 0\.24\);[^}]*animation: gn-v2-tree-status-spin 0\.8s linear infinite;/s);
|
||||
expect(css).toMatch(/@keyframes gn-v2-tree-status-spin \{[^}]*to \{ transform: rotate\(360deg\); \}/s);
|
||||
});
|
||||
|
||||
it('keeps v2 tree status dots circular while using virtual horizontal scroll for long labels', () => {
|
||||
const css = readV2ThemeCss();
|
||||
const source = readSidebarSource();
|
||||
|
||||
@@ -175,6 +175,7 @@ import {
|
||||
shouldCloseV2CommandSearchOnGlobalKey,
|
||||
shouldRunV2CommandSearchEnter,
|
||||
sortSidebarTableEntries,
|
||||
type SidebarConnectionState,
|
||||
type SidebarTreeNode as TreeNode,
|
||||
type V2CommandSearchItem,
|
||||
} from './sidebarV2Utils';
|
||||
@@ -704,8 +705,8 @@ const Sidebar: React.FC<{
|
||||
return () => window.removeEventListener('keydown', handleV2CommandSearchGlobalKeyDown, true);
|
||||
}, [closeV2CommandSearch, isV2CommandSearchOpen]);
|
||||
|
||||
// Connection Status State: key -> 'success' | 'error'
|
||||
const [connectionStates, setConnectionStates] = useState<Record<string, 'success' | 'error'>>({});
|
||||
// Connection Status State: key -> 'loading' | 'success' | 'error'
|
||||
const [connectionStates, setConnectionStates] = useState<Record<string, SidebarConnectionState>>({});
|
||||
const [isTreeDragging, setIsTreeDragging] = useState(false);
|
||||
|
||||
// Create Database Modal
|
||||
|
||||
@@ -9,6 +9,7 @@ import { SIDEBAR_SQL_EDITOR_DRAG_MIME, encodeSidebarSqlEditorDragPayload } from
|
||||
import {
|
||||
resolveSidebarObjectDragText,
|
||||
} from '../sidebarCoreUtils';
|
||||
import type { SidebarConnectionState } from '../sidebarV2Utils';
|
||||
import {
|
||||
shouldHideSchemaPrefix,
|
||||
splitQualifiedName,
|
||||
@@ -16,7 +17,7 @@ import {
|
||||
import { resolveV2ObjectGroupTitle } from './sidebarHelpers';
|
||||
|
||||
type UseSidebarTitleRenderArgs = {
|
||||
connectionStates: Record<string, 'success' | 'error'>;
|
||||
connectionStates: Record<string, SidebarConnectionState>;
|
||||
isV2Ui: boolean;
|
||||
renderV2TreeTitle: (node: any, hoverTitle: string, statusBadge: React.ReactNode) => React.ReactNode;
|
||||
handleAddExternalSQLDirectory: (node: any) => Promise<void>;
|
||||
@@ -36,16 +37,18 @@ export const useSidebarTitleRender = ({
|
||||
treeDragSelectSuppressUntilRef,
|
||||
setIsTreeDragging,
|
||||
}: UseSidebarTitleRenderArgs) => useCallback((node: any) => {
|
||||
let status: 'success' | 'error' | 'default' = 'default';
|
||||
let status: 'loading' | 'success' | 'error' | 'default' = 'default';
|
||||
if (node.type === 'connection' || node.type === 'database') {
|
||||
if (connectionStates[node.key] === 'success') status = 'success';
|
||||
if (connectionStates[node.key] === 'loading') status = 'loading';
|
||||
else if (connectionStates[node.key] === 'success') status = 'success';
|
||||
else if (connectionStates[node.key] === 'error') status = 'error';
|
||||
}
|
||||
const legacyBadgeStatus = status === 'loading' ? 'processing' : status;
|
||||
|
||||
const statusBadge = node.type === 'connection' || node.type === 'database' ? (
|
||||
isV2Ui
|
||||
? <span className={`gn-v2-tree-status is-${status}`} aria-hidden="true" />
|
||||
: <Badge status={status} style={{ marginLeft: 4, marginRight: 8 }} />
|
||||
: <Badge status={legacyBadgeStatus} style={{ marginLeft: 4, marginRight: 8 }} />
|
||||
) : null;
|
||||
|
||||
const displayTitle = String(node.title ?? '');
|
||||
|
||||
@@ -47,6 +47,7 @@ import {
|
||||
buildSidebarTableChildrenForUi,
|
||||
isSidebarTablePinned,
|
||||
sortSidebarTableEntries,
|
||||
type SidebarConnectionState,
|
||||
type SidebarTreeNode as TreeNode,
|
||||
} from '../sidebarV2Utils';
|
||||
import { DBGetDatabases, DBGetTables, DBQuery, GetDriverStatusList, JVMProbeCapabilities } from '../../../wailsjs/go/app/App';
|
||||
@@ -126,7 +127,7 @@ type UseSidebarTreeLoadersOptions = {
|
||||
pinnedSidebarTables: any[];
|
||||
isV2Ui: boolean;
|
||||
loadingNodesRef: React.MutableRefObject<Set<string>>;
|
||||
setConnectionStates: React.Dispatch<React.SetStateAction<Record<string, 'success' | 'error'>>>;
|
||||
setConnectionStates: React.Dispatch<React.SetStateAction<Record<string, SidebarConnectionState>>>;
|
||||
setLoadedKeys: React.Dispatch<React.SetStateAction<React.Key[]>>;
|
||||
replaceTreeNodeChildren: (key: React.Key, children: TreeNode[] | undefined) => TreeNode[];
|
||||
buildRuntimeConfig: (conn: any, overrideDatabase?: string, clearDatabase?: boolean) => any;
|
||||
@@ -219,6 +220,7 @@ export const useSidebarTreeLoaders = ({
|
||||
const loadKey = `dbs-${conn.id}`;
|
||||
if (loadingNodesRef.current.has(loadKey)) return;
|
||||
loadingNodesRef.current.add(loadKey);
|
||||
setConnectionStates(prev => ({ ...prev, [conn.id]: 'loading' }));
|
||||
const config = {
|
||||
...conn.config,
|
||||
port: Number(conn.config.port),
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
} from '../V2TableContextMenu';
|
||||
import {
|
||||
isSidebarTablePinned,
|
||||
type SidebarConnectionState,
|
||||
type SidebarTreeNode as TreeNode,
|
||||
type V2RailConnectionGroup,
|
||||
} from '../sidebarV2Utils';
|
||||
@@ -32,7 +33,7 @@ type UseSidebarV2ActionHandlersArgs = {
|
||||
treeDataRef: MutableRefObject<TreeNode[]>;
|
||||
findTreeNodeByKeyRef: MutableRefObject<(nodes: TreeNode[], targetKey: React.Key) => TreeNode | null>;
|
||||
refreshV2TableContextMenuStatsRef: MutableRefObject<(node: any) => void>;
|
||||
setConnectionStates: Dispatch<SetStateAction<Record<string, 'success' | 'error'>>>;
|
||||
setConnectionStates: Dispatch<SetStateAction<Record<string, SidebarConnectionState>>>;
|
||||
setExpandedKeys: Dispatch<SetStateAction<React.Key[]>>;
|
||||
setLoadedKeys: Dispatch<SetStateAction<React.Key[]>>;
|
||||
setTargetConnection: Dispatch<SetStateAction<any>>;
|
||||
|
||||
@@ -22,7 +22,7 @@ import { getDataSourceCapabilities } from '../../utils/dataSourceCapabilities';
|
||||
import { resolveConnectionHostSummary } from '../../utils/tabDisplay';
|
||||
import { resolveConnectionIconType } from '../../utils/connectionVisual';
|
||||
import { formatSidebarRowCount } from './sidebarHelpers';
|
||||
import { isSidebarTablePinned, type SidebarTreeNode as TreeNode, type V2RailConnectionGroup } from '../sidebarV2Utils';
|
||||
import { isSidebarTablePinned, type SidebarConnectionState, type SidebarTreeNode as TreeNode, type V2RailConnectionGroup } from '../sidebarV2Utils';
|
||||
import { getTableDataDangerActionMeta, supportsTableTruncateAction } from '../tableDataDangerActions';
|
||||
import {
|
||||
SIDEBAR_CONTEXT_MENU_FALLBACK_HEIGHT,
|
||||
@@ -45,7 +45,7 @@ export type SidebarContextMenuState = {
|
||||
|
||||
type SidebarV2ContextMenuOptions = {
|
||||
connections: SavedConnection[];
|
||||
connectionStates: Record<string, 'success' | 'error'>;
|
||||
connectionStates: Record<string, SidebarConnectionState>;
|
||||
connectionTags: Array<{ id: string; name: string; connectionIds: string[] }>;
|
||||
activeShortcutPlatform: any;
|
||||
flattenConnectionNodes: (nodes: TreeNode[]) => TreeNode[];
|
||||
@@ -104,7 +104,7 @@ export const useSidebarV2ContextMenu = ({
|
||||
const [v2TableContextMenuStats, setV2TableContextMenuStats] = useState<Record<string, V2TableContextMenuStats>>({});
|
||||
|
||||
const connectionStatusMap = useMemo(() => {
|
||||
const statusMap = new Map<string, 'live' | 'error' | 'idle'>();
|
||||
const statusMap = new Map<string, 'loading' | 'live' | 'error' | 'idle'>();
|
||||
const sortedConnectionIds = connections
|
||||
.map((conn) => conn.id)
|
||||
.sort((a, b) => b.length - a.length);
|
||||
@@ -114,7 +114,7 @@ export const useSidebarV2ContextMenu = ({
|
||||
Object.entries(connectionStates).forEach(([key, value]) => {
|
||||
const ownState = statusMap.get(key);
|
||||
if (ownState !== undefined) {
|
||||
statusMap.set(key, value === 'success' ? 'live' : 'error');
|
||||
statusMap.set(key, value === 'loading' ? 'loading' : value === 'success' ? 'live' : 'error');
|
||||
return;
|
||||
}
|
||||
if (value !== 'success') return;
|
||||
@@ -126,7 +126,7 @@ export const useSidebarV2ContextMenu = ({
|
||||
return statusMap;
|
||||
}, [connectionStates, connections]);
|
||||
|
||||
const buildRailConnectionStatus = useCallback((connectionId: string): 'live' | 'error' | 'idle' => {
|
||||
const buildRailConnectionStatus = useCallback((connectionId: string): 'loading' | 'live' | 'error' | 'idle' => {
|
||||
return connectionStatusMap.get(connectionId) || 'idle';
|
||||
}, [connectionStatusMap]);
|
||||
|
||||
|
||||
@@ -15,6 +15,8 @@ type SidebarV2Translate = (key: string) => string;
|
||||
const translateSidebarV2Current: SidebarV2Translate = (key) => t(key);
|
||||
const translateSidebarV2ZhCN: SidebarV2Translate = (key) => catalogTranslate('zh-CN', key);
|
||||
|
||||
export type SidebarConnectionState = 'loading' | 'success' | 'error';
|
||||
|
||||
export type SidebarTreeNodeType =
|
||||
| 'connection'
|
||||
| 'database'
|
||||
|
||||
@@ -2098,6 +2098,15 @@ body[data-ui-version="v2"] .gn-v2-live-dot.is-live {
|
||||
box-shadow: 0 0 0 3px rgba(34, 197, 94, 0.18);
|
||||
}
|
||||
|
||||
body[data-ui-version="v2"] .gn-v2-live-dot.is-loading {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border: 2px solid rgba(37, 99, 235, 0.24);
|
||||
border-top-color: #2563eb;
|
||||
background: transparent;
|
||||
animation: gn-v2-tree-status-spin 0.8s linear infinite;
|
||||
}
|
||||
|
||||
body[data-ui-version="v2"] .gn-v2-live-dot.is-error {
|
||||
background: var(--gn-danger);
|
||||
}
|
||||
@@ -2760,11 +2769,26 @@ body[data-ui-version="v2"] .gn-v2-tree-status.is-success::before {
|
||||
box-shadow: 0 0 0 4px rgba(34, 197, 94, 0.18);
|
||||
}
|
||||
|
||||
body[data-ui-version="v2"] .gn-v2-tree-status.is-loading::before {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
flex: 0 0 10px;
|
||||
border: 2px solid rgba(37, 99, 235, 0.24);
|
||||
border-top-color: #2563eb;
|
||||
background: transparent;
|
||||
box-shadow: none;
|
||||
animation: gn-v2-tree-status-spin 0.8s linear infinite;
|
||||
}
|
||||
|
||||
body[data-ui-version="v2"] .gn-v2-tree-status.is-error::before {
|
||||
background: var(--gn-danger);
|
||||
box-shadow: 0 0 0 4px rgba(239, 68, 68, 0.16);
|
||||
}
|
||||
|
||||
@keyframes gn-v2-tree-status-spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
body[data-ui-version="v2"] .gn-v2-tree-label {
|
||||
min-width: 0;
|
||||
flex: 1 1 auto;
|
||||
|
||||
Reference in New Issue
Block a user