mirror of
https://github.com/chartdb/chartdb.git
synced 2025-10-23 16:13:40 +00:00
feat: add table selection for large database imports (#776)
* feat: add table selection UI for large database imports (>50 tables) * some changes * some changes * some changes * fix * fix --------- Co-authored-by: Guy Ben-Aharon <baguy3@gmail.com>
This commit is contained in:
73
package-lock.json
generated
73
package-lock.json
generated
@@ -28,7 +28,7 @@
|
||||
"@radix-ui/react-scroll-area": "1.2.0",
|
||||
"@radix-ui/react-select": "^2.1.1",
|
||||
"@radix-ui/react-separator": "^1.1.2",
|
||||
"@radix-ui/react-slot": "^1.1.2",
|
||||
"@radix-ui/react-slot": "^1.2.3",
|
||||
"@radix-ui/react-tabs": "^1.1.0",
|
||||
"@radix-ui/react-toast": "^1.2.1",
|
||||
"@radix-ui/react-toggle": "^1.1.0",
|
||||
@@ -2255,6 +2255,24 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-slot": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.2.tgz",
|
||||
"integrity": "sha512-YAKxaiGsSQJ38VzKH86/BPRC4rh+b1Jpa+JneA5LRE7skmLPNAyeG8kPJj/oo4STLvlrs8vkf/iYyc3A5stYCQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-compose-refs": "1.1.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-direction": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.0.tgz",
|
||||
@@ -2968,7 +2986,7 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-slot": {
|
||||
"node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-slot": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.2.tgz",
|
||||
"integrity": "sha512-YAKxaiGsSQJ38VzKH86/BPRC4rh+b1Jpa+JneA5LRE7skmLPNAyeG8kPJj/oo4STLvlrs8vkf/iYyc3A5stYCQ==",
|
||||
@@ -2986,6 +3004,39 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-slot": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
|
||||
"integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-compose-refs": "1.1.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-slot/node_modules/@radix-ui/react-compose-refs": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz",
|
||||
"integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-tabs": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.2.tgz",
|
||||
@@ -3267,6 +3318,24 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-slot": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.2.tgz",
|
||||
"integrity": "sha512-YAKxaiGsSQJ38VzKH86/BPRC4rh+b1Jpa+JneA5LRE7skmLPNAyeG8kPJj/oo4STLvlrs8vkf/iYyc3A5stYCQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-compose-refs": "1.1.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-visually-hidden": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.1.2.tgz",
|
||||
|
@@ -36,7 +36,7 @@
|
||||
"@radix-ui/react-scroll-area": "1.2.0",
|
||||
"@radix-ui/react-select": "^2.1.1",
|
||||
"@radix-ui/react-separator": "^1.1.2",
|
||||
"@radix-ui/react-slot": "^1.1.2",
|
||||
"@radix-ui/react-slot": "^1.2.3",
|
||||
"@radix-ui/react-tabs": "^1.1.0",
|
||||
"@radix-ui/react-toast": "^1.2.1",
|
||||
"@radix-ui/react-toggle": "^1.1.0",
|
||||
|
121
src/components/pagination/pagination.tsx
Normal file
121
src/components/pagination/pagination.tsx
Normal file
@@ -0,0 +1,121 @@
|
||||
import React from 'react';
|
||||
import { cn } from '@/lib/utils';
|
||||
import type { ButtonProps } from '../button/button';
|
||||
import { buttonVariants } from '../button/button-variants';
|
||||
import {
|
||||
ChevronLeftIcon,
|
||||
ChevronRightIcon,
|
||||
DotsHorizontalIcon,
|
||||
} from '@radix-ui/react-icons';
|
||||
|
||||
const Pagination = ({ className, ...props }: React.ComponentProps<'nav'>) => (
|
||||
<nav
|
||||
role="navigation"
|
||||
aria-label="pagination"
|
||||
className={cn('mx-auto flex w-full justify-center', className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
Pagination.displayName = 'Pagination';
|
||||
|
||||
const PaginationContent = React.forwardRef<
|
||||
HTMLUListElement,
|
||||
React.ComponentProps<'ul'>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<ul
|
||||
ref={ref}
|
||||
className={cn('flex flex-row items-center gap-1', className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
PaginationContent.displayName = 'PaginationContent';
|
||||
|
||||
const PaginationItem = React.forwardRef<
|
||||
HTMLLIElement,
|
||||
React.ComponentProps<'li'>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<li ref={ref} className={cn('', className)} {...props} />
|
||||
));
|
||||
PaginationItem.displayName = 'PaginationItem';
|
||||
|
||||
type PaginationLinkProps = {
|
||||
isActive?: boolean;
|
||||
} & Pick<ButtonProps, 'size'> &
|
||||
React.ComponentProps<'a'>;
|
||||
|
||||
const PaginationLink = ({
|
||||
className,
|
||||
isActive,
|
||||
size = 'icon',
|
||||
...props
|
||||
}: PaginationLinkProps) => (
|
||||
<a
|
||||
aria-current={isActive ? 'page' : undefined}
|
||||
className={cn(
|
||||
buttonVariants({
|
||||
variant: isActive ? 'outline' : 'ghost',
|
||||
size,
|
||||
}),
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
PaginationLink.displayName = 'PaginationLink';
|
||||
|
||||
const PaginationPrevious = ({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof PaginationLink>) => (
|
||||
<PaginationLink
|
||||
aria-label="Go to previous page"
|
||||
size="default"
|
||||
className={cn('gap-1 pl-2.5', className)}
|
||||
{...props}
|
||||
>
|
||||
<ChevronLeftIcon className="size-4" />
|
||||
<span>Previous</span>
|
||||
</PaginationLink>
|
||||
);
|
||||
PaginationPrevious.displayName = 'PaginationPrevious';
|
||||
|
||||
const PaginationNext = ({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof PaginationLink>) => (
|
||||
<PaginationLink
|
||||
aria-label="Go to next page"
|
||||
size="default"
|
||||
className={cn('gap-1 pr-2.5', className)}
|
||||
{...props}
|
||||
>
|
||||
<span>Next</span>
|
||||
<ChevronRightIcon className="size-4" />
|
||||
</PaginationLink>
|
||||
);
|
||||
PaginationNext.displayName = 'PaginationNext';
|
||||
|
||||
const PaginationEllipsis = ({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<'span'>) => (
|
||||
<span
|
||||
aria-hidden
|
||||
className={cn('flex h-9 w-9 items-center justify-center', className)}
|
||||
{...props}
|
||||
>
|
||||
<DotsHorizontalIcon className="size-4" />
|
||||
<span className="sr-only">More pages</span>
|
||||
</span>
|
||||
);
|
||||
PaginationEllipsis.displayName = 'PaginationEllipsis';
|
||||
|
||||
export {
|
||||
Pagination,
|
||||
PaginationContent,
|
||||
PaginationLink,
|
||||
PaginationItem,
|
||||
PaginationPrevious,
|
||||
PaginationNext,
|
||||
PaginationEllipsis,
|
||||
};
|
@@ -43,6 +43,15 @@ import {
|
||||
} from '@/lib/data/sql-import/sql-validator';
|
||||
import { SQLValidationStatus } from './sql-validation-status';
|
||||
|
||||
const calculateContentSizeMB = (content: string): number => {
|
||||
return content.length / (1024 * 1024); // Convert to MB
|
||||
};
|
||||
|
||||
const calculateIsLargeFile = (content: string): boolean => {
|
||||
const contentSizeMB = calculateContentSizeMB(content);
|
||||
return contentSizeMB > 2; // Consider large if over 2MB
|
||||
};
|
||||
|
||||
const errorScriptOutputMessage =
|
||||
'Invalid JSON. Please correct it or contact us at support@chartdb.io for help.';
|
||||
|
||||
@@ -246,6 +255,16 @@ export const ImportDatabase: React.FC<ImportDatabaseProps> = ({
|
||||
|
||||
const formatEditor = useCallback(() => {
|
||||
if (editorRef.current) {
|
||||
const model = editorRef.current.getModel();
|
||||
if (model) {
|
||||
const content = model.getValue();
|
||||
|
||||
// Skip formatting for large files (> 2MB)
|
||||
if (calculateIsLargeFile(content)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
editorRef.current
|
||||
?.getAction('editor.action.formatDocument')
|
||||
@@ -315,14 +334,17 @@ export const ImportDatabase: React.FC<ImportDatabaseProps> = ({
|
||||
|
||||
const content = model.getValue();
|
||||
|
||||
// Skip formatting for large files (> 2MB) to prevent browser freezing
|
||||
const isLargeFile = calculateIsLargeFile(content);
|
||||
|
||||
// First, detect content type to determine if we should switch modes
|
||||
const detectedType = detectContentType(content);
|
||||
if (detectedType && detectedType !== importMethod) {
|
||||
// Switch to the detected mode immediately
|
||||
setImportMethod(detectedType);
|
||||
|
||||
// Only format if it's JSON (query mode)
|
||||
if (detectedType === 'query') {
|
||||
// Only format if it's JSON (query mode) AND file is not too large
|
||||
if (detectedType === 'query' && !isLargeFile) {
|
||||
// For JSON mode, format after a short delay
|
||||
setTimeout(() => {
|
||||
editor
|
||||
@@ -333,15 +355,15 @@ export const ImportDatabase: React.FC<ImportDatabaseProps> = ({
|
||||
// For DDL mode, do NOT format as it can break the SQL
|
||||
} else {
|
||||
// Content type didn't change, apply formatting based on current mode
|
||||
if (importMethod === 'query') {
|
||||
// Only format JSON content
|
||||
if (importMethod === 'query' && !isLargeFile) {
|
||||
// Only format JSON content if not too large
|
||||
setTimeout(() => {
|
||||
editor
|
||||
.getAction('editor.action.formatDocument')
|
||||
?.run();
|
||||
}, 100);
|
||||
}
|
||||
// For DDL mode, do NOT format
|
||||
// For DDL mode or large files, do NOT format
|
||||
}
|
||||
});
|
||||
|
||||
|
2
src/dialogs/common/select-tables/constants.ts
Normal file
2
src/dialogs/common/select-tables/constants.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export const MAX_TABLES_IN_DIAGRAM = 500;
|
||||
export const MAX_TABLES_WITHOUT_SHOWING_FILTER = 50;
|
665
src/dialogs/common/select-tables/select-tables.tsx
Normal file
665
src/dialogs/common/select-tables/select-tables.tsx
Normal file
@@ -0,0 +1,665 @@
|
||||
import React, { useState, useMemo, useEffect, useCallback } from 'react';
|
||||
import { Button } from '@/components/button/button';
|
||||
import { Input } from '@/components/input/input';
|
||||
import { Search, AlertCircle, Check, X, View, Table } from 'lucide-react';
|
||||
import { Checkbox } from '@/components/checkbox/checkbox';
|
||||
import type { DatabaseMetadata } from '@/lib/data/import-metadata/metadata-types/database-metadata';
|
||||
import { schemaNameToDomainSchemaName } from '@/lib/domain/db-schema';
|
||||
import { cn } from '@/lib/utils';
|
||||
import {
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogInternalContent,
|
||||
DialogTitle,
|
||||
} from '@/components/dialog/dialog';
|
||||
import type { SelectedTable } from '@/lib/data/import-metadata/filter-metadata';
|
||||
import { generateTableKey } from '@/lib/domain';
|
||||
import { Spinner } from '@/components/spinner/spinner';
|
||||
import {
|
||||
Pagination,
|
||||
PaginationContent,
|
||||
PaginationItem,
|
||||
PaginationPrevious,
|
||||
PaginationNext,
|
||||
} from '@/components/pagination/pagination';
|
||||
import { MAX_TABLES_IN_DIAGRAM } from './constants';
|
||||
import { useBreakpoint } from '@/hooks/use-breakpoint';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export interface SelectTablesProps {
|
||||
databaseMetadata?: DatabaseMetadata;
|
||||
onImport: ({
|
||||
selectedTables,
|
||||
databaseMetadata,
|
||||
}: {
|
||||
selectedTables?: SelectedTable[];
|
||||
databaseMetadata?: DatabaseMetadata;
|
||||
}) => Promise<void>;
|
||||
onBack: () => void;
|
||||
isLoading?: boolean;
|
||||
}
|
||||
|
||||
const TABLES_PER_PAGE = 10;
|
||||
|
||||
interface TableInfo {
|
||||
key: string;
|
||||
schema?: string;
|
||||
tableName: string;
|
||||
fullName: string;
|
||||
type: 'table' | 'view';
|
||||
}
|
||||
|
||||
export const SelectTables: React.FC<SelectTablesProps> = ({
|
||||
databaseMetadata,
|
||||
onImport,
|
||||
onBack,
|
||||
isLoading = false,
|
||||
}) => {
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [showTables, setShowTables] = useState(true);
|
||||
const [showViews, setShowViews] = useState(false);
|
||||
const { t } = useTranslation();
|
||||
|
||||
// Prepare all tables and views with their metadata
|
||||
const allTables = useMemo(() => {
|
||||
const tables: TableInfo[] = [];
|
||||
|
||||
// Add regular tables
|
||||
databaseMetadata?.tables.forEach((table) => {
|
||||
const schema = schemaNameToDomainSchemaName(table.schema);
|
||||
const tableName = table.table;
|
||||
|
||||
const key = `table:${generateTableKey({ tableName, schemaName: schema })}`;
|
||||
|
||||
tables.push({
|
||||
key,
|
||||
schema,
|
||||
tableName,
|
||||
fullName: schema ? `${schema}.${tableName}` : tableName,
|
||||
type: 'table',
|
||||
});
|
||||
});
|
||||
|
||||
// Add views
|
||||
databaseMetadata?.views?.forEach((view) => {
|
||||
const schema = schemaNameToDomainSchemaName(view.schema);
|
||||
const viewName = view.view_name;
|
||||
|
||||
if (!viewName) {
|
||||
return;
|
||||
}
|
||||
|
||||
const key = `view:${generateTableKey({
|
||||
tableName: viewName,
|
||||
schemaName: schema,
|
||||
})}`;
|
||||
|
||||
tables.push({
|
||||
key,
|
||||
schema,
|
||||
tableName: viewName,
|
||||
fullName:
|
||||
schema === 'default' ? viewName : `${schema}.${viewName}`,
|
||||
type: 'view',
|
||||
});
|
||||
});
|
||||
|
||||
return tables.sort((a, b) => a.fullName.localeCompare(b.fullName));
|
||||
}, [databaseMetadata?.tables, databaseMetadata?.views]);
|
||||
|
||||
// Count tables and views separately
|
||||
const tableCount = useMemo(
|
||||
() => allTables.filter((t) => t.type === 'table').length,
|
||||
[allTables]
|
||||
);
|
||||
const viewCount = useMemo(
|
||||
() => allTables.filter((t) => t.type === 'view').length,
|
||||
[allTables]
|
||||
);
|
||||
|
||||
// Initialize selectedTables with all tables (not views) if less than 100 tables
|
||||
const [selectedTables, setSelectedTables] = useState<Set<string>>(() => {
|
||||
const tables = allTables.filter((t) => t.type === 'table');
|
||||
if (tables.length < MAX_TABLES_IN_DIAGRAM) {
|
||||
return new Set(tables.map((t) => t.key));
|
||||
}
|
||||
return new Set();
|
||||
});
|
||||
|
||||
// Filter tables based on search term and type filters
|
||||
const filteredTables = useMemo(() => {
|
||||
let filtered = allTables;
|
||||
|
||||
// Filter by type
|
||||
filtered = filtered.filter((table) => {
|
||||
if (table.type === 'table' && !showTables) return false;
|
||||
if (table.type === 'view' && !showViews) return false;
|
||||
return true;
|
||||
});
|
||||
|
||||
// Filter by search term
|
||||
if (searchTerm.trim()) {
|
||||
const searchLower = searchTerm.toLowerCase();
|
||||
filtered = filtered.filter(
|
||||
(table) =>
|
||||
table.tableName.toLowerCase().includes(searchLower) ||
|
||||
table.schema?.toLowerCase().includes(searchLower) ||
|
||||
table.fullName.toLowerCase().includes(searchLower)
|
||||
);
|
||||
}
|
||||
|
||||
return filtered;
|
||||
}, [allTables, searchTerm, showTables, showViews]);
|
||||
|
||||
// Calculate pagination
|
||||
const totalPages = useMemo(
|
||||
() => Math.max(1, Math.ceil(filteredTables.length / TABLES_PER_PAGE)),
|
||||
[filteredTables.length]
|
||||
);
|
||||
|
||||
const paginatedTables = useMemo(() => {
|
||||
const startIndex = (currentPage - 1) * TABLES_PER_PAGE;
|
||||
const endIndex = startIndex + TABLES_PER_PAGE;
|
||||
return filteredTables.slice(startIndex, endIndex);
|
||||
}, [filteredTables, currentPage]);
|
||||
|
||||
// Get currently visible selected tables
|
||||
const visibleSelectedTables = useMemo(() => {
|
||||
return paginatedTables.filter((table) => selectedTables.has(table.key));
|
||||
}, [paginatedTables, selectedTables]);
|
||||
|
||||
const canAddMore = useMemo(
|
||||
() => selectedTables.size < MAX_TABLES_IN_DIAGRAM,
|
||||
[selectedTables.size]
|
||||
);
|
||||
const hasSearchResults = useMemo(
|
||||
() => filteredTables.length > 0,
|
||||
[filteredTables.length]
|
||||
);
|
||||
const allVisibleSelected = useMemo(
|
||||
() =>
|
||||
visibleSelectedTables.length === paginatedTables.length &&
|
||||
paginatedTables.length > 0,
|
||||
[visibleSelectedTables.length, paginatedTables.length]
|
||||
);
|
||||
const canSelectAllFiltered = useMemo(
|
||||
() =>
|
||||
filteredTables.length > 0 &&
|
||||
filteredTables.some((table) => !selectedTables.has(table.key)) &&
|
||||
canAddMore,
|
||||
[filteredTables, selectedTables, canAddMore]
|
||||
);
|
||||
|
||||
// Reset to first page when search changes
|
||||
useEffect(() => {
|
||||
setCurrentPage(1);
|
||||
}, [searchTerm]);
|
||||
|
||||
const handleTableToggle = useCallback(
|
||||
(tableKey: string) => {
|
||||
const newSelected = new Set(selectedTables);
|
||||
|
||||
if (newSelected.has(tableKey)) {
|
||||
newSelected.delete(tableKey);
|
||||
} else if (selectedTables.size < MAX_TABLES_IN_DIAGRAM) {
|
||||
newSelected.add(tableKey);
|
||||
}
|
||||
|
||||
setSelectedTables(newSelected);
|
||||
},
|
||||
[selectedTables]
|
||||
);
|
||||
|
||||
const handleTogglePageSelection = useCallback(() => {
|
||||
const newSelected = new Set(selectedTables);
|
||||
|
||||
if (allVisibleSelected) {
|
||||
// Deselect all on current page
|
||||
for (const table of paginatedTables) {
|
||||
newSelected.delete(table.key);
|
||||
}
|
||||
} else {
|
||||
// Select all on current page
|
||||
for (const table of paginatedTables) {
|
||||
if (newSelected.size >= MAX_TABLES_IN_DIAGRAM) break;
|
||||
newSelected.add(table.key);
|
||||
}
|
||||
}
|
||||
|
||||
setSelectedTables(newSelected);
|
||||
}, [allVisibleSelected, paginatedTables, selectedTables]);
|
||||
|
||||
const handleSelectAllFiltered = useCallback(() => {
|
||||
const newSelected = new Set(selectedTables);
|
||||
|
||||
for (const table of filteredTables) {
|
||||
if (newSelected.size >= MAX_TABLES_IN_DIAGRAM) break;
|
||||
newSelected.add(table.key);
|
||||
}
|
||||
|
||||
setSelectedTables(newSelected);
|
||||
}, [filteredTables, selectedTables]);
|
||||
|
||||
const handleNextPage = useCallback(() => {
|
||||
if (currentPage < totalPages) {
|
||||
setCurrentPage(currentPage + 1);
|
||||
}
|
||||
}, [currentPage, totalPages]);
|
||||
|
||||
const handlePrevPage = useCallback(() => {
|
||||
if (currentPage > 1) {
|
||||
setCurrentPage(currentPage - 1);
|
||||
}
|
||||
}, [currentPage]);
|
||||
|
||||
const handleClearSelection = useCallback(() => {
|
||||
setSelectedTables(new Set());
|
||||
}, []);
|
||||
|
||||
const handleConfirm = useCallback(() => {
|
||||
const selectedTableObjects: SelectedTable[] = Array.from(selectedTables)
|
||||
.map((key): SelectedTable | null => {
|
||||
const table = allTables.find((t) => t.key === key);
|
||||
if (!table) return null;
|
||||
|
||||
return {
|
||||
schema: table.schema,
|
||||
table: table.tableName,
|
||||
type: table.type,
|
||||
} satisfies SelectedTable;
|
||||
})
|
||||
.filter((t): t is SelectedTable => t !== null);
|
||||
|
||||
onImport({ selectedTables: selectedTableObjects, databaseMetadata });
|
||||
}, [selectedTables, allTables, onImport, databaseMetadata]);
|
||||
|
||||
const { isMd: isDesktop } = useBreakpoint('md');
|
||||
|
||||
const renderPagination = useCallback(
|
||||
() => (
|
||||
<Pagination>
|
||||
<PaginationContent>
|
||||
<PaginationItem>
|
||||
<PaginationPrevious
|
||||
onClick={handlePrevPage}
|
||||
className={cn(
|
||||
'cursor-pointer',
|
||||
currentPage === 1 &&
|
||||
'pointer-events-none opacity-50'
|
||||
)}
|
||||
/>
|
||||
</PaginationItem>
|
||||
<PaginationItem>
|
||||
<span className="px-3 text-sm text-muted-foreground">
|
||||
Page {currentPage} of {totalPages}
|
||||
</span>
|
||||
</PaginationItem>
|
||||
<PaginationItem>
|
||||
<PaginationNext
|
||||
onClick={handleNextPage}
|
||||
className={cn(
|
||||
'cursor-pointer',
|
||||
(currentPage >= totalPages ||
|
||||
filteredTables.length === 0) &&
|
||||
'pointer-events-none opacity-50'
|
||||
)}
|
||||
/>
|
||||
</PaginationItem>
|
||||
</PaginationContent>
|
||||
</Pagination>
|
||||
),
|
||||
[
|
||||
currentPage,
|
||||
totalPages,
|
||||
handlePrevPage,
|
||||
handleNextPage,
|
||||
filteredTables.length,
|
||||
]
|
||||
);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex h-[400px] items-center justify-center">
|
||||
<div className="text-center">
|
||||
<Spinner className="mb-4" />
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Parsing database metadata...
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Select Tables to Import</DialogTitle>
|
||||
<DialogDescription>
|
||||
{tableCount} {tableCount === 1 ? 'table' : 'tables'}
|
||||
{viewCount > 0 && (
|
||||
<>
|
||||
{' and '}
|
||||
{viewCount} {viewCount === 1 ? 'view' : 'views'}
|
||||
</>
|
||||
)}
|
||||
{' found. '}
|
||||
{allTables.length > MAX_TABLES_IN_DIAGRAM
|
||||
? `Select up to ${MAX_TABLES_IN_DIAGRAM} to import.`
|
||||
: 'Choose which ones to import.'}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<DialogInternalContent>
|
||||
<div className="flex h-full flex-col space-y-4">
|
||||
{/* Warning/Info Banner */}
|
||||
{allTables.length > MAX_TABLES_IN_DIAGRAM ? (
|
||||
<div
|
||||
className={cn(
|
||||
'flex items-center gap-2 rounded-lg p-3 text-sm',
|
||||
'bg-amber-50 text-amber-800 dark:bg-amber-950 dark:text-amber-200'
|
||||
)}
|
||||
>
|
||||
<AlertCircle className="size-4 shrink-0" />
|
||||
<span>
|
||||
Due to performance limitations, you can import a
|
||||
maximum of {MAX_TABLES_IN_DIAGRAM} tables.
|
||||
</span>
|
||||
</div>
|
||||
) : null}
|
||||
{/* Search Input */}
|
||||
<div className="relative">
|
||||
<Search className="absolute left-3 top-1/2 size-4 -translate-y-1/2 text-muted-foreground" />
|
||||
<Input
|
||||
placeholder="Search tables..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
className="px-9"
|
||||
/>
|
||||
{searchTerm && (
|
||||
<button
|
||||
onClick={() => setSearchTerm('')}
|
||||
className="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
<X className="size-4" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Selection Status and Actions - Responsive layout */}
|
||||
<div className="flex flex-col items-center gap-3 sm:flex-row sm:items-center sm:justify-between sm:gap-4">
|
||||
{/* Left side: selection count -> checkboxes -> results found */}
|
||||
<div className="flex flex-col items-center gap-3 text-sm sm:flex-row sm:items-center sm:gap-4">
|
||||
<div className="flex flex-col items-center gap-1 sm:flex-row sm:items-center sm:gap-4">
|
||||
<span className="text-center font-medium">
|
||||
{selectedTables.size} /{' '}
|
||||
{Math.min(
|
||||
MAX_TABLES_IN_DIAGRAM,
|
||||
allTables.length
|
||||
)}{' '}
|
||||
items selected
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3 sm:border-x sm:px-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<Checkbox
|
||||
checked={showTables}
|
||||
onCheckedChange={(checked) => {
|
||||
// Prevent unchecking if it's the only one checked
|
||||
if (!checked && !showViews) return;
|
||||
setShowTables(!!checked);
|
||||
}}
|
||||
/>
|
||||
<Table
|
||||
className="size-4"
|
||||
strokeWidth={1.5}
|
||||
/>
|
||||
<span>tables</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Checkbox
|
||||
checked={showViews}
|
||||
onCheckedChange={(checked) => {
|
||||
// Prevent unchecking if it's the only one checked
|
||||
if (!checked && !showTables) return;
|
||||
setShowViews(!!checked);
|
||||
}}
|
||||
/>
|
||||
<View
|
||||
className="size-4"
|
||||
strokeWidth={1.5}
|
||||
/>
|
||||
<span>views</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<span className="hidden text-muted-foreground sm:inline">
|
||||
{filteredTables.length}{' '}
|
||||
{filteredTables.length === 1
|
||||
? 'result'
|
||||
: 'results'}{' '}
|
||||
found
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Right side: action buttons */}
|
||||
<div className="flex flex-wrap items-center justify-center gap-2">
|
||||
{hasSearchResults && (
|
||||
<>
|
||||
{/* Show page selection button when not searching and no selection */}
|
||||
{!searchTerm &&
|
||||
selectedTables.size === 0 && (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={
|
||||
handleTogglePageSelection
|
||||
}
|
||||
disabled={
|
||||
paginatedTables.length === 0
|
||||
}
|
||||
>
|
||||
{allVisibleSelected
|
||||
? 'Deselect'
|
||||
: 'Select'}{' '}
|
||||
page
|
||||
</Button>
|
||||
)}
|
||||
{/* Show Select all button when there are unselected tables */}
|
||||
{canSelectAllFiltered &&
|
||||
selectedTables.size === 0 && (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={
|
||||
handleSelectAllFiltered
|
||||
}
|
||||
disabled={!canSelectAllFiltered}
|
||||
title={(() => {
|
||||
const unselectedCount =
|
||||
filteredTables.filter(
|
||||
(table) =>
|
||||
!selectedTables.has(
|
||||
table.key
|
||||
)
|
||||
).length;
|
||||
const remainingCapacity =
|
||||
MAX_TABLES_IN_DIAGRAM -
|
||||
selectedTables.size;
|
||||
if (
|
||||
unselectedCount >
|
||||
remainingCapacity
|
||||
) {
|
||||
return `Can only select ${remainingCapacity} more tables (${MAX_TABLES_IN_DIAGRAM} max limit)`;
|
||||
}
|
||||
return undefined;
|
||||
})()}
|
||||
>
|
||||
{(() => {
|
||||
const unselectedCount =
|
||||
filteredTables.filter(
|
||||
(table) =>
|
||||
!selectedTables.has(
|
||||
table.key
|
||||
)
|
||||
).length;
|
||||
const remainingCapacity =
|
||||
MAX_TABLES_IN_DIAGRAM -
|
||||
selectedTables.size;
|
||||
if (
|
||||
unselectedCount >
|
||||
remainingCapacity
|
||||
) {
|
||||
return `Select ${remainingCapacity} of ${unselectedCount}`;
|
||||
}
|
||||
return `Select all ${unselectedCount}`;
|
||||
})()}
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{selectedTables.size > 0 && (
|
||||
<>
|
||||
{/* Show page selection/deselection button when user has selections */}
|
||||
{paginatedTables.length > 0 && (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handleTogglePageSelection}
|
||||
>
|
||||
{allVisibleSelected
|
||||
? 'Deselect'
|
||||
: 'Select'}{' '}
|
||||
page
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handleClearSelection}
|
||||
>
|
||||
Clear selection
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Table List */}
|
||||
<div className="flex min-h-[428px] flex-1 flex-col">
|
||||
{hasSearchResults ? (
|
||||
<>
|
||||
<div className="flex-1 py-4">
|
||||
<div className="space-y-1">
|
||||
{paginatedTables.map((table) => {
|
||||
const isSelected = selectedTables.has(
|
||||
table.key
|
||||
);
|
||||
const isDisabled =
|
||||
!isSelected &&
|
||||
selectedTables.size >=
|
||||
MAX_TABLES_IN_DIAGRAM;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={table.key}
|
||||
className={cn(
|
||||
'flex items-center gap-3 rounded-md px-3 py-2 text-sm transition-colors',
|
||||
{
|
||||
'cursor-not-allowed':
|
||||
isDisabled,
|
||||
|
||||
'bg-muted hover:bg-muted/80':
|
||||
isSelected,
|
||||
'hover:bg-accent':
|
||||
!isSelected &&
|
||||
!isDisabled,
|
||||
}
|
||||
)}
|
||||
>
|
||||
<Checkbox
|
||||
checked={isSelected}
|
||||
disabled={isDisabled}
|
||||
onCheckedChange={() =>
|
||||
handleTableToggle(
|
||||
table.key
|
||||
)
|
||||
}
|
||||
/>
|
||||
{table.type === 'view' ? (
|
||||
<View
|
||||
className="size-4"
|
||||
strokeWidth={1.5}
|
||||
/>
|
||||
) : (
|
||||
<Table
|
||||
className="size-4"
|
||||
strokeWidth={1.5}
|
||||
/>
|
||||
)}
|
||||
<span className="flex-1">
|
||||
{table.schema ? (
|
||||
<span className="text-muted-foreground">
|
||||
{table.schema}.
|
||||
</span>
|
||||
) : null}
|
||||
<span className="font-medium">
|
||||
{table.tableName}
|
||||
</span>
|
||||
{table.type === 'view' && (
|
||||
<span className="ml-2 text-xs text-muted-foreground">
|
||||
(view)
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
{isSelected && (
|
||||
<Check className="size-4 text-pink-600" />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div className="flex h-full items-center justify-center py-4">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{searchTerm
|
||||
? 'No tables found matching your search.'
|
||||
: 'Start typing to search for tables...'}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{isDesktop ? renderPagination() : null}
|
||||
</DialogInternalContent>
|
||||
<DialogFooter
|
||||
// className={cn(
|
||||
// 'gap-2',
|
||||
// isDesktop
|
||||
// ? 'flex items-center justify-between'
|
||||
// : 'flex flex-col'
|
||||
// )}
|
||||
className="flex flex-col-reverse gap-2 sm:flex-row sm:justify-end sm:space-x-2 md:justify-between md:gap-0"
|
||||
>
|
||||
{/* Desktop layout */}
|
||||
|
||||
<Button type="button" variant="secondary" onClick={onBack}>
|
||||
{t('new_diagram_dialog.back')}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
onClick={handleConfirm}
|
||||
disabled={selectedTables.size === 0}
|
||||
className="bg-pink-500 text-white hover:bg-pink-600"
|
||||
>
|
||||
Import {selectedTables.size} Tables
|
||||
</Button>
|
||||
|
||||
{!isDesktop ? renderPagination() : null}
|
||||
</DialogFooter>
|
||||
</>
|
||||
);
|
||||
};
|
@@ -1,4 +1,5 @@
|
||||
export enum CreateDiagramDialogStep {
|
||||
SELECT_DATABASE = 'SELECT_DATABASE',
|
||||
IMPORT_DATABASE = 'IMPORT_DATABASE',
|
||||
SELECT_TABLES = 'SELECT_TABLES',
|
||||
}
|
||||
|
@@ -15,9 +15,13 @@ import type { DatabaseEdition } from '@/lib/domain/database-edition';
|
||||
import { SelectDatabase } from './select-database/select-database';
|
||||
import { CreateDiagramDialogStep } from './create-diagram-dialog-step';
|
||||
import { ImportDatabase } from '../common/import-database/import-database';
|
||||
import { SelectTables } from '../common/select-tables/select-tables';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import type { BaseDialogProps } from '../common/base-dialog-props';
|
||||
import { sqlImportToDiagram } from '@/lib/data/sql-import';
|
||||
import type { SelectedTable } from '@/lib/data/import-metadata/filter-metadata';
|
||||
import { filterMetadataByTables } from '@/lib/data/import-metadata/filter-metadata';
|
||||
import { MAX_TABLES_WITHOUT_SHOWING_FILTER } from '../common/select-tables/constants';
|
||||
|
||||
export interface CreateDiagramDialogProps extends BaseDialogProps {}
|
||||
|
||||
@@ -42,6 +46,8 @@ export const CreateDiagramDialog: React.FC<CreateDiagramDialogProps> = ({
|
||||
const { listDiagrams, addDiagram } = useStorage();
|
||||
const [diagramNumber, setDiagramNumber] = useState<number>(1);
|
||||
const navigate = useNavigate();
|
||||
const [parsedMetadata, setParsedMetadata] = useState<DatabaseMetadata>();
|
||||
const [isParsingMetadata, setIsParsingMetadata] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setDatabaseEdition(undefined);
|
||||
@@ -62,49 +68,72 @@ export const CreateDiagramDialog: React.FC<CreateDiagramDialogProps> = ({
|
||||
setDatabaseEdition(undefined);
|
||||
setScriptResult('');
|
||||
setImportMethod('query');
|
||||
setParsedMetadata(undefined);
|
||||
}, [dialog.open]);
|
||||
|
||||
const hasExistingDiagram = (diagramId ?? '').trim().length !== 0;
|
||||
|
||||
const importNewDiagram = useCallback(async () => {
|
||||
let diagram: Diagram | undefined;
|
||||
const importNewDiagram = useCallback(
|
||||
async ({
|
||||
selectedTables,
|
||||
databaseMetadata,
|
||||
}: {
|
||||
selectedTables?: SelectedTable[];
|
||||
databaseMetadata?: DatabaseMetadata;
|
||||
} = {}) => {
|
||||
let diagram: Diagram | undefined;
|
||||
|
||||
if (importMethod === 'ddl') {
|
||||
diagram = await sqlImportToDiagram({
|
||||
sqlContent: scriptResult,
|
||||
sourceDatabaseType: databaseType,
|
||||
targetDatabaseType: databaseType,
|
||||
if (importMethod === 'ddl') {
|
||||
diagram = await sqlImportToDiagram({
|
||||
sqlContent: scriptResult,
|
||||
sourceDatabaseType: databaseType,
|
||||
targetDatabaseType: databaseType,
|
||||
});
|
||||
} else {
|
||||
let metadata: DatabaseMetadata | undefined = databaseMetadata;
|
||||
|
||||
if (!metadata) {
|
||||
metadata = loadDatabaseMetadata(scriptResult);
|
||||
}
|
||||
|
||||
if (selectedTables && selectedTables.length > 0) {
|
||||
metadata = filterMetadataByTables({
|
||||
metadata,
|
||||
selectedTables,
|
||||
});
|
||||
}
|
||||
|
||||
diagram = await loadFromDatabaseMetadata({
|
||||
databaseType,
|
||||
databaseMetadata: metadata,
|
||||
diagramNumber,
|
||||
databaseEdition:
|
||||
databaseEdition?.trim().length === 0
|
||||
? undefined
|
||||
: databaseEdition,
|
||||
});
|
||||
}
|
||||
|
||||
await addDiagram({ diagram });
|
||||
await updateConfig({
|
||||
config: { defaultDiagramId: diagram.id },
|
||||
});
|
||||
} else {
|
||||
const databaseMetadata: DatabaseMetadata =
|
||||
loadDatabaseMetadata(scriptResult);
|
||||
|
||||
diagram = await loadFromDatabaseMetadata({
|
||||
databaseType,
|
||||
databaseMetadata,
|
||||
diagramNumber,
|
||||
databaseEdition:
|
||||
databaseEdition?.trim().length === 0
|
||||
? undefined
|
||||
: databaseEdition,
|
||||
});
|
||||
}
|
||||
|
||||
await addDiagram({ diagram });
|
||||
await updateConfig({ config: { defaultDiagramId: diagram.id } });
|
||||
closeCreateDiagramDialog();
|
||||
navigate(`/diagrams/${diagram.id}`);
|
||||
}, [
|
||||
importMethod,
|
||||
databaseType,
|
||||
addDiagram,
|
||||
databaseEdition,
|
||||
closeCreateDiagramDialog,
|
||||
navigate,
|
||||
updateConfig,
|
||||
scriptResult,
|
||||
diagramNumber,
|
||||
]);
|
||||
closeCreateDiagramDialog();
|
||||
navigate(`/diagrams/${diagram.id}`);
|
||||
},
|
||||
[
|
||||
importMethod,
|
||||
databaseType,
|
||||
addDiagram,
|
||||
databaseEdition,
|
||||
closeCreateDiagramDialog,
|
||||
navigate,
|
||||
updateConfig,
|
||||
scriptResult,
|
||||
diagramNumber,
|
||||
]
|
||||
);
|
||||
|
||||
const createEmptyDiagram = useCallback(async () => {
|
||||
const diagram: Diagram = {
|
||||
@@ -138,10 +167,56 @@ export const CreateDiagramDialog: React.FC<CreateDiagramDialogProps> = ({
|
||||
openImportDBMLDialog,
|
||||
]);
|
||||
|
||||
const importNewDiagramOrFilterTables = useCallback(async () => {
|
||||
try {
|
||||
setIsParsingMetadata(true);
|
||||
|
||||
if (importMethod === 'ddl') {
|
||||
await importNewDiagram();
|
||||
} else {
|
||||
// Parse metadata asynchronously to avoid blocking the UI
|
||||
const metadata = await new Promise<DatabaseMetadata>(
|
||||
(resolve, reject) => {
|
||||
setTimeout(() => {
|
||||
try {
|
||||
const result =
|
||||
loadDatabaseMetadata(scriptResult);
|
||||
resolve(result);
|
||||
} catch (err) {
|
||||
reject(err);
|
||||
}
|
||||
}, 0);
|
||||
}
|
||||
);
|
||||
|
||||
const totalTablesAndViews =
|
||||
metadata.tables.length + (metadata.views?.length || 0);
|
||||
|
||||
setParsedMetadata(metadata);
|
||||
|
||||
// Check if it's a large database that needs table selection
|
||||
if (totalTablesAndViews > MAX_TABLES_WITHOUT_SHOWING_FILTER) {
|
||||
setStep(CreateDiagramDialogStep.SELECT_TABLES);
|
||||
} else {
|
||||
await importNewDiagram({
|
||||
databaseMetadata: metadata,
|
||||
});
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
setIsParsingMetadata(false);
|
||||
}
|
||||
}, [importMethod, scriptResult, importNewDiagram]);
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
{...dialog}
|
||||
onOpenChange={(open) => {
|
||||
// Don't allow closing while parsing metadata
|
||||
if (isParsingMetadata) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!hasExistingDiagram) {
|
||||
return;
|
||||
}
|
||||
@@ -154,6 +229,8 @@ export const CreateDiagramDialog: React.FC<CreateDiagramDialogProps> = ({
|
||||
<DialogContent
|
||||
className="flex max-h-dvh w-full flex-col md:max-w-[900px]"
|
||||
showClose={hasExistingDiagram}
|
||||
onInteractOutside={(e) => e.preventDefault()}
|
||||
onEscapeKeyDown={(e) => e.preventDefault()}
|
||||
>
|
||||
{step === CreateDiagramDialogStep.SELECT_DATABASE ? (
|
||||
<SelectDatabase
|
||||
@@ -165,9 +242,9 @@ export const CreateDiagramDialog: React.FC<CreateDiagramDialogProps> = ({
|
||||
setStep(CreateDiagramDialogStep.IMPORT_DATABASE)
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
) : step === CreateDiagramDialogStep.IMPORT_DATABASE ? (
|
||||
<ImportDatabase
|
||||
onImport={importNewDiagram}
|
||||
onImport={importNewDiagramOrFilterTables}
|
||||
onCreateEmptyDiagram={createEmptyDiagram}
|
||||
databaseEdition={databaseEdition}
|
||||
databaseType={databaseType}
|
||||
@@ -180,8 +257,18 @@ export const CreateDiagramDialog: React.FC<CreateDiagramDialogProps> = ({
|
||||
title={t('new_diagram_dialog.import_database.title')}
|
||||
importMethod={importMethod}
|
||||
setImportMethod={setImportMethod}
|
||||
keepDialogAfterImport={true}
|
||||
/>
|
||||
)}
|
||||
) : step === CreateDiagramDialogStep.SELECT_TABLES ? (
|
||||
<SelectTables
|
||||
isLoading={isParsingMetadata || !parsedMetadata}
|
||||
databaseMetadata={parsedMetadata}
|
||||
onImport={importNewDiagram}
|
||||
onBack={() =>
|
||||
setStep(CreateDiagramDialogStep.IMPORT_DATABASE)
|
||||
}
|
||||
/>
|
||||
) : null}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
|
@@ -21,7 +21,7 @@ import { useTranslation } from 'react-i18next';
|
||||
export interface TableSchemaDialogProps extends BaseDialogProps {
|
||||
table?: DBTable;
|
||||
schemas: DBSchema[];
|
||||
onConfirm: (schema: string) => void;
|
||||
onConfirm: ({ schema }: { schema: DBSchema }) => void;
|
||||
}
|
||||
|
||||
export const TableSchemaDialog: React.FC<TableSchemaDialogProps> = ({
|
||||
@@ -31,7 +31,7 @@ export const TableSchemaDialog: React.FC<TableSchemaDialogProps> = ({
|
||||
onConfirm,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const [selectedSchema, setSelectedSchema] = React.useState<string>(
|
||||
const [selectedSchemaId, setSelectedSchemaId] = React.useState<string>(
|
||||
table?.schema
|
||||
? schemaNameToSchemaId(table.schema)
|
||||
: (schemas?.[0]?.id ?? '')
|
||||
@@ -39,7 +39,7 @@ export const TableSchemaDialog: React.FC<TableSchemaDialogProps> = ({
|
||||
|
||||
useEffect(() => {
|
||||
if (!dialog.open) return;
|
||||
setSelectedSchema(
|
||||
setSelectedSchemaId(
|
||||
table?.schema
|
||||
? schemaNameToSchemaId(table.schema)
|
||||
: (schemas?.[0]?.id ?? '')
|
||||
@@ -48,8 +48,11 @@ export const TableSchemaDialog: React.FC<TableSchemaDialogProps> = ({
|
||||
const { closeTableSchemaDialog } = useDialog();
|
||||
|
||||
const handleConfirm = useCallback(() => {
|
||||
onConfirm(selectedSchema);
|
||||
}, [onConfirm, selectedSchema]);
|
||||
const schema = schemas.find((s) => s.id === selectedSchemaId);
|
||||
if (!schema) return;
|
||||
|
||||
onConfirm({ schema });
|
||||
}, [onConfirm, selectedSchemaId, schemas]);
|
||||
|
||||
const schemaOptions: SelectBoxOption[] = useMemo(
|
||||
() =>
|
||||
@@ -89,9 +92,9 @@ export const TableSchemaDialog: React.FC<TableSchemaDialogProps> = ({
|
||||
<SelectBox
|
||||
options={schemaOptions}
|
||||
multiple={false}
|
||||
value={selectedSchema}
|
||||
value={selectedSchemaId}
|
||||
onChange={(value) =>
|
||||
setSelectedSchema(value as string)
|
||||
setSelectedSchemaId(value as string)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
126
src/lib/data/import-metadata/filter-metadata.ts
Normal file
126
src/lib/data/import-metadata/filter-metadata.ts
Normal file
@@ -0,0 +1,126 @@
|
||||
import type { DatabaseMetadata } from './metadata-types/database-metadata';
|
||||
import { schemaNameToDomainSchemaName } from '@/lib/domain/db-schema';
|
||||
|
||||
export interface SelectedTable {
|
||||
schema?: string | null;
|
||||
table: string;
|
||||
type: 'table' | 'view';
|
||||
}
|
||||
|
||||
export function filterMetadataByTables({
|
||||
metadata,
|
||||
selectedTables: inputSelectedTables,
|
||||
}: {
|
||||
metadata: DatabaseMetadata;
|
||||
selectedTables: SelectedTable[];
|
||||
}): DatabaseMetadata {
|
||||
const selectedTables = inputSelectedTables.map((st) => {
|
||||
// Normalize schema names to ensure consistent filtering
|
||||
const schema = schemaNameToDomainSchemaName(st.schema) ?? '';
|
||||
return {
|
||||
...st,
|
||||
schema,
|
||||
};
|
||||
});
|
||||
|
||||
// Create sets for faster lookup
|
||||
const selectedTableSet = new Set(
|
||||
selectedTables
|
||||
.filter((st) => st.type === 'table')
|
||||
.map((st) => `${st.schema}.${st.table}`)
|
||||
);
|
||||
const selectedViewSet = new Set(
|
||||
selectedTables
|
||||
.filter((st) => st.type === 'view')
|
||||
.map((st) => `${st.schema}.${st.table}`)
|
||||
);
|
||||
|
||||
// Filter tables
|
||||
const filteredTables = metadata.tables.filter((table) => {
|
||||
const schema = schemaNameToDomainSchemaName(table.schema) ?? '';
|
||||
const tableId = `${schema}.${table.table}`;
|
||||
return selectedTableSet.has(tableId);
|
||||
});
|
||||
|
||||
// Filter views - include views that were explicitly selected
|
||||
const filteredViews =
|
||||
metadata.views?.filter((view) => {
|
||||
const schema = schemaNameToDomainSchemaName(view.schema) ?? '';
|
||||
const viewName = view.view_name ?? '';
|
||||
const viewId = `${schema}.${viewName}`;
|
||||
return selectedViewSet.has(viewId);
|
||||
}) || [];
|
||||
|
||||
// Filter columns - include columns from both tables and views
|
||||
const filteredColumns = metadata.columns.filter((col) => {
|
||||
const fromTable = filteredTables.some(
|
||||
(tb) => tb.schema === col.schema && tb.table === col.table
|
||||
);
|
||||
// For views, the column.table field might contain the view name
|
||||
const fromView = filteredViews.some(
|
||||
(view) => view.schema === col.schema && view.view_name === col.table
|
||||
);
|
||||
return fromTable || fromView;
|
||||
});
|
||||
|
||||
// Filter primary keys
|
||||
const filteredPrimaryKeys = metadata.pk_info.filter((pk) =>
|
||||
filteredTables.some(
|
||||
(tb) => tb.schema === pk.schema && tb.table === pk.table
|
||||
)
|
||||
);
|
||||
|
||||
// Filter indexes
|
||||
const filteredIndexes = metadata.indexes.filter((idx) =>
|
||||
filteredTables.some(
|
||||
(tb) => tb.schema === idx.schema && tb.table === idx.table
|
||||
)
|
||||
);
|
||||
|
||||
// Filter foreign keys - include if either source or target table is selected
|
||||
// This ensures all relationships related to selected tables are preserved
|
||||
const filteredForeignKeys = metadata.fk_info.filter((fk) => {
|
||||
// Handle reference_schema and reference_table fields from the JSON
|
||||
const targetSchema = fk.reference_schema;
|
||||
const targetTable = (fk.reference_table || '').replace(/^"+|"+$/g, ''); // Remove extra quotes
|
||||
|
||||
const sourceIncluded = filteredTables.some(
|
||||
(tb) => tb.schema === fk.schema && tb.table === fk.table
|
||||
);
|
||||
const targetIncluded = filteredTables.some(
|
||||
(tb) => tb.schema === targetSchema && tb.table === targetTable
|
||||
);
|
||||
return sourceIncluded || targetIncluded;
|
||||
});
|
||||
|
||||
const schemasWithTables = new Set(filteredTables.map((tb) => tb.schema));
|
||||
const schemasWithViews = new Set(filteredViews.map((view) => view.schema));
|
||||
|
||||
// Filter custom types if they exist
|
||||
const filteredCustomTypes =
|
||||
metadata.custom_types?.filter((customType) => {
|
||||
// Also check if the type is used by any of the selected tables' columns
|
||||
const typeUsedInColumns = filteredColumns.some(
|
||||
(col) =>
|
||||
col.type === customType.type ||
|
||||
col.type.includes(customType.type) // Handle array types like "custom_type[]"
|
||||
);
|
||||
|
||||
return (
|
||||
schemasWithTables.has(customType.schema) ||
|
||||
schemasWithViews.has(customType.schema) ||
|
||||
typeUsedInColumns
|
||||
);
|
||||
}) || [];
|
||||
|
||||
return {
|
||||
...metadata,
|
||||
tables: filteredTables,
|
||||
columns: filteredColumns,
|
||||
pk_info: filteredPrimaryKeys,
|
||||
indexes: filteredIndexes,
|
||||
fk_info: filteredForeignKeys,
|
||||
views: filteredViews,
|
||||
custom_types: filteredCustomTypes,
|
||||
};
|
||||
}
|
@@ -1,4 +1,3 @@
|
||||
import { schemaNameToDomainSchemaName } from '@/lib/domain/db-schema';
|
||||
import type { TableInfo } from './table-info';
|
||||
import { z } from 'zod';
|
||||
|
||||
@@ -33,20 +32,12 @@ export type AggregatedIndexInfo = Omit<IndexInfo, 'column'> & {
|
||||
};
|
||||
|
||||
export const createAggregatedIndexes = ({
|
||||
tableInfo,
|
||||
tableSchema,
|
||||
indexes,
|
||||
tableIndexes,
|
||||
}: {
|
||||
tableInfo: TableInfo;
|
||||
indexes: IndexInfo[];
|
||||
tableIndexes: IndexInfo[];
|
||||
tableSchema?: string;
|
||||
}): AggregatedIndexInfo[] => {
|
||||
const tableIndexes = indexes.filter((idx) => {
|
||||
const indexSchema = schemaNameToDomainSchemaName(idx.schema);
|
||||
|
||||
return idx.table === tableInfo.table && indexSchema === tableSchema;
|
||||
});
|
||||
|
||||
return Object.values(
|
||||
tableIndexes.reduce(
|
||||
(acc, idx) => {
|
||||
|
@@ -60,6 +60,10 @@ export const createDependenciesFromMetadata = async ({
|
||||
tables: DBTable[];
|
||||
databaseType: DatabaseType;
|
||||
}): Promise<DBDependency[]> => {
|
||||
if (!views || views.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const { Parser } = await import('node-sql-parser');
|
||||
const parser = new Parser();
|
||||
|
||||
|
@@ -4,7 +4,6 @@ import type { ColumnInfo } from '../data/import-metadata/metadata-types/column-i
|
||||
import type { AggregatedIndexInfo } from '../data/import-metadata/metadata-types/index-info';
|
||||
import type { PrimaryKeyInfo } from '../data/import-metadata/metadata-types/primary-key-info';
|
||||
import type { TableInfo } from '../data/import-metadata/metadata-types/table-info';
|
||||
import { schemaNameToDomainSchemaName } from './db-schema';
|
||||
import { generateId } from '../utils';
|
||||
|
||||
export interface DBField {
|
||||
@@ -42,42 +41,30 @@ export const dbFieldSchema: z.ZodType<DBField> = z.object({
|
||||
});
|
||||
|
||||
export const createFieldsFromMetadata = ({
|
||||
columns,
|
||||
tableSchema,
|
||||
tableInfo,
|
||||
primaryKeys,
|
||||
tableColumns,
|
||||
tablePrimaryKeys,
|
||||
aggregatedIndexes,
|
||||
}: {
|
||||
columns: ColumnInfo[];
|
||||
tableColumns: ColumnInfo[];
|
||||
tableSchema?: string;
|
||||
tableInfo: TableInfo;
|
||||
primaryKeys: PrimaryKeyInfo[];
|
||||
tablePrimaryKeys: PrimaryKeyInfo[];
|
||||
aggregatedIndexes: AggregatedIndexInfo[];
|
||||
}) => {
|
||||
const uniqueColumns = columns
|
||||
.filter(
|
||||
(col) =>
|
||||
schemaNameToDomainSchemaName(col.schema) === tableSchema &&
|
||||
col.table === tableInfo.table
|
||||
)
|
||||
.reduce((acc, col) => {
|
||||
if (!acc.has(col.name)) {
|
||||
acc.set(col.name, col);
|
||||
}
|
||||
return acc;
|
||||
}, new Map<string, ColumnInfo>());
|
||||
const uniqueColumns = tableColumns.reduce((acc, col) => {
|
||||
if (!acc.has(col.name)) {
|
||||
acc.set(col.name, col);
|
||||
}
|
||||
return acc;
|
||||
}, new Map<string, ColumnInfo>());
|
||||
|
||||
const sortedColumns = Array.from(uniqueColumns.values()).sort(
|
||||
(a, b) => a.ordinal_position - b.ordinal_position
|
||||
);
|
||||
|
||||
const tablePrimaryKeys = primaryKeys
|
||||
.filter(
|
||||
(pk) =>
|
||||
pk.table === tableInfo.table &&
|
||||
schemaNameToDomainSchemaName(pk.schema) === tableSchema
|
||||
)
|
||||
.map((pk) => pk.column.trim());
|
||||
const tablePrimaryKeysColumns = tablePrimaryKeys.map((pk) =>
|
||||
pk.column.trim()
|
||||
);
|
||||
|
||||
return sortedColumns.map(
|
||||
(col: ColumnInfo): DBField => ({
|
||||
@@ -87,7 +74,7 @@ export const createFieldsFromMetadata = ({
|
||||
id: col.type.split(' ').join('_').toLowerCase(),
|
||||
name: col.type.toLowerCase(),
|
||||
},
|
||||
primaryKey: tablePrimaryKeys.includes(col.name),
|
||||
primaryKey: tablePrimaryKeysColumns.includes(col.name),
|
||||
unique: Object.values(aggregatedIndexes).some(
|
||||
(idx) =>
|
||||
idx.unique &&
|
||||
|
@@ -69,6 +69,14 @@ export const dbTableSchema: z.ZodType<DBTable> = z.object({
|
||||
parentAreaId: z.string().or(z.null()).optional(),
|
||||
});
|
||||
|
||||
export const generateTableKey = ({
|
||||
schemaName,
|
||||
tableName,
|
||||
}: {
|
||||
schemaName: string | null | undefined;
|
||||
tableName: string;
|
||||
}) => `${schemaNameToDomainSchemaName(schemaName) ?? ''}.${tableName}`;
|
||||
|
||||
export const shouldShowTableSchemaBySchemaFilter = ({
|
||||
filteredSchemas,
|
||||
tableSchema,
|
||||
@@ -122,20 +130,93 @@ export const createTablesFromMetadata = ({
|
||||
views: views,
|
||||
} = databaseMetadata;
|
||||
|
||||
return tableInfos.map((tableInfo: TableInfo) => {
|
||||
// Pre-compute view names for faster lookup if there are views
|
||||
const viewNamesSet = new Set<string>();
|
||||
const materializedViewNamesSet = new Set<string>();
|
||||
|
||||
if (views && views.length > 0) {
|
||||
views.forEach((view) => {
|
||||
const key = generateTableKey({
|
||||
schemaName: view.schema,
|
||||
tableName: view.view_name,
|
||||
});
|
||||
viewNamesSet.add(key);
|
||||
|
||||
if (
|
||||
view.view_definition &&
|
||||
decodeViewDefinition(databaseType, view.view_definition)
|
||||
.toLowerCase()
|
||||
.includes('materialized')
|
||||
) {
|
||||
materializedViewNamesSet.add(key);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Pre-compute lookup maps for better performance
|
||||
const columnsByTable = new Map<string, (typeof columns)[0][]>();
|
||||
const indexesByTable = new Map<string, (typeof indexes)[0][]>();
|
||||
const primaryKeysByTable = new Map<string, (typeof primaryKeys)[0][]>();
|
||||
|
||||
// Group columns by table
|
||||
columns.forEach((col) => {
|
||||
const key = generateTableKey({
|
||||
schemaName: col.schema,
|
||||
tableName: col.table,
|
||||
});
|
||||
if (!columnsByTable.has(key)) {
|
||||
columnsByTable.set(key, []);
|
||||
}
|
||||
columnsByTable.get(key)!.push(col);
|
||||
});
|
||||
|
||||
// Group indexes by table
|
||||
indexes.forEach((idx) => {
|
||||
const key = generateTableKey({
|
||||
schemaName: idx.schema,
|
||||
tableName: idx.table,
|
||||
});
|
||||
if (!indexesByTable.has(key)) {
|
||||
indexesByTable.set(key, []);
|
||||
}
|
||||
indexesByTable.get(key)!.push(idx);
|
||||
});
|
||||
|
||||
// Group primary keys by table
|
||||
primaryKeys.forEach((pk) => {
|
||||
const key = generateTableKey({
|
||||
schemaName: pk.schema,
|
||||
tableName: pk.table,
|
||||
});
|
||||
if (!primaryKeysByTable.has(key)) {
|
||||
primaryKeysByTable.set(key, []);
|
||||
}
|
||||
primaryKeysByTable.get(key)!.push(pk);
|
||||
});
|
||||
|
||||
const result = tableInfos.map((tableInfo: TableInfo) => {
|
||||
const tableSchema = schemaNameToDomainSchemaName(tableInfo.schema);
|
||||
const tableKey = generateTableKey({
|
||||
schemaName: tableInfo.schema,
|
||||
tableName: tableInfo.table,
|
||||
});
|
||||
|
||||
// Use pre-computed lookups instead of filtering entire arrays
|
||||
const tableIndexes = indexesByTable.get(tableKey) || [];
|
||||
const tablePrimaryKeys = primaryKeysByTable.get(tableKey) || [];
|
||||
const tableColumns = columnsByTable.get(tableKey) || [];
|
||||
|
||||
// Aggregate indexes with multiple columns
|
||||
const aggregatedIndexes = createAggregatedIndexes({
|
||||
tableInfo,
|
||||
tableSchema,
|
||||
indexes,
|
||||
tableIndexes,
|
||||
});
|
||||
|
||||
const fields = createFieldsFromMetadata({
|
||||
aggregatedIndexes,
|
||||
columns,
|
||||
primaryKeys,
|
||||
tableColumns,
|
||||
tablePrimaryKeys,
|
||||
tableInfo,
|
||||
tableSchema,
|
||||
});
|
||||
@@ -145,21 +226,13 @@ export const createTablesFromMetadata = ({
|
||||
fields,
|
||||
});
|
||||
|
||||
// Determine if the current table is a view by checking against viewInfo
|
||||
const isView = views.some(
|
||||
(view) =>
|
||||
schemaNameToDomainSchemaName(view.schema) === tableSchema &&
|
||||
view.view_name === tableInfo.table
|
||||
);
|
||||
|
||||
const isMaterializedView = views.some(
|
||||
(view) =>
|
||||
schemaNameToDomainSchemaName(view.schema) === tableSchema &&
|
||||
view.view_name === tableInfo.table &&
|
||||
decodeViewDefinition(databaseType, view.view_definition)
|
||||
.toLowerCase()
|
||||
.includes('materialized')
|
||||
);
|
||||
// Determine if the current table is a view by checking against pre-computed sets
|
||||
const viewKey = generateTableKey({
|
||||
schemaName: tableSchema,
|
||||
tableName: tableInfo.table,
|
||||
});
|
||||
const isView = viewNamesSet.has(viewKey);
|
||||
const isMaterializedView = materializedViewNamesSet.has(viewKey);
|
||||
|
||||
// Initial random positions; these will be adjusted later
|
||||
return {
|
||||
@@ -181,6 +254,72 @@ export const createTablesFromMetadata = ({
|
||||
comments: tableInfo.comment ? tableInfo.comment : undefined,
|
||||
};
|
||||
});
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
// Simple grid-based positioning for large databases
|
||||
const adjustTablePositionsSimple = (
|
||||
tables: DBTable[],
|
||||
mode: 'all' | 'perSchema' = 'all'
|
||||
): DBTable[] => {
|
||||
const TABLES_PER_ROW = 20;
|
||||
const TABLE_WIDTH = 250;
|
||||
const TABLE_HEIGHT = 350;
|
||||
const GAP_X = 50;
|
||||
const GAP_Y = 50;
|
||||
const START_X = 100;
|
||||
const START_Y = 100;
|
||||
|
||||
if (mode === 'perSchema') {
|
||||
// Group tables by schema for better organization
|
||||
const tablesBySchema = new Map<string, DBTable[]>();
|
||||
tables.forEach((table) => {
|
||||
const schema = table.schema || 'default';
|
||||
if (!tablesBySchema.has(schema)) {
|
||||
tablesBySchema.set(schema, []);
|
||||
}
|
||||
tablesBySchema.get(schema)!.push(table);
|
||||
});
|
||||
|
||||
const result: DBTable[] = [];
|
||||
let currentSchemaOffset = 0;
|
||||
|
||||
// Position each schema's tables in its own section
|
||||
tablesBySchema.forEach((schemaTables) => {
|
||||
schemaTables.forEach((table, index) => {
|
||||
const row = Math.floor(index / TABLES_PER_ROW);
|
||||
const col = index % TABLES_PER_ROW;
|
||||
|
||||
result.push({
|
||||
...table,
|
||||
x: START_X + col * (TABLE_WIDTH + GAP_X),
|
||||
y:
|
||||
START_Y +
|
||||
currentSchemaOffset +
|
||||
row * (TABLE_HEIGHT + GAP_Y),
|
||||
});
|
||||
});
|
||||
|
||||
// Add extra spacing between schemas
|
||||
const schemaRows = Math.ceil(schemaTables.length / TABLES_PER_ROW);
|
||||
currentSchemaOffset += schemaRows * (TABLE_HEIGHT + GAP_Y) + 200;
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// Simple mode - just arrange all tables in a grid
|
||||
return tables.map((table, index) => {
|
||||
const row = Math.floor(index / TABLES_PER_ROW);
|
||||
const col = index % TABLES_PER_ROW;
|
||||
|
||||
return {
|
||||
...table,
|
||||
x: START_X + col * (TABLE_WIDTH + GAP_X),
|
||||
y: START_Y + row * (TABLE_HEIGHT + GAP_Y),
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
export const adjustTablePositions = ({
|
||||
@@ -192,6 +331,13 @@ export const adjustTablePositions = ({
|
||||
relationships: DBRelationship[];
|
||||
mode?: 'all' | 'perSchema';
|
||||
}): DBTable[] => {
|
||||
// For large databases, use simple grid layout for better performance
|
||||
if (inputTables.length > 200) {
|
||||
const result = adjustTablePositionsSimple(inputTables, mode);
|
||||
return result;
|
||||
}
|
||||
|
||||
// For smaller databases, use the existing complex algorithm
|
||||
const tables = deepCopy(inputTables);
|
||||
const relationships = deepCopy(inputRelationships);
|
||||
|
||||
|
@@ -108,7 +108,7 @@ export const loadFromDatabaseMetadata = async ({
|
||||
return a.isView ? 1 : -1;
|
||||
});
|
||||
|
||||
return {
|
||||
const diagram = {
|
||||
id: generateDiagramId(),
|
||||
name: databaseMetadata.database_name
|
||||
? `${databaseMetadata.database_name}-db`
|
||||
@@ -124,4 +124,6 @@ export const loadFromDatabaseMetadata = async ({
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
};
|
||||
|
||||
return diagram;
|
||||
};
|
||||
|
@@ -32,11 +32,11 @@ export const CanvasContextMenu: React.FC<React.PropsWithChildren> = ({
|
||||
|
||||
if ((filteredSchemas?.length ?? 0) > 1) {
|
||||
openTableSchemaDialog({
|
||||
onConfirm: (schema) =>
|
||||
onConfirm: ({ schema }) =>
|
||||
createTable({
|
||||
x: position.x,
|
||||
y: position.y,
|
||||
schema,
|
||||
schema: schema.name,
|
||||
}),
|
||||
schemas: schemas.filter((schema) =>
|
||||
filteredSchemas?.includes(schema.id)
|
||||
|
@@ -37,6 +37,7 @@ import {
|
||||
TooltipTrigger,
|
||||
} from '@/components/tooltip/tooltip';
|
||||
import { cloneTable } from '@/lib/clone';
|
||||
import type { DBSchema } from '@/lib/domain';
|
||||
|
||||
export interface TableListItemHeaderProps {
|
||||
table: DBTable;
|
||||
@@ -126,8 +127,8 @@ export const TableListItemHeader: React.FC<TableListItemHeaderProps> = ({
|
||||
}, [table.id, removeTable]);
|
||||
|
||||
const updateTableSchema = useCallback(
|
||||
(schema: string) => {
|
||||
updateTable(table.id, { schema });
|
||||
({ schema }: { schema: DBSchema }) => {
|
||||
updateTable(table.id, { schema: schema.name });
|
||||
},
|
||||
[table.id, updateTable]
|
||||
);
|
||||
|
@@ -20,6 +20,7 @@ import { useDialog } from '@/hooks/use-dialog';
|
||||
import { TableDBML } from './table-dbml/table-dbml';
|
||||
import { useHotkeys } from 'react-hotkeys-hook';
|
||||
import { getOperatingSystem } from '@/lib/utils';
|
||||
import type { DBSchema } from '@/lib/domain';
|
||||
|
||||
export interface TablesSectionProps {}
|
||||
|
||||
@@ -45,7 +46,7 @@ export const TablesSection: React.FC<TablesSectionProps> = () => {
|
||||
}, [tables, filterText, filteredSchemas]);
|
||||
|
||||
const createTableWithLocation = useCallback(
|
||||
async (schema?: string) => {
|
||||
async ({ schema }: { schema?: DBSchema }) => {
|
||||
const padding = 80;
|
||||
const centerX =
|
||||
-viewport.x / viewport.zoom + padding / viewport.zoom;
|
||||
@@ -54,7 +55,7 @@ export const TablesSection: React.FC<TablesSectionProps> = () => {
|
||||
const table = await createTable({
|
||||
x: centerX,
|
||||
y: centerY,
|
||||
schema,
|
||||
schema: schema?.name,
|
||||
});
|
||||
openTableFromSidebar(table.id);
|
||||
},
|
||||
@@ -80,9 +81,9 @@ export const TablesSection: React.FC<TablesSectionProps> = () => {
|
||||
} else {
|
||||
const schema =
|
||||
filteredSchemas?.length === 1
|
||||
? schemas.find((s) => s.id === filteredSchemas[0])?.name
|
||||
? schemas.find((s) => s.id === filteredSchemas[0])
|
||||
: undefined;
|
||||
createTableWithLocation(schema);
|
||||
createTableWithLocation({ schema });
|
||||
}
|
||||
}, [
|
||||
createTableWithLocation,
|
||||
|
Reference in New Issue
Block a user