mirror of
				https://github.com/chartdb/chartdb.git
				synced 2025-11-03 21:43:23 +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-scroll-area": "1.2.0",
 | 
				
			||||||
                "@radix-ui/react-select": "^2.1.1",
 | 
					                "@radix-ui/react-select": "^2.1.1",
 | 
				
			||||||
                "@radix-ui/react-separator": "^1.1.2",
 | 
					                "@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-tabs": "^1.1.0",
 | 
				
			||||||
                "@radix-ui/react-toast": "^1.2.1",
 | 
					                "@radix-ui/react-toast": "^1.2.1",
 | 
				
			||||||
                "@radix-ui/react-toggle": "^1.1.0",
 | 
					                "@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": {
 | 
					        "node_modules/@radix-ui/react-direction": {
 | 
				
			||||||
            "version": "1.1.0",
 | 
					            "version": "1.1.0",
 | 
				
			||||||
            "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.0.tgz",
 | 
					            "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",
 | 
					            "version": "1.1.2",
 | 
				
			||||||
            "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.2.tgz",
 | 
					            "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.2.tgz",
 | 
				
			||||||
            "integrity": "sha512-YAKxaiGsSQJ38VzKH86/BPRC4rh+b1Jpa+JneA5LRE7skmLPNAyeG8kPJj/oo4STLvlrs8vkf/iYyc3A5stYCQ==",
 | 
					            "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": {
 | 
					        "node_modules/@radix-ui/react-tabs": {
 | 
				
			||||||
            "version": "1.1.2",
 | 
					            "version": "1.1.2",
 | 
				
			||||||
            "resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.2.tgz",
 | 
					            "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": {
 | 
					        "node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-visually-hidden": {
 | 
				
			||||||
            "version": "1.1.2",
 | 
					            "version": "1.1.2",
 | 
				
			||||||
            "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.1.2.tgz",
 | 
					            "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-scroll-area": "1.2.0",
 | 
				
			||||||
        "@radix-ui/react-select": "^2.1.1",
 | 
					        "@radix-ui/react-select": "^2.1.1",
 | 
				
			||||||
        "@radix-ui/react-separator": "^1.1.2",
 | 
					        "@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-tabs": "^1.1.0",
 | 
				
			||||||
        "@radix-ui/react-toast": "^1.2.1",
 | 
					        "@radix-ui/react-toast": "^1.2.1",
 | 
				
			||||||
        "@radix-ui/react-toggle": "^1.1.0",
 | 
					        "@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';
 | 
					} from '@/lib/data/sql-import/sql-validator';
 | 
				
			||||||
import { SQLValidationStatus } from './sql-validation-status';
 | 
					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 =
 | 
					const errorScriptOutputMessage =
 | 
				
			||||||
    'Invalid JSON. Please correct it or contact us at support@chartdb.io for help.';
 | 
					    '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(() => {
 | 
					    const formatEditor = useCallback(() => {
 | 
				
			||||||
        if (editorRef.current) {
 | 
					        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(() => {
 | 
					            setTimeout(() => {
 | 
				
			||||||
                editorRef.current
 | 
					                editorRef.current
 | 
				
			||||||
                    ?.getAction('editor.action.formatDocument')
 | 
					                    ?.getAction('editor.action.formatDocument')
 | 
				
			||||||
@@ -315,14 +334,17 @@ export const ImportDatabase: React.FC<ImportDatabaseProps> = ({
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
                const content = model.getValue();
 | 
					                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
 | 
					                // First, detect content type to determine if we should switch modes
 | 
				
			||||||
                const detectedType = detectContentType(content);
 | 
					                const detectedType = detectContentType(content);
 | 
				
			||||||
                if (detectedType && detectedType !== importMethod) {
 | 
					                if (detectedType && detectedType !== importMethod) {
 | 
				
			||||||
                    // Switch to the detected mode immediately
 | 
					                    // Switch to the detected mode immediately
 | 
				
			||||||
                    setImportMethod(detectedType);
 | 
					                    setImportMethod(detectedType);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                    // Only format if it's JSON (query mode)
 | 
					                    // Only format if it's JSON (query mode) AND file is not too large
 | 
				
			||||||
                    if (detectedType === 'query') {
 | 
					                    if (detectedType === 'query' && !isLargeFile) {
 | 
				
			||||||
                        // For JSON mode, format after a short delay
 | 
					                        // For JSON mode, format after a short delay
 | 
				
			||||||
                        setTimeout(() => {
 | 
					                        setTimeout(() => {
 | 
				
			||||||
                            editor
 | 
					                            editor
 | 
				
			||||||
@@ -333,15 +355,15 @@ export const ImportDatabase: React.FC<ImportDatabaseProps> = ({
 | 
				
			|||||||
                    // For DDL mode, do NOT format as it can break the SQL
 | 
					                    // For DDL mode, do NOT format as it can break the SQL
 | 
				
			||||||
                } else {
 | 
					                } else {
 | 
				
			||||||
                    // Content type didn't change, apply formatting based on current mode
 | 
					                    // Content type didn't change, apply formatting based on current mode
 | 
				
			||||||
                    if (importMethod === 'query') {
 | 
					                    if (importMethod === 'query' && !isLargeFile) {
 | 
				
			||||||
                        // Only format JSON content
 | 
					                        // Only format JSON content if not too large
 | 
				
			||||||
                        setTimeout(() => {
 | 
					                        setTimeout(() => {
 | 
				
			||||||
                            editor
 | 
					                            editor
 | 
				
			||||||
                                .getAction('editor.action.formatDocument')
 | 
					                                .getAction('editor.action.formatDocument')
 | 
				
			||||||
                                ?.run();
 | 
					                                ?.run();
 | 
				
			||||||
                        }, 100);
 | 
					                        }, 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 {
 | 
					export enum CreateDiagramDialogStep {
 | 
				
			||||||
    SELECT_DATABASE = 'SELECT_DATABASE',
 | 
					    SELECT_DATABASE = 'SELECT_DATABASE',
 | 
				
			||||||
    IMPORT_DATABASE = 'IMPORT_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 { SelectDatabase } from './select-database/select-database';
 | 
				
			||||||
import { CreateDiagramDialogStep } from './create-diagram-dialog-step';
 | 
					import { CreateDiagramDialogStep } from './create-diagram-dialog-step';
 | 
				
			||||||
import { ImportDatabase } from '../common/import-database/import-database';
 | 
					import { ImportDatabase } from '../common/import-database/import-database';
 | 
				
			||||||
 | 
					import { SelectTables } from '../common/select-tables/select-tables';
 | 
				
			||||||
import { useTranslation } from 'react-i18next';
 | 
					import { useTranslation } from 'react-i18next';
 | 
				
			||||||
import type { BaseDialogProps } from '../common/base-dialog-props';
 | 
					import type { BaseDialogProps } from '../common/base-dialog-props';
 | 
				
			||||||
import { sqlImportToDiagram } from '@/lib/data/sql-import';
 | 
					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 {}
 | 
					export interface CreateDiagramDialogProps extends BaseDialogProps {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -42,6 +46,8 @@ export const CreateDiagramDialog: React.FC<CreateDiagramDialogProps> = ({
 | 
				
			|||||||
    const { listDiagrams, addDiagram } = useStorage();
 | 
					    const { listDiagrams, addDiagram } = useStorage();
 | 
				
			||||||
    const [diagramNumber, setDiagramNumber] = useState<number>(1);
 | 
					    const [diagramNumber, setDiagramNumber] = useState<number>(1);
 | 
				
			||||||
    const navigate = useNavigate();
 | 
					    const navigate = useNavigate();
 | 
				
			||||||
 | 
					    const [parsedMetadata, setParsedMetadata] = useState<DatabaseMetadata>();
 | 
				
			||||||
 | 
					    const [isParsingMetadata, setIsParsingMetadata] = useState(false);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    useEffect(() => {
 | 
					    useEffect(() => {
 | 
				
			||||||
        setDatabaseEdition(undefined);
 | 
					        setDatabaseEdition(undefined);
 | 
				
			||||||
@@ -62,11 +68,19 @@ export const CreateDiagramDialog: React.FC<CreateDiagramDialogProps> = ({
 | 
				
			|||||||
        setDatabaseEdition(undefined);
 | 
					        setDatabaseEdition(undefined);
 | 
				
			||||||
        setScriptResult('');
 | 
					        setScriptResult('');
 | 
				
			||||||
        setImportMethod('query');
 | 
					        setImportMethod('query');
 | 
				
			||||||
 | 
					        setParsedMetadata(undefined);
 | 
				
			||||||
    }, [dialog.open]);
 | 
					    }, [dialog.open]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const hasExistingDiagram = (diagramId ?? '').trim().length !== 0;
 | 
					    const hasExistingDiagram = (diagramId ?? '').trim().length !== 0;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const importNewDiagram = useCallback(async () => {
 | 
					    const importNewDiagram = useCallback(
 | 
				
			||||||
 | 
					        async ({
 | 
				
			||||||
 | 
					            selectedTables,
 | 
				
			||||||
 | 
					            databaseMetadata,
 | 
				
			||||||
 | 
					        }: {
 | 
				
			||||||
 | 
					            selectedTables?: SelectedTable[];
 | 
				
			||||||
 | 
					            databaseMetadata?: DatabaseMetadata;
 | 
				
			||||||
 | 
					        } = {}) => {
 | 
				
			||||||
            let diagram: Diagram | undefined;
 | 
					            let diagram: Diagram | undefined;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            if (importMethod === 'ddl') {
 | 
					            if (importMethod === 'ddl') {
 | 
				
			||||||
@@ -76,12 +90,22 @@ export const CreateDiagramDialog: React.FC<CreateDiagramDialogProps> = ({
 | 
				
			|||||||
                    targetDatabaseType: databaseType,
 | 
					                    targetDatabaseType: databaseType,
 | 
				
			||||||
                });
 | 
					                });
 | 
				
			||||||
            } else {
 | 
					            } else {
 | 
				
			||||||
            const databaseMetadata: DatabaseMetadata =
 | 
					                let metadata: DatabaseMetadata | undefined = databaseMetadata;
 | 
				
			||||||
                loadDatabaseMetadata(scriptResult);
 | 
					
 | 
				
			||||||
 | 
					                if (!metadata) {
 | 
				
			||||||
 | 
					                    metadata = loadDatabaseMetadata(scriptResult);
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                if (selectedTables && selectedTables.length > 0) {
 | 
				
			||||||
 | 
					                    metadata = filterMetadataByTables({
 | 
				
			||||||
 | 
					                        metadata,
 | 
				
			||||||
 | 
					                        selectedTables,
 | 
				
			||||||
 | 
					                    });
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                diagram = await loadFromDatabaseMetadata({
 | 
					                diagram = await loadFromDatabaseMetadata({
 | 
				
			||||||
                    databaseType,
 | 
					                    databaseType,
 | 
				
			||||||
                databaseMetadata,
 | 
					                    databaseMetadata: metadata,
 | 
				
			||||||
                    diagramNumber,
 | 
					                    diagramNumber,
 | 
				
			||||||
                    databaseEdition:
 | 
					                    databaseEdition:
 | 
				
			||||||
                        databaseEdition?.trim().length === 0
 | 
					                        databaseEdition?.trim().length === 0
 | 
				
			||||||
@@ -91,10 +115,14 @@ export const CreateDiagramDialog: React.FC<CreateDiagramDialogProps> = ({
 | 
				
			|||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            await addDiagram({ diagram });
 | 
					            await addDiagram({ diagram });
 | 
				
			||||||
        await updateConfig({ config: { defaultDiagramId: diagram.id } });
 | 
					            await updateConfig({
 | 
				
			||||||
 | 
					                config: { defaultDiagramId: diagram.id },
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            closeCreateDiagramDialog();
 | 
					            closeCreateDiagramDialog();
 | 
				
			||||||
            navigate(`/diagrams/${diagram.id}`);
 | 
					            navigate(`/diagrams/${diagram.id}`);
 | 
				
			||||||
    }, [
 | 
					        },
 | 
				
			||||||
 | 
					        [
 | 
				
			||||||
            importMethod,
 | 
					            importMethod,
 | 
				
			||||||
            databaseType,
 | 
					            databaseType,
 | 
				
			||||||
            addDiagram,
 | 
					            addDiagram,
 | 
				
			||||||
@@ -104,7 +132,8 @@ export const CreateDiagramDialog: React.FC<CreateDiagramDialogProps> = ({
 | 
				
			|||||||
            updateConfig,
 | 
					            updateConfig,
 | 
				
			||||||
            scriptResult,
 | 
					            scriptResult,
 | 
				
			||||||
            diagramNumber,
 | 
					            diagramNumber,
 | 
				
			||||||
    ]);
 | 
					        ]
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const createEmptyDiagram = useCallback(async () => {
 | 
					    const createEmptyDiagram = useCallback(async () => {
 | 
				
			||||||
        const diagram: Diagram = {
 | 
					        const diagram: Diagram = {
 | 
				
			||||||
@@ -138,10 +167,56 @@ export const CreateDiagramDialog: React.FC<CreateDiagramDialogProps> = ({
 | 
				
			|||||||
        openImportDBMLDialog,
 | 
					        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 (
 | 
					    return (
 | 
				
			||||||
        <Dialog
 | 
					        <Dialog
 | 
				
			||||||
            {...dialog}
 | 
					            {...dialog}
 | 
				
			||||||
            onOpenChange={(open) => {
 | 
					            onOpenChange={(open) => {
 | 
				
			||||||
 | 
					                // Don't allow closing while parsing metadata
 | 
				
			||||||
 | 
					                if (isParsingMetadata) {
 | 
				
			||||||
 | 
					                    return;
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                if (!hasExistingDiagram) {
 | 
					                if (!hasExistingDiagram) {
 | 
				
			||||||
                    return;
 | 
					                    return;
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
@@ -154,6 +229,8 @@ export const CreateDiagramDialog: React.FC<CreateDiagramDialogProps> = ({
 | 
				
			|||||||
            <DialogContent
 | 
					            <DialogContent
 | 
				
			||||||
                className="flex max-h-dvh w-full flex-col md:max-w-[900px]"
 | 
					                className="flex max-h-dvh w-full flex-col md:max-w-[900px]"
 | 
				
			||||||
                showClose={hasExistingDiagram}
 | 
					                showClose={hasExistingDiagram}
 | 
				
			||||||
 | 
					                onInteractOutside={(e) => e.preventDefault()}
 | 
				
			||||||
 | 
					                onEscapeKeyDown={(e) => e.preventDefault()}
 | 
				
			||||||
            >
 | 
					            >
 | 
				
			||||||
                {step === CreateDiagramDialogStep.SELECT_DATABASE ? (
 | 
					                {step === CreateDiagramDialogStep.SELECT_DATABASE ? (
 | 
				
			||||||
                    <SelectDatabase
 | 
					                    <SelectDatabase
 | 
				
			||||||
@@ -165,9 +242,9 @@ export const CreateDiagramDialog: React.FC<CreateDiagramDialogProps> = ({
 | 
				
			|||||||
                            setStep(CreateDiagramDialogStep.IMPORT_DATABASE)
 | 
					                            setStep(CreateDiagramDialogStep.IMPORT_DATABASE)
 | 
				
			||||||
                        }
 | 
					                        }
 | 
				
			||||||
                    />
 | 
					                    />
 | 
				
			||||||
                ) : (
 | 
					                ) : step === CreateDiagramDialogStep.IMPORT_DATABASE ? (
 | 
				
			||||||
                    <ImportDatabase
 | 
					                    <ImportDatabase
 | 
				
			||||||
                        onImport={importNewDiagram}
 | 
					                        onImport={importNewDiagramOrFilterTables}
 | 
				
			||||||
                        onCreateEmptyDiagram={createEmptyDiagram}
 | 
					                        onCreateEmptyDiagram={createEmptyDiagram}
 | 
				
			||||||
                        databaseEdition={databaseEdition}
 | 
					                        databaseEdition={databaseEdition}
 | 
				
			||||||
                        databaseType={databaseType}
 | 
					                        databaseType={databaseType}
 | 
				
			||||||
@@ -180,8 +257,18 @@ export const CreateDiagramDialog: React.FC<CreateDiagramDialogProps> = ({
 | 
				
			|||||||
                        title={t('new_diagram_dialog.import_database.title')}
 | 
					                        title={t('new_diagram_dialog.import_database.title')}
 | 
				
			||||||
                        importMethod={importMethod}
 | 
					                        importMethod={importMethod}
 | 
				
			||||||
                        setImportMethod={setImportMethod}
 | 
					                        setImportMethod={setImportMethod}
 | 
				
			||||||
 | 
					                        keepDialogAfterImport={true}
 | 
				
			||||||
                    />
 | 
					                    />
 | 
				
			||||||
                )}
 | 
					                ) : step === CreateDiagramDialogStep.SELECT_TABLES ? (
 | 
				
			||||||
 | 
					                    <SelectTables
 | 
				
			||||||
 | 
					                        isLoading={isParsingMetadata || !parsedMetadata}
 | 
				
			||||||
 | 
					                        databaseMetadata={parsedMetadata}
 | 
				
			||||||
 | 
					                        onImport={importNewDiagram}
 | 
				
			||||||
 | 
					                        onBack={() =>
 | 
				
			||||||
 | 
					                            setStep(CreateDiagramDialogStep.IMPORT_DATABASE)
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					                    />
 | 
				
			||||||
 | 
					                ) : null}
 | 
				
			||||||
            </DialogContent>
 | 
					            </DialogContent>
 | 
				
			||||||
        </Dialog>
 | 
					        </Dialog>
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -21,7 +21,7 @@ import { useTranslation } from 'react-i18next';
 | 
				
			|||||||
export interface TableSchemaDialogProps extends BaseDialogProps {
 | 
					export interface TableSchemaDialogProps extends BaseDialogProps {
 | 
				
			||||||
    table?: DBTable;
 | 
					    table?: DBTable;
 | 
				
			||||||
    schemas: DBSchema[];
 | 
					    schemas: DBSchema[];
 | 
				
			||||||
    onConfirm: (schema: string) => void;
 | 
					    onConfirm: ({ schema }: { schema: DBSchema }) => void;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const TableSchemaDialog: React.FC<TableSchemaDialogProps> = ({
 | 
					export const TableSchemaDialog: React.FC<TableSchemaDialogProps> = ({
 | 
				
			||||||
@@ -31,7 +31,7 @@ export const TableSchemaDialog: React.FC<TableSchemaDialogProps> = ({
 | 
				
			|||||||
    onConfirm,
 | 
					    onConfirm,
 | 
				
			||||||
}) => {
 | 
					}) => {
 | 
				
			||||||
    const { t } = useTranslation();
 | 
					    const { t } = useTranslation();
 | 
				
			||||||
    const [selectedSchema, setSelectedSchema] = React.useState<string>(
 | 
					    const [selectedSchemaId, setSelectedSchemaId] = React.useState<string>(
 | 
				
			||||||
        table?.schema
 | 
					        table?.schema
 | 
				
			||||||
            ? schemaNameToSchemaId(table.schema)
 | 
					            ? schemaNameToSchemaId(table.schema)
 | 
				
			||||||
            : (schemas?.[0]?.id ?? '')
 | 
					            : (schemas?.[0]?.id ?? '')
 | 
				
			||||||
@@ -39,7 +39,7 @@ export const TableSchemaDialog: React.FC<TableSchemaDialogProps> = ({
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    useEffect(() => {
 | 
					    useEffect(() => {
 | 
				
			||||||
        if (!dialog.open) return;
 | 
					        if (!dialog.open) return;
 | 
				
			||||||
        setSelectedSchema(
 | 
					        setSelectedSchemaId(
 | 
				
			||||||
            table?.schema
 | 
					            table?.schema
 | 
				
			||||||
                ? schemaNameToSchemaId(table.schema)
 | 
					                ? schemaNameToSchemaId(table.schema)
 | 
				
			||||||
                : (schemas?.[0]?.id ?? '')
 | 
					                : (schemas?.[0]?.id ?? '')
 | 
				
			||||||
@@ -48,8 +48,11 @@ export const TableSchemaDialog: React.FC<TableSchemaDialogProps> = ({
 | 
				
			|||||||
    const { closeTableSchemaDialog } = useDialog();
 | 
					    const { closeTableSchemaDialog } = useDialog();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const handleConfirm = useCallback(() => {
 | 
					    const handleConfirm = useCallback(() => {
 | 
				
			||||||
        onConfirm(selectedSchema);
 | 
					        const schema = schemas.find((s) => s.id === selectedSchemaId);
 | 
				
			||||||
    }, [onConfirm, selectedSchema]);
 | 
					        if (!schema) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        onConfirm({ schema });
 | 
				
			||||||
 | 
					    }, [onConfirm, selectedSchemaId, schemas]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const schemaOptions: SelectBoxOption[] = useMemo(
 | 
					    const schemaOptions: SelectBoxOption[] = useMemo(
 | 
				
			||||||
        () =>
 | 
					        () =>
 | 
				
			||||||
@@ -89,9 +92,9 @@ export const TableSchemaDialog: React.FC<TableSchemaDialogProps> = ({
 | 
				
			|||||||
                        <SelectBox
 | 
					                        <SelectBox
 | 
				
			||||||
                            options={schemaOptions}
 | 
					                            options={schemaOptions}
 | 
				
			||||||
                            multiple={false}
 | 
					                            multiple={false}
 | 
				
			||||||
                            value={selectedSchema}
 | 
					                            value={selectedSchemaId}
 | 
				
			||||||
                            onChange={(value) =>
 | 
					                            onChange={(value) =>
 | 
				
			||||||
                                setSelectedSchema(value as string)
 | 
					                                setSelectedSchemaId(value as string)
 | 
				
			||||||
                            }
 | 
					                            }
 | 
				
			||||||
                        />
 | 
					                        />
 | 
				
			||||||
                    </div>
 | 
					                    </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 type { TableInfo } from './table-info';
 | 
				
			||||||
import { z } from 'zod';
 | 
					import { z } from 'zod';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -33,20 +32,12 @@ export type AggregatedIndexInfo = Omit<IndexInfo, 'column'> & {
 | 
				
			|||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const createAggregatedIndexes = ({
 | 
					export const createAggregatedIndexes = ({
 | 
				
			||||||
    tableInfo,
 | 
					    tableIndexes,
 | 
				
			||||||
    tableSchema,
 | 
					 | 
				
			||||||
    indexes,
 | 
					 | 
				
			||||||
}: {
 | 
					}: {
 | 
				
			||||||
    tableInfo: TableInfo;
 | 
					    tableInfo: TableInfo;
 | 
				
			||||||
    indexes: IndexInfo[];
 | 
					    tableIndexes: IndexInfo[];
 | 
				
			||||||
    tableSchema?: string;
 | 
					    tableSchema?: string;
 | 
				
			||||||
}): AggregatedIndexInfo[] => {
 | 
					}): AggregatedIndexInfo[] => {
 | 
				
			||||||
    const tableIndexes = indexes.filter((idx) => {
 | 
					 | 
				
			||||||
        const indexSchema = schemaNameToDomainSchemaName(idx.schema);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        return idx.table === tableInfo.table && indexSchema === tableSchema;
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    return Object.values(
 | 
					    return Object.values(
 | 
				
			||||||
        tableIndexes.reduce(
 | 
					        tableIndexes.reduce(
 | 
				
			||||||
            (acc, idx) => {
 | 
					            (acc, idx) => {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -60,6 +60,10 @@ export const createDependenciesFromMetadata = async ({
 | 
				
			|||||||
    tables: DBTable[];
 | 
					    tables: DBTable[];
 | 
				
			||||||
    databaseType: DatabaseType;
 | 
					    databaseType: DatabaseType;
 | 
				
			||||||
}): Promise<DBDependency[]> => {
 | 
					}): Promise<DBDependency[]> => {
 | 
				
			||||||
 | 
					    if (!views || views.length === 0) {
 | 
				
			||||||
 | 
					        return [];
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const { Parser } = await import('node-sql-parser');
 | 
					    const { Parser } = await import('node-sql-parser');
 | 
				
			||||||
    const parser = new 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 { AggregatedIndexInfo } from '../data/import-metadata/metadata-types/index-info';
 | 
				
			||||||
import type { PrimaryKeyInfo } from '../data/import-metadata/metadata-types/primary-key-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 type { TableInfo } from '../data/import-metadata/metadata-types/table-info';
 | 
				
			||||||
import { schemaNameToDomainSchemaName } from './db-schema';
 | 
					 | 
				
			||||||
import { generateId } from '../utils';
 | 
					import { generateId } from '../utils';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export interface DBField {
 | 
					export interface DBField {
 | 
				
			||||||
@@ -42,25 +41,17 @@ export const dbFieldSchema: z.ZodType<DBField> = z.object({
 | 
				
			|||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const createFieldsFromMetadata = ({
 | 
					export const createFieldsFromMetadata = ({
 | 
				
			||||||
    columns,
 | 
					    tableColumns,
 | 
				
			||||||
    tableSchema,
 | 
					    tablePrimaryKeys,
 | 
				
			||||||
    tableInfo,
 | 
					 | 
				
			||||||
    primaryKeys,
 | 
					 | 
				
			||||||
    aggregatedIndexes,
 | 
					    aggregatedIndexes,
 | 
				
			||||||
}: {
 | 
					}: {
 | 
				
			||||||
    columns: ColumnInfo[];
 | 
					    tableColumns: ColumnInfo[];
 | 
				
			||||||
    tableSchema?: string;
 | 
					    tableSchema?: string;
 | 
				
			||||||
    tableInfo: TableInfo;
 | 
					    tableInfo: TableInfo;
 | 
				
			||||||
    primaryKeys: PrimaryKeyInfo[];
 | 
					    tablePrimaryKeys: PrimaryKeyInfo[];
 | 
				
			||||||
    aggregatedIndexes: AggregatedIndexInfo[];
 | 
					    aggregatedIndexes: AggregatedIndexInfo[];
 | 
				
			||||||
}) => {
 | 
					}) => {
 | 
				
			||||||
    const uniqueColumns = columns
 | 
					    const uniqueColumns = tableColumns.reduce((acc, col) => {
 | 
				
			||||||
        .filter(
 | 
					 | 
				
			||||||
            (col) =>
 | 
					 | 
				
			||||||
                schemaNameToDomainSchemaName(col.schema) === tableSchema &&
 | 
					 | 
				
			||||||
                col.table === tableInfo.table
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
        .reduce((acc, col) => {
 | 
					 | 
				
			||||||
        if (!acc.has(col.name)) {
 | 
					        if (!acc.has(col.name)) {
 | 
				
			||||||
            acc.set(col.name, col);
 | 
					            acc.set(col.name, col);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
@@ -71,13 +62,9 @@ export const createFieldsFromMetadata = ({
 | 
				
			|||||||
        (a, b) => a.ordinal_position - b.ordinal_position
 | 
					        (a, b) => a.ordinal_position - b.ordinal_position
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const tablePrimaryKeys = primaryKeys
 | 
					    const tablePrimaryKeysColumns = tablePrimaryKeys.map((pk) =>
 | 
				
			||||||
        .filter(
 | 
					        pk.column.trim()
 | 
				
			||||||
            (pk) =>
 | 
					    );
 | 
				
			||||||
                pk.table === tableInfo.table &&
 | 
					 | 
				
			||||||
                schemaNameToDomainSchemaName(pk.schema) === tableSchema
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
        .map((pk) => pk.column.trim());
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return sortedColumns.map(
 | 
					    return sortedColumns.map(
 | 
				
			||||||
        (col: ColumnInfo): DBField => ({
 | 
					        (col: ColumnInfo): DBField => ({
 | 
				
			||||||
@@ -87,7 +74,7 @@ export const createFieldsFromMetadata = ({
 | 
				
			|||||||
                id: col.type.split(' ').join('_').toLowerCase(),
 | 
					                id: col.type.split(' ').join('_').toLowerCase(),
 | 
				
			||||||
                name: col.type.toLowerCase(),
 | 
					                name: col.type.toLowerCase(),
 | 
				
			||||||
            },
 | 
					            },
 | 
				
			||||||
            primaryKey: tablePrimaryKeys.includes(col.name),
 | 
					            primaryKey: tablePrimaryKeysColumns.includes(col.name),
 | 
				
			||||||
            unique: Object.values(aggregatedIndexes).some(
 | 
					            unique: Object.values(aggregatedIndexes).some(
 | 
				
			||||||
                (idx) =>
 | 
					                (idx) =>
 | 
				
			||||||
                    idx.unique &&
 | 
					                    idx.unique &&
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -69,6 +69,14 @@ export const dbTableSchema: z.ZodType<DBTable> = z.object({
 | 
				
			|||||||
    parentAreaId: z.string().or(z.null()).optional(),
 | 
					    parentAreaId: z.string().or(z.null()).optional(),
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const generateTableKey = ({
 | 
				
			||||||
 | 
					    schemaName,
 | 
				
			||||||
 | 
					    tableName,
 | 
				
			||||||
 | 
					}: {
 | 
				
			||||||
 | 
					    schemaName: string | null | undefined;
 | 
				
			||||||
 | 
					    tableName: string;
 | 
				
			||||||
 | 
					}) => `${schemaNameToDomainSchemaName(schemaName) ?? ''}.${tableName}`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const shouldShowTableSchemaBySchemaFilter = ({
 | 
					export const shouldShowTableSchemaBySchemaFilter = ({
 | 
				
			||||||
    filteredSchemas,
 | 
					    filteredSchemas,
 | 
				
			||||||
    tableSchema,
 | 
					    tableSchema,
 | 
				
			||||||
@@ -122,20 +130,93 @@ export const createTablesFromMetadata = ({
 | 
				
			|||||||
        views: views,
 | 
					        views: views,
 | 
				
			||||||
    } = databaseMetadata;
 | 
					    } = 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 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
 | 
					        // Aggregate indexes with multiple columns
 | 
				
			||||||
        const aggregatedIndexes = createAggregatedIndexes({
 | 
					        const aggregatedIndexes = createAggregatedIndexes({
 | 
				
			||||||
            tableInfo,
 | 
					            tableInfo,
 | 
				
			||||||
            tableSchema,
 | 
					            tableSchema,
 | 
				
			||||||
            indexes,
 | 
					            tableIndexes,
 | 
				
			||||||
        });
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        const fields = createFieldsFromMetadata({
 | 
					        const fields = createFieldsFromMetadata({
 | 
				
			||||||
            aggregatedIndexes,
 | 
					            aggregatedIndexes,
 | 
				
			||||||
            columns,
 | 
					            tableColumns,
 | 
				
			||||||
            primaryKeys,
 | 
					            tablePrimaryKeys,
 | 
				
			||||||
            tableInfo,
 | 
					            tableInfo,
 | 
				
			||||||
            tableSchema,
 | 
					            tableSchema,
 | 
				
			||||||
        });
 | 
					        });
 | 
				
			||||||
@@ -145,21 +226,13 @@ export const createTablesFromMetadata = ({
 | 
				
			|||||||
            fields,
 | 
					            fields,
 | 
				
			||||||
        });
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        // Determine if the current table is a view by checking against viewInfo
 | 
					        // Determine if the current table is a view by checking against pre-computed sets
 | 
				
			||||||
        const isView = views.some(
 | 
					        const viewKey = generateTableKey({
 | 
				
			||||||
            (view) =>
 | 
					            schemaName: tableSchema,
 | 
				
			||||||
                schemaNameToDomainSchemaName(view.schema) === tableSchema &&
 | 
					            tableName: tableInfo.table,
 | 
				
			||||||
                view.view_name === tableInfo.table
 | 
					        });
 | 
				
			||||||
        );
 | 
					        const isView = viewNamesSet.has(viewKey);
 | 
				
			||||||
 | 
					        const isMaterializedView = materializedViewNamesSet.has(viewKey);
 | 
				
			||||||
        const isMaterializedView = views.some(
 | 
					 | 
				
			||||||
            (view) =>
 | 
					 | 
				
			||||||
                schemaNameToDomainSchemaName(view.schema) === tableSchema &&
 | 
					 | 
				
			||||||
                view.view_name === tableInfo.table &&
 | 
					 | 
				
			||||||
                decodeViewDefinition(databaseType, view.view_definition)
 | 
					 | 
				
			||||||
                    .toLowerCase()
 | 
					 | 
				
			||||||
                    .includes('materialized')
 | 
					 | 
				
			||||||
        );
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
        // Initial random positions; these will be adjusted later
 | 
					        // Initial random positions; these will be adjusted later
 | 
				
			||||||
        return {
 | 
					        return {
 | 
				
			||||||
@@ -181,6 +254,72 @@ export const createTablesFromMetadata = ({
 | 
				
			|||||||
            comments: tableInfo.comment ? tableInfo.comment : undefined,
 | 
					            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 = ({
 | 
					export const adjustTablePositions = ({
 | 
				
			||||||
@@ -192,6 +331,13 @@ export const adjustTablePositions = ({
 | 
				
			|||||||
    relationships: DBRelationship[];
 | 
					    relationships: DBRelationship[];
 | 
				
			||||||
    mode?: 'all' | 'perSchema';
 | 
					    mode?: 'all' | 'perSchema';
 | 
				
			||||||
}): DBTable[] => {
 | 
					}): 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 tables = deepCopy(inputTables);
 | 
				
			||||||
    const relationships = deepCopy(inputRelationships);
 | 
					    const relationships = deepCopy(inputRelationships);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -108,7 +108,7 @@ export const loadFromDatabaseMetadata = async ({
 | 
				
			|||||||
        return a.isView ? 1 : -1;
 | 
					        return a.isView ? 1 : -1;
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return {
 | 
					    const diagram = {
 | 
				
			||||||
        id: generateDiagramId(),
 | 
					        id: generateDiagramId(),
 | 
				
			||||||
        name: databaseMetadata.database_name
 | 
					        name: databaseMetadata.database_name
 | 
				
			||||||
            ? `${databaseMetadata.database_name}-db`
 | 
					            ? `${databaseMetadata.database_name}-db`
 | 
				
			||||||
@@ -124,4 +124,6 @@ export const loadFromDatabaseMetadata = async ({
 | 
				
			|||||||
        createdAt: new Date(),
 | 
					        createdAt: new Date(),
 | 
				
			||||||
        updatedAt: new Date(),
 | 
					        updatedAt: new Date(),
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return diagram;
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -32,11 +32,11 @@ export const CanvasContextMenu: React.FC<React.PropsWithChildren> = ({
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
            if ((filteredSchemas?.length ?? 0) > 1) {
 | 
					            if ((filteredSchemas?.length ?? 0) > 1) {
 | 
				
			||||||
                openTableSchemaDialog({
 | 
					                openTableSchemaDialog({
 | 
				
			||||||
                    onConfirm: (schema) =>
 | 
					                    onConfirm: ({ schema }) =>
 | 
				
			||||||
                        createTable({
 | 
					                        createTable({
 | 
				
			||||||
                            x: position.x,
 | 
					                            x: position.x,
 | 
				
			||||||
                            y: position.y,
 | 
					                            y: position.y,
 | 
				
			||||||
                            schema,
 | 
					                            schema: schema.name,
 | 
				
			||||||
                        }),
 | 
					                        }),
 | 
				
			||||||
                    schemas: schemas.filter((schema) =>
 | 
					                    schemas: schemas.filter((schema) =>
 | 
				
			||||||
                        filteredSchemas?.includes(schema.id)
 | 
					                        filteredSchemas?.includes(schema.id)
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -37,6 +37,7 @@ import {
 | 
				
			|||||||
    TooltipTrigger,
 | 
					    TooltipTrigger,
 | 
				
			||||||
} from '@/components/tooltip/tooltip';
 | 
					} from '@/components/tooltip/tooltip';
 | 
				
			||||||
import { cloneTable } from '@/lib/clone';
 | 
					import { cloneTable } from '@/lib/clone';
 | 
				
			||||||
 | 
					import type { DBSchema } from '@/lib/domain';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export interface TableListItemHeaderProps {
 | 
					export interface TableListItemHeaderProps {
 | 
				
			||||||
    table: DBTable;
 | 
					    table: DBTable;
 | 
				
			||||||
@@ -126,8 +127,8 @@ export const TableListItemHeader: React.FC<TableListItemHeaderProps> = ({
 | 
				
			|||||||
    }, [table.id, removeTable]);
 | 
					    }, [table.id, removeTable]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const updateTableSchema = useCallback(
 | 
					    const updateTableSchema = useCallback(
 | 
				
			||||||
        (schema: string) => {
 | 
					        ({ schema }: { schema: DBSchema }) => {
 | 
				
			||||||
            updateTable(table.id, { schema });
 | 
					            updateTable(table.id, { schema: schema.name });
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        [table.id, updateTable]
 | 
					        [table.id, updateTable]
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -20,6 +20,7 @@ import { useDialog } from '@/hooks/use-dialog';
 | 
				
			|||||||
import { TableDBML } from './table-dbml/table-dbml';
 | 
					import { TableDBML } from './table-dbml/table-dbml';
 | 
				
			||||||
import { useHotkeys } from 'react-hotkeys-hook';
 | 
					import { useHotkeys } from 'react-hotkeys-hook';
 | 
				
			||||||
import { getOperatingSystem } from '@/lib/utils';
 | 
					import { getOperatingSystem } from '@/lib/utils';
 | 
				
			||||||
 | 
					import type { DBSchema } from '@/lib/domain';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export interface TablesSectionProps {}
 | 
					export interface TablesSectionProps {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -45,7 +46,7 @@ export const TablesSection: React.FC<TablesSectionProps> = () => {
 | 
				
			|||||||
    }, [tables, filterText, filteredSchemas]);
 | 
					    }, [tables, filterText, filteredSchemas]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const createTableWithLocation = useCallback(
 | 
					    const createTableWithLocation = useCallback(
 | 
				
			||||||
        async (schema?: string) => {
 | 
					        async ({ schema }: { schema?: DBSchema }) => {
 | 
				
			||||||
            const padding = 80;
 | 
					            const padding = 80;
 | 
				
			||||||
            const centerX =
 | 
					            const centerX =
 | 
				
			||||||
                -viewport.x / viewport.zoom + padding / viewport.zoom;
 | 
					                -viewport.x / viewport.zoom + padding / viewport.zoom;
 | 
				
			||||||
@@ -54,7 +55,7 @@ export const TablesSection: React.FC<TablesSectionProps> = () => {
 | 
				
			|||||||
            const table = await createTable({
 | 
					            const table = await createTable({
 | 
				
			||||||
                x: centerX,
 | 
					                x: centerX,
 | 
				
			||||||
                y: centerY,
 | 
					                y: centerY,
 | 
				
			||||||
                schema,
 | 
					                schema: schema?.name,
 | 
				
			||||||
            });
 | 
					            });
 | 
				
			||||||
            openTableFromSidebar(table.id);
 | 
					            openTableFromSidebar(table.id);
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
@@ -80,9 +81,9 @@ export const TablesSection: React.FC<TablesSectionProps> = () => {
 | 
				
			|||||||
        } else {
 | 
					        } else {
 | 
				
			||||||
            const schema =
 | 
					            const schema =
 | 
				
			||||||
                filteredSchemas?.length === 1
 | 
					                filteredSchemas?.length === 1
 | 
				
			||||||
                    ? schemas.find((s) => s.id === filteredSchemas[0])?.name
 | 
					                    ? schemas.find((s) => s.id === filteredSchemas[0])
 | 
				
			||||||
                    : undefined;
 | 
					                    : undefined;
 | 
				
			||||||
            createTableWithLocation(schema);
 | 
					            createTableWithLocation({ schema });
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }, [
 | 
					    }, [
 | 
				
			||||||
        createTableWithLocation,
 | 
					        createTableWithLocation,
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user