add import database dialog

This commit is contained in:
Guy Ben-Aharon
2024-09-17 21:23:31 +03:00
committed by Guy Ben-Aharon
parent 970bd4d801
commit 45f4bf88a8
16 changed files with 738 additions and 128 deletions

View File

@@ -44,11 +44,19 @@ export interface ChartDBContext {
table: DBTable,
options?: { updateHistory: boolean }
) => Promise<void>;
addTables: (
tables: DBTable[],
options?: { updateHistory: boolean }
) => Promise<void>;
getTable: (id: string) => DBTable | null;
removeTable: (
id: string,
options?: { updateHistory: boolean }
) => Promise<void>;
removeTables: (
ids: string[],
options?: { updateHistory: boolean }
) => Promise<void>;
updateTable: (
id: string,
table: Partial<DBTable>,
@@ -163,7 +171,9 @@ export const chartDBContext = createContext<ChartDBContext>({
createTable: emptyFn,
getTable: emptyFn,
addTable: emptyFn,
addTables: emptyFn,
removeTable: emptyFn,
removeTables: emptyFn,
updateTable: emptyFn,
updateTablesState: emptyFn,

View File

@@ -253,21 +253,21 @@ export const ChartDBProvider: React.FC<React.PropsWithChildren> = ({
]
);
const addTable: ChartDBContext['addTable'] = useCallback(
async (table: DBTable, options = { updateHistory: true }) => {
setTables((tables) => [...tables, table]);
const addTables: ChartDBContext['addTables'] = useCallback(
async (tables: DBTable[], options = { updateHistory: true }) => {
setTables((currentTables) => [...currentTables, ...tables]);
const updatedAt = new Date();
setDiagramUpdatedAt(updatedAt);
await Promise.all([
db.updateDiagram({ id: diagramId, attributes: { updatedAt } }),
db.addTable({ diagramId, table }),
...tables.map((table) => db.addTable({ diagramId, table })),
]);
if (options.updateHistory) {
addUndoAction({
action: 'addTable',
redoData: { table },
undoData: { tableId: table.id },
action: 'addTables',
redoData: { tables },
undoData: { tableIds: tables.map((t) => t.id) },
});
resetRedoStack();
}
@@ -275,6 +275,13 @@ export const ChartDBProvider: React.FC<React.PropsWithChildren> = ({
[db, diagramId, setTables, addUndoAction, resetRedoStack]
);
const addTable: ChartDBContext['addTable'] = useCallback(
async (table: DBTable, options = { updateHistory: true }) => {
return addTables([table], options);
},
[addTables]
);
const createTable: ChartDBContext['createTable'] = useCallback(
async (attributes) => {
const table: DBTable = {
@@ -314,22 +321,27 @@ export const ChartDBProvider: React.FC<React.PropsWithChildren> = ({
[tables]
);
const removeTable: ChartDBContext['removeTable'] = useCallback(
async (id: string, options = { updateHistory: true }) => {
const table = getTable(id);
const removeTables: ChartDBContext['removeTables'] = useCallback(
async (ids, options) => {
const tables = ids.map((id) => getTable(id)).filter((t) => !!t);
const relationshipsToRemove = relationships.filter(
(relationship) =>
relationship.sourceTableId === id ||
relationship.targetTableId === id
ids.includes(relationship.sourceTableId) ||
ids.includes(relationship.targetTableId)
);
setRelationships((relationships) =>
relationships.filter(
(relationship) =>
relationship.sourceTableId !== id &&
relationship.targetTableId !== id
!relationshipsToRemove.some(
(r) => r.id === relationship.id
)
)
);
setTables((tables) => tables.filter((table) => table.id !== id));
setTables((tables) =>
tables.filter((table) => !ids.includes(table.id))
);
const updatedAt = new Date();
setDiagramUpdatedAt(updatedAt);
@@ -338,14 +350,16 @@ export const ChartDBProvider: React.FC<React.PropsWithChildren> = ({
...relationshipsToRemove.map((relationship) =>
db.deleteRelationship({ diagramId, id: relationship.id })
),
db.deleteTable({ diagramId, id }),
...ids.map((id) => db.deleteTable({ diagramId, id })),
]);
if (!!table && options.updateHistory) {
if (tables.length > 0 && options?.updateHistory) {
addUndoAction({
action: 'removeTable',
redoData: { tableId: id },
undoData: { table, relationships: relationshipsToRemove },
action: 'removeTables',
redoData: {
tableIds: ids,
},
undoData: { tables, relationships: relationshipsToRemove },
});
resetRedoStack();
}
@@ -361,6 +375,13 @@ export const ChartDBProvider: React.FC<React.PropsWithChildren> = ({
]
);
const removeTable: ChartDBContext['removeTable'] = useCallback(
async (id: string, options = { updateHistory: true }) => {
return removeTables([id], options);
},
[removeTables]
);
const updateTable: ChartDBContext['updateTable'] = useCallback(
async (
id: string,
@@ -1147,8 +1168,10 @@ export const ChartDBProvider: React.FC<React.PropsWithChildren> = ({
updateDiagramUpdatedAt,
createTable,
addTable,
addTables,
getTable,
removeTable,
removeTables,
updateTable,
updateTablesState,
updateField,

View File

@@ -23,6 +23,10 @@ export interface DialogContext {
// Create relationship dialog
openCreateRelationshipDialog: () => void;
closeCreateRelationshipDialog: () => void;
// Import database dialog
openImportDatabaseDialog: (params: { databaseType: DatabaseType }) => void;
closeImportDatabaseDialog: () => void;
}
export const dialogContext = createContext<DialogContext>({
@@ -36,4 +40,6 @@ export const dialogContext = createContext<DialogContext>({
showAlert: emptyFn,
closeCreateRelationshipDialog: emptyFn,
openCreateRelationshipDialog: emptyFn,
openImportDatabaseDialog: emptyFn,
closeImportDatabaseDialog: emptyFn,
});

View File

@@ -9,6 +9,7 @@ import {
BaseAlertDialogProps,
} from '@/dialogs/base-alert-dialog/base-alert-dialog';
import { CreateRelationshipDialog } from '@/dialogs/create-relationship-dialog/create-relationship-dialog';
import { ImportDatabaseDialog } from '@/dialogs/import-database-dialog/import-database-dialog';
export const DialogProvider: React.FC<React.PropsWithChildren> = ({
children,
@@ -21,6 +22,12 @@ export const DialogProvider: React.FC<React.PropsWithChildren> = ({
}>({ targetDatabaseType: DatabaseType.GENERIC });
const [openCreateRelationshipDialog, setOpenCreateRelationshipDialog] =
useState(false);
const [openImportDatabaseDialog, setOpenImportDatabaseDialog] =
useState(false);
const [openImportDatabaseDialogParams, setOpenImportDatabaseDialogParams] =
useState<{ databaseType: DatabaseType }>({
databaseType: DatabaseType.GENERIC,
});
const [showAlert, setShowAlert] = useState(false);
const [alertParams, setAlertParams] = useState<BaseAlertDialogProps>({
title: '',
@@ -35,6 +42,15 @@ export const DialogProvider: React.FC<React.PropsWithChildren> = ({
[setOpenExportSQLDialog]
);
const openImportDatabaseDialogHandler: DialogContext['openImportDatabaseDialog'] =
useCallback(
({ databaseType }) => {
setOpenImportDatabaseDialog(true);
setOpenImportDatabaseDialogParams({ databaseType });
},
[setOpenImportDatabaseDialog]
);
const showAlertHandler: DialogContext['showAlert'] = useCallback(
(params) => {
setAlertParams(params);
@@ -62,6 +78,9 @@ export const DialogProvider: React.FC<React.PropsWithChildren> = ({
setOpenCreateRelationshipDialog(true),
closeCreateRelationshipDialog: () =>
setOpenCreateRelationshipDialog(false),
openImportDatabaseDialog: openImportDatabaseDialogHandler,
closeImportDatabaseDialog: () =>
setOpenImportDatabaseDialog(false),
}}
>
{children}
@@ -75,6 +94,10 @@ export const DialogProvider: React.FC<React.PropsWithChildren> = ({
<CreateRelationshipDialog
dialog={{ open: openCreateRelationshipDialog }}
/>
<ImportDatabaseDialog
dialog={{ open: openImportDatabaseDialog }}
{...openImportDatabaseDialogParams}
/>
</dialogContext.Provider>
);
};

View File

@@ -17,7 +17,9 @@ export const HistoryProvider: React.FC<React.PropsWithChildren> = ({
} = useRedoUndoStack();
const {
addTable,
addTables,
removeTable,
removeTables,
updateTable,
updateDiagramName,
removeField,
@@ -42,9 +44,15 @@ export const HistoryProvider: React.FC<React.PropsWithChildren> = ({
addTable: ({ redoData: { table } }) => {
return addTable(table, { updateHistory: false });
},
addTables: ({ redoData: { tables } }) => {
return addTables(tables, { updateHistory: false });
},
removeTable: ({ redoData: { tableId } }) => {
return removeTable(tableId, { updateHistory: false });
},
removeTables: ({ redoData: { tableIds } }) => {
return removeTables(tableIds, { updateHistory: false });
},
updateTable: ({ redoData: { tableId, table } }) => {
return updateTable(tableId, table, { updateHistory: false });
},
@@ -104,7 +112,9 @@ export const HistoryProvider: React.FC<React.PropsWithChildren> = ({
}),
[
addTable,
addTables,
removeTable,
removeTables,
updateTable,
updateDiagramName,
removeField,
@@ -130,12 +140,21 @@ export const HistoryProvider: React.FC<React.PropsWithChildren> = ({
addTable: ({ undoData: { tableId } }) => {
return removeTable(tableId, { updateHistory: false });
},
addTables: ({ undoData: { tableIds } }) => {
return removeTables(tableIds, { updateHistory: false });
},
removeTable: async ({ undoData: { table, relationships } }) => {
await Promise.all([
addTable(table, { updateHistory: false }),
addRelationships(relationships, { updateHistory: false }),
]);
},
removeTables: async ({ undoData: { tables, relationships } }) => {
await Promise.all([
addTables(tables, { updateHistory: false }),
addRelationships(relationships, { updateHistory: false }),
]);
},
updateTable: ({ undoData: { tableId, table } }) => {
return updateTable(tableId, table, { updateHistory: false });
},
@@ -200,7 +219,9 @@ export const HistoryProvider: React.FC<React.PropsWithChildren> = ({
}),
[
addTable,
addTables,
removeTable,
removeTables,
updateTable,
updateDiagramName,
removeField,

View File

@@ -30,12 +30,24 @@ type RedoUndoActionAddTable = RedoUndoActionBase<
{ tableId: string }
>;
type RedoUndoActionAddTables = RedoUndoActionBase<
'addTables',
{ tables: DBTable[] },
{ tableIds: string[] }
>;
type RedoUndoActionRemoveTable = RedoUndoActionBase<
'removeTable',
{ tableId: string },
{ table: DBTable; relationships: DBRelationship[] }
>;
type RedoUndoActionRemoveTables = RedoUndoActionBase<
'removeTables',
{ tableIds: string[] },
{ tables: DBTable[]; relationships: DBRelationship[] }
>;
type RedoUndoActionUpdateTablesState = RedoUndoActionBase<
'updateTablesState',
{ tables: DBTable[] },
@@ -110,7 +122,9 @@ type RedoUndoActionRemoveRelationships = RedoUndoActionBase<
export type RedoUndoAction =
| RedoUndoActionAddTable
| RedoUndoActionAddTables
| RedoUndoActionRemoveTable
| RedoUndoActionRemoveTables
| RedoUndoActionUpdateTable
| RedoUndoActionUpdateDiagramName
| RedoUndoActionUpdateTablesState

View File

@@ -19,6 +19,7 @@ export interface BaseAlertDialogProps {
closeLabel?: string;
onAction?: () => void;
dialog?: AlertDialogProps;
onClose?: () => void;
content?: React.ReactNode;
}
@@ -30,8 +31,15 @@ export const BaseAlertDialog: React.FC<BaseAlertDialogProps> = ({
onAction,
dialog,
content,
onClose,
}) => {
const { closeAlert } = useDialog();
const closeAlertHandler = useCallback(() => {
onClose?.();
closeAlert();
}, [onClose, closeAlert]);
const alertHandler = useCallback(() => {
onAction?.();
closeAlert();
@@ -57,7 +65,7 @@ export const BaseAlertDialog: React.FC<BaseAlertDialogProps> = ({
</AlertDialogHeader>
<AlertDialogFooter>
{closeLabel && (
<AlertDialogCancel onClick={closeAlert}>
<AlertDialogCancel onClick={closeAlertHandler}>
{closeLabel}
</AlertDialogCancel>
)}

View File

@@ -1,7 +1,8 @@
import React, { useCallback, useState } from 'react';
import React, { useCallback, useEffect, useState } from 'react';
import { Button } from '@/components/button/button';
import {
DialogClose,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
@@ -23,7 +24,6 @@ import {
AvatarFallback,
AvatarImage,
} from '@/components/avatar/avatar';
import { CreateDiagramDialogStep } from '../create-diagram-dialog-step';
import { SSMSInfo } from './ssms-info/ssms-info';
import { useTranslation } from 'react-i18next';
import { Tabs, TabsList, TabsTrigger } from '@/components/tabs/tabs';
@@ -32,10 +32,15 @@ import {
databaseClientToLabelMap,
databaseTypeToClientsMap,
} from '@/lib/domain/database-clients';
import { isDatabaseMetadata } from '@/lib/data/import-metadata/metadata-types/database-metadata';
export interface ImportDatabaseStepProps {
setStep: React.Dispatch<React.SetStateAction<CreateDiagramDialogStep>>;
createNewDiagram: () => void;
const errorScriptOutputMessage =
'Invalid JSON. Please correct it or contact us at chartdb.io@gmail.com for help.';
export interface ImportDatabaseProps {
goBack?: () => void;
onImport: () => void;
onCreateEmptyDiagram?: () => void;
scriptResult: string;
setScriptResult: React.Dispatch<React.SetStateAction<string>>;
databaseType: DatabaseType;
@@ -43,24 +48,54 @@ export interface ImportDatabaseStepProps {
setDatabaseEdition: React.Dispatch<
React.SetStateAction<DatabaseEdition | undefined>
>;
errorMessage: string;
keepDialogAfterImport?: boolean;
title: string;
}
export const ImportDatabaseStep: React.FC<ImportDatabaseStepProps> = ({
export const ImportDatabase: React.FC<ImportDatabaseProps> = ({
setScriptResult,
setStep,
goBack,
scriptResult,
createNewDiagram,
onImport,
onCreateEmptyDiagram,
databaseType,
databaseEdition,
setDatabaseEdition,
errorMessage,
keepDialogAfterImport,
title,
}) => {
const databaseClients = databaseTypeToClientsMap[databaseType];
const [errorMessage, setErrorMessage] = useState('');
const [databaseClient, setDatabaseClient] = useState<
DatabaseClient | undefined
>();
const { t } = useTranslation();
useEffect(() => {
if (scriptResult.trim().length === 0) {
setErrorMessage('');
return;
}
try {
const parsedResult = JSON.parse(scriptResult);
if (isDatabaseMetadata(parsedResult)) {
setErrorMessage('');
} else {
setErrorMessage(errorScriptOutputMessage);
}
} catch (error) {
setErrorMessage(errorScriptOutputMessage);
}
}, [scriptResult]);
const handleImport = useCallback(() => {
if (errorMessage.length === 0 && scriptResult.trim().length !== 0) {
onImport();
}
}, [errorMessage.length, onImport, scriptResult]);
const handleInputChange = useCallback(
(e: React.ChangeEvent<HTMLTextAreaElement>) => {
const inputValue = e.target.value;
@@ -72,12 +107,11 @@ export const ImportDatabaseStep: React.FC<ImportDatabaseStepProps> = ({
const renderHeader = useCallback(() => {
return (
<DialogHeader>
<DialogTitle>
{t('new_diagram_dialog.import_database.title')}
</DialogTitle>
<DialogTitle>{title}</DialogTitle>
<DialogDescription className="hidden" />
</DialogHeader>
);
}, [t]);
}, [title]);
const renderContent = useCallback(() => {
return (
@@ -253,27 +287,30 @@ export const ImportDatabaseStep: React.FC<ImportDatabaseStepProps> = ({
return (
<DialogFooter className="mt-4 flex !justify-between gap-2">
<div className="flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2">
<Button
type="button"
variant="secondary"
onClick={() =>
setStep(CreateDiagramDialogStep.SELECT_DATABASE)
}
>
{t('new_diagram_dialog.back')}
</Button>
</div>
<div className="flex flex-col-reverse gap-2 sm:flex-row sm:justify-end sm:space-x-2">
<DialogClose asChild>
{goBack && (
<Button
type="button"
variant="outline"
onClick={createNewDiagram}
variant="secondary"
onClick={goBack}
>
{t('new_diagram_dialog.empty_diagram')}
{t('new_diagram_dialog.back')}
</Button>
</DialogClose>
<DialogClose asChild>
)}
</div>
<div className="flex flex-col-reverse gap-2 sm:flex-row sm:justify-end sm:space-x-2">
{onCreateEmptyDiagram && (
<DialogClose asChild>
<Button
type="button"
variant="outline"
onClick={onCreateEmptyDiagram}
>
{t('new_diagram_dialog.empty_diagram')}
</Button>
</DialogClose>
)}
{keepDialogAfterImport ? (
<Button
type="button"
variant="default"
@@ -281,15 +318,37 @@ export const ImportDatabaseStep: React.FC<ImportDatabaseStepProps> = ({
scriptResult.trim().length === 0 ||
errorMessage.length > 0
}
onClick={createNewDiagram}
onClick={handleImport}
>
{t('new_diagram_dialog.import')}
</Button>
</DialogClose>
) : (
<DialogClose asChild>
<Button
type="button"
variant="default"
disabled={
scriptResult.trim().length === 0 ||
errorMessage.length > 0
}
onClick={handleImport}
>
{t('new_diagram_dialog.import')}
</Button>
</DialogClose>
)}
</div>
</DialogFooter>
);
}, [createNewDiagram, errorMessage.length, scriptResult, setStep, t]);
}, [
handleImport,
keepDialogAfterImport,
onCreateEmptyDiagram,
errorMessage.length,
scriptResult,
goBack,
t,
]);
return (
<>

View File

@@ -8,19 +8,16 @@ import { useNavigate } from 'react-router-dom';
import { useConfig } from '@/hooks/use-config';
import {
DatabaseMetadata,
isDatabaseMetadata,
loadDatabaseMetadata,
} from '@/lib/data/import-metadata/metadata-types/database-metadata';
import { generateDiagramId } from '@/lib/utils';
import { useChartDB } from '@/hooks/use-chartdb';
import { useDialog } from '@/hooks/use-dialog';
import { DatabaseEdition } from '@/lib/domain/database-edition';
import { SelectDatabaseStep } from './select-database-step/select-database-step';
import { SelectDatabase } from './select-database/select-database';
import { CreateDiagramDialogStep } from './create-diagram-dialog-step';
import { ImportDatabaseStep } from './import-database-step/import-database-step';
const errorScriptOutputMessage =
'Invalid JSON. Please correct it or contact us at chartdb.io@gmail.com for help.';
import { ImportDatabase } from '../common/import-database/import-database';
import { useTranslation } from 'react-i18next';
export interface CreateDiagramDialogProps {
dialog: DialogProps;
@@ -30,13 +27,13 @@ export const CreateDiagramDialog: React.FC<CreateDiagramDialogProps> = ({
dialog,
}) => {
const { diagramId } = useChartDB();
const { t } = useTranslation();
const [databaseType, setDatabaseType] = useState<DatabaseType>(
DatabaseType.GENERIC
);
const { closeCreateDiagramDialog } = useDialog();
const { updateConfig } = useConfig();
const [scriptResult, setScriptResult] = useState('');
const [errorMessage, setErrorMessage] = useState('');
const [databaseEdition, setDatabaseEdition] = useState<
DatabaseEdition | undefined
>();
@@ -60,57 +57,23 @@ export const CreateDiagramDialog: React.FC<CreateDiagramDialogProps> = ({
setDatabaseType(DatabaseType.GENERIC);
setDatabaseEdition(undefined);
setScriptResult('');
setErrorMessage('');
}, [dialog.open]);
useEffect(() => {
if (scriptResult.trim().length === 0) {
setErrorMessage('');
return;
}
try {
const parsedResult = JSON.parse(scriptResult);
if (isDatabaseMetadata(parsedResult)) {
setErrorMessage('');
} else {
setErrorMessage(errorScriptOutputMessage);
}
} catch (error) {
setErrorMessage(errorScriptOutputMessage);
}
}, [scriptResult]);
const hasExistingDiagram = (diagramId ?? '').trim().length !== 0;
const createNewDiagram = useCallback(async () => {
let diagram: Diagram = {
id: generateDiagramId(),
name: `Diagram ${diagramNumber}`,
databaseType: databaseType ?? DatabaseType.GENERIC,
const importNewDiagram = useCallback(async () => {
const databaseMetadata: DatabaseMetadata =
loadDatabaseMetadata(scriptResult);
const diagram = loadFromDatabaseMetadata({
databaseType,
databaseMetadata,
diagramNumber,
databaseEdition:
databaseEdition?.trim().length === 0
? undefined
: databaseEdition,
createdAt: new Date(),
updatedAt: new Date(),
};
if (errorMessage.length === 0 && scriptResult.trim().length !== 0) {
const databaseMetadata: DatabaseMetadata =
loadDatabaseMetadata(scriptResult);
diagram = loadFromDatabaseMetadata({
databaseType,
databaseMetadata,
diagramNumber,
databaseEdition:
databaseEdition?.trim().length === 0
? undefined
: databaseEdition,
});
}
});
await addDiagram({ diagram });
await updateConfig({ defaultDiagramId: diagram.id });
@@ -125,7 +88,33 @@ export const CreateDiagramDialog: React.FC<CreateDiagramDialogProps> = ({
updateConfig,
scriptResult,
diagramNumber,
errorMessage,
]);
const createEmptyDiagram = useCallback(async () => {
const diagram: Diagram = {
id: generateDiagramId(),
name: `Diagram ${diagramNumber}`,
databaseType: databaseType ?? DatabaseType.GENERIC,
databaseEdition:
databaseEdition?.trim().length === 0
? undefined
: databaseEdition,
createdAt: new Date(),
updatedAt: new Date(),
};
await addDiagram({ diagram });
await updateConfig({ defaultDiagramId: diagram.id });
closeCreateDiagramDialog();
navigate(`/diagrams/${diagram.id}`);
}, [
databaseType,
addDiagram,
databaseEdition,
closeCreateDiagramDialog,
navigate,
updateConfig,
diagramNumber,
]);
return (
@@ -146,23 +135,28 @@ export const CreateDiagramDialog: React.FC<CreateDiagramDialogProps> = ({
showClose={hasExistingDiagram}
>
{step === CreateDiagramDialogStep.SELECT_DATABASE ? (
<SelectDatabaseStep
createNewDiagram={createNewDiagram}
<SelectDatabase
createNewDiagram={createEmptyDiagram}
databaseType={databaseType}
hasExistingDiagram={hasExistingDiagram}
setDatabaseType={setDatabaseType}
setStep={setStep}
onContinue={() =>
setStep(CreateDiagramDialogStep.IMPORT_DATABASE)
}
/>
) : (
<ImportDatabaseStep
createNewDiagram={createNewDiagram}
<ImportDatabase
onImport={importNewDiagram}
onCreateEmptyDiagram={createEmptyDiagram}
databaseEdition={databaseEdition}
databaseType={databaseType}
errorMessage={errorMessage}
scriptResult={scriptResult}
setDatabaseEdition={setDatabaseEdition}
setStep={setStep}
goBack={() =>
setStep(CreateDiagramDialogStep.SELECT_DATABASE)
}
setScriptResult={setScriptResult}
title={t('new_diagram_dialog.import_database.title')}
/>
)}
</DialogContent>

View File

@@ -13,20 +13,19 @@ import { DatabaseType } from '@/lib/domain/database-type';
import { databaseTypeToLabelMap, getDatabaseLogo } from '@/lib/databases';
import { Link } from '@/components/link/link';
import { LayoutGrid } from 'lucide-react';
import { CreateDiagramDialogStep } from '../create-diagram-dialog-step';
import { useTranslation } from 'react-i18next';
import { useTheme } from '@/hooks/use-theme';
export interface SelectDatabaseStepProps {
setStep: React.Dispatch<React.SetStateAction<CreateDiagramDialogStep>>;
export interface SelectDatabaseProps {
onContinue: () => void;
databaseType: DatabaseType;
setDatabaseType: React.Dispatch<React.SetStateAction<DatabaseType>>;
hasExistingDiagram: boolean;
createNewDiagram: () => void;
}
export const SelectDatabaseStep: React.FC<SelectDatabaseStepProps> = ({
setStep,
export const SelectDatabase: React.FC<SelectDatabaseProps> = ({
onContinue,
databaseType,
setDatabaseType,
hasExistingDiagram,
@@ -98,7 +97,7 @@ export const SelectDatabaseStep: React.FC<SelectDatabaseStepProps> = ({
setDatabaseType(DatabaseType.GENERIC);
} else {
setDatabaseType(value);
setStep(CreateDiagramDialogStep.IMPORT_DATABASE);
onContinue();
}
}}
type="single"
@@ -118,7 +117,7 @@ export const SelectDatabaseStep: React.FC<SelectDatabaseStepProps> = ({
renderDatabaseOption,
renderExamplesOption,
setDatabaseType,
setStep,
onContinue,
]);
const renderFooter = useCallback(() => {
@@ -145,16 +144,14 @@ export const SelectDatabaseStep: React.FC<SelectDatabaseStepProps> = ({
type="button"
variant="default"
disabled={databaseType === DatabaseType.GENERIC}
onClick={() =>
setStep(CreateDiagramDialogStep.IMPORT_DATABASE)
}
onClick={onContinue}
>
{t('new_diagram_dialog.continue')}
</Button>
</div>
</DialogFooter>
);
}, [createNewDiagram, databaseType, hasExistingDiagram, setStep, t]);
}, [createNewDiagram, databaseType, hasExistingDiagram, onContinue, t]);
return (
<>

View File

@@ -0,0 +1,346 @@
import { Dialog, DialogContent } from '@/components/dialog/dialog';
import { useDialog } from '@/hooks/use-dialog';
import { DatabaseType } from '@/lib/domain/database-type';
import { DialogProps } from '@radix-ui/react-dialog';
import React, { useCallback, useEffect, useState } from 'react';
import { ImportDatabase } from '../common/import-database/import-database';
import { DatabaseEdition } from '@/lib/domain/database-edition';
import {
DatabaseMetadata,
loadDatabaseMetadata,
} from '@/lib/data/import-metadata/metadata-types/database-metadata';
import { loadFromDatabaseMetadata } from '@/lib/domain/diagram';
import { useChartDB } from '@/hooks/use-chartdb';
import { useRedoUndoStack } from '@/hooks/use-redo-undo-stack';
import { Trans, useTranslation } from 'react-i18next';
import { useReactFlow } from '@xyflow/react';
export interface ImportDatabaseDialogProps {
dialog: DialogProps;
databaseType: DatabaseType;
}
export const ImportDatabaseDialog: React.FC<ImportDatabaseDialogProps> = ({
dialog,
databaseType,
}) => {
const { closeImportDatabaseDialog, showAlert } = useDialog();
const {
tables,
relationships,
removeTables,
removeRelationships,
addTables,
addRelationships,
diagramName,
} = useChartDB();
const [scriptResult, setScriptResult] = useState('');
const { resetRedoStack, resetUndoStack } = useRedoUndoStack();
const { setNodes } = useReactFlow();
const { t } = useTranslation();
const [databaseEdition, setDatabaseEdition] = useState<
DatabaseEdition | undefined
>();
useEffect(() => {
if (!dialog.open) return;
setDatabaseEdition(undefined);
setScriptResult('');
}, [dialog.open]);
const importDatabase = useCallback(async () => {
const databaseMetadata: DatabaseMetadata =
loadDatabaseMetadata(scriptResult);
const diagram = loadFromDatabaseMetadata({
databaseType,
databaseMetadata,
databaseEdition:
databaseEdition?.trim().length === 0
? undefined
: databaseEdition,
});
const tableIdsToRemove = tables
.filter((table) =>
diagram.tables?.some(
(t) => t.name === table.name && t.schema === table.schema
)
)
.map((table) => table.id);
const relationshipIdsToRemove = relationships
.filter((relationship) => {
const sourceTable = tables.find(
(table) => table.id === relationship.sourceTableId
);
const targetTable = tables.find(
(table) => table.id === relationship.targetTableId
);
if (!sourceTable || !targetTable) return true; // should not happen
const sourceField = sourceTable.fields.find(
(field) => field.id === relationship.sourceFieldId
);
const targetField = targetTable.fields.find(
(field) => field.id === relationship.targetFieldId
);
if (!sourceField || !targetField) return true; // should not happen
const replacementSourceTable = diagram.tables?.find(
(table) =>
table.name === sourceTable.name &&
table.schema === sourceTable.schema
);
const replacementTargetTable = diagram.tables?.find(
(table) =>
table.name === targetTable.name &&
table.schema === targetTable.schema
);
// if the source or target field of the relationship is not in the new table, remove the relationship
if (
(replacementSourceTable &&
!replacementSourceTable.fields.some(
(field) => field.name === sourceField.name
)) ||
(replacementTargetTable &&
!replacementTargetTable.fields.some(
(field) => field.name === targetField.name
))
) {
return true;
}
return diagram.relationships?.some((r) => {
const sourceNewTable = diagram.tables?.find(
(table) => table.id === r.sourceTableId
);
const targetNewTable = diagram.tables?.find(
(table) => table.id === r.targetTableId
);
const sourceNewField = sourceNewTable?.fields.find(
(field) => field.id === r.sourceFieldId
);
const targetNewField = targetNewTable?.fields.find(
(field) => field.id === r.targetFieldId
);
return (
sourceField.name === sourceNewField?.name &&
sourceTable.name === sourceNewTable?.name &&
sourceTable.schema === sourceNewTable?.schema &&
targetField.name === targetNewField?.name &&
targetTable.name === targetNewTable?.name &&
targetTable.schema === targetNewTable?.schema
);
});
})
.map((relationship) => relationship.id);
const newRelationshipsNumber = diagram.relationships?.filter(
(relationship) => {
const newSourceTable = diagram.tables?.find(
(table) => table.id === relationship.sourceTableId
);
const newTargetTable = diagram.tables?.find(
(table) => table.id === relationship.targetTableId
);
const newSourceField = newSourceTable?.fields.find(
(field) => field.id === relationship.sourceFieldId
);
const newTargetField = newTargetTable?.fields.find(
(field) => field.id === relationship.targetFieldId
);
return !relationships.some((r) => {
const sourceTable = tables.find(
(table) => table.id === r.sourceTableId
);
const targetTable = tables.find(
(table) => table.id === r.targetTableId
);
const sourceField = sourceTable?.fields.find(
(field) => field.id === r.sourceFieldId
);
const targetField = targetTable?.fields.find(
(field) => field.id === r.targetFieldId
);
return (
sourceField?.name === newSourceField?.name &&
sourceTable?.name === newSourceTable?.name &&
sourceTable?.schema === newSourceTable?.schema &&
targetField?.name === newTargetField?.name &&
targetTable?.name === newTargetTable?.name &&
targetTable?.schema === newTargetTable?.schema
);
});
}
).length;
const newTablesNumber = diagram.tables?.filter(
(table) =>
!tables.some(
(t) => t.name === table.name && t.schema === table.schema
)
).length;
const shouldRemove = new Promise<boolean>((resolve) => {
if (
tableIdsToRemove.length === 0 &&
relationshipIdsToRemove.length === 0 &&
newTablesNumber === 0 &&
newRelationshipsNumber === 0
) {
resolve(true);
return;
}
const content = (
<>
<div className="!mb-2">
{t(
'import_database_dialog.override_alert.content.alert'
)}
</div>
{(newTablesNumber ?? 0 > 0) ? (
<div className="!m-0 text-blue-500">
<Trans
i18nKey="import_database_dialog.override_alert.content.new_tables"
values={{
newTablesNumber,
}}
components={{
bold: <span className="font-bold" />,
}}
/>
</div>
) : null}
{(newRelationshipsNumber ?? 0 > 0) ? (
<div className="!m-0 text-blue-500">
<Trans
i18nKey="import_database_dialog.override_alert.content.new_relationships"
values={{
newRelationshipsNumber,
}}
components={{
bold: <span className="font-bold" />,
}}
/>
</div>
) : null}
{tableIdsToRemove.length > 0 && (
<div className="!m-0 text-red-500">
<Trans
i18nKey="import_database_dialog.override_alert.content.tables_override"
values={{
tablesOverrideNumber:
tableIdsToRemove.length,
}}
components={{
bold: <span className="font-bold" />,
}}
/>
</div>
)}
<div className="!mt-2">
{t(
'import_database_dialog.override_alert.content.proceed'
)}
</div>
</>
);
showAlert({
title: t('import_database_dialog.override_alert.title'),
content,
actionLabel: t('import_database_dialog.override_alert.import'),
closeLabel: t('import_database_dialog.override_alert.cancel'),
onAction: () => resolve(true),
onClose: () => resolve(false),
});
});
if (!(await shouldRemove)) return;
await Promise.all([
removeTables(tableIdsToRemove, { updateHistory: false }),
removeRelationships(relationshipIdsToRemove, {
updateHistory: false,
}),
]);
await Promise.all([
addTables(diagram.tables ?? [], { updateHistory: false }),
addRelationships(diagram.relationships ?? [], {
updateHistory: false,
}),
]);
setNodes((nodes) =>
nodes.map((node) => ({
...node,
selected:
diagram.tables?.some((table) => table.id === node.id) ??
false,
}))
);
resetRedoStack();
resetUndoStack();
closeImportDatabaseDialog();
}, [
databaseEdition,
databaseType,
scriptResult,
tables,
addRelationships,
addTables,
closeImportDatabaseDialog,
relationships,
removeRelationships,
removeTables,
resetRedoStack,
resetUndoStack,
showAlert,
setNodes,
t,
]);
return (
<Dialog
{...dialog}
onOpenChange={(open) => {
if (!open) {
closeImportDatabaseDialog();
}
}}
>
<DialogContent
className="flex max-h-[90vh] w-[90vw] flex-col overflow-y-auto md:overflow-visible xl:min-w-[45vw]"
showClose
>
<ImportDatabase
databaseType={databaseType}
databaseEdition={databaseEdition}
setDatabaseEdition={setDatabaseEdition}
onImport={importDatabase}
scriptResult={scriptResult}
setScriptResult={setScriptResult}
keepDialogAfterImport
title={t('import_database_dialog.title', { diagramName })}
/>
</DialogContent>
</Dialog>
);
};

View File

@@ -8,6 +8,7 @@ export const en = {
new: 'New',
open: 'Open',
save: 'Save',
import_database: 'Import Database',
export_sql: 'Export SQL',
export_as: 'Export as',
delete_diagram: 'Delete Diagram',
@@ -240,6 +241,25 @@ export const en = {
cancel: 'Cancel',
},
import_database_dialog: {
title: 'Import to Current Diagram',
override_alert: {
title: 'Import Database',
content: {
alert: 'Importing this diagram will affect existing tables and relationships.',
new_tables:
'<bold>{{newTablesNumber}}</bold> new tables will be added.',
new_relationships:
'<bold>{{newRelationshipsNumber}}</bold> new relationships will be created.',
tables_override:
'<bold>{{tablesOverrideNumber}}</bold> tables will be overwritten.',
proceed: 'Do you want to proceed?',
},
import: 'Import',
cancel: 'Cancel',
},
},
relationship_type: {
one_to_one: 'One to One',
one_to_many: 'One to Many',

View File

@@ -8,6 +8,7 @@ export const es: LanguageTranslation = {
new: 'Nuevo',
open: 'Abrir',
save: 'Guardar',
import_database: 'Importar Base de Datos',
export_sql: 'Exportar SQL',
export_as: 'Exportar como',
delete_diagram: 'Eliminar Diagrama',
@@ -241,6 +242,25 @@ export const es: LanguageTranslation = {
title: 'Crear Relación',
},
import_database_dialog: {
title: 'Importar a Diagrama Actual',
override_alert: {
title: 'Importar Base de Datos',
content: {
alert: 'Importar este diagrama afectará las tablas y relaciones existentes.',
new_tables:
'<bold>{{newTablesNumber}}</bold> nuevas tablas se agregarán.',
new_relationships:
'<bold>{{newRelationshipsNumber}}</bold> nuevas relaciones se crearán.',
tables_override:
'<bold>{{tablesOverrideNumber}}</bold> tablas se sobrescribirán.',
proceed: '¿Deseas continuar?',
},
import: 'Importar',
cancel: 'Cancelar',
},
},
relationship_type: {
one_to_one: 'Uno a Uno',
one_to_many: 'Uno a Muchos',

View File

@@ -30,7 +30,7 @@ export const loadFromDatabaseMetadata = ({
}: {
databaseType: DatabaseType;
databaseMetadata: DatabaseMetadata;
diagramNumber: number;
diagramNumber?: number;
databaseEdition?: DatabaseEdition;
}): Diagram => {
const {
@@ -71,9 +71,11 @@ export const loadFromDatabaseMetadata = ({
return {
id: generateDiagramId(),
name:
`${databaseMetadata.database_name}-db` ||
`Diagram ${diagramNumber}`,
name: databaseMetadata.database_name
? `${databaseMetadata.database_name}-db`
: diagramNumber
? `Diagram ${diagramNumber}`
: 'New Diagram',
databaseType: databaseType ?? DatabaseType.GENERIC,
databaseEdition,
tables: sortedTables,

View File

@@ -62,6 +62,7 @@ export const TopNavbar: React.FC<TopNavbarProps> = () => {
openCreateDiagramDialog,
openOpenDiagramDialog,
openExportSQLDialog,
openImportDatabaseDialog,
showAlert,
} = useDialog();
const { setTheme, theme } = useTheme();
@@ -352,6 +353,72 @@ export const TopNavbar: React.FC<TopNavbarProps> = () => {
</MenubarShortcut>
</MenubarItem>
<MenubarSeparator />
<MenubarSub>
<MenubarSubTrigger>
{t('menu.file.import_database')}
</MenubarSubTrigger>
<MenubarSubContent>
<MenubarItem
onClick={() =>
openImportDatabaseDialog({
databaseType:
DatabaseType.POSTGRESQL,
})
}
>
{
databaseTypeToLabelMap[
'postgresql'
]
}
</MenubarItem>
<MenubarItem
onClick={() =>
openImportDatabaseDialog({
databaseType:
DatabaseType.MYSQL,
})
}
>
{databaseTypeToLabelMap['mysql']}
</MenubarItem>
<MenubarItem
onClick={() =>
openImportDatabaseDialog({
databaseType:
DatabaseType.SQL_SERVER,
})
}
>
{
databaseTypeToLabelMap[
'sql_server'
]
}
</MenubarItem>
<MenubarItem
onClick={() =>
openImportDatabaseDialog({
databaseType:
DatabaseType.MARIADB,
})
}
>
{databaseTypeToLabelMap['mariadb']}
</MenubarItem>
<MenubarItem
onClick={() =>
openImportDatabaseDialog({
databaseType:
DatabaseType.SQLITE,
})
}
>
{databaseTypeToLabelMap['sqlite']}
</MenubarItem>
</MenubarSubContent>
</MenubarSub>
<MenubarSeparator />
<MenubarSub>
<MenubarSubTrigger>
{t('menu.file.export_sql')}