Add Templates (#299)

* add templates

* update tags

* remove pic
This commit is contained in:
Guy Ben-Aharon
2024-10-29 16:31:55 +02:00
committed by GitHub
parent ba14d42803
commit 86efeb6d16
19 changed files with 1566 additions and 77 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 284 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 304 KiB

View File

@@ -0,0 +1,115 @@
import * as React from 'react';
import { ChevronRightIcon, DotsHorizontalIcon } from '@radix-ui/react-icons';
import { Slot } from '@radix-ui/react-slot';
import { cn } from '@/lib/utils';
const Breadcrumb = React.forwardRef<
HTMLElement,
React.ComponentPropsWithoutRef<'nav'> & {
separator?: React.ReactNode;
}
>(({ ...props }, ref) => <nav ref={ref} aria-label="breadcrumb" {...props} />);
Breadcrumb.displayName = 'Breadcrumb';
const BreadcrumbList = React.forwardRef<
HTMLOListElement,
React.ComponentPropsWithoutRef<'ol'>
>(({ className, ...props }, ref) => (
<ol
ref={ref}
className={cn(
'flex flex-wrap items-center gap-1.5 break-words text-sm text-muted-foreground sm:gap-2.5',
className
)}
{...props}
/>
));
BreadcrumbList.displayName = 'BreadcrumbList';
const BreadcrumbItem = React.forwardRef<
HTMLLIElement,
React.ComponentPropsWithoutRef<'li'>
>(({ className, ...props }, ref) => (
<li
ref={ref}
className={cn('inline-flex items-center gap-1.5', className)}
{...props}
/>
));
BreadcrumbItem.displayName = 'BreadcrumbItem';
const BreadcrumbLink = React.forwardRef<
HTMLAnchorElement,
React.ComponentPropsWithoutRef<'a'> & {
asChild?: boolean;
}
>(({ asChild, className, ...props }, ref) => {
const Comp = asChild ? Slot : 'a';
return (
<Comp
ref={ref}
className={cn('transition-colors hover:text-foreground', className)}
{...props}
/>
);
});
BreadcrumbLink.displayName = 'BreadcrumbLink';
const BreadcrumbPage = React.forwardRef<
HTMLSpanElement,
React.ComponentPropsWithoutRef<'span'>
>(({ className, ...props }, ref) => (
<span
ref={ref}
role="link"
aria-disabled="true"
aria-current="page"
className={cn('font-normal text-foreground', className)}
{...props}
/>
));
BreadcrumbPage.displayName = 'BreadcrumbPage';
const BreadcrumbSeparator = ({
children,
className,
...props
}: React.ComponentProps<'li'>) => (
<li
role="presentation"
aria-hidden="true"
className={cn('[&>svg]:w-3.5 [&>svg]:h-3.5', className)}
{...props}
>
{children ?? <ChevronRightIcon />}
</li>
);
BreadcrumbSeparator.displayName = 'BreadcrumbSeparator';
const BreadcrumbEllipsis = ({
className,
...props
}: React.ComponentProps<'span'>) => (
<span
role="presentation"
aria-hidden="true"
className={cn('flex h-9 w-9 items-center justify-center', className)}
{...props}
>
<DotsHorizontalIcon className="size-4" />
<span className="sr-only">More</span>
</span>
);
BreadcrumbEllipsis.displayName = 'BreadcrumbElipssis';
export {
Breadcrumb,
BreadcrumbList,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbPage,
BreadcrumbSeparator,
BreadcrumbEllipsis,
};

View File

@@ -1,3 +1,4 @@
import { cn } from '@/lib/utils';
import React from 'react';
export const Link = React.forwardRef<
@@ -6,7 +7,7 @@ export const Link = React.forwardRef<
>(({ className, children, ...props }, ref) => (
<a
ref={ref}
className={`text-pink-600 hover:underline ${className}`}
className={cn('text-pink-600 hover:underline', className)}
{...props}
>
{children}

View File

@@ -0,0 +1,45 @@
import React from 'react';
import { Link } from '@/components/link/link';
import type { LucideIcon } from 'lucide-react';
import { cn } from '@/lib/utils';
export interface ListMenuItem {
title: string;
href: string;
icon?: LucideIcon;
selected?: boolean;
}
export interface ListMenuProps extends React.HTMLAttributes<HTMLDivElement> {
items: ListMenuItem[];
}
export const ListMenu = React.forwardRef<HTMLDivElement, ListMenuProps>(
({ className, items }, ref) => {
return (
<div className={cn('flex flex-col gap-0.5', className)} ref={ref}>
{items.map((item) => (
<Link
key={item.href}
className={cn(
'flex h-7 w-full text-pink-600 dark:text-white items-center gap-1 rounded-sm p-1 text-sm transition-colors hover:bg-pink-100 dark:hover:bg-pink-900 hover:no-underline',
item.selected
? 'bg-pink-100 dark:bg-pink-900 font-semibold'
: 'text-muted-foreground hover:bg-pink-50 dark:hover:bg-pink-950 hover:text-pink-600 dark:hover:text-white'
)}
href={item.href}
>
{item.icon ? (
<item.icon
size="13"
strokeWidth={item.selected ? 2.4 : 2}
/>
) : null}
{item.title}
</Link>
))}
</div>
);
}
);
ListMenu.displayName = 'ListMenu';

View File

@@ -72,6 +72,7 @@ export interface ChartDBContext {
dependencies: DBDependency[];
currentDiagram: Diagram;
events: EventEmitter<ChartDBEvent>;
readonly?: boolean;
filteredSchemas?: string[];
filterSchemas: (schemaIds: string[]) => void;

View File

@@ -23,11 +23,17 @@ import { useLocalConfig } from '@/hooks/use-local-config';
import { defaultSchemas } from '@/lib/data/default-schemas';
import { useEventEmitter } from 'ahooks';
import type { DBDependency } from '@/lib/domain/db-dependency';
import { storageInitialValue } from '../storage-context/storage-context';
export const ChartDBProvider: React.FC<React.PropsWithChildren> = ({
children,
}) => {
const db = useStorage();
export interface ChartDBProviderProps {
diagram?: Diagram;
readonly?: boolean;
skipTitleUpdate?: boolean;
}
export const ChartDBProvider: React.FC<
React.PropsWithChildren<ChartDBProviderProps>
> = ({ children, diagram, readonly, skipTitleUpdate }) => {
let db = useStorage();
const events = useEventEmitter<ChartDBEvent>();
const navigate = useNavigate();
const { setSchemasFilter, schemasFilter } = useLocalConfig();
@@ -44,20 +50,32 @@ export const ChartDBProvider: React.FC<React.PropsWithChildren> = ({
const [databaseEdition, setDatabaseEdition] = useState<
DatabaseEdition | undefined
>();
const [tables, setTables] = useState<DBTable[]>([]);
const [relationships, setRelationships] = useState<DBRelationship[]>([]);
const [dependencies, setDependencies] = useState<DBDependency[]>([]);
const [tables, setTables] = useState<DBTable[]>(diagram?.tables ?? []);
const [relationships, setRelationships] = useState<DBRelationship[]>(
diagram?.relationships ?? []
);
const [dependencies, setDependencies] = useState<DBDependency[]>(
diagram?.dependencies ?? []
);
const defaultSchemaName = defaultSchemas[databaseType];
if (readonly) {
db = storageInitialValue;
}
useEffect(() => {
if (skipTitleUpdate) {
return;
}
if (diagramName) {
document.title = `ChartDB - ${diagramName} Diagram | Visualize Database Schemas`;
} else {
document.title =
'ChartDB - Create & Visualize Database Schema Diagrams';
}
}, [diagramName]);
}, [diagramName, skipTitleUpdate]);
const schemas = useMemo(
() =>
@@ -1407,6 +1425,7 @@ export const ChartDBProvider: React.FC<React.PropsWithChildren> = ({
schemas,
filteredSchemas,
events,
readonly,
filterSchemas,
updateDiagramId,
updateDiagramName,

View File

@@ -88,7 +88,7 @@ export interface StorageContext {
deleteDiagramDependencies: (diagramId: string) => Promise<void>;
}
export const storageContext = createContext<StorageContext>({
export const storageInitialValue: StorageContext = {
getConfig: emptyFn,
updateConfig: emptyFn,
@@ -119,4 +119,7 @@ export const storageContext = createContext<StorageContext>({
deleteDependency: emptyFn,
listDependencies: emptyFn,
deleteDiagramDependencies: emptyFn,
});
};
export const storageContext =
createContext<StorageContext>(storageInitialValue);

View File

@@ -103,9 +103,10 @@ const tableToTableNode = (
export interface CanvasProps {
initialTables: DBTable[];
readonly?: boolean;
}
export const Canvas: React.FC<CanvasProps> = ({ initialTables }) => {
export const Canvas: React.FC<CanvasProps> = ({ initialTables, readonly }) => {
const { getEdge, getInternalNode, fitView, getEdges, getNode } =
useReactFlow();
const [selectedTableIds, setSelectedTableIds] = useState<string[]>([]);
@@ -387,7 +388,15 @@ export const Canvas: React.FC<CanvasProps> = ({ initialTables }) => {
const onEdgesChangeHandler: OnEdgesChange<EdgeType> = useCallback(
(changes) => {
const removeChanges: NodeRemoveChange[] = changes.filter(
let changesToApply = changes;
if (readonly) {
changesToApply = changesToApply.filter(
(change) => change.type !== 'remove'
);
}
const removeChanges: NodeRemoveChange[] = changesToApply.filter(
(change) => change.type === 'remove'
) as NodeRemoveChange[];
@@ -415,9 +424,15 @@ export const Canvas: React.FC<CanvasProps> = ({ initialTables }) => {
removeDependencies(dependenciesToRemove);
}
return onEdgesChange(changes);
return onEdgesChange(changesToApply);
},
[getEdge, onEdgesChange, removeRelationships, removeDependencies]
[
getEdge,
onEdgesChange,
removeRelationships,
removeDependencies,
readonly,
]
);
const updateOverlappingGraphOnChanges = useCallback(
@@ -460,15 +475,23 @@ export const Canvas: React.FC<CanvasProps> = ({ initialTables }) => {
const onNodesChangeHandler: OnNodesChange<TableNodeType> = useCallback(
(changes) => {
const positionChanges: NodePositionChange[] = changes.filter(
let changesToApply = changes;
if (readonly) {
changesToApply = changesToApply.filter(
(change) => change.type !== 'remove'
);
}
const positionChanges: NodePositionChange[] = changesToApply.filter(
(change) => change.type === 'position' && !change.dragging
) as NodePositionChange[];
const removeChanges: NodeRemoveChange[] = changes.filter(
const removeChanges: NodeRemoveChange[] = changesToApply.filter(
(change) => change.type === 'remove'
) as NodeRemoveChange[];
const sizeChanges: NodeDimensionChange[] = changes.filter(
const sizeChanges: NodeDimensionChange[] = changesToApply.filter(
(change) => change.type === 'dimensions' && change.resizing
) as NodeDimensionChange[];
@@ -521,12 +544,13 @@ export const Canvas: React.FC<CanvasProps> = ({ initialTables }) => {
sizeChanges,
});
return onNodesChange(changes);
return onNodesChange(changesToApply);
},
[
onNodesChange,
updateTablesState,
updateOverlappingGraphOnChangesDebounced,
readonly,
]
);
@@ -697,22 +721,26 @@ export const Canvas: React.FC<CanvasProps> = ({ initialTables }) => {
className="!shadow-none"
>
<div className="flex flex-col items-center gap-2 md:flex-row">
<Tooltip>
<TooltipTrigger asChild>
<span>
<Button
variant="secondary"
className="size-8 p-1 shadow-none"
onClick={showReorderConfirmation}
>
<LayoutGrid className="size-4" />
</Button>
</span>
</TooltipTrigger>
<TooltipContent>
{t('toolbar.reorder_diagram')}
</TooltipContent>
</Tooltip>
{!readonly ? (
<Tooltip>
<TooltipTrigger asChild>
<span>
<Button
variant="secondary"
className="size-8 p-1 shadow-none"
onClick={
showReorderConfirmation
}
>
<LayoutGrid className="size-4" />
</Button>
</span>
</TooltipTrigger>
<TooltipContent>
{t('toolbar.reorder_diagram')}
</TooltipContent>
</Tooltip>
) : null}
<div
className={`transition-opacity duration-300 ease-in-out ${
@@ -760,7 +788,7 @@ export const Canvas: React.FC<CanvasProps> = ({ initialTables }) => {
</Controls>
) : null}
{!isDesktop ? (
{!isDesktop && !readonly ? (
<Controls
position="bottom-left"
orientation="horizontal"
@@ -785,7 +813,7 @@ export const Canvas: React.FC<CanvasProps> = ({ initialTables }) => {
showInteractive={false}
className="!shadow-none"
>
<Toolbar />
<Toolbar readonly={readonly} />
</Controls>
<MiniMap
style={{

View File

@@ -10,6 +10,7 @@ import { KeyRound, Trash2 } from 'lucide-react';
import type { DBField } from '@/lib/domain/db-field';
import { useChartDB } from '@/hooks/use-chartdb';
import { cn } from '@/lib/utils';
export const LEFT_HANDLE_ID_PREFIX = 'left_rel_';
export const RIGHT_HANDLE_ID_PREFIX = 'right_rel_';
@@ -26,7 +27,7 @@ export interface TableNodeFieldProps {
export const TableNodeField: React.FC<TableNodeFieldProps> = React.memo(
({ field, focused, tableNodeId, highlighted, visible, isConnectable }) => {
const { removeField, relationships } = useChartDB();
const { removeField, relationships, readonly } = useChartDB();
const updateNodeInternals = useUpdateNodeInternals();
const connection = useConnection();
const isTarget = useMemo(
@@ -70,22 +71,22 @@ export const TableNodeField: React.FC<TableNodeFieldProps> = React.memo(
: 'z-0 max-h-0 overflow-hidden opacity-0'
}`}
>
{isConnectable && (
{isConnectable ? (
<>
<Handle
id={`${RIGHT_HANDLE_ID_PREFIX}${field.id}`}
className={`!h-4 !w-4 !border-2 !bg-pink-600 ${!focused ? '!invisible' : ''}`}
className={`!h-4 !w-4 !border-2 !bg-pink-600 ${!focused || readonly ? '!invisible' : ''}`}
position={Position.Right}
type="source"
/>
<Handle
id={`${LEFT_HANDLE_ID_PREFIX}${field.id}`}
className={`!h-4 !w-4 !border-2 !bg-pink-600 ${!focused ? '!invisible' : ''}`}
className={`!h-4 !w-4 !border-2 !bg-pink-600 ${!focused || readonly ? '!invisible' : ''}`}
position={Position.Left}
type="source"
/>
</>
)}
) : null}
{(!connection.inProgress || isTarget) && isConnectable && (
<>
{Array.from(
@@ -115,26 +116,38 @@ export const TableNodeField: React.FC<TableNodeFieldProps> = React.memo(
<div className="block truncate text-left">{field.name}</div>
<div className="flex max-w-[35%] justify-end gap-2 truncate hover:shrink-0">
{field.primaryKey ? (
<div className="text-muted-foreground group-hover:hidden">
<div
className={cn(
'text-muted-foreground',
!readonly ? 'group-hover:hidden' : ''
)}
>
<KeyRound size={14} />
</div>
) : null}
<div className="content-center truncate text-right text-xs text-muted-foreground group-hover:hidden">
<div
className={cn(
'content-center truncate text-right text-xs text-muted-foreground',
!readonly ? 'group-hover:hidden' : ''
)}
>
{field.type.name}
</div>
<div className="hidden flex-row group-hover:flex">
<Button
variant="ghost"
className="size-6 p-0 hover:bg-primary-foreground"
onClick={(e) => {
e.stopPropagation();
removeField(tableNodeId, field.id);
}}
>
<Trash2 className="size-3.5 text-red-700" />
</Button>
</div>
{readonly ? null : (
<div className="hidden flex-row group-hover:flex">
<Button
variant="ghost"
className="size-6 p-0 hover:bg-primary-foreground"
onClick={(e) => {
e.stopPropagation();
removeField(tableNodeId, field.id);
}}
>
<Trash2 className="size-3.5 text-red-700" />
</Button>
</div>
)}
</div>
</div>
);

View File

@@ -44,7 +44,7 @@ export const TableNode: React.FC<NodeProps<TableNodeType>> = React.memo(
id,
data: { table, isOverlapping, highlightOverlappingTables },
}) => {
const { updateTable, relationships } = useChartDB();
const { updateTable, relationships, readonly } = useChartDB();
const edges = useStore((store) => store.edges) as EdgeType[];
const { openTableFromSidebar, selectSidebarSection } = useLayout();
const [expanded, setExpanded] = useState(false);
@@ -173,13 +173,15 @@ export const TableNode: React.FC<NodeProps<TableNodeType>> = React.memo(
</Label>
</div>
<div className="hidden shrink-0 flex-row group-hover:flex">
<Button
variant="ghost"
className="size-6 p-0 text-slate-500 hover:bg-primary-foreground hover:text-slate-700 dark:text-slate-400 dark:hover:bg-slate-800 dark:hover:text-slate-200"
onClick={openTableInEditor}
>
<Pencil className="size-4" />
</Button>
{readonly ? null : (
<Button
variant="ghost"
className="size-6 p-0 text-slate-500 hover:bg-primary-foreground hover:text-slate-700 dark:text-slate-400 dark:hover:bg-slate-800 dark:hover:text-slate-200"
onClick={openTableInEditor}
>
<Pencil className="size-4" />
</Button>
)}
<Button
variant="ghost"
className="size-6 p-0 text-slate-500 hover:bg-primary-foreground hover:text-slate-700 dark:text-slate-400 dark:hover:bg-slate-800 dark:hover:text-slate-200"

View File

@@ -16,9 +16,11 @@ import { Button } from '@/components/button/button';
const convertToPercentage = (value: number) => `${Math.round(value * 100)}%`;
export interface ToolbarProps {}
export interface ToolbarProps {
readonly?: boolean;
}
export const Toolbar: React.FC<ToolbarProps> = () => {
export const Toolbar: React.FC<ToolbarProps> = ({ readonly }) => {
const { updateDiagramUpdatedAt } = useChartDB();
const { t } = useTranslation();
const { redo, undo, hasRedo, hasUndo } = useHistory();
@@ -59,17 +61,25 @@ export const Toolbar: React.FC<ToolbarProps> = () => {
<div className="px-1">
<Card className="h-[44px] bg-secondary p-0 shadow-none">
<CardContent className="flex h-full flex-row items-center p-1">
<Tooltip>
<TooltipTrigger asChild>
<span>
<ToolbarButton onClick={updateDiagramUpdatedAt}>
<Save />
</ToolbarButton>
</span>
</TooltipTrigger>
<TooltipContent>{t('toolbar.save')}</TooltipContent>
</Tooltip>
<Separator orientation="vertical" />
{!readonly ? (
<>
<Tooltip>
<TooltipTrigger asChild>
<span>
<ToolbarButton
onClick={updateDiagramUpdatedAt}
>
<Save />
</ToolbarButton>
</span>
</TooltipTrigger>
<TooltipContent>
{t('toolbar.save')}
</TooltipContent>
</Tooltip>
<Separator orientation="vertical" />
</>
) : null}
<Tooltip>
<TooltipTrigger asChild>
<span>

View File

@@ -0,0 +1,251 @@
import React, { useCallback, useEffect } from 'react';
import ChartDBLogo from '@/assets/logo-light.png';
import ChartDBDarkLogo from '@/assets/logo-dark.png';
import { useTheme } from '@/hooks/use-theme';
import { LocalConfigProvider } from '@/context/local-config-context/local-config-provider';
import { StorageProvider } from '@/context/storage-context/storage-provider';
import { ThemeProvider } from '@/context/theme-context/theme-provider';
import { Button } from '@/components/button/button';
import { CloudDownload } from 'lucide-react';
import { useNavigate, useParams } from 'react-router-dom';
import type { Template } from '../../templates-data/templates-data';
import {
Breadcrumb,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbList,
BreadcrumbSeparator,
} from '@/components/breadcrumb/breadcrumb';
import { Spinner } from '@/components/spinner/spinner';
import { Separator } from '@/components/separator/separator';
import {
databaseSecondaryLogoMap,
databaseTypeToLabelMap,
} from '@/lib/databases';
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from '@/components/tooltip/tooltip';
import { Badge } from '@/components/badge/badge';
import { Canvas } from '../editor-page/canvas/canvas';
import { ReactFlowProvider } from '@xyflow/react';
import { ChartDBProvider } from '@/context/chartdb-context/chartdb-provider';
import { convertTemplateToNewDiagram } from '@/templates-data/template-utils';
import { useStorage } from '@/hooks/use-storage';
import type { Diagram } from '@/lib/domain/diagram';
const TemplatePageComponent: React.FC = () => {
const { addDiagram } = useStorage();
const { templateSlug } = useParams<{ templateSlug: string }>();
const [template, setTemplate] = React.useState<Template>();
const navigate = useNavigate();
useEffect(() => {
const loadTemplate = async () => {
const { templates } = await import(
'@/templates-data/templates-data'
);
const template = templates.find((t) => t.slug === templateSlug);
if (!template) {
navigate('/templates');
return;
}
setTemplate(template);
};
loadTemplate();
}, [templateSlug, navigate]);
const { effectiveTheme } = useTheme();
useEffect(() => {
if (template) {
document.title = `ChartDB - ${template.name} - ${template.shortDescription}`;
} else {
document.title = 'ChartDB - Database Schema Template';
}
}, [template]);
const cloneTemplate = useCallback(async () => {
if (!template) {
return;
}
const diagram = convertTemplateToNewDiagram(template);
const now = new Date();
const diagramToAdd: Diagram = {
...diagram,
createdAt: now,
updatedAt: now,
};
await addDiagram({ diagram: diagramToAdd });
navigate(`/diagrams/${diagramToAdd.id}`);
}, [addDiagram, navigate, template]);
return (
<section className="flex h-screen w-screen flex-col bg-background">
<nav className="flex h-12 shrink-0 flex-row items-center justify-between border-b px-4">
<div className="flex flex-1 justify-start gap-x-3">
<div className="flex items-center font-primary">
<a
href="https://chartdb.io"
className="cursor-pointer"
rel="noreferrer"
>
<img
src={
effectiveTheme === 'light'
? ChartDBLogo
: ChartDBDarkLogo
}
alt="chartDB"
className="h-4 max-w-fit"
/>
</a>
</div>
</div>
<div className="group flex flex-1 flex-row items-center justify-center"></div>
<div className="hidden flex-1 justify-end sm:flex"></div>
</nav>
{!template ? (
<Spinner size={'large'} className="mt-20 text-pink-600" />
) : (
<div className="flex flex-1 flex-col p-3 pb-5 text-center md:px-28 md:text-left">
<Breadcrumb className="mb-2">
<BreadcrumbList>
<BreadcrumbItem>
<BreadcrumbLink href={`/templates`}>
Templates
</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator />
<BreadcrumbItem>
<BreadcrumbLink
href={`/templates/${templateSlug}`}
>
{templateSlug}
</BreadcrumbLink>
</BreadcrumbItem>
</BreadcrumbList>
</Breadcrumb>
<div className="flex flex-col items-center gap-4 md:flex-row md:items-start md:justify-between md:gap-0">
<div className="flex flex-col pr-0 md:pr-20">
<h1 className="font-primary text-2xl font-bold">
{template?.name}
</h1>
<h2 className="mt-3">
<span className="font-semibold">
{template?.shortDescription}
{': '}
</span>
{template?.description}
</h2>
</div>
<div className="flex justify-end">
<Button onClick={cloneTemplate}>
<CloudDownload className="mr-2" size="16" />
Clone Template
</Button>
</div>
</div>
<Separator className="my-5" />
<div className="flex w-full flex-1 flex-col gap-4 md:flex-row">
<div className="relative top-0 flex h-fit w-full shrink-0 flex-col gap-4 md:sticky md:top-1 md:w-60">
<div>
<h4 className="mb-1 text-base font-semibold md:text-left">
Metadata
</h4>
<div className="text-sm text-muted-foreground">
<div className="inline-flex">
<span className="mr-2">Database:</span>
<Tooltip>
<TooltipTrigger asChild>
<img
src={
databaseSecondaryLogoMap[
template.diagram
.databaseType
]
}
className="h-5 max-w-fit"
alt="database"
/>
</TooltipTrigger>
<TooltipContent>
{
databaseTypeToLabelMap[
template.diagram
.databaseType
]
}
</TooltipContent>
</Tooltip>
</div>
</div>
<div className="text-sm text-muted-foreground">
<span>Tables:</span>
<span className="ml-2 font-semibold">
{template?.diagram?.tables?.length ?? 0}
</span>
</div>
<div className="text-sm text-muted-foreground">
<span>Relationships:</span>
<span className="ml-2 font-semibold">
{template?.diagram?.relationships
?.length ?? 0}
</span>
</div>
</div>
<div>
<h4 className="mb-1 text-base font-semibold md:text-left">
Tags
</h4>
<div className="flex flex-wrap justify-center gap-1 md:justify-start">
{template.tags.map((tag) => (
<Badge
variant="outline"
key={`${template.id}_${tag}`}
>
{tag}
</Badge>
))}
</div>
</div>
</div>
<div className="flex min-h-96 overflow-hidden rounded border md:flex-1 md:rounded-lg">
<div className="size-full">
<ChartDBProvider
diagram={template.diagram}
readonly
skipTitleUpdate
>
<Canvas
readonly
initialTables={
template.diagram.tables ?? []
}
/>
</ChartDBProvider>
</div>
</div>
</div>
</div>
)}
</section>
);
};
export const TemplatePage: React.FC = () => (
<LocalConfigProvider>
<StorageProvider>
<ThemeProvider>
<ReactFlowProvider>
<TemplatePageComponent />
</ReactFlowProvider>
</ThemeProvider>
</StorageProvider>
</LocalConfigProvider>
);

View File

@@ -0,0 +1,82 @@
import React from 'react';
import { randomColor } from '@/lib/colors';
import {
databaseSecondaryLogoMap,
databaseTypeToLabelMap,
} from '@/lib/databases';
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from '@/components/tooltip/tooltip';
import { useTheme } from '@/hooks/use-theme';
import type { Template } from '../../../templates-data/templates-data';
import { Badge } from '@/components/badge/badge';
export interface TemplateCardProps {
template: Template;
}
export const TemplateCard: React.FC<TemplateCardProps> = ({ template }) => {
const { effectiveTheme } = useTheme();
return (
<a href={`/templates/${template.slug}`}>
<div className="flex h-80 w-full cursor-pointer flex-col rounded-lg border-2 border-slate-500 bg-slate-50 shadow-sm transition duration-300 ease-in-out hover:scale-[102%] hover:border-pink-600 dark:border-slate-700 dark:bg-slate-950">
<div
className="h-2 rounded-t-[6px]"
style={{ backgroundColor: randomColor() }}
></div>
<div className="grow overflow-hidden p-1">
<img
src={
effectiveTheme === 'dark'
? template.imageDark
: template.image
}
alt={template.name}
className="size-full rounded object-fill"
/>
</div>
<div className="mt-2 flex items-center justify-between px-2">
<div className="flex items-center gap-1">
<h3 className="cursor-pointer text-base font-semibold">
{template.name}
</h3>
</div>
<div className="flex flex-row">
<Tooltip>
<TooltipTrigger className="mr-1">
<img
src={
databaseSecondaryLogoMap[
template.diagram.databaseType
]
}
className="h-5 max-w-fit"
alt="database"
/>
</TooltipTrigger>
<TooltipContent>
{
databaseTypeToLabelMap[
template.diagram.databaseType
]
}
</TooltipContent>
</Tooltip>
</div>
</div>
<div className="flex p-2 text-sm">
{template.shortDescription}
</div>
<div className="flex flex-wrap gap-1 p-2">
{template.tags.map((tag) => (
<Badge variant="outline" key={`${template.id}_${tag}`}>
{tag}
</Badge>
))}
</div>
</div>
</a>
);
};

View File

@@ -0,0 +1,149 @@
import React, { useEffect } from 'react';
import ChartDBLogo from '@/assets/logo-light.png';
import ChartDBDarkLogo from '@/assets/logo-dark.png';
import { useTheme } from '@/hooks/use-theme';
import { LocalConfigProvider } from '@/context/local-config-context/local-config-provider';
import { ThemeProvider } from '@/context/theme-context/theme-provider';
import { Component, Star } from 'lucide-react';
import { ListMenu } from '@/components/list-menu/list-menu';
import { TemplateCard } from './template-card/template-card';
import { useMatches, useParams } from 'react-router-dom';
import type { Template } from '@/templates-data/templates-data';
import { Spinner } from '@/components/spinner/spinner';
import { removeDups } from '@/lib/utils';
const TemplatesPageComponent: React.FC = () => {
const { effectiveTheme } = useTheme();
const { tag } = useParams<{ tag: string }>();
const matches = useMatches();
const [templates, setTemplates] = React.useState<Template[]>();
const [tags, setTags] = React.useState<string[]>();
const isFeatured = matches.some(
(match) => match.id === 'templates_featured'
);
const isAllTemplates = matches.some((match) => match.id === 'templates');
const isTags = matches.some((match) => match.id === 'templates_tags');
useEffect(() => {
document.title = 'ChartDB - Database Schema Templates';
}, []);
useEffect(() => {
const loadTemplates = async () => {
const { templates: loadedTemplates } = await import(
'@/templates-data/templates-data'
);
let templatesToLoad = loadedTemplates;
if (isFeatured) {
templatesToLoad = loadedTemplates.filter((t) => t.featured);
}
if (isTags && tag) {
templatesToLoad = loadedTemplates.filter((t) =>
t.tags.includes(tag)
);
}
setTemplates(templatesToLoad);
setTags(removeDups(loadedTemplates?.flatMap((t) => t.tags) ?? []));
};
loadTemplates();
}, [isFeatured, isTags, tag]);
return (
<section className="flex w-screen flex-col bg-background">
<nav className="flex h-12 shrink-0 flex-row items-center justify-between border-b px-4">
<div className="flex flex-1 justify-start gap-x-3">
<div className="flex items-center font-primary">
<a
href="https://chartdb.io"
className="cursor-pointer"
rel="noreferrer"
>
<img
src={
effectiveTheme === 'light'
? ChartDBLogo
: ChartDBDarkLogo
}
alt="chartDB"
className="h-4 max-w-fit"
/>
</a>
</div>
</div>
<div className="group flex flex-1 flex-row items-center justify-center"></div>
<div className="hidden flex-1 justify-end sm:flex"></div>
</nav>
<div className="flex flex-col p-3 text-center md:px-28 md:text-left">
<h1 className="font-primary text-2xl font-bold">
Database Schema Templates
</h1>
<h2 className="mt-1 font-primary text-base text-muted-foreground">
Explore a collection of real-world database schemas drawn
from real-world live applications and open-source projects.
Use these as a foundation or source of inspiration when
designing your apps architecture.
</h2>
{!templates ? (
<Spinner size={'large'} className="mt-20 text-pink-600" />
) : (
<div className="mt-6 flex w-full flex-col-reverse gap-4 md:flex-row">
<div className="relative top-0 flex h-fit w-full shrink-0 flex-col md:sticky md:top-1 md:w-44">
<ListMenu
items={[
{
title: 'Featured',
href: '/templates/featured',
icon: Star,
selected: isFeatured,
},
{
title: 'All Templates',
href: '/templates',
icon: Component,
selected: isAllTemplates,
},
]}
/>
<h4 className="mt-4 text-left text-sm font-semibold">
Tags
</h4>
{tags ? (
<ListMenu
className="mt-1 w-44 shrink-0"
items={tags.map((currentTag) => ({
title: currentTag,
href: `/templates/tags/${currentTag}`,
selected: tag === currentTag,
}))}
/>
) : null}
</div>
<div className="grid flex-1 grid-flow-row grid-cols-1 gap-6 md:grid-cols-1 lg:grid-cols-2 xl:grid-cols-4">
{templates.map((template) => (
<TemplateCard
key={`${template.id}`}
template={template}
/>
))}
</div>
</div>
)}
</div>
</section>
);
};
export const TemplatesPage: React.FC = () => (
<LocalConfigProvider>
<ThemeProvider>
<TemplatesPageComponent />
</ThemeProvider>
</LocalConfigProvider>
);

View File

@@ -26,6 +26,53 @@ const routes: RouteObject[] = [
};
},
},
{
id: 'templates',
path: 'templates',
async lazy() {
const { TemplatesPage } = await import(
'./pages/templates-page/templates-page'
);
return {
element: <TemplatesPage />,
};
},
},
{
id: 'templates_featured',
path: 'templates/featured',
async lazy() {
const { TemplatesPage } = await import(
'./pages/templates-page/templates-page'
);
return {
element: <TemplatesPage />,
};
},
},
{
id: 'templates_tags',
path: 'templates/tags/:tag',
async lazy() {
const { TemplatesPage } = await import(
'./pages/templates-page/templates-page'
);
return {
element: <TemplatesPage />,
};
},
},
{
path: 'templates/:templateSlug',
async lazy() {
const { TemplatePage } = await import(
'./pages/template-page/template-page'
);
return {
element: <TemplatePage />,
};
},
},
{
path: '*',
async lazy() {

View File

@@ -0,0 +1,88 @@
import type { Diagram } from '@/lib/domain/diagram';
import type { Template } from './templates-data';
import { generateDiagramId, generateId } from '@/lib/utils';
import type { DBTable } from '@/lib/domain/db-table';
import type { DBField } from '@/lib/domain/db-field';
import type { DBIndex } from '@/lib/domain/db-index';
import type { DBRelationship } from '@/lib/domain/db-relationship';
import type { DBDependency } from '@/lib/domain/db-dependency';
export const convertTemplateToNewDiagram = (template: Template): Diagram => {
const diagramId = generateDiagramId();
const idsMap = new Map<string, string>();
template.diagram.tables?.forEach((table) => {
idsMap.set(table.id, generateId());
table.fields.forEach((field) => {
idsMap.set(field.id, generateId());
});
table.indexes.forEach((index) => {
idsMap.set(index.id, generateId());
});
});
template.diagram.relationships?.forEach((relationship) => {
idsMap.set(relationship.id, generateId());
});
template.diagram.dependencies?.forEach((dependency) => {
idsMap.set(dependency.id, generateId());
});
const getNewId = (id: string) => {
const newId = idsMap.get(id);
if (!newId) {
throw new Error(`Id not found for ${id}`);
}
return newId;
};
const tables: DBTable[] =
template.diagram.tables?.map((table) => {
const newTable: DBTable = { ...table, id: getNewId(table.id) };
newTable.fields = table.fields.map(
(field): DBField => ({
...field,
id: getNewId(field.id),
})
);
newTable.indexes = table.indexes.map(
(index): DBIndex => ({
...index,
id: getNewId(index.id),
})
);
return newTable;
}) ?? [];
const relationships: DBRelationship[] =
template.diagram.relationships?.map(
(relationship): DBRelationship => ({
...relationship,
id: getNewId(relationship.id),
sourceTableId: getNewId(relationship.sourceTableId),
targetTableId: getNewId(relationship.targetTableId),
sourceFieldId: getNewId(relationship.sourceFieldId),
targetFieldId: getNewId(relationship.targetFieldId),
})
) ?? [];
const dependencies: DBDependency[] =
template.diagram.dependencies?.map(
(dependency): DBDependency => ({
...dependency,
id: getNewId(dependency.id),
dependentTableId: getNewId(dependency.dependentTableId),
tableId: getNewId(dependency.tableId),
})
) ?? [];
return {
...template.diagram,
id: diagramId,
dependencies,
relationships,
tables,
};
};

View File

@@ -0,0 +1,17 @@
import type { Diagram } from '@/lib/domain/diagram';
import { employeeDb } from './templates/employee-db';
export interface Template {
id: string;
slug: string;
name: string;
shortDescription: string;
description: string;
image: string;
imageDark: string;
diagram: Diagram;
tags: string[];
featured: boolean;
}
export const templates: Template[] = [employeeDb];

View File

@@ -0,0 +1,618 @@
import { DatabaseType } from '@/lib/domain/database-type';
import type { Template } from '../templates-data';
import image from '@/assets/templates/employeedb.png';
import imageDark from '@/assets/templates/employeedb-dark.png';
export const employeeDb: Template = {
id: '1',
slug: 'employees-db',
name: 'Employees schema',
shortDescription: 'Employees, departments, and salaries',
description:
'A schema for database of employees, departments, and salaries.',
image,
imageDark,
tags: ['mysql'],
featured: true,
diagram: {
id: 'diagramexample01',
name: 'employees-db',
createdAt: new Date(),
updatedAt: new Date(),
databaseType: DatabaseType.MYSQL,
tables: [
{
id: '6e70s6dhdfnve9xljbih6bo7x',
name: 'departments',
x: 488.2056573620456,
y: -116.26128764468365,
fields: [
{
id: 'gaj3scrtaz46ezfmc162ingxf',
name: 'dept_no',
type: { id: 'char', name: 'chat' },
primaryKey: true,
unique: true,
nullable: false,
characterMaximumLength: '4',
collation: 'utf8mb4_general_ci',
createdAt: Date.now(),
},
{
id: 'pb0j4xvevy9dics5euelx7ay9',
name: 'dept_name',
type: { id: 'varchar', name: 'varchar' },
primaryKey: false,
unique: true,
nullable: false,
characterMaximumLength: '40',
collation: 'utf8mb4_general_ci',
createdAt: Date.now(),
},
],
indexes: [
{
id: '87iu197demih0wymjooqm9dmh',
name: 'PRIMARY',
unique: true,
fieldIds: ['gaj3scrtaz46ezfmc162ingxf'],
createdAt: Date.now(),
},
{
id: 'ltt6su8loqpf29ok7okzqblg2',
name: 'dept_name',
unique: true,
fieldIds: ['pb0j4xvevy9dics5euelx7ay9'],
createdAt: Date.now(),
},
],
color: '#b067e9',
isView: false,
isMaterializedView: false,
createdAt: Date.now(),
},
{
id: 'rkc38w1yqrvhz2pmveunp6nsw',
name: 'dept_emp',
x: 809.6786878331093,
y: 13.918352368775231,
fields: [
{
id: 'wcgycjif09xrq0ly3txkq6ocu',
name: 'emp_no',
type: { id: 'int', name: 'int' },
primaryKey: true,
unique: true,
nullable: false,
createdAt: Date.now(),
},
{
id: 'jdw1yrh9xf1i7927gzs9pob2p',
name: 'dept_no',
type: { id: 'char', name: 'chat' },
primaryKey: true,
unique: true,
nullable: false,
characterMaximumLength: '4',
collation: 'utf8mb4_general_ci',
createdAt: Date.now(),
},
{
id: 'm3zu12iy2jmfraliisks0rqcv',
name: 'from_date',
type: { id: 'date', name: 'date' },
primaryKey: false,
unique: false,
nullable: false,
createdAt: Date.now(),
},
{
id: 'yq4k0bqt39aap0956aejicud4',
name: 'to_date',
type: { id: 'date', name: 'date' },
primaryKey: false,
unique: false,
nullable: false,
createdAt: Date.now(),
},
],
indexes: [
{
id: 'rqb91465yc51xpvd54o5a8d0l',
name: 'PRIMARY',
unique: true,
fieldIds: ['wcgycjif09xrq0ly3txkq6ocu'],
createdAt: Date.now(),
},
{
id: '8wh6op49abv143qdfjzm211xj',
name: 'PRIMARY',
unique: true,
fieldIds: ['jdw1yrh9xf1i7927gzs9pob2p'],
createdAt: Date.now(),
},
{
id: 'iw9hjbmuchq0jisgd8zb13qy6',
name: 'dept_no',
unique: false,
fieldIds: ['jdw1yrh9xf1i7927gzs9pob2p'],
createdAt: Date.now(),
},
],
color: '#8a61f5',
isView: false,
isMaterializedView: false,
createdAt: Date.now(),
},
{
id: 'd2xqiqlffjfsg3kgsmpck5xay',
name: 'dept_manager',
x: -248.93599999999998,
y: -84.16474999999997,
fields: [
{
id: 'ecx2zbzdc5o54e04aeg7tlg54',
name: 'emp_no',
type: { id: 'int', name: 'int' },
primaryKey: true,
unique: true,
nullable: false,
createdAt: Date.now(),
},
{
id: 'v8plj7wq1cly03y178bysft2f',
name: 'dept_no',
type: { id: 'char', name: 'chat' },
primaryKey: true,
unique: true,
nullable: false,
characterMaximumLength: '4',
collation: 'utf8mb4_general_ci',
createdAt: Date.now(),
},
{
id: '3u0rfkvw0yokndqhfqx0nuzpi',
name: 'from_date',
type: { id: 'date', name: 'date' },
primaryKey: false,
unique: false,
nullable: false,
createdAt: Date.now(),
},
{
id: 'xrcw2488t50shssn4vn3n6vad',
name: 'to_date',
type: { id: 'date', name: 'date' },
primaryKey: false,
unique: false,
nullable: false,
createdAt: Date.now(),
},
],
indexes: [
{
id: 'cbahnbrxaaj7cg29act50izy4',
name: 'PRIMARY',
unique: true,
fieldIds: ['ecx2zbzdc5o54e04aeg7tlg54'],
createdAt: Date.now(),
},
{
id: 'vgxv8rkf4890yf659o2oklffv',
name: 'PRIMARY',
unique: true,
fieldIds: ['v8plj7wq1cly03y178bysft2f'],
createdAt: Date.now(),
},
{
id: '60gtoaq9vnwwbii97ks47ph82',
name: 'dept_no',
unique: false,
fieldIds: ['v8plj7wq1cly03y178bysft2f'],
createdAt: Date.now(),
},
],
color: '#ff6363',
isView: false,
isMaterializedView: false,
createdAt: Date.now(),
},
{
id: '1c03hu41ko98myywerwbazeli',
name: 'employees',
x: 82.72000000000003,
y: 98.27199999999999,
fields: [
{
id: '04csyx8ds9t3rh93txiqs4dm4',
name: 'emp_no',
type: { id: 'int', name: 'int' },
primaryKey: true,
unique: true,
nullable: false,
createdAt: Date.now(),
},
{
id: 'gnvcnj2i5jgktg7vauhveaorb',
name: 'birth_date',
type: { id: 'date', name: 'date' },
primaryKey: false,
unique: false,
nullable: false,
createdAt: Date.now(),
},
{
id: '8savn7ht0fogo4odxdhekrret',
name: 'first_name',
type: { id: 'varchar', name: 'varchar' },
primaryKey: false,
unique: false,
nullable: false,
characterMaximumLength: '14',
collation: 'utf8mb4_general_ci',
createdAt: Date.now(),
},
{
id: 'ol8kezsspmjx25avlf2dvic5q',
name: 'last_name',
type: { id: 'varchar', name: 'varchar' },
primaryKey: false,
unique: false,
nullable: false,
characterMaximumLength: '16',
collation: 'utf8mb4_general_ci',
createdAt: Date.now(),
},
{
id: 'jga5lfkkoxueqslcljj2vng9q',
name: 'gender',
type: { id: 'enum', name: 'enum' },
primaryKey: false,
unique: false,
nullable: false,
characterMaximumLength: '1',
collation: 'utf8mb4_general_ci',
createdAt: Date.now(),
},
{
id: 'i0kgsun3nzrjpaz8ykwjgogyb',
name: 'hire_date',
type: { id: 'date', name: 'date' },
primaryKey: false,
unique: false,
nullable: false,
createdAt: Date.now(),
},
],
indexes: [
{
id: '8zg1ccoj4jb4kv6eleih38ni5',
name: 'PRIMARY',
unique: true,
fieldIds: ['04csyx8ds9t3rh93txiqs4dm4'],
createdAt: Date.now(),
},
],
color: '#4dee8a',
isView: false,
isMaterializedView: false,
createdAt: Date.now(),
},
{
id: 'u97myqcs1osilg7x0v263qpzd',
name: 'salaries',
x: 493.50755288021537,
y: 227.8719999999999,
fields: [
{
id: 'b8c9v5vtpbnt5tjzcd3iat85f',
name: 'emp_no',
type: { id: 'int', name: 'int' },
primaryKey: true,
unique: true,
nullable: false,
createdAt: Date.now(),
},
{
id: 'n654h28i8yeeadznzht9mjc8f',
name: 'salary',
type: { id: 'int', name: 'int' },
primaryKey: false,
unique: false,
nullable: false,
createdAt: Date.now(),
},
{
id: '0s10erufqpl6y3hpqmvbcneol',
name: 'from_date',
type: { id: 'date', name: 'date' },
primaryKey: true,
unique: true,
nullable: false,
createdAt: Date.now(),
},
{
id: 'bwohji7dj67xpa6p5diyy6pis',
name: 'to_date',
type: { id: 'date', name: 'date' },
primaryKey: false,
unique: false,
nullable: false,
createdAt: Date.now(),
},
{
id: 'orkgizf8qbmtly3rw5cbxtc2i',
name: 'price',
type: { id: 'decimal', name: 'decimal' },
primaryKey: false,
unique: false,
nullable: true,
precision: 10,
scale: 2,
default: '13.21',
createdAt: Date.now(),
},
],
indexes: [
{
id: 'nky2wepp8yr5g6rzvnbta1hxb',
name: 'PRIMARY',
unique: true,
fieldIds: ['b8c9v5vtpbnt5tjzcd3iat85f'],
createdAt: Date.now(),
},
{
id: 'w40nnsrsnlz7z7vycs4yf0s8d',
name: 'PRIMARY',
unique: true,
fieldIds: ['0s10erufqpl6y3hpqmvbcneol'],
createdAt: Date.now(),
},
],
color: '#ff6b8a',
isView: false,
isMaterializedView: false,
createdAt: Date.now(),
},
{
id: 'fa0ozznyrckx572fqztyw3w4z',
name: 'titles',
x: -251.04799999999966,
y: 220.9599999999999,
fields: [
{
id: 'hr2gdoc0wtwvs4pfqo6m0fwc3',
name: 'emp_no',
type: { id: 'int', name: 'int' },
primaryKey: true,
unique: true,
nullable: false,
createdAt: Date.now(),
},
{
id: '5evr59tury66sayiu59esoc61',
name: 'title',
type: { id: 'varchar', name: 'varchar' },
primaryKey: true,
unique: true,
nullable: false,
characterMaximumLength: '50',
collation: 'utf8mb4_general_ci',
createdAt: Date.now(),
},
{
id: '0vs1nqvrb6t53niz5ns2eskre',
name: 'from_date',
type: { id: 'date', name: 'date' },
primaryKey: true,
unique: true,
nullable: false,
createdAt: Date.now(),
},
{
id: 'n6csxwmdm60y920p5jovlx4c6',
name: 'to_date',
type: { id: 'date', name: 'date' },
primaryKey: false,
unique: false,
nullable: true,
default: 'NULL',
createdAt: Date.now(),
},
],
indexes: [
{
id: 'ijhmb7tq6i4fd72ndvotnwo45',
name: 'PRIMARY',
unique: true,
fieldIds: ['hr2gdoc0wtwvs4pfqo6m0fwc3'],
createdAt: Date.now(),
},
{
id: 'wgneqfte0nq7d5vzed2hcqie6',
name: 'PRIMARY',
unique: true,
fieldIds: ['5evr59tury66sayiu59esoc61'],
createdAt: Date.now(),
},
{
id: 'jbe9t9adhluqy8d3i7w1vgygd',
name: 'PRIMARY',
unique: true,
fieldIds: ['0vs1nqvrb6t53niz5ns2eskre'],
createdAt: Date.now(),
},
],
color: '#b067e9',
isView: false,
isMaterializedView: false,
createdAt: Date.now(),
},
{
id: 'gq5r3cuh74h1xzgzjmiu26t1e',
name: 'current_dept_emp',
x: 393.01599999999996,
y: 488.65600000000006,
fields: [
{
id: '8tz9jdtfrbbl4c0e7nthrj90g',
name: 'emp_no',
type: { id: 'int', name: 'int' },
primaryKey: false,
unique: false,
nullable: false,
createdAt: Date.now(),
},
{
id: 'fv7o6txqvmy2349aq3pg0hnkm',
name: 'dept_no',
type: { id: 'char', name: 'chat' },
primaryKey: false,
unique: false,
nullable: false,
characterMaximumLength: '4',
collation: 'utf8mb4_general_ci',
createdAt: Date.now(),
},
{
id: 'hneqjqobdvcumv91ymvqhv42a',
name: 'from_date',
type: { id: 'date', name: 'date' },
primaryKey: false,
unique: false,
nullable: true,
createdAt: Date.now(),
},
{
id: 'n9yj0xtw6uu0aqn2ankvniuat',
name: 'to_date',
type: { id: 'date', name: 'date' },
primaryKey: false,
unique: false,
nullable: true,
createdAt: Date.now(),
},
],
indexes: [],
color: '#b0b0b0',
isView: true,
isMaterializedView: false,
createdAt: Date.now(),
},
{
id: 'q248uisjcid20tdqfnbj5qec6',
name: 'dept_emp_latest_date',
x: 70.62399999999991,
y: 469.6479999999999,
fields: [
{
id: 'q3oiwd0p27bipsy4kg5dkxri0',
name: 'emp_no',
type: { id: 'int', name: 'int' },
primaryKey: false,
unique: false,
nullable: false,
createdAt: Date.now(),
},
{
id: 'dxhqoscu6zk87ob7sfvxo7if4',
name: 'from_date',
type: { id: 'date', name: 'date' },
primaryKey: false,
unique: false,
nullable: true,
createdAt: Date.now(),
},
{
id: 'ys76pzey5i9twf13g2g0taju7',
name: 'to_date',
type: { id: 'date', name: 'date' },
primaryKey: false,
unique: false,
nullable: true,
createdAt: Date.now(),
},
],
indexes: [],
color: '#b0b0b0',
isView: true,
isMaterializedView: false,
createdAt: Date.now(),
},
],
relationships: [
{
id: 'cciaonuhfnjdvntl9gv4lrsbk',
name: 'dept_emp_ibfk_1',
sourceTableId: 'rkc38w1yqrvhz2pmveunp6nsw',
targetTableId: '1c03hu41ko98myywerwbazeli',
sourceFieldId: 'wcgycjif09xrq0ly3txkq6ocu',
targetFieldId: '04csyx8ds9t3rh93txiqs4dm4',
sourceCardinality: 'many',
targetCardinality: 'one',
createdAt: Date.now(),
},
{
id: 'b9y9q200df95qtzdi4lkeiw2w',
name: 'dept_emp_ibfk_2',
sourceTableId: 'rkc38w1yqrvhz2pmveunp6nsw',
targetTableId: '6e70s6dhdfnve9xljbih6bo7x',
sourceFieldId: 'jdw1yrh9xf1i7927gzs9pob2p',
targetFieldId: 'gaj3scrtaz46ezfmc162ingxf',
sourceCardinality: 'many',
targetCardinality: 'one',
createdAt: Date.now(),
},
{
id: 'tt4jz3plk26zz3p8hvu3e4m27',
name: 'dept_manager_ibfk_1',
sourceTableId: 'd2xqiqlffjfsg3kgsmpck5xay',
targetTableId: '1c03hu41ko98myywerwbazeli',
sourceFieldId: 'ecx2zbzdc5o54e04aeg7tlg54',
targetFieldId: '04csyx8ds9t3rh93txiqs4dm4',
sourceCardinality: 'many',
targetCardinality: 'one',
createdAt: Date.now(),
},
{
id: 'y3p9kp0rcfle3ivoe2owx7tu3',
name: 'dept_manager_ibfk_2',
sourceTableId: 'd2xqiqlffjfsg3kgsmpck5xay',
targetTableId: '6e70s6dhdfnve9xljbih6bo7x',
sourceFieldId: 'v8plj7wq1cly03y178bysft2f',
targetFieldId: 'gaj3scrtaz46ezfmc162ingxf',
sourceCardinality: 'many',
targetCardinality: 'one',
createdAt: Date.now(),
},
{
id: 'imavnkr77bjlanlaxj3og9fh6',
name: 'salaries_ibfk_1',
sourceTableId: 'u97myqcs1osilg7x0v263qpzd',
targetTableId: '1c03hu41ko98myywerwbazeli',
sourceFieldId: 'b8c9v5vtpbnt5tjzcd3iat85f',
targetFieldId: '04csyx8ds9t3rh93txiqs4dm4',
sourceCardinality: 'many',
targetCardinality: 'one',
createdAt: Date.now(),
},
{
id: 'x4m88eqis6owszjfozerntmzt',
name: 'titles_ibfk_1',
sourceTableId: 'fa0ozznyrckx572fqztyw3w4z',
targetTableId: '1c03hu41ko98myywerwbazeli',
sourceFieldId: 'hr2gdoc0wtwvs4pfqo6m0fwc3',
targetFieldId: '04csyx8ds9t3rh93txiqs4dm4',
sourceCardinality: 'many',
targetCardinality: 'one',
createdAt: Date.now(),
},
],
},
};