diff --git a/package-lock.json b/package-lock.json
index 44d037f4..97ae43b9 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -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",
diff --git a/package.json b/package.json
index 9827bf73..d39eddc6 100644
--- a/package.json
+++ b/package.json
@@ -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",
diff --git a/src/components/pagination/pagination.tsx b/src/components/pagination/pagination.tsx
new file mode 100644
index 00000000..fd7540f2
--- /dev/null
+++ b/src/components/pagination/pagination.tsx
@@ -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'>) => (
+
+);
+Pagination.displayName = 'Pagination';
+
+const PaginationContent = React.forwardRef<
+ HTMLUListElement,
+ React.ComponentProps<'ul'>
+>(({ className, ...props }, ref) => (
+
+));
+PaginationContent.displayName = 'PaginationContent';
+
+const PaginationItem = React.forwardRef<
+ HTMLLIElement,
+ React.ComponentProps<'li'>
+>(({ className, ...props }, ref) => (
+
+));
+PaginationItem.displayName = 'PaginationItem';
+
+type PaginationLinkProps = {
+ isActive?: boolean;
+} & Pick &
+ React.ComponentProps<'a'>;
+
+const PaginationLink = ({
+ className,
+ isActive,
+ size = 'icon',
+ ...props
+}: PaginationLinkProps) => (
+
+);
+PaginationLink.displayName = 'PaginationLink';
+
+const PaginationPrevious = ({
+ className,
+ ...props
+}: React.ComponentProps) => (
+
+
+ Previous
+
+);
+PaginationPrevious.displayName = 'PaginationPrevious';
+
+const PaginationNext = ({
+ className,
+ ...props
+}: React.ComponentProps) => (
+
+ Next
+
+
+);
+PaginationNext.displayName = 'PaginationNext';
+
+const PaginationEllipsis = ({
+ className,
+ ...props
+}: React.ComponentProps<'span'>) => (
+
+
+ More pages
+
+);
+PaginationEllipsis.displayName = 'PaginationEllipsis';
+
+export {
+ Pagination,
+ PaginationContent,
+ PaginationLink,
+ PaginationItem,
+ PaginationPrevious,
+ PaginationNext,
+ PaginationEllipsis,
+};
diff --git a/src/dialogs/common/import-database/import-database.tsx b/src/dialogs/common/import-database/import-database.tsx
index 980d2cc7..e818da5f 100644
--- a/src/dialogs/common/import-database/import-database.tsx
+++ b/src/dialogs/common/import-database/import-database.tsx
@@ -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 = ({
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 = ({
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 = ({
// 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
}
});
diff --git a/src/dialogs/common/select-tables/constants.ts b/src/dialogs/common/select-tables/constants.ts
new file mode 100644
index 00000000..27b56aa0
--- /dev/null
+++ b/src/dialogs/common/select-tables/constants.ts
@@ -0,0 +1,2 @@
+export const MAX_TABLES_IN_DIAGRAM = 500;
+export const MAX_TABLES_WITHOUT_SHOWING_FILTER = 50;
diff --git a/src/dialogs/common/select-tables/select-tables.tsx b/src/dialogs/common/select-tables/select-tables.tsx
new file mode 100644
index 00000000..5a3740a0
--- /dev/null
+++ b/src/dialogs/common/select-tables/select-tables.tsx
@@ -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;
+ 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 = ({
+ 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>(() => {
+ 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(
+ () => (
+
+
+
+
+
+
+
+ Page {currentPage} of {totalPages}
+
+
+
+ = totalPages ||
+ filteredTables.length === 0) &&
+ 'pointer-events-none opacity-50'
+ )}
+ />
+
+
+
+ ),
+ [
+ currentPage,
+ totalPages,
+ handlePrevPage,
+ handleNextPage,
+ filteredTables.length,
+ ]
+ );
+
+ if (isLoading) {
+ return (
+
+
+
+
+ Parsing database metadata...
+
+
+
+ );
+ }
+
+ return (
+ <>
+
+ Select Tables to Import
+
+ {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.'}
+
+
+
+
+ {/* Warning/Info Banner */}
+ {allTables.length > MAX_TABLES_IN_DIAGRAM ? (
+
+
+
+ Due to performance limitations, you can import a
+ maximum of {MAX_TABLES_IN_DIAGRAM} tables.
+
+
+ ) : null}
+ {/* Search Input */}
+
+
+ setSearchTerm(e.target.value)}
+ className="px-9"
+ />
+ {searchTerm && (
+
+ )}
+
+
+ {/* Selection Status and Actions - Responsive layout */}
+
+ {/* Left side: selection count -> checkboxes -> results found */}
+
+
+
+ {selectedTables.size} /{' '}
+ {Math.min(
+ MAX_TABLES_IN_DIAGRAM,
+ allTables.length
+ )}{' '}
+ items selected
+
+
+
+
+
+
{
+ // Prevent unchecking if it's the only one checked
+ if (!checked && !showViews) return;
+ setShowTables(!!checked);
+ }}
+ />
+
+ tables
+
+
+ {
+ // Prevent unchecking if it's the only one checked
+ if (!checked && !showTables) return;
+ setShowViews(!!checked);
+ }}
+ />
+
+ views
+
+
+
+
+ {filteredTables.length}{' '}
+ {filteredTables.length === 1
+ ? 'result'
+ : 'results'}{' '}
+ found
+
+
+
+ {/* Right side: action buttons */}
+
+ {hasSearchResults && (
+ <>
+ {/* Show page selection button when not searching and no selection */}
+ {!searchTerm &&
+ selectedTables.size === 0 && (
+
+ )}
+ {/* Show Select all button when there are unselected tables */}
+ {canSelectAllFiltered &&
+ selectedTables.size === 0 && (
+
+ )}
+ >
+ )}
+ {selectedTables.size > 0 && (
+ <>
+ {/* Show page selection/deselection button when user has selections */}
+ {paginatedTables.length > 0 && (
+
+ )}
+
+ >
+ )}
+
+
+
+
+ {/* Table List */}
+
+ {hasSearchResults ? (
+ <>
+
+
+ {paginatedTables.map((table) => {
+ const isSelected = selectedTables.has(
+ table.key
+ );
+ const isDisabled =
+ !isSelected &&
+ selectedTables.size >=
+ MAX_TABLES_IN_DIAGRAM;
+
+ return (
+
+
+ handleTableToggle(
+ table.key
+ )
+ }
+ />
+ {table.type === 'view' ? (
+
+ ) : (
+
+ )}
+
+ {table.schema ? (
+
+ {table.schema}.
+
+ ) : null}
+
+ {table.tableName}
+
+ {table.type === 'view' && (
+
+ (view)
+
+ )}
+
+ {isSelected && (
+
+ )}
+
+ );
+ })}
+
+
+ >
+ ) : (
+
+
+ {searchTerm
+ ? 'No tables found matching your search.'
+ : 'Start typing to search for tables...'}
+
+
+ )}
+
+ {isDesktop ? renderPagination() : null}
+
+
+ {/* Desktop layout */}
+
+
+
+
+
+ {!isDesktop ? renderPagination() : null}
+
+ >
+ );
+};
diff --git a/src/dialogs/create-diagram-dialog/create-diagram-dialog-step.ts b/src/dialogs/create-diagram-dialog/create-diagram-dialog-step.ts
index ce57df1d..25df1357 100644
--- a/src/dialogs/create-diagram-dialog/create-diagram-dialog-step.ts
+++ b/src/dialogs/create-diagram-dialog/create-diagram-dialog-step.ts
@@ -1,4 +1,5 @@
export enum CreateDiagramDialogStep {
SELECT_DATABASE = 'SELECT_DATABASE',
IMPORT_DATABASE = 'IMPORT_DATABASE',
+ SELECT_TABLES = 'SELECT_TABLES',
}
diff --git a/src/dialogs/create-diagram-dialog/create-diagram-dialog.tsx b/src/dialogs/create-diagram-dialog/create-diagram-dialog.tsx
index 1a3e05e9..ab8af742 100644
--- a/src/dialogs/create-diagram-dialog/create-diagram-dialog.tsx
+++ b/src/dialogs/create-diagram-dialog/create-diagram-dialog.tsx
@@ -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 = ({
const { listDiagrams, addDiagram } = useStorage();
const [diagramNumber, setDiagramNumber] = useState(1);
const navigate = useNavigate();
+ const [parsedMetadata, setParsedMetadata] = useState();
+ const [isParsingMetadata, setIsParsingMetadata] = useState(false);
useEffect(() => {
setDatabaseEdition(undefined);
@@ -62,49 +68,72 @@ export const CreateDiagramDialog: React.FC = ({
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 = ({
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(
+ (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 (
);
diff --git a/src/dialogs/table-schema-dialog/table-schema-dialog.tsx b/src/dialogs/table-schema-dialog/table-schema-dialog.tsx
index 5ad5be21..d397f09f 100644
--- a/src/dialogs/table-schema-dialog/table-schema-dialog.tsx
+++ b/src/dialogs/table-schema-dialog/table-schema-dialog.tsx
@@ -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 = ({
@@ -31,7 +31,7 @@ export const TableSchemaDialog: React.FC = ({
onConfirm,
}) => {
const { t } = useTranslation();
- const [selectedSchema, setSelectedSchema] = React.useState(
+ const [selectedSchemaId, setSelectedSchemaId] = React.useState(
table?.schema
? schemaNameToSchemaId(table.schema)
: (schemas?.[0]?.id ?? '')
@@ -39,7 +39,7 @@ export const TableSchemaDialog: React.FC = ({
useEffect(() => {
if (!dialog.open) return;
- setSelectedSchema(
+ setSelectedSchemaId(
table?.schema
? schemaNameToSchemaId(table.schema)
: (schemas?.[0]?.id ?? '')
@@ -48,8 +48,11 @@ export const TableSchemaDialog: React.FC = ({
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 = ({
- setSelectedSchema(value as string)
+ setSelectedSchemaId(value as string)
}
/>
diff --git a/src/lib/data/import-metadata/filter-metadata.ts b/src/lib/data/import-metadata/filter-metadata.ts
new file mode 100644
index 00000000..f69e851f
--- /dev/null
+++ b/src/lib/data/import-metadata/filter-metadata.ts
@@ -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,
+ };
+}
diff --git a/src/lib/data/import-metadata/metadata-types/index-info.ts b/src/lib/data/import-metadata/metadata-types/index-info.ts
index 7d48cbc6..acc6b0e5 100644
--- a/src/lib/data/import-metadata/metadata-types/index-info.ts
+++ b/src/lib/data/import-metadata/metadata-types/index-info.ts
@@ -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 & {
};
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) => {
diff --git a/src/lib/domain/db-dependency.ts b/src/lib/domain/db-dependency.ts
index e9f19060..753dabae 100644
--- a/src/lib/domain/db-dependency.ts
+++ b/src/lib/domain/db-dependency.ts
@@ -60,6 +60,10 @@ export const createDependenciesFromMetadata = async ({
tables: DBTable[];
databaseType: DatabaseType;
}): Promise => {
+ if (!views || views.length === 0) {
+ return [];
+ }
+
const { Parser } = await import('node-sql-parser');
const parser = new Parser();
diff --git a/src/lib/domain/db-field.ts b/src/lib/domain/db-field.ts
index 473fe888..093d708c 100644
--- a/src/lib/domain/db-field.ts
+++ b/src/lib/domain/db-field.ts
@@ -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 = 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());
+ const uniqueColumns = tableColumns.reduce((acc, col) => {
+ if (!acc.has(col.name)) {
+ acc.set(col.name, col);
+ }
+ return acc;
+ }, new Map());
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 &&
diff --git a/src/lib/domain/db-table.ts b/src/lib/domain/db-table.ts
index f40625a0..2d55a05a 100644
--- a/src/lib/domain/db-table.ts
+++ b/src/lib/domain/db-table.ts
@@ -69,6 +69,14 @@ export const dbTableSchema: z.ZodType = 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();
+ const materializedViewNamesSet = new Set();
+
+ 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();
+ const indexesByTable = new Map();
+ const primaryKeysByTable = new Map();
+
+ // 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();
+ 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);
diff --git a/src/lib/domain/diagram.ts b/src/lib/domain/diagram.ts
index 7741a8bc..49081323 100644
--- a/src/lib/domain/diagram.ts
+++ b/src/lib/domain/diagram.ts
@@ -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;
};
diff --git a/src/pages/editor-page/canvas/canvas-context-menu.tsx b/src/pages/editor-page/canvas/canvas-context-menu.tsx
index b547ff72..c7bd793b 100644
--- a/src/pages/editor-page/canvas/canvas-context-menu.tsx
+++ b/src/pages/editor-page/canvas/canvas-context-menu.tsx
@@ -32,11 +32,11 @@ export const CanvasContextMenu: React.FC = ({
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)
diff --git a/src/pages/editor-page/side-panel/tables-section/table-list/table-list-item/table-list-item-header/table-list-item-header.tsx b/src/pages/editor-page/side-panel/tables-section/table-list/table-list-item/table-list-item-header/table-list-item-header.tsx
index a0577454..93503e1a 100644
--- a/src/pages/editor-page/side-panel/tables-section/table-list/table-list-item/table-list-item-header/table-list-item-header.tsx
+++ b/src/pages/editor-page/side-panel/tables-section/table-list/table-list-item/table-list-item-header/table-list-item-header.tsx
@@ -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 = ({
}, [table.id, removeTable]);
const updateTableSchema = useCallback(
- (schema: string) => {
- updateTable(table.id, { schema });
+ ({ schema }: { schema: DBSchema }) => {
+ updateTable(table.id, { schema: schema.name });
},
[table.id, updateTable]
);
diff --git a/src/pages/editor-page/side-panel/tables-section/tables-section.tsx b/src/pages/editor-page/side-panel/tables-section/tables-section.tsx
index dc620baf..16455dfd 100644
--- a/src/pages/editor-page/side-panel/tables-section/tables-section.tsx
+++ b/src/pages/editor-page/side-panel/tables-section/tables-section.tsx
@@ -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 = () => {
}, [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 = () => {
const table = await createTable({
x: centerX,
y: centerY,
- schema,
+ schema: schema?.name,
});
openTableFromSidebar(table.id);
},
@@ -80,9 +81,9 @@ export const TablesSection: React.FC = () => {
} 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,