Files
chartdb/src/dialogs/export-sql-dialog/export-sql-dialog.tsx
Guy Ben-Aharon 4f1d3295c0 fix(filters): refactor diagram filters - remove schema filter (#832)
* refactor(filters): refactor diagram filters

* replace old filters

* fix storage

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix
2025-08-07 14:55:35 +03:00

283 lines
10 KiB
TypeScript

import { Button } from '@/components/button/button';
import { CodeSnippet } from '@/components/code-snippet/code-snippet';
import {
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogInternalContent,
DialogTitle,
} from '@/components/dialog/dialog';
import { Label } from '@/components/label/label';
import { Spinner } from '@/components/spinner/spinner';
import { useChartDB } from '@/hooks/use-chartdb';
import { useDialog } from '@/hooks/use-dialog';
import {
exportBaseSQL,
exportSQL,
} from '@/lib/data/export-metadata/export-sql-script';
import { databaseTypeToLabelMap } from '@/lib/databases';
import { DatabaseType } from '@/lib/domain/database-type';
import { Annoyed, Sparkles } from 'lucide-react';
import React, { useCallback, useEffect, useRef } from 'react';
import { Trans, useTranslation } from 'react-i18next';
import type { BaseDialogProps } from '../common/base-dialog-props';
import type { Diagram } from '@/lib/domain/diagram';
import { useDiagramFilter } from '@/context/diagram-filter-context/use-diagram-filter';
import {
filterDependency,
filterRelationship,
filterTable,
} from '@/lib/domain/diagram-filter/filter';
import { defaultSchemas } from '@/lib/data/default-schemas';
export interface ExportSQLDialogProps extends BaseDialogProps {
targetDatabaseType: DatabaseType;
}
export const ExportSQLDialog: React.FC<ExportSQLDialogProps> = ({
dialog,
targetDatabaseType,
}) => {
const { closeExportSQLDialog } = useDialog();
const { currentDiagram } = useChartDB();
const { filter } = useDiagramFilter();
const { t } = useTranslation();
const [script, setScript] = React.useState<string>();
const [error, setError] = React.useState<boolean>(false);
const [isScriptLoading, setIsScriptLoading] =
React.useState<boolean>(false);
const abortControllerRef = useRef<AbortController | null>(null);
const exportSQLScript = useCallback(async () => {
const filteredDiagram: Diagram = {
...currentDiagram,
tables: currentDiagram.tables?.filter((table) =>
filterTable({
table: {
id: table.id,
schema: table.schema,
},
filter,
options: {
defaultSchema: defaultSchemas[targetDatabaseType],
},
})
),
relationships: currentDiagram.relationships?.filter((rel) => {
const sourceTable = currentDiagram.tables?.find(
(t) => t.id === rel.sourceTableId
);
const targetTable = currentDiagram.tables?.find(
(t) => t.id === rel.targetTableId
);
return (
sourceTable &&
targetTable &&
filterRelationship({
tableA: {
id: sourceTable.id,
schema: sourceTable.schema,
},
tableB: {
id: targetTable.id,
schema: targetTable.schema,
},
filter,
options: {
defaultSchema: defaultSchemas[targetDatabaseType],
},
})
);
}),
dependencies: currentDiagram.dependencies?.filter((dep) => {
const table = currentDiagram.tables?.find(
(t) => t.id === dep.tableId
);
const dependentTable = currentDiagram.tables?.find(
(t) => t.id === dep.dependentTableId
);
return (
table &&
dependentTable &&
filterDependency({
tableA: {
id: table.id,
schema: table.schema,
},
tableB: {
id: dependentTable.id,
schema: dependentTable.schema,
},
filter,
options: {
defaultSchema: defaultSchemas[targetDatabaseType],
},
})
);
}),
};
if (targetDatabaseType === DatabaseType.GENERIC) {
return Promise.resolve(
exportBaseSQL({
diagram: filteredDiagram,
targetDatabaseType,
})
);
} else {
return exportSQL(filteredDiagram, targetDatabaseType, {
stream: true,
onResultStream: (text) =>
setScript((prev) => (prev ? prev + text : text)),
signal: abortControllerRef.current?.signal,
});
}
}, [targetDatabaseType, currentDiagram, filter]);
useEffect(() => {
if (!dialog.open) {
abortControllerRef.current?.abort();
return;
}
abortControllerRef.current = new AbortController();
setScript(undefined);
setError(false);
const fetchScript = async () => {
try {
setIsScriptLoading(true);
const script = await exportSQLScript();
setScript(script);
setIsScriptLoading(false);
} catch {
setError(true);
}
};
fetchScript();
return () => {
abortControllerRef.current?.abort();
};
}, [dialog.open, setScript, exportSQLScript, setError]);
const renderError = useCallback(
() => (
<div className="flex flex-col gap-2">
<div className="flex flex-col items-center justify-center gap-1 text-sm">
<Annoyed className="size-10" />
<Label className="text-sm">
<Trans
i18nKey="export_sql_dialog.error.message" // optional -> fallbacks to defaults if not provided
components={[
<a
key={0}
href="mailto:support@chartdb.io"
target="_blank"
className="text-pink-600 hover:underline"
rel="noreferrer"
/>,
]}
/>
</Label>
<div>
<Trans
i18nKey="export_sql_dialog.error.description" // optional -> fallbacks to defaults if not provided
components={[
<a
key={0}
href="https://github.com/chartdb/chartdb"
target="_blank"
rel="noreferrer"
className="text-pink-600 hover:underline"
/>,
]}
/>
</div>
</div>
</div>
),
[]
);
const renderLoader = useCallback(
() => (
<div className="flex flex-col gap-2">
<Spinner />
<div className="flex items-center justify-center gap-1">
<Sparkles className="h-5" />
<Label className="text-lg">
{t('export_sql_dialog.loading.text', {
databaseType:
databaseTypeToLabelMap[targetDatabaseType],
})}
</Label>
</div>
<div className="flex items-center justify-center gap-1">
<Label className="text-sm">
{t('export_sql_dialog.loading.description')}
</Label>
</div>
</div>
),
[targetDatabaseType, t]
);
return (
<Dialog
{...dialog}
onOpenChange={(open) => {
if (!open) {
closeExportSQLDialog();
}
}}
>
<DialogContent
className="flex max-h-screen flex-col overflow-y-auto xl:min-w-[75vw]"
showClose
>
<DialogHeader>
<DialogTitle>{t('export_sql_dialog.title')}</DialogTitle>
<DialogDescription>
{t('export_sql_dialog.description', {
databaseType:
targetDatabaseType === DatabaseType.GENERIC
? 'SQL'
: databaseTypeToLabelMap[
targetDatabaseType
],
})}
</DialogDescription>
</DialogHeader>
<DialogInternalContent>
<div className="flex flex-1 items-center justify-center">
{error ? (
renderError()
) : script === undefined ? (
renderLoader()
) : script.length === 0 ? (
renderError()
) : (
<CodeSnippet
className="h-96 w-full"
code={script!}
autoScroll={true}
isComplete={!isScriptLoading}
/>
)}
</div>
</DialogInternalContent>
<DialogFooter className="flex !justify-between gap-2">
<div />
<DialogClose asChild>
<Button type="button" variant="secondary">
{t('export_sql_dialog.close')}
</Button>
</DialogClose>
</DialogFooter>
</DialogContent>
</Dialog>
);
};