mirror of
https://github.com/chartdb/chartdb.git
synced 2026-01-05 03:09:55 -06:00
BIN
src/assets/templates/employeedb-dark.png
Normal file
BIN
src/assets/templates/employeedb-dark.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 284 KiB |
BIN
src/assets/templates/employeedb.png
Normal file
BIN
src/assets/templates/employeedb.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 304 KiB |
115
src/components/breadcrumb/breadcrumb.tsx
Normal file
115
src/components/breadcrumb/breadcrumb.tsx
Normal 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,
|
||||
};
|
||||
@@ -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}
|
||||
|
||||
45
src/components/list-menu/list-menu.tsx
Normal file
45
src/components/list-menu/list-menu.tsx
Normal 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';
|
||||
@@ -72,6 +72,7 @@ export interface ChartDBContext {
|
||||
dependencies: DBDependency[];
|
||||
currentDiagram: Diagram;
|
||||
events: EventEmitter<ChartDBEvent>;
|
||||
readonly?: boolean;
|
||||
|
||||
filteredSchemas?: string[];
|
||||
filterSchemas: (schemaIds: string[]) => void;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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={{
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
|
||||
251
src/pages/template-page/template-page.tsx
Normal file
251
src/pages/template-page/template-page.tsx
Normal 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>
|
||||
);
|
||||
82
src/pages/templates-page/template-card/template-card.tsx
Normal file
82
src/pages/templates-page/template-card/template-card.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
149
src/pages/templates-page/templates-page.tsx
Normal file
149
src/pages/templates-page/templates-page.tsx
Normal 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 app’s 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>
|
||||
);
|
||||
@@ -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() {
|
||||
|
||||
88
src/templates-data/template-utils.ts
Normal file
88
src/templates-data/template-utils.ts
Normal 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,
|
||||
};
|
||||
};
|
||||
17
src/templates-data/templates-data.ts
Normal file
17
src/templates-data/templates-data.ts
Normal 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];
|
||||
618
src/templates-data/templates/employee-db.ts
Normal file
618
src/templates-data/templates/employee-db.ts
Normal 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(),
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user