mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-06-28 17:31:32 +08:00
✨ feat(sidebar): 优化表备注悬浮信息展示
- 读取不同数据源表备注并写入左侧表节点 - 支持表分组菜单切换备注显示 - 表节点悬浮复用 Tab 信息卡并移除原生双提示 Fixes #569
This commit is contained in:
@@ -52,6 +52,8 @@ import {
|
||||
buildSidebarRootTagToken,
|
||||
buildSidebarTablePinKey,
|
||||
} from '../store';
|
||||
import { renderSidebarV2TreeTitle } from './sidebar/SidebarTreeTitle';
|
||||
import { buildSidebarTableStatusSQL } from './sidebar/sidebarMetadataLoaders';
|
||||
import {
|
||||
DEFAULT_SHORTCUT_OPTIONS,
|
||||
cloneShortcutOptions,
|
||||
@@ -200,6 +202,8 @@ vi.mock('../store', () => ({
|
||||
recordTableAccess: mocks.noop,
|
||||
setTableSortPreference: mocks.noop,
|
||||
setSidebarTablePinned: mocks.noop,
|
||||
queryOptions: { showSidebarTableComment: false },
|
||||
setQueryOptions: mocks.noop,
|
||||
addSqlLog: mocks.noop,
|
||||
sqlLogs: [],
|
||||
shortcutOptions: mocks.state.shortcutOptions ?? cloneShortcutOptions(DEFAULT_SHORTCUT_OPTIONS),
|
||||
@@ -2820,6 +2824,7 @@ describe('Sidebar locate toolbar', () => {
|
||||
dbName="mkefu_ai_dev"
|
||||
count={15}
|
||||
currentSort="frequency"
|
||||
showTableComments
|
||||
/>,
|
||||
);
|
||||
|
||||
@@ -2831,6 +2836,8 @@ describe('Sidebar locate toolbar', () => {
|
||||
sort: t('sidebar.v2_table_group_menu.sort_frequency'),
|
||||
}));
|
||||
expect(markup).toContain(t('sidebar.menu.create_table'));
|
||||
expect(markup).toContain(t('sidebar.v2_table_group_menu.display_section'));
|
||||
expect(markup).toContain(t('sidebar.v2_table_group_menu.show_table_comments'));
|
||||
expect(markup).toContain(t('data_grid.context_menu.sort_section'));
|
||||
expect(markup).toContain(t('sidebar.menu.sort_by_name'));
|
||||
expect(markup).toContain(t('sidebar.menu.sort_by_frequency'));
|
||||
@@ -2846,6 +2853,7 @@ describe('Sidebar locate toolbar', () => {
|
||||
expect(end).toBeGreaterThan(start);
|
||||
const tableGroupCallSource = sidebarSource.slice(start, end);
|
||||
expect(tableGroupCallSource).toContain('<V2TableGroupContextMenuView');
|
||||
expect(tableGroupCallSource).toContain('showTableComments={showSidebarTableComment}');
|
||||
expect(tableGroupCallSource).not.toContain('title=');
|
||||
['? ? tables', '表 · tables'].forEach((rawSnippet) => {
|
||||
expect(tableGroupCallSource).not.toContain(rawSnippet);
|
||||
@@ -2900,6 +2908,81 @@ describe('Sidebar locate toolbar', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('renders sidebar table comments as an opt-in suffix while using the tab-style table hover card', () => {
|
||||
const baseNode = {
|
||||
type: 'table',
|
||||
title: 'users',
|
||||
key: 'conn-main-users',
|
||||
dataRef: {
|
||||
id: 'conn',
|
||||
dbName: 'main',
|
||||
tableName: 'users',
|
||||
tableComment: '用户表',
|
||||
},
|
||||
};
|
||||
const baseOptions = {
|
||||
node: baseNode,
|
||||
hoverTitle: 'users',
|
||||
statusBadge: null,
|
||||
getV2TreeMetaText: () => '',
|
||||
toggleSidebarTablePinned: vi.fn(),
|
||||
snapshotTreeSelectionBeforeDrag: vi.fn(),
|
||||
restoreTreeSelectionAfterDrag: vi.fn(),
|
||||
treeDragSelectSuppressUntilRef: { current: 0 },
|
||||
setIsTreeDragging: vi.fn(),
|
||||
};
|
||||
|
||||
const hiddenSuffixMarkup = renderToStaticMarkup(renderSidebarV2TreeTitle({
|
||||
...baseOptions,
|
||||
showSidebarTableComment: false,
|
||||
}));
|
||||
expect(hiddenSuffixMarkup).not.toContain('gn-v2-tree-table-comment');
|
||||
|
||||
const visibleSuffixMarkup = renderToStaticMarkup(renderSidebarV2TreeTitle({
|
||||
...baseOptions,
|
||||
showSidebarTableComment: true,
|
||||
}));
|
||||
expect(visibleSuffixMarkup).toContain('gn-v2-tree-table-comment');
|
||||
expect(visibleSuffixMarkup).toContain('用户表');
|
||||
|
||||
const treeTitleSource = readSourceFile('./sidebar/SidebarTreeTitle.tsx');
|
||||
expect(treeTitleSource).toContain('data-sidebar-table-hover-info="true"');
|
||||
expect(treeTitleSource).toContain('rootClassName="gn-v2-tab-hover-tooltip gn-v2-sidebar-table-hover-tooltip"');
|
||||
expect(treeTitleSource).toContain('title={tableHoverInfo ? undefined : effectiveHoverTitle}');
|
||||
expect(treeTitleSource).toContain("const SIDEBAR_TREE_NODE_CONTENT_SELECTOR = '.ant-tree-node-content-wrapper';");
|
||||
expect(treeTitleSource).toContain("removeAttribute('title')");
|
||||
expect(treeTitleSource).toContain('ref={tableHoverInfo ? clearSidebarTableNativeHoverTitleRef : undefined}');
|
||||
expect(treeTitleSource).toContain('onPointerOverCapture={tableHoverInfo ? clearSidebarTableNativeHoverTitle : undefined}');
|
||||
expect(treeTitleSource).toContain("resolveConnectionHostSummary(dataRef.config)");
|
||||
expect(treeTitleSource).toContain("t('tab_manager.kind_badge.table')");
|
||||
expect(treeTitleSource).toContain("t('tab_manager.hover.kind.table')");
|
||||
expect(treeTitleSource).toContain("t('table_designer.action.table_comment')");
|
||||
expect(treeTitleSource).toContain('mouseEnterDelay={1.2}');
|
||||
|
||||
const css = readV2ThemeCss();
|
||||
expect(css).toMatch(/\.gn-v2-tree-table-comment \{[^}]*max-width: 24em;[^}]*text-overflow: ellipsis;/s);
|
||||
expect(css).toMatch(/\.gn-v2-tab-hover-tooltip \.ant-tooltip-inner \{[^}]*min-width: 260px;[^}]*padding: 0;/s);
|
||||
expect(css).toMatch(/\.gn-v2-tab-hover-card \{[^}]*cursor: text;[^}]*user-select: text;/s);
|
||||
expect(css).toContain('--gn-v2-tab-hover-grid-columns: 56px minmax(0, 1fr);');
|
||||
expect(css).toMatch(/\.gn-v2-tab-hover-row \{[^}]*grid-template-columns: var\(--gn-v2-tab-hover-grid-columns\);/s);
|
||||
});
|
||||
|
||||
it('loads table comments through the sidebar table status metadata query', () => {
|
||||
const mysqlSql = buildSidebarTableStatusSQL({ config: { type: 'mysql' } } as any, 'app');
|
||||
const pgSql = buildSidebarTableStatusSQL({ config: { type: 'postgres' } } as any, 'app');
|
||||
const sqlServerSql = buildSidebarTableStatusSQL({ config: { type: 'sqlserver' } } as any, 'app');
|
||||
const oracleSql = buildSidebarTableStatusSQL({ config: { type: 'oracle' } } as any, 'APP');
|
||||
|
||||
expect(mysqlSql).toContain('TABLE_COMMENT AS table_comment');
|
||||
expect(pgSql).toContain("obj_description(c.oid, 'pg_class') AS table_comment");
|
||||
expect(sqlServerSql).toContain('ep.value AS table_comment');
|
||||
expect(oracleSql).toContain('comments AS table_comment');
|
||||
|
||||
const loaderSource = readSourceFile('./sidebar/useSidebarTreeLoaders.tsx');
|
||||
expect(loaderSource).toContain('tableCommentMap');
|
||||
expect(loaderSource).toContain('tableComment: entry.tableComment');
|
||||
});
|
||||
|
||||
it('listens for table overview pin changes to refresh the matching sidebar database node', () => {
|
||||
const source = readSidebarSource();
|
||||
|
||||
|
||||
@@ -418,6 +418,8 @@ const Sidebar: React.FC<{
|
||||
const recordTableAccess = useStore(state => state.recordTableAccess);
|
||||
const setTableSortPreference = useStore(state => state.setTableSortPreference);
|
||||
const setSidebarTablePinned = useStore(state => state.setSidebarTablePinned);
|
||||
const queryOptions = useStore(state => state.queryOptions);
|
||||
const setQueryOptions = useStore(state => state.setQueryOptions);
|
||||
const addSqlLog = useStore(state => state.addSqlLog);
|
||||
const sqlLogs = useStore(state => state.sqlLogs) || [];
|
||||
const shortcutOptions = useStore(state => state.shortcutOptions);
|
||||
@@ -429,6 +431,7 @@ const Sidebar: React.FC<{
|
||||
const darkMode = theme === 'dark';
|
||||
const resolvedAppearance = resolveAppearanceValues(appearance);
|
||||
const opacity = normalizeOpacityForPlatform(resolvedAppearance.opacity);
|
||||
const showSidebarTableComment = queryOptions?.showSidebarTableComment === true;
|
||||
const { exportProgressModal, runExportWithProgress } = useExportProgressDialog();
|
||||
const disableLocalBackdropFilter = isMacLikePlatform();
|
||||
const autoFetchVisible = useAutoFetchVisibility();
|
||||
@@ -2035,6 +2038,8 @@ const Sidebar: React.FC<{
|
||||
moveConnectionToTag,
|
||||
setSidebarTablePinned,
|
||||
setTableSortPreference,
|
||||
setQueryOptions,
|
||||
showSidebarTableComment,
|
||||
replaceTreeNodeChildren,
|
||||
loadDatabases,
|
||||
loadTables,
|
||||
@@ -2162,6 +2167,7 @@ const Sidebar: React.FC<{
|
||||
v2TreeMetrics,
|
||||
tableSortPreference,
|
||||
pinnedSidebarTables,
|
||||
showSidebarTableComment,
|
||||
getConnectionNodeForAction,
|
||||
buildRuntimeConfig,
|
||||
extractObjectName,
|
||||
@@ -2186,6 +2192,7 @@ const Sidebar: React.FC<{
|
||||
hoverTitle,
|
||||
statusBadge,
|
||||
getV2TreeMetaText,
|
||||
showSidebarTableComment,
|
||||
toggleSidebarTablePinned,
|
||||
snapshotTreeSelectionBeforeDrag,
|
||||
restoreTreeSelectionAfterDrag,
|
||||
|
||||
@@ -264,6 +264,7 @@ export const V2TableContextMenuView: React.FC<{
|
||||
|
||||
export type V2TableGroupContextMenuActionKey =
|
||||
| 'new-table'
|
||||
| 'toggle-table-comments'
|
||||
| 'sort-by-name'
|
||||
| 'sort-by-frequency';
|
||||
|
||||
@@ -273,6 +274,7 @@ export const V2TableGroupContextMenuView: React.FC<{
|
||||
dbName?: string;
|
||||
count?: number;
|
||||
currentSort?: 'name' | 'frequency';
|
||||
showTableComments?: boolean;
|
||||
onAction?: (action: V2TableGroupContextMenuActionKey) => void;
|
||||
}> = ({
|
||||
title,
|
||||
@@ -280,6 +282,7 @@ export const V2TableGroupContextMenuView: React.FC<{
|
||||
dbName,
|
||||
count,
|
||||
currentSort = 'name',
|
||||
showTableComments = false,
|
||||
onAction,
|
||||
}) => {
|
||||
const sortLabel = currentSort === 'frequency'
|
||||
@@ -310,6 +313,17 @@ export const V2TableGroupContextMenuView: React.FC<{
|
||||
{ action: 'new-table', icon: <TableOutlined />, title: t('sidebar.menu.create_table'), kbd: primaryShortcut('N', shortcutPlatform), featured: true },
|
||||
])}
|
||||
|
||||
<div className="gn-v2-context-menu-section-title">{t('sidebar.v2_table_group_menu.display_section')}</div>
|
||||
{renderItems([
|
||||
{
|
||||
action: 'toggle-table-comments',
|
||||
icon: showTableComments ? <CheckSquareOutlined /> : <FileTextOutlined />,
|
||||
title: t('sidebar.v2_table_group_menu.show_table_comments'),
|
||||
kbd: showTableComments ? t('data_grid.context_menu.current_marker') : undefined,
|
||||
selected: showTableComments,
|
||||
},
|
||||
])}
|
||||
|
||||
<div className="gn-v2-context-menu-section-title">{t('data_grid.context_menu.sort_section')}</div>
|
||||
{renderItems([
|
||||
{ action: 'sort-by-name', icon: currentSort === 'name' ? <CheckSquareOutlined /> : <ReloadOutlined />, title: t('sidebar.menu.sort_by_name'), kbd: currentSort === 'name' ? t('data_grid.context_menu.current_marker') : undefined, selected: currentSort === 'name' },
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import React from 'react';
|
||||
import { Tooltip } from 'antd';
|
||||
import { StarFilled, StarOutlined } from '@ant-design/icons';
|
||||
import { t } from '../../i18n';
|
||||
import { SIDEBAR_SQL_EDITOR_DRAG_MIME, encodeSidebarSqlEditorDragPayload } from '../../utils/sidebarSqlDrag';
|
||||
import { sanitizeRedisDbAlias } from '../../utils/redisDbAlias';
|
||||
import { resolveConnectionHostSummary } from '../../utils/tabDisplay';
|
||||
import { resolveSidebarObjectDragText } from '../sidebarCoreUtils';
|
||||
import { resolveV2ObjectGroupTitle } from './sidebarHelpers';
|
||||
|
||||
@@ -11,6 +13,7 @@ type SidebarV2TreeTitleOptions = {
|
||||
hoverTitle: string;
|
||||
statusBadge: React.ReactNode;
|
||||
getV2TreeMetaText: (node: any) => string;
|
||||
showSidebarTableComment: boolean;
|
||||
toggleSidebarTablePinned: (node: any) => void;
|
||||
snapshotTreeSelectionBeforeDrag: () => void;
|
||||
restoreTreeSelectionAfterDrag: () => void;
|
||||
@@ -18,11 +21,86 @@ type SidebarV2TreeTitleOptions = {
|
||||
setIsTreeDragging: (dragging: boolean) => void;
|
||||
};
|
||||
|
||||
const SIDEBAR_TREE_NODE_CONTENT_SELECTOR = '.ant-tree-node-content-wrapper';
|
||||
|
||||
const stopSidebarTableHoverPropagation = (event: React.SyntheticEvent<HTMLElement>) => {
|
||||
event.stopPropagation();
|
||||
};
|
||||
|
||||
const clearSidebarTableNativeHoverTitleElement = (element: HTMLElement | null) => {
|
||||
element?.closest(SIDEBAR_TREE_NODE_CONTENT_SELECTOR)?.removeAttribute('title');
|
||||
};
|
||||
|
||||
const clearSidebarTableNativeHoverTitleRef: React.RefCallback<HTMLSpanElement> = (element) => {
|
||||
clearSidebarTableNativeHoverTitleElement(element);
|
||||
};
|
||||
|
||||
const clearSidebarTableNativeHoverTitle = (event: React.SyntheticEvent<HTMLElement>) => {
|
||||
clearSidebarTableNativeHoverTitleElement(event.currentTarget);
|
||||
};
|
||||
|
||||
const renderSidebarTableHoverInfo = (
|
||||
node: any,
|
||||
displayTitle: string,
|
||||
tableComment: string,
|
||||
): React.ReactNode => {
|
||||
const dataRef = node?.dataRef || {};
|
||||
const tableName = String(dataRef.tableName || displayTitle || node?.title || '').trim();
|
||||
const schemaName = String(dataRef.schemaName || '').trim();
|
||||
const dbName = String(dataRef.dbName || dataRef?.config?.database || '').trim();
|
||||
const connectionLabel = String(dataRef.name || '').trim();
|
||||
const hostSummary = resolveConnectionHostSummary(dataRef.config);
|
||||
const rows = [
|
||||
[t('tab_manager.hover.label.type'), t('tab_manager.hover.kind.table')],
|
||||
[t('tab_manager.hover.label.connection'), connectionLabel || t('tab_manager.hover.fallback.unbound_connection')],
|
||||
['Host', hostSummary || t('tab_manager.hover.fallback.host_not_configured')],
|
||||
[t('tab_manager.hover.label.database'), dbName || t('tab_manager.hover.fallback.database_not_specified')],
|
||||
['Schema', schemaName],
|
||||
[t('tab_manager.hover.label.object'), tableName],
|
||||
[t('table_designer.action.table_comment'), tableComment],
|
||||
].filter(([, value]) => Boolean(value));
|
||||
|
||||
return (
|
||||
<div
|
||||
className="gn-v2-tab-hover-card"
|
||||
data-tab-hover-info="true"
|
||||
data-sidebar-table-hover-info="true"
|
||||
onPointerDown={stopSidebarTableHoverPropagation}
|
||||
onPointerMove={stopSidebarTableHoverPropagation}
|
||||
onPointerUp={stopSidebarTableHoverPropagation}
|
||||
onPointerDownCapture={stopSidebarTableHoverPropagation}
|
||||
onPointerUpCapture={stopSidebarTableHoverPropagation}
|
||||
onMouseDown={stopSidebarTableHoverPropagation}
|
||||
onMouseMove={stopSidebarTableHoverPropagation}
|
||||
onMouseUp={stopSidebarTableHoverPropagation}
|
||||
onClick={stopSidebarTableHoverPropagation}
|
||||
onClickCapture={stopSidebarTableHoverPropagation}
|
||||
onTouchStart={stopSidebarTableHoverPropagation}
|
||||
onTouchMove={stopSidebarTableHoverPropagation}
|
||||
onTouchEnd={stopSidebarTableHoverPropagation}
|
||||
>
|
||||
<div className="gn-v2-tab-hover-head">
|
||||
<span>{t('tab_manager.kind_badge.table')}</span>
|
||||
<strong>{tableName || displayTitle}</strong>
|
||||
</div>
|
||||
<div className="gn-v2-tab-hover-rows">
|
||||
{rows.map(([label, value], index) => (
|
||||
<div className="gn-v2-tab-hover-row" key={`${String(label)}-${index}`}>
|
||||
<span>{label}</span>
|
||||
<strong>{value}</strong>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const renderSidebarV2TreeTitle = ({
|
||||
node,
|
||||
hoverTitle,
|
||||
statusBadge,
|
||||
getV2TreeMetaText,
|
||||
showSidebarTableComment,
|
||||
toggleSidebarTablePinned,
|
||||
snapshotTreeSelectionBeforeDrag,
|
||||
restoreTreeSelectionAfterDrag,
|
||||
@@ -52,6 +130,14 @@ export const renderSidebarV2TreeTitle = ({
|
||||
}
|
||||
return rawTitle;
|
||||
})();
|
||||
const tableComment = node.type === 'table'
|
||||
? String(node?.dataRef?.tableComment || '').trim()
|
||||
: '';
|
||||
const tableCommentSuffix = showSidebarTableComment && tableComment ? tableComment : '';
|
||||
const effectiveHoverTitle = hoverTitle;
|
||||
const tableHoverInfo = node.type === 'table'
|
||||
? renderSidebarTableHoverInfo(node, displayTitle, tableComment)
|
||||
: null;
|
||||
const metaText = getV2TreeMetaText(node);
|
||||
const redisDbAlias = node.type === 'redis-db'
|
||||
? sanitizeRedisDbAlias(node?.dataRef?.redisDbAlias)
|
||||
@@ -107,7 +193,7 @@ export const renderSidebarV2TreeTitle = ({
|
||||
return (
|
||||
<span
|
||||
className={`${titleClassName} is-connection`}
|
||||
title={hoverTitle}
|
||||
title={effectiveHoverTitle}
|
||||
data-node-type={node.type}
|
||||
data-sidebar-node-key={String(node.key || '')}
|
||||
data-sidebar-node-type={String(node.type || '')}
|
||||
@@ -119,49 +205,71 @@ export const renderSidebarV2TreeTitle = ({
|
||||
</span>
|
||||
);
|
||||
}
|
||||
const titleNode = (
|
||||
<span
|
||||
ref={tableHoverInfo ? clearSidebarTableNativeHoverTitleRef : undefined}
|
||||
className={titleClassName}
|
||||
title={tableHoverInfo ? undefined : effectiveHoverTitle}
|
||||
draggable={!!dragText}
|
||||
data-node-type={node.type}
|
||||
data-group-key={groupKey || undefined}
|
||||
data-sidebar-node-key={String(node.key || '')}
|
||||
data-sidebar-node-type={String(node.type || '')}
|
||||
onPointerOverCapture={tableHoverInfo ? clearSidebarTableNativeHoverTitle : undefined}
|
||||
onMouseOverCapture={tableHoverInfo ? clearSidebarTableNativeHoverTitle : undefined}
|
||||
onDragStart={dragText ? (event) => {
|
||||
snapshotTreeSelectionBeforeDrag();
|
||||
treeDragSelectSuppressUntilRef.current = Date.now() + 600;
|
||||
setIsTreeDragging(true);
|
||||
event.stopPropagation();
|
||||
event.dataTransfer.effectAllowed = 'copy';
|
||||
event.dataTransfer.setData('text/plain', dragText);
|
||||
event.dataTransfer.setData(
|
||||
SIDEBAR_SQL_EDITOR_DRAG_MIME,
|
||||
encodeSidebarSqlEditorDragPayload({
|
||||
text: dragText,
|
||||
nodeType: node.type,
|
||||
connectionId: String(node?.dataRef?.id || ''),
|
||||
dbName: String(node?.dataRef?.dbName || ''),
|
||||
}),
|
||||
);
|
||||
} : undefined}
|
||||
onDragEnd={dragText ? () => {
|
||||
restoreTreeSelectionAfterDrag();
|
||||
setIsTreeDragging(false);
|
||||
} : undefined}
|
||||
>
|
||||
{statusBadge}
|
||||
<span className="gn-v2-tree-label">
|
||||
{redisDbAlias ? (
|
||||
<>
|
||||
<span className="gn-v2-redis-db-name">{redisDbBaseTitle}</span>
|
||||
<span className="gn-v2-redis-db-alias">{redisDbAlias}</span>
|
||||
</>
|
||||
) : displayTitle}
|
||||
</span>
|
||||
{tableCommentSuffix && (
|
||||
<span className="gn-v2-tree-table-comment">{tableCommentSuffix}</span>
|
||||
)}
|
||||
{metaText && <span className="gn-v2-tree-count">{metaText}</span>}
|
||||
</span>
|
||||
);
|
||||
|
||||
const wrappedTitleNode = tableHoverInfo ? (
|
||||
<Tooltip
|
||||
title={tableHoverInfo}
|
||||
placement="right"
|
||||
mouseEnterDelay={1.2}
|
||||
destroyOnHidden
|
||||
rootClassName="gn-v2-tab-hover-tooltip gn-v2-sidebar-table-hover-tooltip"
|
||||
>
|
||||
{titleNode}
|
||||
</Tooltip>
|
||||
) : titleNode;
|
||||
|
||||
return (
|
||||
<>
|
||||
<span
|
||||
className={titleClassName}
|
||||
title={hoverTitle}
|
||||
draggable={!!dragText}
|
||||
data-node-type={node.type}
|
||||
data-group-key={groupKey || undefined}
|
||||
data-sidebar-node-key={String(node.key || '')}
|
||||
data-sidebar-node-type={String(node.type || '')}
|
||||
onDragStart={dragText ? (event) => {
|
||||
snapshotTreeSelectionBeforeDrag();
|
||||
treeDragSelectSuppressUntilRef.current = Date.now() + 600;
|
||||
setIsTreeDragging(true);
|
||||
event.stopPropagation();
|
||||
event.dataTransfer.effectAllowed = 'copy';
|
||||
event.dataTransfer.setData('text/plain', dragText);
|
||||
event.dataTransfer.setData(
|
||||
SIDEBAR_SQL_EDITOR_DRAG_MIME,
|
||||
encodeSidebarSqlEditorDragPayload({
|
||||
text: dragText,
|
||||
nodeType: node.type,
|
||||
connectionId: String(node?.dataRef?.id || ''),
|
||||
dbName: String(node?.dataRef?.dbName || ''),
|
||||
}),
|
||||
);
|
||||
} : undefined}
|
||||
onDragEnd={dragText ? () => {
|
||||
restoreTreeSelectionAfterDrag();
|
||||
setIsTreeDragging(false);
|
||||
} : undefined}
|
||||
>
|
||||
{statusBadge}
|
||||
<span className="gn-v2-tree-label">
|
||||
{redisDbAlias ? (
|
||||
<>
|
||||
<span className="gn-v2-redis-db-name">{redisDbBaseTitle}</span>
|
||||
<span className="gn-v2-redis-db-alias">{redisDbAlias}</span>
|
||||
</>
|
||||
) : displayTitle}
|
||||
</span>
|
||||
{metaText && <span className="gn-v2-tree-count">{metaText}</span>}
|
||||
</span>
|
||||
{wrappedTitleNode}
|
||||
{tablePinAction}
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -262,7 +262,7 @@ const buildSidebarTableStatusSQL = (
|
||||
case "mysql":
|
||||
case "starrocks":
|
||||
return [
|
||||
"SELECT TABLE_NAME AS table_name, TABLE_ROWS AS table_rows",
|
||||
"SELECT TABLE_NAME AS table_name, TABLE_COMMENT AS table_comment, TABLE_ROWS AS table_rows",
|
||||
"FROM information_schema.tables",
|
||||
`WHERE table_schema = '${safeDbName}'`,
|
||||
"AND table_type = 'BASE TABLE'",
|
||||
@@ -275,7 +275,7 @@ const buildSidebarTableStatusSQL = (
|
||||
case "opengauss":
|
||||
case "gaussdb":
|
||||
return [
|
||||
"SELECT n.nspname || '.' || c.relname AS table_name, c.reltuples::bigint AS table_rows",
|
||||
"SELECT n.nspname || '.' || c.relname AS table_name, obj_description(c.oid, 'pg_class') AS table_comment, c.reltuples::bigint AS table_rows",
|
||||
"FROM pg_class c",
|
||||
"JOIN pg_namespace n ON n.oid = c.relnamespace",
|
||||
"WHERE c.relkind = 'r'",
|
||||
@@ -286,18 +286,19 @@ const buildSidebarTableStatusSQL = (
|
||||
case "sqlserver": {
|
||||
const safeDb = quoteSqlServerIdentifier(dbName);
|
||||
return [
|
||||
"SELECT s.name + '.' + t.name AS table_name, SUM(p.rows) AS table_rows",
|
||||
"SELECT s.name + '.' + t.name AS table_name, ep.value AS table_comment, SUM(p.rows) AS table_rows",
|
||||
`FROM ${safeDb}.sys.tables t`,
|
||||
`JOIN ${safeDb}.sys.schemas s ON t.schema_id = s.schema_id`,
|
||||
`LEFT JOIN ${safeDb}.sys.extended_properties ep ON ep.major_id = t.object_id AND ep.minor_id = 0 AND ep.name = 'MS_Description'`,
|
||||
`LEFT JOIN ${safeDb}.sys.partitions p ON t.object_id = p.object_id AND p.index_id IN (0, 1)`,
|
||||
"WHERE t.type = 'U'",
|
||||
"GROUP BY s.name, t.name",
|
||||
"GROUP BY s.name, t.name, ep.value",
|
||||
"ORDER BY s.name, t.name",
|
||||
].join("\n");
|
||||
}
|
||||
case "clickhouse":
|
||||
return [
|
||||
"SELECT name AS table_name, total_rows AS table_rows",
|
||||
"SELECT name AS table_name, comment AS table_comment, total_rows AS table_rows",
|
||||
"FROM system.tables",
|
||||
`WHERE database = '${safeDbName}'`,
|
||||
"AND engine NOT IN ('View', 'MaterializedView')",
|
||||
@@ -307,8 +308,8 @@ const buildSidebarTableStatusSQL = (
|
||||
case "dm": {
|
||||
const owner = escapeSQLLiteral(dbName).toUpperCase();
|
||||
return [
|
||||
"SELECT table_name, num_rows AS table_rows",
|
||||
"FROM all_tables",
|
||||
"SELECT table_name, comments AS table_comment, num_rows AS table_rows",
|
||||
"FROM all_tab_comments JOIN all_tables USING (table_name, owner)",
|
||||
`WHERE owner = '${owner}'`,
|
||||
"ORDER BY table_name",
|
||||
].join("\n");
|
||||
|
||||
@@ -62,6 +62,10 @@ export const useSidebarTitleRender = ({
|
||||
hoverTitle = rawTableName;
|
||||
}
|
||||
}
|
||||
const tableComment = node.type === 'table' ? String(node?.dataRef?.tableComment || '').trim() : '';
|
||||
if (tableComment) {
|
||||
hoverTitle = `${hoverTitle}\n${t('sidebar.v2_table_group_menu.table_comment_tooltip', { comment: tableComment })}`;
|
||||
}
|
||||
} else if (node.type === 'object-group') {
|
||||
const objectGroupTitle = resolveV2ObjectGroupTitle(node);
|
||||
if (objectGroupTitle) {
|
||||
|
||||
@@ -486,6 +486,16 @@ export const useSidebarTreeLoaders = ({
|
||||
? await DBQuery(buildRpcConnectionConfig(config) as any, conn.dbName, tableStatusSql).catch(() => ({ success: false, data: [] as any[] }))
|
||||
: { success: false, data: [] as any[] };
|
||||
const tableRowCountMap = new Map<string, number>();
|
||||
const tableCommentMap = new Map<string, string>();
|
||||
const putTableComment = (rawTableName: string, rawComment: string) => {
|
||||
const tableName = String(rawTableName || '').trim();
|
||||
const comment = String(rawComment || '').trim();
|
||||
if (!tableName || !comment) return;
|
||||
const keys = new Set<string>([tableName.toLowerCase()]);
|
||||
const parsed = splitQualifiedName(tableName);
|
||||
if (parsed.objectName) keys.add(parsed.objectName.toLowerCase());
|
||||
keys.forEach((metadataKey) => tableCommentMap.set(metadataKey, comment));
|
||||
};
|
||||
if (tableStatsResult?.success && Array.isArray(tableStatsResult.data)) {
|
||||
tableStatsResult.data.forEach((row: Record<string, any>) => {
|
||||
const rawTableName = String(
|
||||
@@ -494,6 +504,15 @@ export const useSidebarTreeLoaders = ({
|
||||
|| ''
|
||||
).trim();
|
||||
if (!rawTableName) return;
|
||||
putTableComment(rawTableName, getCaseInsensitiveValue(row, [
|
||||
'table_comment',
|
||||
'TABLE_COMMENT',
|
||||
'comment',
|
||||
'Comment',
|
||||
'comments',
|
||||
'COMMENTS',
|
||||
'MS_Description',
|
||||
]));
|
||||
const rowCount = parseMetadataRowCount(row);
|
||||
if (rowCount === undefined) return;
|
||||
tableRowCountMap.set(rawTableName.toLowerCase(), rowCount);
|
||||
@@ -502,11 +521,23 @@ export const useSidebarTreeLoaders = ({
|
||||
const tableEntries = tableRows.map((row: any) => {
|
||||
const tableName = Object.values(row)[0] as string;
|
||||
const parsed = splitQualifiedName(tableName);
|
||||
const rowComment = getCaseInsensitiveValue(row, [
|
||||
'table_comment',
|
||||
'TABLE_COMMENT',
|
||||
'comment',
|
||||
'Comment',
|
||||
'comments',
|
||||
'COMMENTS',
|
||||
]);
|
||||
return {
|
||||
tableName,
|
||||
schemaName: parsed.schemaName,
|
||||
displayName: getSidebarTableDisplayName(conn, tableName),
|
||||
rowCount: tableRowCountMap.get(String(tableName || '').trim().toLowerCase()),
|
||||
tableComment: rowComment
|
||||
|| tableCommentMap.get(String(tableName || '').trim().toLowerCase())
|
||||
|| tableCommentMap.get(String(parsed.objectName || '').trim().toLowerCase())
|
||||
|| '',
|
||||
};
|
||||
});
|
||||
|
||||
@@ -660,7 +691,7 @@ export const useSidebarTreeLoaders = ({
|
||||
|
||||
eventEntries.sort((a, b) => a.displayName.toLowerCase().localeCompare(b.displayName.toLowerCase()));
|
||||
|
||||
const buildTableNode = (entry: { tableName: string; schemaName: string; displayName: string; rowCount?: number }): TreeNode => {
|
||||
const buildTableNode = (entry: { tableName: string; schemaName: string; displayName: string; rowCount?: number; tableComment?: string }): TreeNode => {
|
||||
const isPinned = isV2Ui && isSidebarTablePinned(
|
||||
currentPinnedSidebarTables,
|
||||
conn.id,
|
||||
@@ -678,6 +709,7 @@ export const useSidebarTreeLoaders = ({
|
||||
tableName: entry.tableName,
|
||||
schemaName: entry.schemaName,
|
||||
rowCount: entry.rowCount,
|
||||
tableComment: entry.tableComment,
|
||||
...(isPinned ? { pinnedSidebarTable: true } : {}),
|
||||
},
|
||||
isLeaf: false,
|
||||
|
||||
@@ -5,6 +5,7 @@ import type { FormInstance } from 'antd/es/form';
|
||||
import Modal from '../common/ResizableDraggableModal';
|
||||
import { t } from '../../i18n';
|
||||
import type { SavedConnection } from '../../types';
|
||||
import type { QueryOptions } from '../../store';
|
||||
import { buildRpcConnectionConfig } from '../../utils/connectionRpcConfig';
|
||||
import { resolveConnectionAccentColor, resolveConnectionIconType } from '../../utils/connectionVisual';
|
||||
import { buildTableSelectQuery } from '../../utils/objectQueryTemplates';
|
||||
@@ -55,6 +56,8 @@ type UseSidebarV2ActionHandlersArgs = {
|
||||
moveConnectionToTag: (connectionId: string, tagId: string | null) => void;
|
||||
setSidebarTablePinned: (connectionId: string, dbName: string, tableName: string, schemaName: string, pinned: boolean) => void;
|
||||
setTableSortPreference: (connectionId: string, dbName: string, sortBy: 'name' | 'frequency') => void;
|
||||
setQueryOptions: (options: Partial<QueryOptions>) => void;
|
||||
showSidebarTableComment: boolean;
|
||||
replaceTreeNodeChildren: (key: React.Key, children: TreeNode[] | undefined) => void;
|
||||
loadDatabases: (node: any) => Promise<void>;
|
||||
loadTables: (node: any) => Promise<void>;
|
||||
@@ -118,6 +121,8 @@ export const useSidebarV2ActionHandlers = ({
|
||||
moveConnectionToTag,
|
||||
setSidebarTablePinned,
|
||||
setTableSortPreference,
|
||||
setQueryOptions,
|
||||
showSidebarTableComment,
|
||||
replaceTreeNodeChildren,
|
||||
loadDatabases,
|
||||
loadTables,
|
||||
@@ -262,6 +267,9 @@ export const useSidebarV2ActionHandlers = ({
|
||||
case 'new-table':
|
||||
openNewTableDesign(node);
|
||||
return;
|
||||
case 'toggle-table-comments':
|
||||
setQueryOptions({ showSidebarTableComment: !showSidebarTableComment });
|
||||
return;
|
||||
case 'sort-by-name':
|
||||
handleTableGroupSortAction(node, 'name');
|
||||
return;
|
||||
|
||||
@@ -55,6 +55,7 @@ type SidebarV2ContextMenuOptions = {
|
||||
};
|
||||
tableSortPreference: Record<string, any>;
|
||||
pinnedSidebarTables: any[];
|
||||
showSidebarTableComment: boolean;
|
||||
getConnectionNodeForAction: (conn: SavedConnection) => TreeNode;
|
||||
buildRuntimeConfig: (conn: any, overrideDatabase?: string, clearDatabase?: boolean) => any;
|
||||
extractObjectName: (fullName: string) => string;
|
||||
@@ -82,6 +83,7 @@ export const useSidebarV2ContextMenu = ({
|
||||
v2TreeMetrics,
|
||||
tableSortPreference,
|
||||
pinnedSidebarTables,
|
||||
showSidebarTableComment,
|
||||
getConnectionNodeForAction,
|
||||
buildRuntimeConfig,
|
||||
extractObjectName,
|
||||
@@ -311,6 +313,7 @@ export const useSidebarV2ContextMenu = ({
|
||||
dbName={String(groupData.dbName || '')}
|
||||
count={Array.isArray(node.children) ? node.children.length : 0}
|
||||
currentSort={currentSort}
|
||||
showTableComments={showSidebarTableComment}
|
||||
onAction={(action) => {
|
||||
setContextMenu(null);
|
||||
handleV2TableGroupContextMenuAction(node, action);
|
||||
|
||||
@@ -1231,6 +1231,7 @@ export interface SqlLog {
|
||||
export interface QueryOptions {
|
||||
maxRows: number;
|
||||
showColumnComment: boolean;
|
||||
showSidebarTableComment?: boolean;
|
||||
showColumnType: boolean;
|
||||
showQueryResultsPanel: boolean;
|
||||
}
|
||||
@@ -1922,16 +1923,21 @@ const sanitizeQueryOptions = (value: unknown): QueryOptions => {
|
||||
const maxRows = Number(raw.maxRows);
|
||||
const showColumnComment =
|
||||
typeof raw.showColumnComment === "boolean" ? raw.showColumnComment : true;
|
||||
const showSidebarTableComment =
|
||||
typeof raw.showSidebarTableComment === "boolean"
|
||||
? raw.showSidebarTableComment
|
||||
: false;
|
||||
const showColumnType =
|
||||
typeof raw.showColumnType === "boolean" ? raw.showColumnType : true;
|
||||
const showQueryResultsPanel =
|
||||
typeof raw.showQueryResultsPanel === "boolean" ? raw.showQueryResultsPanel : false;
|
||||
if (!Number.isFinite(maxRows) || maxRows <= 0) {
|
||||
return { maxRows: 5000, showColumnComment, showColumnType, showQueryResultsPanel };
|
||||
return { maxRows: 5000, showColumnComment, showSidebarTableComment, showColumnType, showQueryResultsPanel };
|
||||
}
|
||||
return {
|
||||
maxRows: Math.min(50000, Math.trunc(maxRows)),
|
||||
showColumnComment,
|
||||
showSidebarTableComment,
|
||||
showColumnType,
|
||||
showQueryResultsPanel,
|
||||
};
|
||||
@@ -2361,6 +2367,7 @@ export const useStore = create<AppState>()(
|
||||
queryOptions: {
|
||||
maxRows: 5000,
|
||||
showColumnComment: true,
|
||||
showSidebarTableComment: false,
|
||||
showColumnType: true,
|
||||
showQueryResultsPanel: false,
|
||||
},
|
||||
|
||||
@@ -2847,6 +2847,100 @@ body[data-ui-version="v2"] .gn-v2-tree-title.is-redis-db .gn-v2-tree-label {
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
body[data-ui-version="v2"] .gn-v2-tree-table-comment {
|
||||
max-width: 24em;
|
||||
flex: 0 1 auto;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
color: var(--gn-fg-5);
|
||||
font-family: var(--gn-font-sans);
|
||||
font-size: clamp(10px, calc(var(--gn-sidebar-tree-font-size, var(--gn-font-size-sm, 12px)) - 1px), 16px);
|
||||
font-weight: 400 !important;
|
||||
opacity: 0.78;
|
||||
}
|
||||
|
||||
body[data-ui-version="v2"] .gn-v2-tab-hover-tooltip .ant-tooltip-inner {
|
||||
min-width: 260px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
body[data-ui-version="v2"] .gn-v2-tab-hover-tooltip {
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
body[data-ui-version="v2"] .gn-v2-tab-hover-card {
|
||||
--gn-v2-tab-hover-grid-columns: 56px minmax(0, 1fr);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
padding: 10px;
|
||||
color: var(--gn-fg-2);
|
||||
cursor: text;
|
||||
user-select: text;
|
||||
-webkit-user-select: text;
|
||||
}
|
||||
|
||||
body[data-ui-version="v2"] .gn-v2-tab-hover-card * {
|
||||
user-select: text;
|
||||
-webkit-user-select: text;
|
||||
}
|
||||
|
||||
body[data-ui-version="v2"] .gn-v2-tab-hover-head {
|
||||
display: grid;
|
||||
grid-template-columns: var(--gn-v2-tab-hover-grid-columns);
|
||||
align-items: start;
|
||||
gap: 8px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
body[data-ui-version="v2"] .gn-v2-tab-hover-head > span {
|
||||
justify-self: start;
|
||||
padding: 2px 6px;
|
||||
border-radius: 5px;
|
||||
background: var(--gn-bg-active);
|
||||
color: var(--gn-accent-2);
|
||||
font-family: var(--gn-font-mono);
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
line-height: 14px;
|
||||
}
|
||||
|
||||
body[data-ui-version="v2"] .gn-v2-tab-hover-head > strong {
|
||||
min-width: 0;
|
||||
overflow-wrap: anywhere;
|
||||
color: var(--gn-fg-1);
|
||||
font-size: var(--gn-font-size-sm, 12px);
|
||||
font-weight: 700;
|
||||
line-height: 18px;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
body[data-ui-version="v2"] .gn-v2-tab-hover-rows {
|
||||
display: grid;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
body[data-ui-version="v2"] .gn-v2-tab-hover-row {
|
||||
display: grid;
|
||||
grid-template-columns: var(--gn-v2-tab-hover-grid-columns);
|
||||
align-items: start;
|
||||
gap: 8px;
|
||||
font-size: var(--gn-font-size-sm, 12px);
|
||||
line-height: 18px;
|
||||
}
|
||||
|
||||
body[data-ui-version="v2"] .gn-v2-tab-hover-row > span {
|
||||
color: var(--gn-fg-5);
|
||||
}
|
||||
|
||||
body[data-ui-version="v2"] .gn-v2-tab-hover-row > strong {
|
||||
min-width: 0;
|
||||
overflow-wrap: anywhere;
|
||||
color: var(--gn-fg-2);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
body[data-ui-version="v2"] .gn-v2-redis-db-alias {
|
||||
color: var(--gn-fg-5);
|
||||
font-family: var(--gn-font-sans);
|
||||
|
||||
@@ -7074,9 +7074,12 @@
|
||||
"sidebar.v2_schema_menu.export_current_schema_sql": "Tabellenstrukturen des aktuellen Schemas exportieren · SQL",
|
||||
"sidebar.v2_schema_menu.meta": "{{database}} · Schema-Aktionen",
|
||||
"sidebar.v2_table_group_menu.current_database": "Aktuelle Datenbank",
|
||||
"sidebar.v2_table_group_menu.display_section": "Anzeige",
|
||||
"sidebar.v2_table_group_menu.meta": "{{database}} · {{count}} Tabellen · nach {{sort}} sortiert",
|
||||
"sidebar.v2_table_group_menu.show_table_comments": "Tabellenkommentare anzeigen",
|
||||
"sidebar.v2_table_group_menu.sort_frequency": "Nutzungshäufigkeit",
|
||||
"sidebar.v2_table_group_menu.sort_name": "Name",
|
||||
"sidebar.v2_table_group_menu.table_comment_tooltip": "Kommentar: {{comment}}",
|
||||
"sidebar.v2_table_group_menu.title": "Tabellen",
|
||||
"sidebar.v2_table_menu.ai_explain_table": "Mit AI diese Tabelle erklären",
|
||||
"sidebar.v2_table_menu.ai_generate_query": "Mit AI eine Abfrage erzeugen",
|
||||
|
||||
@@ -7074,9 +7074,12 @@
|
||||
"sidebar.v2_schema_menu.export_current_schema_sql": "Export current schema table structures · SQL",
|
||||
"sidebar.v2_schema_menu.meta": "{{database}} · Schema actions",
|
||||
"sidebar.v2_table_group_menu.current_database": "Current database",
|
||||
"sidebar.v2_table_group_menu.display_section": "Display",
|
||||
"sidebar.v2_table_group_menu.meta": "{{database}} · {{count}} tables · sorted by {{sort}}",
|
||||
"sidebar.v2_table_group_menu.show_table_comments": "Show table comments",
|
||||
"sidebar.v2_table_group_menu.sort_frequency": "usage frequency",
|
||||
"sidebar.v2_table_group_menu.sort_name": "name",
|
||||
"sidebar.v2_table_group_menu.table_comment_tooltip": "Comment: {{comment}}",
|
||||
"sidebar.v2_table_group_menu.title": "Tables",
|
||||
"sidebar.v2_table_menu.ai_explain_table": "Use AI to explain this table",
|
||||
"sidebar.v2_table_menu.ai_generate_query": "Use AI to generate a query",
|
||||
|
||||
@@ -7074,9 +7074,12 @@
|
||||
"sidebar.v2_schema_menu.export_current_schema_sql": "現在のスキーマのテーブル構造をエクスポート · SQL",
|
||||
"sidebar.v2_schema_menu.meta": "{{database}} · スキーマ操作",
|
||||
"sidebar.v2_table_group_menu.current_database": "現在のデータベース",
|
||||
"sidebar.v2_table_group_menu.display_section": "表示",
|
||||
"sidebar.v2_table_group_menu.meta": "{{database}} · {{count}} テーブル · {{sort}}順で並べ替え中",
|
||||
"sidebar.v2_table_group_menu.show_table_comments": "テーブルコメントを表示",
|
||||
"sidebar.v2_table_group_menu.sort_frequency": "使用頻度",
|
||||
"sidebar.v2_table_group_menu.sort_name": "名前",
|
||||
"sidebar.v2_table_group_menu.table_comment_tooltip": "コメント: {{comment}}",
|
||||
"sidebar.v2_table_group_menu.title": "テーブル",
|
||||
"sidebar.v2_table_menu.ai_explain_table": "AI でこのテーブルを説明",
|
||||
"sidebar.v2_table_menu.ai_generate_query": "AI でクエリを生成",
|
||||
|
||||
@@ -7074,9 +7074,12 @@
|
||||
"sidebar.v2_schema_menu.export_current_schema_sql": "Экспортировать структуры таблиц текущей схемы · SQL",
|
||||
"sidebar.v2_schema_menu.meta": "{{database}} · Действия со схемой",
|
||||
"sidebar.v2_table_group_menu.current_database": "Текущая база данных",
|
||||
"sidebar.v2_table_group_menu.display_section": "Отображение",
|
||||
"sidebar.v2_table_group_menu.meta": "{{database}} · {{count}} таблиц · сортировка по {{sort}}",
|
||||
"sidebar.v2_table_group_menu.show_table_comments": "Показывать комментарии таблиц",
|
||||
"sidebar.v2_table_group_menu.sort_frequency": "частоте использования",
|
||||
"sidebar.v2_table_group_menu.sort_name": "имени",
|
||||
"sidebar.v2_table_group_menu.table_comment_tooltip": "Комментарий: {{comment}}",
|
||||
"sidebar.v2_table_group_menu.title": "Таблицы",
|
||||
"sidebar.v2_table_menu.ai_explain_table": "Объяснить эту таблицу с помощью AI",
|
||||
"sidebar.v2_table_menu.ai_generate_query": "Сгенерировать запрос с помощью AI",
|
||||
|
||||
@@ -7074,9 +7074,12 @@
|
||||
"sidebar.v2_schema_menu.export_current_schema_sql": "导出当前模式表结构 · SQL",
|
||||
"sidebar.v2_schema_menu.meta": "{{database}} · 模式操作",
|
||||
"sidebar.v2_table_group_menu.current_database": "当前数据库",
|
||||
"sidebar.v2_table_group_menu.display_section": "显示",
|
||||
"sidebar.v2_table_group_menu.meta": "{{database}} · {{count}} 张表 · 当前按{{sort}}排序",
|
||||
"sidebar.v2_table_group_menu.show_table_comments": "显示表备注",
|
||||
"sidebar.v2_table_group_menu.sort_frequency": "使用频率",
|
||||
"sidebar.v2_table_group_menu.sort_name": "名称",
|
||||
"sidebar.v2_table_group_menu.table_comment_tooltip": "备注:{{comment}}",
|
||||
"sidebar.v2_table_group_menu.title": "表",
|
||||
"sidebar.v2_table_menu.ai_explain_table": "用 AI 解释这张表",
|
||||
"sidebar.v2_table_menu.ai_generate_query": "用 AI 生成查询",
|
||||
|
||||
@@ -7074,9 +7074,12 @@
|
||||
"sidebar.v2_schema_menu.export_current_schema_sql": "匯出目前模式的資料表結構 · SQL",
|
||||
"sidebar.v2_schema_menu.meta": "{{database}} · 模式操作",
|
||||
"sidebar.v2_table_group_menu.current_database": "目前資料庫",
|
||||
"sidebar.v2_table_group_menu.display_section": "顯示",
|
||||
"sidebar.v2_table_group_menu.meta": "{{database}} · {{count}} 張資料表 · 目前依{{sort}}排序",
|
||||
"sidebar.v2_table_group_menu.show_table_comments": "顯示資料表備註",
|
||||
"sidebar.v2_table_group_menu.sort_frequency": "使用頻率",
|
||||
"sidebar.v2_table_group_menu.sort_name": "名稱",
|
||||
"sidebar.v2_table_group_menu.table_comment_tooltip": "備註:{{comment}}",
|
||||
"sidebar.v2_table_group_menu.title": "資料表",
|
||||
"sidebar.v2_table_menu.ai_explain_table": "用 AI 解釋這張資料表",
|
||||
"sidebar.v2_table_menu.ai_generate_query": "用 AI 產生查詢",
|
||||
|
||||
Reference in New Issue
Block a user