Add dependency to views based on view definition (#236)

* init dependency

* add dependency api support

* add 'dependency-edge' type

* add dependencies to sidebar

* ast in multi database types

* double click on dependency to view it + fix vizualize adding dependency

* set view to always be the dependent table on creating dependency

* set dependency edge color

* find the shortest path on dependencies

* add handle id diff

* fix for parsing + add dependencies to diagaram

* hide or show dependencies from view menu

* fix for MATERIALIZED VIEWS

* rebase + fix

* fix build

---------

Co-authored-by: Guy Ben-Aharon <baguy3@gmail.com>
This commit is contained in:
Jonathan Fishner
2024-10-06 00:00:10 +03:00
committed by GitHub
parent 9219b821ab
commit ea93e394af
38 changed files with 8222 additions and 9308 deletions

15140
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -37,8 +37,8 @@
"@radix-ui/react-toggle-group": "^1.1.0",
"@radix-ui/react-tooltip": "^1.1.2",
"@uidotdev/usehooks": "^2.4.1",
"@xyflow/react": "^12.0.4",
"ahooks": "^3.8.1",
"@xyflow/react": "^12.3.1",
"ai": "^3.3.14",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
@@ -49,6 +49,7 @@
"i18next": "^23.14.0",
"lucide-react": "^0.441.0",
"nanoid": "^5.0.7",
"node-sql-parser": "^5.3.2",
"react": "^18.3.1",
"react-code-blocks": "^0.1.6",
"react-dom": "^18.3.1",

View File

@@ -8,6 +8,7 @@ import type { DBRelationship } from '@/lib/domain/db-relationship';
import type { Diagram } from '@/lib/domain/diagram';
import type { DatabaseEdition } from '@/lib/domain/database-edition';
import type { DBSchema } from '@/lib/domain/db-schema';
import type { DBDependency } from '@/lib/domain/db-dependency';
import { EventEmitter } from 'ahooks/lib/useEventEmitter';
export type ChartDBEventType =
@@ -68,6 +69,7 @@ export interface ChartDBContext {
tables: DBTable[];
schemas: DBSchema[];
relationships: DBRelationship[];
dependencies: DBDependency[];
currentDiagram: Diagram;
events: EventEmitter<ChartDBEvent>;
@@ -189,6 +191,34 @@ export interface ChartDBContext {
relationship: Partial<DBRelationship>,
options?: { updateHistory: boolean }
) => Promise<void>;
// Dependency operations
createDependency: (params: {
tableId: string;
dependentTableId: string;
}) => Promise<DBDependency>;
addDependency: (
dependency: DBDependency,
options?: { updateHistory: boolean }
) => Promise<void>;
addDependencies: (
dependencies: DBDependency[],
options?: { updateHistory: boolean }
) => Promise<void>;
getDependency: (id: string) => DBDependency | null;
removeDependency: (
id: string,
options?: { updateHistory: boolean }
) => Promise<void>;
removeDependencies: (
ids: string[],
options?: { updateHistory: boolean }
) => Promise<void>;
updateDependency: (
id: string,
dependency: Partial<DBDependency>,
options?: { updateHistory: boolean }
) => Promise<void>;
}
export const chartDBContext = createContext<ChartDBContext>({
@@ -197,6 +227,7 @@ export const chartDBContext = createContext<ChartDBContext>({
diagramId: '',
tables: [],
relationships: [],
dependencies: [],
schemas: [],
filteredSchemas: [],
filterSchemas: emptyFn,
@@ -253,4 +284,13 @@ export const chartDBContext = createContext<ChartDBContext>({
updateRelationship: emptyFn,
removeRelationships: emptyFn,
addRelationships: emptyFn,
// Dependency operations
createDependency: emptyFn,
addDependency: emptyFn,
getDependency: emptyFn,
removeDependency: emptyFn,
removeDependencies: emptyFn,
addDependencies: emptyFn,
updateDependency: emptyFn,
});

View File

@@ -19,6 +19,7 @@ import { schemaNameToSchemaId } from '@/lib/domain/db-schema';
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';
export const ChartDBProvider: React.FC<React.PropsWithChildren> = ({
children,
@@ -42,6 +43,7 @@ export const ChartDBProvider: React.FC<React.PropsWithChildren> = ({
>();
const [tables, setTables] = useState<DBTable[]>([]);
const [relationships, setRelationships] = useState<DBRelationship[]>([]);
const [dependencies, setDependencies] = useState<DBDependency[]>([]);
const defaultSchemaName = defaultSchemas[databaseType];
@@ -118,6 +120,7 @@ export const ChartDBProvider: React.FC<React.PropsWithChildren> = ({
databaseEdition,
tables,
relationships,
dependencies,
}),
[
diagramId,
@@ -126,6 +129,7 @@ export const ChartDBProvider: React.FC<React.PropsWithChildren> = ({
databaseEdition,
tables,
relationships,
dependencies,
diagramCreatedAt,
diagramUpdatedAt,
]
@@ -136,6 +140,7 @@ export const ChartDBProvider: React.FC<React.PropsWithChildren> = ({
const updatedAt = new Date();
setTables([]);
setRelationships([]);
setDependencies([]);
setDiagramUpdatedAt(updatedAt);
resetRedoStack();
@@ -145,6 +150,7 @@ export const ChartDBProvider: React.FC<React.PropsWithChildren> = ({
db.updateDiagram({ id: diagramId, attributes: { updatedAt } }),
db.deleteDiagramTables(diagramId),
db.deleteDiagramRelationships(diagramId),
db.deleteDiagramDependencies(diagramId),
]);
}, [db, diagramId, resetRedoStack, resetUndoStack]);
@@ -156,6 +162,7 @@ export const ChartDBProvider: React.FC<React.PropsWithChildren> = ({
setDatabaseEdition(undefined);
setTables([]);
setRelationships([]);
setDependencies([]);
resetRedoStack();
resetUndoStack();
@@ -164,6 +171,7 @@ export const ChartDBProvider: React.FC<React.PropsWithChildren> = ({
db.deleteDiagramTables(diagramId),
db.deleteDiagramRelationships(diagramId),
db.deleteDiagram(diagramId),
db.deleteDiagramDependencies(diagramId),
]);
if (config?.defaultDiagramId === diagramId) {
@@ -343,6 +351,12 @@ export const ChartDBProvider: React.FC<React.PropsWithChildren> = ({
ids.includes(relationship.targetTableId)
);
const dependenciesToRemove = dependencies.filter(
(dependency) =>
ids.includes(dependency.tableId) ||
ids.includes(dependency.dependentTableId)
);
setRelationships((relationships) =>
relationships.filter(
(relationship) =>
@@ -352,6 +366,15 @@ export const ChartDBProvider: React.FC<React.PropsWithChildren> = ({
)
);
setDependencies((dependencies) =>
dependencies.filter(
(dependency) =>
!dependenciesToRemove.some(
(d) => d.id === dependency.id
)
)
);
setTables((tables) =>
tables.filter((table) => !ids.includes(table.id))
);
@@ -365,6 +388,9 @@ export const ChartDBProvider: React.FC<React.PropsWithChildren> = ({
...relationshipsToRemove.map((relationship) =>
db.deleteRelationship({ diagramId, id: relationship.id })
),
...dependenciesToRemove.map((dependency) =>
db.deleteDependency({ diagramId, id: dependency.id })
),
...ids.map((id) => db.deleteTable({ diagramId, id })),
]);
@@ -374,7 +400,11 @@ export const ChartDBProvider: React.FC<React.PropsWithChildren> = ({
redoData: {
tableIds: ids,
},
undoData: { tables, relationships: relationshipsToRemove },
undoData: {
tables,
relationships: relationshipsToRemove,
dependencies: dependenciesToRemove,
},
});
resetRedoStack();
}
@@ -388,6 +418,7 @@ export const ChartDBProvider: React.FC<React.PropsWithChildren> = ({
getTable,
relationships,
events,
dependencies,
]
);
@@ -481,6 +512,14 @@ export const ChartDBProvider: React.FC<React.PropsWithChildren> = ({
)
);
const dependenciesToRemove = dependencies.filter((dependency) =>
tablesToDelete.some(
(table) =>
table.id === dependency.tableId ||
table.id === dependency.dependentTableId
)
);
setRelationships((relationships) =>
relationships.filter(
(relationship) =>
@@ -490,6 +529,15 @@ export const ChartDBProvider: React.FC<React.PropsWithChildren> = ({
)
);
setDependencies((dependencies) =>
dependencies.filter(
(dependency) =>
!dependenciesToRemove.some(
(d) => d.id === dependency.id
)
)
);
setTables(updateTables);
events.emit({
@@ -517,6 +565,12 @@ export const ChartDBProvider: React.FC<React.PropsWithChildren> = ({
);
}
for (const dependency of dependenciesToRemove) {
promises.push(
db.deleteDependency({ diagramId, id: dependency.id })
);
}
const updatedAt = new Date();
setDiagramUpdatedAt(updatedAt);
promises.push(
@@ -532,6 +586,7 @@ export const ChartDBProvider: React.FC<React.PropsWithChildren> = ({
undoData: {
tables: prevTables,
relationships: relationshipsToRemove,
dependencies: dependenciesToRemove,
},
});
resetRedoStack();
@@ -546,6 +601,7 @@ export const ChartDBProvider: React.FC<React.PropsWithChildren> = ({
resetRedoStack,
relationships,
events,
dependencies,
]
);
@@ -949,35 +1005,6 @@ export const ChartDBProvider: React.FC<React.PropsWithChildren> = ({
[db, diagramId, setTables, addUndoAction, resetRedoStack, getIndex]
);
const addRelationship: ChartDBContext['addRelationship'] = useCallback(
async (
relationship: DBRelationship,
options = { updateHistory: true }
) => {
setRelationships((relationships) => [
...relationships,
relationship,
]);
const updatedAt = new Date();
setDiagramUpdatedAt(updatedAt);
await Promise.all([
db.updateDiagram({ id: diagramId, attributes: { updatedAt } }),
db.addRelationship({ diagramId, relationship }),
]);
if (options.updateHistory) {
addUndoAction({
action: 'addRelationship',
redoData: { relationship },
undoData: { relationshipId: relationship.id },
});
resetRedoStack();
}
},
[db, diagramId, setRelationships, addUndoAction, resetRedoStack]
);
const addRelationships: ChartDBContext['addRelationships'] = useCallback(
async (
relationships: DBRelationship[],
@@ -1012,6 +1039,16 @@ export const ChartDBProvider: React.FC<React.PropsWithChildren> = ({
[db, diagramId, setRelationships, addUndoAction, resetRedoStack]
);
const addRelationship: ChartDBContext['addRelationship'] = useCallback(
async (
relationship: DBRelationship,
options = { updateHistory: true }
) => {
return addRelationships([relationship], options);
},
[addRelationships]
);
const createRelationship: ChartDBContext['createRelationship'] =
useCallback(
async ({
@@ -1032,7 +1069,9 @@ export const ChartDBProvider: React.FC<React.PropsWithChildren> = ({
const relationship: DBRelationship = {
id: generateId(),
name: `${sourceTableName}_${sourceFieldName}_fk`,
sourceSchema: sourceTable?.schema,
sourceTableId,
targetSchema: sourceTable?.schema,
targetTableId,
sourceFieldId,
targetFieldId,
@@ -1055,45 +1094,6 @@ export const ChartDBProvider: React.FC<React.PropsWithChildren> = ({
[relationships]
);
const removeRelationship: ChartDBContext['removeRelationship'] =
useCallback(
async (id: string, options = { updateHistory: true }) => {
const relationship = getRelationship(id);
setRelationships((relationships) =>
relationships.filter(
(relationship) => relationship.id !== id
)
);
const updatedAt = new Date();
setDiagramUpdatedAt(updatedAt);
await Promise.all([
db.updateDiagram({
id: diagramId,
attributes: { updatedAt },
}),
db.deleteRelationship({ diagramId, id }),
]);
if (!!relationship && options.updateHistory) {
addUndoAction({
action: 'removeRelationship',
redoData: { relationshipId: id },
undoData: { relationship },
});
resetRedoStack();
}
},
[
db,
diagramId,
setRelationships,
addUndoAction,
resetRedoStack,
getRelationship,
]
);
const removeRelationships: ChartDBContext['removeRelationships'] =
useCallback(
async (ids: string[], options = { updateHistory: true }) => {
@@ -1140,6 +1140,14 @@ export const ChartDBProvider: React.FC<React.PropsWithChildren> = ({
]
);
const removeRelationship: ChartDBContext['removeRelationship'] =
useCallback(
async (id: string, options = { updateHistory: true }) => {
return removeRelationships([id], options);
},
[removeRelationships]
);
const updateRelationship: ChartDBContext['updateRelationship'] =
useCallback(
async (
@@ -1186,11 +1194,170 @@ export const ChartDBProvider: React.FC<React.PropsWithChildren> = ({
]
);
const addDependencies: ChartDBContext['addDependencies'] = useCallback(
async (
dependencies: DBDependency[],
options = { updateHistory: true }
) => {
setDependencies((currentDependencies) => [
...currentDependencies,
...dependencies,
]);
const updatedAt = new Date();
setDiagramUpdatedAt(updatedAt);
await Promise.all([
...dependencies.map((dependency) =>
db.addDependency({ diagramId, dependency })
),
db.updateDiagram({ id: diagramId, attributes: { updatedAt } }),
]);
if (options.updateHistory) {
addUndoAction({
action: 'addDependencies',
redoData: { dependencies },
undoData: {
dependenciesIds: dependencies.map((r) => r.id),
},
});
resetRedoStack();
}
},
[db, diagramId, setDependencies, addUndoAction, resetRedoStack]
);
const addDependency: ChartDBContext['addDependency'] = useCallback(
async (dependency: DBDependency, options = { updateHistory: true }) => {
return addDependencies([dependency], options);
},
[addDependencies]
);
const createDependency: ChartDBContext['createDependency'] = useCallback(
async ({ tableId, dependentTableId }) => {
const table = getTable(tableId);
const dependentTable = getTable(dependentTableId);
const dependency: DBDependency = {
id: generateId(),
tableId,
dependentTableId,
dependentSchema: dependentTable?.schema,
schema: table?.schema,
createdAt: Date.now(),
};
await addDependency(dependency);
return dependency;
},
[addDependency, getTable]
);
const getDependency: ChartDBContext['getDependency'] = useCallback(
(id: string) =>
dependencies.find((dependency) => dependency.id === id) ?? null,
[dependencies]
);
const removeDependencies: ChartDBContext['removeDependencies'] =
useCallback(
async (ids: string[], options = { updateHistory: true }) => {
const prevDependencies = [
...dependencies.filter((dependency) =>
ids.includes(dependency.id)
),
];
setDependencies((dependencies) =>
dependencies.filter(
(dependency) => !ids.includes(dependency.id)
)
);
const updatedAt = new Date();
setDiagramUpdatedAt(updatedAt);
await Promise.all([
...ids.map((id) => db.deleteDependency({ diagramId, id })),
db.updateDiagram({
id: diagramId,
attributes: { updatedAt },
}),
]);
if (prevDependencies.length > 0 && options.updateHistory) {
addUndoAction({
action: 'removeDependencies',
redoData: { dependenciesIds: ids },
undoData: { dependencies: prevDependencies },
});
resetRedoStack();
}
},
[
db,
diagramId,
setDependencies,
addUndoAction,
resetRedoStack,
dependencies,
]
);
const removeDependency: ChartDBContext['removeDependency'] = useCallback(
async (id: string, options = { updateHistory: true }) => {
return removeDependencies([id], options);
},
[removeDependencies]
);
const updateDependency: ChartDBContext['updateDependency'] = useCallback(
async (
id: string,
dependency: Partial<DBDependency>,
options = { updateHistory: true }
) => {
const prevDependency = getDependency(id);
setDependencies((dependencies) =>
dependencies.map((d) =>
d.id === id ? { ...d, ...dependency } : d
)
);
const updatedAt = new Date();
setDiagramUpdatedAt(updatedAt);
await Promise.all([
db.updateDiagram({ id: diagramId, attributes: { updatedAt } }),
db.updateDependency({ id, attributes: dependency }),
]);
if (!!prevDependency && options.updateHistory) {
addUndoAction({
action: 'updateDependency',
redoData: { dependencyId: id, dependency },
undoData: { dependencyId: id, dependency: prevDependency },
});
resetRedoStack();
}
},
[
db,
diagramId,
setDependencies,
addUndoAction,
resetRedoStack,
getDependency,
]
);
const loadDiagram: ChartDBContext['loadDiagram'] = useCallback(
async (diagramId: string) => {
const diagram = await db.getDiagram(diagramId, {
includeRelationships: true,
includeTables: true,
includeDependencies: true,
});
if (diagram) {
@@ -1200,6 +1367,7 @@ export const ChartDBProvider: React.FC<React.PropsWithChildren> = ({
setDatabaseEdition(diagram.databaseEdition);
setTables(diagram?.tables ?? []);
setRelationships(diagram?.relationships ?? []);
setDependencies(diagram?.dependencies ?? []);
setDiagramCreatedAt(diagram.createdAt);
setDiagramUpdatedAt(diagram.updatedAt);
@@ -1216,6 +1384,7 @@ export const ChartDBProvider: React.FC<React.PropsWithChildren> = ({
setDatabaseEdition,
setTables,
setRelationships,
setDependencies,
setDiagramCreatedAt,
setDiagramUpdatedAt,
events,
@@ -1230,6 +1399,7 @@ export const ChartDBProvider: React.FC<React.PropsWithChildren> = ({
databaseType,
tables,
relationships,
dependencies,
currentDiagram,
schemas,
filteredSchemas,
@@ -1268,6 +1438,13 @@ export const ChartDBProvider: React.FC<React.PropsWithChildren> = ({
removeRelationship,
removeRelationships,
updateRelationship,
addDependency,
addDependencies,
createDependency,
getDependency,
removeDependency,
removeDependencies,
updateDependency,
}}
>
{children}

View File

@@ -16,18 +16,17 @@ export const HistoryProvider: React.FC<React.PropsWithChildren> = ({
hasUndo,
} = useRedoUndoStack();
const {
addTable,
addTables,
removeTable,
removeTables,
updateTable,
updateDiagramName,
removeField,
addField,
updateField,
addRelationship,
addRelationships,
removeRelationship,
addDependencies,
removeDependencies,
updateDependency,
updateRelationship,
updateTablesState,
addIndex,
@@ -41,15 +40,9 @@ export const HistoryProvider: React.FC<React.PropsWithChildren> = ({
updateDiagramName: ({ redoData: { name } }) => {
return updateDiagramName(name, { updateHistory: false });
},
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 });
},
@@ -73,19 +66,11 @@ export const HistoryProvider: React.FC<React.PropsWithChildren> = ({
updateHistory: false,
});
},
addRelationship: ({ redoData: { relationship } }) => {
return addRelationship(relationship, { updateHistory: false });
},
addRelationships: ({ redoData: { relationships } }) => {
return addRelationships(relationships, {
updateHistory: false,
});
},
removeRelationship: ({ redoData: { relationshipId } }) => {
return removeRelationship(relationshipId, {
updateHistory: false,
});
},
updateRelationship: ({
redoData: { relationshipId, relationship },
}) => {
@@ -98,6 +83,19 @@ export const HistoryProvider: React.FC<React.PropsWithChildren> = ({
updateHistory: false,
});
},
addDependencies: ({ redoData: { dependencies } }) => {
return addDependencies(dependencies, { updateHistory: false });
},
removeDependencies: ({ redoData: { dependenciesIds } }) => {
return removeDependencies(dependenciesIds, {
updateHistory: false,
});
},
updateDependency: ({ redoData: { dependencyId, dependency } }) => {
return updateDependency(dependencyId, dependency, {
updateHistory: false,
});
},
addIndex: ({ redoData: { tableId, index } }) => {
return addIndex(tableId, index, { updateHistory: false });
},
@@ -111,24 +109,23 @@ export const HistoryProvider: React.FC<React.PropsWithChildren> = ({
},
}),
[
addTable,
addTables,
removeTable,
removeTables,
updateTable,
updateDiagramName,
removeField,
addField,
updateField,
addRelationship,
addRelationships,
removeRelationship,
updateRelationship,
updateTablesState,
addIndex,
removeIndex,
updateIndex,
removeRelationships,
addDependencies,
removeDependencies,
updateDependency,
]
);
@@ -137,22 +134,16 @@ export const HistoryProvider: React.FC<React.PropsWithChildren> = ({
updateDiagramName: ({ undoData: { name } }) => {
return updateDiagramName(name, { updateHistory: false });
},
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 } }) => {
removeTables: async ({
undoData: { tables, relationships, dependencies },
}) => {
await Promise.all([
addTables(tables, { updateHistory: false }),
addRelationships(relationships, { updateHistory: false }),
addDependencies(dependencies, { updateHistory: false }),
]);
},
updateTable: ({ undoData: { tableId, table } }) => {
@@ -169,14 +160,11 @@ export const HistoryProvider: React.FC<React.PropsWithChildren> = ({
updateHistory: false,
});
},
addRelationship: ({ undoData: { relationshipId } }) => {
return removeRelationship(relationshipId, {
addRelationships: ({ undoData: { relationshipIds } }) => {
return removeRelationships(relationshipIds, {
updateHistory: false,
});
},
removeRelationship: ({ undoData: { relationship } }) => {
return addRelationship(relationship, { updateHistory: false });
},
removeRelationships: ({ undoData: { relationships } }) => {
return addRelationships(relationships, {
updateHistory: false,
@@ -189,8 +177,23 @@ export const HistoryProvider: React.FC<React.PropsWithChildren> = ({
updateHistory: false,
});
},
addDependencies: ({ undoData: { dependenciesIds } }) => {
return removeDependencies(dependenciesIds, {
updateHistory: false,
});
},
removeDependencies: ({ undoData: { dependencies } }) => {
return addDependencies(dependencies, {
updateHistory: false,
});
},
updateDependency: ({ undoData: { dependencyId, dependency } }) => {
return updateDependency(dependencyId, dependency, {
updateHistory: false,
});
},
updateTablesState: async ({
undoData: { tables, relationships },
undoData: { tables, relationships, dependencies },
}) => {
await Promise.all([
updateTablesState(() => tables, {
@@ -198,6 +201,7 @@ export const HistoryProvider: React.FC<React.PropsWithChildren> = ({
forceOverride: true,
}),
addRelationships(relationships, { updateHistory: false }),
addDependencies(dependencies, { updateHistory: false }),
]);
},
addIndex: ({ undoData: { tableId, indexId } }) => {
@@ -211,31 +215,25 @@ export const HistoryProvider: React.FC<React.PropsWithChildren> = ({
updateHistory: false,
});
},
addRelationships: ({ undoData: { relationshipIds } }) => {
return removeRelationships(relationshipIds, {
updateHistory: false,
});
},
}),
[
addTable,
addTables,
removeTable,
removeTables,
updateTable,
updateDiagramName,
removeField,
addField,
updateField,
addRelationship,
addRelationships,
removeRelationship,
updateRelationship,
updateTablesState,
addIndex,
removeIndex,
updateIndex,
removeRelationships,
addDependencies,
removeDependencies,
updateDependency,
]
);

View File

@@ -3,6 +3,7 @@ import type { ChartDBContext } from '../chartdb-context/chartdb-context';
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';
type Action = keyof ChartDBContext;
@@ -24,34 +25,30 @@ type RedoUndoActionUpdateTable = RedoUndoActionBase<
{ tableId: string; table: Partial<DBTable> }
>;
type RedoUndoActionAddTable = RedoUndoActionBase<
'addTable',
{ table: DBTable },
{ 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[] }
{
tables: DBTable[];
relationships: DBRelationship[];
dependencies: DBDependency[];
}
>;
type RedoUndoActionUpdateTablesState = RedoUndoActionBase<
'updateTablesState',
{ tables: DBTable[] },
{ tables: DBTable[]; relationships: DBRelationship[] }
{
tables: DBTable[];
relationships: DBRelationship[];
dependencies: DBDependency[];
}
>;
type RedoUndoActionAddField = RedoUndoActionBase<
@@ -90,24 +87,12 @@ type RedoUndoActionUpdateIndex = RedoUndoActionBase<
{ tableId: string; indexId: string; index: Partial<DBIndex> }
>;
type RedoUndoActionAddRelationship = RedoUndoActionBase<
'addRelationship',
{ relationship: DBRelationship },
{ relationshipId: string }
>;
type RedoUndoActionAddRelationships = RedoUndoActionBase<
'addRelationships',
{ relationships: DBRelationship[] },
{ relationshipIds: string[] }
>;
type RedoUndoActionRemoveRelationship = RedoUndoActionBase<
'removeRelationship',
{ relationshipId: string },
{ relationship: DBRelationship }
>;
type RedoUndoActionUpdateRelationship = RedoUndoActionBase<
'updateRelationship',
{ relationshipId: string; relationship: Partial<DBRelationship> },
@@ -120,10 +105,26 @@ type RedoUndoActionRemoveRelationships = RedoUndoActionBase<
{ relationships: DBRelationship[] }
>;
type RedoUndoActionAddDependencies = RedoUndoActionBase<
'addDependencies',
{ dependencies: DBDependency[] },
{ dependenciesIds: string[] }
>;
type RedoUndoActionUpdateDependency = RedoUndoActionBase<
'updateDependency',
{ dependencyId: string; dependency: Partial<DBDependency> },
{ dependencyId: string; dependency: Partial<DBDependency> }
>;
type RedoUndoActionRemoveDependencies = RedoUndoActionBase<
'removeDependencies',
{ dependenciesIds: string[] },
{ dependencies: DBDependency[] }
>;
export type RedoUndoAction =
| RedoUndoActionAddTable
| RedoUndoActionAddTables
| RedoUndoActionRemoveTable
| RedoUndoActionRemoveTables
| RedoUndoActionUpdateTable
| RedoUndoActionUpdateDiagramName
@@ -134,11 +135,12 @@ export type RedoUndoAction =
| RedoUndoActionAddIndex
| RedoUndoActionRemoveIndex
| RedoUndoActionUpdateIndex
| RedoUndoActionAddRelationship
| RedoUndoActionAddRelationships
| RedoUndoActionRemoveRelationship
| RedoUndoActionUpdateRelationship
| RedoUndoActionRemoveRelationships;
| RedoUndoActionRemoveRelationships
| RedoUndoActionAddDependencies
| RedoUndoActionUpdateDependency
| RedoUndoActionRemoveDependencies;
export type RedoActionData<T extends Action> = Extract<
RedoUndoAction,

View File

@@ -1,7 +1,7 @@
import { emptyFn } from '@/lib/utils';
import { createContext } from 'react';
export type SidebarSection = 'tables' | 'relationships';
export type SidebarSection = 'tables' | 'relationships' | 'dependencies';
export interface LayoutContext {
openedTableInSidebar: string | undefined;
@@ -12,6 +12,10 @@ export interface LayoutContext {
openRelationshipFromSidebar: (relationshipId: string) => void;
closeAllRelationshipsInSidebar: () => void;
openedDependencyInSidebar: string | undefined;
openDependencyFromSidebar: (dependencyId: string) => void;
closeAllDependenciesInSidebar: () => void;
selectedSidebarSection: SidebarSection;
selectSidebarSection: (section: SidebarSection) => void;
@@ -32,6 +36,10 @@ export const layoutContext = createContext<LayoutContext>({
openRelationshipFromSidebar: emptyFn,
closeAllRelationshipsInSidebar: emptyFn,
openedDependencyInSidebar: undefined,
openDependencyFromSidebar: emptyFn,
closeAllDependenciesInSidebar: emptyFn,
selectSidebarSection: emptyFn,
openTableFromSidebar: emptyFn,
closeAllTablesInSidebar: emptyFn,

View File

@@ -12,6 +12,8 @@ export const LayoutProvider: React.FC<React.PropsWithChildren> = ({
>();
const [openedRelationshipInSidebar, setOpenedRelationshipInSidebar] =
React.useState<string | undefined>();
const [openedDependencyInSidebar, setOpenedDependencyInSidebar] =
React.useState<string | undefined>();
const [selectedSidebarSection, setSelectedSidebarSection] =
React.useState<SidebarSection>('tables');
const [isSidePanelShowed, setIsSidePanelShowed] =
@@ -25,6 +27,9 @@ export const LayoutProvider: React.FC<React.PropsWithChildren> = ({
const closeAllRelationshipsInSidebar: LayoutContext['closeAllRelationshipsInSidebar'] =
() => setOpenedRelationshipInSidebar('');
const closeAllDependenciesInSidebar: LayoutContext['closeAllDependenciesInSidebar'] =
() => setOpenedDependencyInSidebar('');
const hideSidePanel: LayoutContext['hideSidePanel'] = () =>
setIsSidePanelShowed(false);
@@ -46,6 +51,13 @@ export const LayoutProvider: React.FC<React.PropsWithChildren> = ({
setOpenedRelationshipInSidebar(relationshipId);
};
const openDependencyFromSidebar: LayoutContext['openDependencyFromSidebar'] =
(dependencyId) => {
showSidePanel();
setSelectedSidebarSection('dependencies');
setOpenedDependencyInSidebar(dependencyId);
};
const openSelectSchema: LayoutContext['openSelectSchema'] = () =>
setIsSelectSchemaOpen(true);
@@ -68,6 +80,9 @@ export const LayoutProvider: React.FC<React.PropsWithChildren> = ({
isSelectSchemaOpen,
openSelectSchema,
closeSelectSchema,
openedDependencyInSidebar,
openDependencyFromSidebar,
closeAllDependenciesInSidebar,
}}
>
{children}

View File

@@ -29,6 +29,9 @@ export interface LocalConfigContext {
starUsDialogLastOpen: number;
setStarUsDialogLastOpen: (lastOpen: number) => void;
showDependenciesOnCanvas: boolean;
setShowDependenciesOnCanvas: (showDependenciesOnCanvas: boolean) => void;
}
export const LocalConfigContext = createContext<LocalConfigContext>({
@@ -52,4 +55,7 @@ export const LocalConfigContext = createContext<LocalConfigContext>({
starUsDialogLastOpen: 0,
setStarUsDialogLastOpen: emptyFn,
showDependenciesOnCanvas: false,
setShowDependenciesOnCanvas: emptyFn,
});

View File

@@ -10,6 +10,7 @@ const showCardinalityKey = 'show_cardinality';
const hideMultiSchemaNotificationKey = 'hide_multi_schema_notification';
const githubRepoOpenedKey = 'github_repo_opened';
const starUsDialogLastOpenKey = 'star_us_dialog_last_open';
const showDependenciesOnCanvasKey = 'show_dependencies_on_canvas';
export const LocalConfigProvider: React.FC<React.PropsWithChildren> = ({
children,
@@ -47,6 +48,12 @@ export const LocalConfigProvider: React.FC<React.PropsWithChildren> = ({
parseInt(localStorage.getItem(starUsDialogLastOpenKey) || '0')
);
const [showDependenciesOnCanvas, setShowDependenciesOnCanvas] =
React.useState<boolean>(
(localStorage.getItem(showDependenciesOnCanvasKey) || 'false') ===
'true'
);
useEffect(() => {
localStorage.setItem(
starUsDialogLastOpenKey,
@@ -81,6 +88,13 @@ export const LocalConfigProvider: React.FC<React.PropsWithChildren> = ({
localStorage.setItem(showCardinalityKey, showCardinality.toString());
}, [showCardinality]);
useEffect(() => {
localStorage.setItem(
showDependenciesOnCanvasKey,
showDependenciesOnCanvas.toString()
);
}, [showDependenciesOnCanvas]);
return (
<LocalConfigContext.Provider
value={{
@@ -98,6 +112,8 @@ export const LocalConfigProvider: React.FC<React.PropsWithChildren> = ({
githubRepoOpened,
starUsDialogLastOpen,
setStarUsDialogLastOpen,
showDependenciesOnCanvas,
setShowDependenciesOnCanvas,
}}
>
{children}

View File

@@ -4,6 +4,7 @@ import { emptyFn } from '@/lib/utils';
import type { DBRelationship } from '@/lib/domain/db-relationship';
import type { DBTable } from '@/lib/domain/db-table';
import type { ChartDBConfig } from '@/lib/domain/config';
import type { DBDependency } from '@/lib/domain/db-dependency';
export interface StorageContext {
// Config operations
@@ -21,6 +22,7 @@ export interface StorageContext {
options?: {
includeTables?: boolean;
includeRelationships?: boolean;
includeDependencies?: boolean;
}
) => Promise<Diagram | undefined>;
updateDiagram: (params: {
@@ -63,6 +65,26 @@ export interface StorageContext {
}) => Promise<void>;
listRelationships: (diagramId: string) => Promise<DBRelationship[]>;
deleteDiagramRelationships: (diagramId: string) => Promise<void>;
// Dependencies operations
addDependency: (params: {
diagramId: string;
dependency: DBDependency;
}) => Promise<void>;
getDependency: (params: {
diagramId: string;
id: string;
}) => Promise<DBDependency | undefined>;
updateDependency: (params: {
id: string;
attributes: Partial<DBDependency>;
}) => Promise<void>;
deleteDependency: (params: {
diagramId: string;
id: string;
}) => Promise<void>;
listDependencies: (diagramId: string) => Promise<DBDependency[]>;
deleteDiagramDependencies: (diagramId: string) => Promise<void>;
}
export const storageContext = createContext<StorageContext>({
@@ -89,4 +111,11 @@ export const storageContext = createContext<StorageContext>({
deleteRelationship: emptyFn,
listRelationships: emptyFn,
deleteDiagramRelationships: emptyFn,
addDependency: emptyFn,
getDependency: emptyFn,
updateDependency: emptyFn,
deleteDependency: emptyFn,
listDependencies: emptyFn,
deleteDiagramDependencies: emptyFn,
});

View File

@@ -7,6 +7,7 @@ import type { DBTable } from '@/lib/domain/db-table';
import type { DBRelationship } from '@/lib/domain/db-relationship';
import { determineCardinalities } from '@/lib/domain/db-relationship';
import type { ChartDBConfig } from '@/lib/domain/config';
import type { DBDependency } from '@/lib/domain/db-dependency';
export const StorageProvider: React.FC<React.PropsWithChildren> = ({
children,
@@ -24,6 +25,10 @@ export const StorageProvider: React.FC<React.PropsWithChildren> = ({
DBRelationship & { diagramId: string },
'id' // primary key "id" (for the typings only)
>;
db_dependencies: EntityTable<
DBDependency & { diagramId: string },
'id' // primary key "id" (for the typings only)
>;
config: EntityTable<
ChartDBConfig & { id: number },
'id' // primary key "id" (for the typings only)
@@ -105,6 +110,18 @@ export const StorageProvider: React.FC<React.PropsWithChildren> = ({
})
);
db.version(7).stores({
diagrams:
'++id, name, databaseType, databaseEdition, createdAt, updatedAt',
db_tables:
'++id, diagramId, name, schema, x, y, fields, indexes, color, createdAt, width, comment',
db_relationships:
'++id, diagramId, name, sourceSchema, sourceTableId, targetSchema, targetTableId, sourceFieldId, targetFieldId, type, createdAt',
db_dependencies:
'++id, diagramId, schema, tableId, dependentSchema, dependentTableId, createdAt',
config: '++id, defaultDiagramId',
});
db.on('ready', async () => {
const config = await getConfig();
@@ -159,6 +176,13 @@ export const StorageProvider: React.FC<React.PropsWithChildren> = ({
)
);
const dependencies = diagram.dependencies ?? [];
promises.push(
...dependencies.map((dependency) =>
addDependency({ diagramId: diagram.id, dependency })
)
);
await Promise.all(promises);
};
@@ -196,6 +220,7 @@ export const StorageProvider: React.FC<React.PropsWithChildren> = ({
options: {
includeTables?: boolean;
includeRelationships?: boolean;
includeDependencies?: boolean;
} = { includeRelationships: false, includeTables: false }
): Promise<Diagram | undefined> => {
const diagram = await db.diagrams.get(id);
@@ -212,6 +237,10 @@ export const StorageProvider: React.FC<React.PropsWithChildren> = ({
diagram.relationships = await listRelationships(id);
}
if (options.includeDependencies) {
diagram.dependencies = await listDependencies(id);
}
return diagram;
};
@@ -373,6 +402,54 @@ export const StorageProvider: React.FC<React.PropsWithChildren> = ({
});
};
const addDependency: StorageContext['addDependency'] = async ({
diagramId,
dependency,
}) => {
await db.db_dependencies.add({
...dependency,
diagramId,
});
};
const getDependency: StorageContext['getDependency'] = async ({
diagramId,
id,
}) => {
return await db.db_dependencies.get({ id, diagramId });
};
const updateDependency: StorageContext['updateDependency'] = async ({
id,
attributes,
}) => {
await db.db_dependencies.update(id, attributes);
};
const deleteDependency: StorageContext['deleteDependency'] = async ({
diagramId,
id,
}) => {
await db.db_dependencies.where({ id, diagramId }).delete();
};
const listDependencies: StorageContext['listDependencies'] = async (
diagramId
) => {
return await db.db_dependencies
.where('diagramId')
.equals(diagramId)
.toArray();
};
const deleteDiagramDependencies: StorageContext['deleteDiagramDependencies'] =
async (diagramId) => {
await db.db_dependencies
.where('diagramId')
.equals(diagramId)
.delete();
};
return (
<storageContext.Provider
value={{
@@ -396,6 +473,12 @@ export const StorageProvider: React.FC<React.PropsWithChildren> = ({
listRelationships,
deleteDiagramTables,
deleteDiagramRelationships,
addDependency,
getDependency,
updateDependency,
deleteDependency,
listDependencies,
deleteDiagramDependencies,
}}
>
{children}

View File

@@ -37,7 +37,6 @@ export const ExportImageDialog: React.FC<ExportImageDialogProps> = ({
const { closeExportImageDialog } = useDialog();
const handleExport = useCallback(() => {
console.log({ format, scale });
exportImage(format, Number(scale));
}, [exportImage, format, scale]);

View File

@@ -29,6 +29,9 @@ export const de: LanguageTranslation = {
zoom_on_scroll: 'Zoom beim Scrollen',
theme: 'Stil',
change_language: 'Sprache',
// TODO: Translate
show_dependencies: 'Show Dependencies',
hide_dependencies: 'Hide Dependencies',
},
help: {
help: 'Hilfe',
@@ -164,6 +167,25 @@ export const de: LanguageTranslation = {
'Erstellen Sie eine Beziehung, um Tabellen zu verbinden',
},
},
// TODO: Translate
dependencies_section: {
dependencies: 'Dependencies',
filter: 'Filter',
collapse: 'Collapse All',
dependency: {
table: 'Table',
dependent_table: 'Dependent View',
delete_dependency: 'Delete',
dependency_actions: {
title: 'Actions',
delete_dependency: 'Delete',
},
},
empty_state: {
title: 'No dependencies',
description: 'Create a view to get started',
},
},
},
toolbar: {

View File

@@ -29,6 +29,8 @@ export const en = {
zoom_on_scroll: 'Zoom on Scroll',
theme: 'Theme',
change_language: 'Language',
show_dependencies: 'Show Dependencies',
hide_dependencies: 'Hide Dependencies',
},
help: {
help: 'Help',
@@ -163,6 +165,24 @@ export const en = {
description: 'Create a relationship to connect tables',
},
},
dependencies_section: {
dependencies: 'Dependencies',
filter: 'Filter',
collapse: 'Collapse All',
dependency: {
table: 'Table',
dependent_table: 'Dependent View',
delete_dependency: 'Delete',
dependency_actions: {
title: 'Actions',
delete_dependency: 'Delete',
},
},
empty_state: {
title: 'No dependencies',
description: 'Create a view to get started',
},
},
},
toolbar: {

View File

@@ -29,6 +29,9 @@ export const es: LanguageTranslation = {
zoom_on_scroll: 'Zoom al Desplazarse',
theme: 'Tema',
change_language: 'Idioma',
// TODO: Translate
show_dependencies: 'Show Dependencies',
hide_dependencies: 'Hide Dependencies',
},
help: {
help: 'Ayuda',
@@ -154,6 +157,25 @@ export const es: LanguageTranslation = {
description: 'Crea una relación para conectar tablas',
},
},
// TODO: Translate
dependencies_section: {
dependencies: 'Dependencies',
filter: 'Filter',
collapse: 'Collapse All',
dependency: {
table: 'Table',
dependent_table: 'Dependent View',
delete_dependency: 'Delete',
dependency_actions: {
title: 'Actions',
delete_dependency: 'Delete',
},
},
empty_state: {
title: 'No dependencies',
description: 'Create a view to get started',
},
},
},
toolbar: {

View File

@@ -29,6 +29,9 @@ export const fr: LanguageTranslation = {
zoom_on_scroll: 'Zoom sur le Défilement',
theme: 'Thème',
change_language: 'Langue',
// TODO: Translate
show_dependencies: 'Show Dependencies',
hide_dependencies: 'Hide Dependencies',
},
help: {
help: 'Aide',
@@ -154,6 +157,25 @@ export const fr: LanguageTranslation = {
description: 'Créez une relation pour connecter les tables',
},
},
// TODO: Translate
dependencies_section: {
dependencies: 'Dependencies',
filter: 'Filter',
collapse: 'Collapse All',
dependency: {
table: 'Table',
dependent_table: 'Dependent View',
delete_dependency: 'Delete',
dependency_actions: {
title: 'Actions',
delete_dependency: 'Delete',
},
},
empty_state: {
title: 'No dependencies',
description: 'Create a view to get started',
},
},
},
toolbar: {

View File

@@ -29,6 +29,9 @@ export const ja: LanguageTranslation = {
zoom_on_scroll: 'スクロールでズーム',
theme: 'テーマ',
change_language: '言語',
// TODO: Translate
show_dependencies: 'Show Dependencies',
hide_dependencies: 'Hide Dependencies',
},
help: {
help: 'ヘルプ',
@@ -164,6 +167,25 @@ export const ja: LanguageTranslation = {
'テーブルを接続するためにリレーションシップを作成してください',
},
},
// TODO: Translate
dependencies_section: {
dependencies: 'Dependencies',
filter: 'Filter',
collapse: 'Collapse All',
dependency: {
table: 'Table',
dependent_table: 'Dependent View',
delete_dependency: 'Delete',
dependency_actions: {
title: 'Actions',
delete_dependency: 'Delete',
},
},
empty_state: {
title: 'No dependencies',
description: 'Create a view to get started',
},
},
},
toolbar: {

View File

@@ -17,3 +17,4 @@ export const randomColor = () => {
};
export const greyColor = '#b0b0b0'; // A Cloudy Grey.
export const blackColor = '#000'; // A Blacky black.

View File

@@ -1,4 +1,5 @@
export interface ViewInfo {
schema: string;
view_name: string;
view_definition?: string;
}

View File

@@ -0,0 +1,209 @@
/* eslint-disable */
import type { ViewInfo } from '../data/import-metadata/metadata-types/view-info';
import { DatabaseType } from './database-type';
import { schemaNameToSchemaId } from './db-schema';
import type { DBTable } from './db-table';
import { generateId } from '@/lib/utils';
import { Parser } from 'node-sql-parser';
export interface DBDependency {
id: string;
schema?: string;
tableId: string;
dependentSchema?: string;
dependentTableId: string;
createdAt: number;
}
export const shouldShowDependencyBySchemaFilter = (
dependency: DBDependency,
filteredSchemas?: string[]
): boolean =>
!filteredSchemas ||
!dependency.schema ||
!dependency.dependentSchema ||
(filteredSchemas.includes(schemaNameToSchemaId(dependency.schema)) &&
filteredSchemas.includes(
schemaNameToSchemaId(dependency.dependentSchema)
));
const astDatabaseTypes: Record<DatabaseType, string> = {
[DatabaseType.POSTGRESQL]: 'postgresql',
[DatabaseType.MYSQL]: 'mysql',
[DatabaseType.MARIADB]: 'mariadb',
[DatabaseType.GENERIC]: 'postgresql',
[DatabaseType.SQLITE]: 'sqlite',
[DatabaseType.SQL_SERVER]: 'postgresql',
};
export const createDependenciesFromMetadata = ({
views,
tables,
databaseType,
}: {
views: ViewInfo[];
tables: DBTable[];
databaseType: DatabaseType;
}): DBDependency[] => {
const parser = new Parser();
const dependencies = views
.flatMap((view) => {
const sourceTable = tables.find(
(table) =>
table.name === view.view_name &&
table.schema === view.schema
);
if (!sourceTable) {
console.warn(
`Source table for view ${view.schema}.${view.view_name} not found`
);
return []; // Skip this view and proceed to the next
}
if (view.view_definition) {
try {
// Pre-process the view_definition
const modifiedViewDefinition = preprocessViewDefinition(
view.view_definition
);
// Parse using PostgreSQL dialect
const ast = parser.astify(modifiedViewDefinition, {
database: astDatabaseTypes[databaseType],
});
const dependentTables = extractTablesFromAST(ast, view.schema);
return dependentTables.map((depTable) => {
const depSchema = depTable.schema ?? view.schema; // Use view's schema if depSchema is undefined
const depTableName = depTable.tableName;
const targetTable = tables.find(
(table) =>
table.name === depTableName &&
table.schema === depSchema
);
if (targetTable) {
const dependency: DBDependency = {
id: generateId(),
schema: view.schema,
tableId: sourceTable.id,
dependentSchema: targetTable.schema,
dependentTableId: targetTable.id,
createdAt: Date.now(),
};
return dependency;
} else {
console.warn(
`Dependent table ${depSchema}.${depTableName} not found for view ${view.schema}.${view.view_name}`
);
return null;
}
});
} catch (error) {
console.error(
`Error parsing view ${view.schema}.${view.view_name}:`,
error
);
return [];
}
} else {
console.warn(
`View definition missing for ${view.schema}.${view.view_name}`
);
return [];
}
})
.filter((dependency) => dependency !== null);
return dependencies;
};
// Preprocess the view_definition to remove schema from CREATE VIEW
function preprocessViewDefinition(viewDefinition: string): string {
if (!viewDefinition) {
return '';
}
// Replace 'CREATE MATERIALIZED VIEW' with 'CREATE VIEW'
viewDefinition = viewDefinition.replace(/CREATE\s+MATERIALIZED\s+VIEW/i, 'CREATE VIEW');
// Regular expression to match 'CREATE VIEW [schema.]view_name [ (column definitions) ] AS'
// This regex captures the view name and skips any content between the view name and 'AS'
const regex = /CREATE\s+VIEW\s+(?:(?:`[^`]+`|"[^"]+"|\w+)\.)?(?:`([^`]+)`|"([^"]+)"|(\w+))[\s\S]*?\bAS\b\s+/i;
const match = viewDefinition.match(regex);
let modifiedDefinition: string;
if (match) {
const viewName = match[1] || match[2] || match[3];
// Extract the SQL after the 'AS' keyword
const restOfDefinition = viewDefinition.substring(match.index! + match[0].length);
// Replace double-quoted identifiers with unquoted ones
let modifiedSQL = restOfDefinition.replace(/"(\w+)"/g, '$1');
// Replace '::' type casts with 'CAST' expressions
modifiedSQL = modifiedSQL.replace(/\(([^()]+)\)::(\w+)/g, 'CAST($1 AS $2)');
// Remove ClickHouse-specific syntax that may still be present
// For example, remove SETTINGS clauses inside the SELECT statement
modifiedSQL = modifiedSQL.replace(/\bSETTINGS\b[\s\S]*$/i, '');
modifiedDefinition = `CREATE VIEW ${viewName} AS ${modifiedSQL}`;
} else {
console.warn('Could not preprocess view definition:', viewDefinition);
modifiedDefinition = viewDefinition;
}
return modifiedDefinition;
}
function extractTablesFromAST(
ast: any,
defaultSchema: string
): { schema?: string; tableName: string }[] {
const tablesMap = new Map<string, { schema: string; tableName: string }>();
const visitedNodes = new Set();
function traverse(node: any) {
if (!node || visitedNodes.has(node)) return;
visitedNodes.add(node);
if (Array.isArray(node)) {
node.forEach(traverse);
} else if (typeof node === 'object') {
// Check if node represents a table
if (
node.hasOwnProperty('table') &&
typeof node.table === 'string'
) {
let schema = node.db || node.schema;
const tableName = node.table;
if (tableName) {
// Assign default schema if undefined
schema = schema || defaultSchema;
const key = `${schema}.${tableName}`;
if (!tablesMap.has(key)) {
tablesMap.set(key, { schema, tableName });
}
}
}
// Recursively traverse all properties
for (const key in node) {
if (node.hasOwnProperty(key)) {
traverse(node[key]);
}
}
}
}
traverse(ast);
return Array.from(tablesMap.values());
}

View File

@@ -3,7 +3,7 @@ import type { DBField } from './db-field';
import type { TableInfo } from '../data/import-metadata/metadata-types/table-info';
import type { ColumnInfo } from '../data/import-metadata/metadata-types/column-info';
import type { IndexInfo } from '../data/import-metadata/metadata-types/index-info';
import { greyColor, randomColor } from '@/lib/colors';
import { blackColor, greyColor, randomColor } from '@/lib/colors';
import type { DBRelationship } from './db-relationship';
import type { PrimaryKeyInfo } from '../data/import-metadata/metadata-types/primary-key-info';
import type { ViewInfo } from '../data/import-metadata/metadata-types/view-info';
@@ -23,6 +23,7 @@ export interface DBTable {
indexes: DBIndex[];
color: string;
isView: boolean;
isMaterializedView?: boolean;
createdAt: number;
width?: number;
comments?: string;
@@ -159,6 +160,12 @@ export const createTablesFromMetadata = ({
view.view_name === tableInfo.table
);
const isMaterializedView = views.some(
(view) =>
view.view_definition?.includes('MATERIALIZED') &&
view.view_name === tableInfo.table
);
// Initial random positions; these will be adjusted later
return {
id: generateId(),
@@ -168,8 +175,13 @@ export const createTablesFromMetadata = ({
y: Math.random() * 800, // Placeholder Y
fields,
indexes: dbIndexes,
color: isView ? greyColor : randomColor(),
color: isMaterializedView
? blackColor
: isView
? greyColor
: randomColor(),
isView: isView,
isMaterializedView: isMaterializedView,
createdAt: Date.now(),
comments: tableInfo.comment ? tableInfo.comment : undefined,
};

View File

@@ -1,6 +1,8 @@
import type { DatabaseMetadata } from '../data/import-metadata/metadata-types/database-metadata';
import type { DatabaseEdition } from './database-edition';
import { DatabaseType } from './database-type';
import type { DBDependency } from './db-dependency';
import { createDependenciesFromMetadata } from './db-dependency';
import type { DBRelationship } from './db-relationship';
import { createRelationshipsFromMetadata } from './db-relationship';
import type { DBTable } from './db-table';
@@ -13,6 +15,7 @@ export interface Diagram {
databaseEdition?: DatabaseEdition;
tables?: DBTable[];
relationships?: DBRelationship[];
dependencies?: DBDependency[];
createdAt: Date;
updatedAt: Date;
}
@@ -52,6 +55,13 @@ export const loadFromDatabaseMetadata = ({
tables,
});
// First pass: Create dependencies
const dependencies = createDependenciesFromMetadata({
views,
tables,
databaseType,
});
// Second pass: Adjust table positions based on relationships
const adjustedTables = adjustTablePositions({
tables,
@@ -79,6 +89,7 @@ export const loadFromDatabaseMetadata = ({
databaseEdition,
tables: sortedTables,
relationships,
dependencies,
createdAt: new Date(),
updatedAt: new Date(),
};

View File

@@ -27,8 +27,8 @@ import '@xyflow/react/dist/style.css';
import equal from 'fast-deep-equal';
import type { TableNodeType } from './table-node/table-node';
import { MIN_TABLE_SIZE, TableNode } from './table-node/table-node';
import type { TableEdgeType } from './table-edge';
import { TableEdge } from './table-edge';
import type { RelationshipEdgeType } from './relationship-edge';
import { RelationshipEdge } from './relationship-edge';
import { useChartDB } from '@/hooks/use-chartdb';
import {
LEFT_HANDLE_ID_PREFIX,
@@ -67,10 +67,24 @@ import type { Graph } from '@/lib/graph';
import { createGraph, removeVertex } from '@/lib/graph';
import type { ChartDBEvent } from '@/context/chartdb-context/chartdb-context';
import { debounce } from '@/lib/utils';
import type { DependencyEdgeType } from './dependency-edge';
import { DependencyEdge } from './dependency-edge';
import {
BOTTOM_SOURCE_HANDLE_ID_PREFIX,
TARGET_DEP_PREFIX,
TOP_SOURCE_HANDLE_ID_PREFIX,
} from './table-node/table-node-dependency-indicator';
type AddEdgeParams = Parameters<typeof addEdge<TableEdgeType>>[0];
export type EdgeType = RelationshipEdgeType | DependencyEdgeType;
const initialEdges: TableEdgeType[] = [];
type AddEdgeParams = Parameters<typeof addEdge<EdgeType>>[0];
const edgeTypes = {
'relationship-edge': RelationshipEdge,
'dependency-edge': DependencyEdge,
};
const initialEdges: EdgeType[] = [];
const tableToTableNode = (
table: DBTable,
@@ -104,20 +118,24 @@ export const Canvas: React.FC<CanvasProps> = ({ initialTables }) => {
tables,
relationships,
createRelationship,
createDependency,
updateTablesState,
removeRelationships,
removeDependencies,
getField,
getTable,
databaseType,
filteredSchemas,
events,
dependencies,
} = useChartDB();
const { showSidePanel } = useLayout();
const { effectiveTheme } = useTheme();
const { scrollAction } = useLocalConfig();
const { scrollAction, showDependenciesOnCanvas } = useLocalConfig();
const { showAlert } = useDialog();
const { isMd: isDesktop } = useBreakpoint('md');
const nodeTypes = useMemo(() => ({ table: TableNode }), []);
const edgeTypes = useMemo(() => ({ 'table-edge': TableEdge }), []);
const [isInitialLoadingNodes, setIsInitialLoadingNodes] = useState(true);
const [overlapGraph, setOverlapGraph] =
useState<Graph<string>>(createGraph());
@@ -126,7 +144,7 @@ export const Canvas: React.FC<CanvasProps> = ({ initialTables }) => {
initialTables.map((table) => tableToTableNode(table, filteredSchemas))
);
const [edges, setEdges, onEdgesChange] =
useEdgesState<TableEdgeType>(initialEdges);
useEdgesState<EdgeType>(initialEdges);
useEffect(() => {
setIsInitialLoadingNodes(true);
@@ -157,18 +175,41 @@ export const Canvas: React.FC<CanvasProps> = ({ initialTables }) => {
},
{} as Record<string, number>
);
setEdges(
relationships.map((relationship) => ({
id: relationship.id,
source: relationship.sourceTableId,
target: relationship.targetTableId,
sourceHandle: `${LEFT_HANDLE_ID_PREFIX}${relationship.sourceFieldId}`,
targetHandle: `${TARGET_ID_PREFIX}${targetIndexes[`${relationship.targetTableId}${relationship.targetFieldId}`]++}_${relationship.targetFieldId}`,
type: 'table-edge',
data: { relationship },
}))
const targetDepIndexes: Record<string, number> = dependencies.reduce(
(acc, dep) => {
acc[dep.tableId] = 0;
return acc;
},
{} as Record<string, number>
);
}, [relationships, setEdges]);
setEdges([
...relationships.map(
(relationship): RelationshipEdgeType => ({
id: relationship.id,
source: relationship.sourceTableId,
target: relationship.targetTableId,
sourceHandle: `${LEFT_HANDLE_ID_PREFIX}${relationship.sourceFieldId}`,
targetHandle: `${TARGET_ID_PREFIX}${targetIndexes[`${relationship.targetTableId}${relationship.targetFieldId}`]++}_${relationship.targetFieldId}`,
type: 'relationship-edge',
data: { relationship },
})
),
...dependencies.map(
(dep): DependencyEdgeType => ({
id: dep.id,
source: dep.dependentTableId,
target: dep.tableId,
sourceHandle: `${TOP_SOURCE_HANDLE_ID_PREFIX}${dep.dependentTableId}`,
targetHandle: `${TARGET_DEP_PREFIX}${targetDepIndexes[dep.tableId]++}_${dep.tableId}`,
type: 'dependency-edge',
data: { dependency: dep },
hidden: !showDependenciesOnCanvas,
})
),
]);
}, [relationships, dependencies, setEdges, showDependenciesOnCanvas]);
useEffect(() => {
const selectedNodesIds = nodes
@@ -209,18 +250,32 @@ export const Canvas: React.FC<CanvasProps> = ({ initialTables }) => {
];
setEdges((edges) =>
edges.map((edge) => {
edges.map((edge): EdgeType => {
const selected = allSelectedEdges.includes(edge.id);
return {
...edge,
data: {
...edge.data!,
highlighted: selected,
},
animated: selected,
zIndex: selected ? 1 : 0,
};
if (edge.type === 'dependency-edge') {
const dependencyEdge = edge as DependencyEdgeType;
return {
...dependencyEdge,
data: {
...dependencyEdge.data!,
highlighted: selected,
},
animated: selected,
zIndex: selected ? 1 : 0,
};
} else {
const relationshipEdge = edge as RelationshipEdgeType;
return {
...relationshipEdge,
data: {
...relationshipEdge.data!,
highlighted: selected,
},
animated: selected,
zIndex: selected ? 1 : 0,
};
}
})
);
}, [selectedRelationshipIds, selectedTableIds, setEdges, getEdges]);
@@ -271,6 +326,40 @@ export const Canvas: React.FC<CanvasProps> = ({ initialTables }) => {
const onConnectHandler = useCallback(
async (params: AddEdgeParams) => {
if (
params.sourceHandle?.startsWith?.(
TOP_SOURCE_HANDLE_ID_PREFIX
) ||
params.sourceHandle?.startsWith?.(
BOTTOM_SOURCE_HANDLE_ID_PREFIX
)
) {
const tableOne = getTable(params.source);
const tableTwo = getTable(params.target);
if (!tableOne || !tableTwo) {
return;
}
let tableId;
let dependentTableId;
if (tableOne.isMaterializedView || tableOne.isView) {
tableId = tableTwo.id;
dependentTableId = tableOne.id;
} else {
tableId = tableOne.id;
dependentTableId = tableTwo.id;
}
createDependency({
tableId,
dependentTableId,
});
return;
}
const sourceTableId = params.source;
const targetTableId = params.target;
const sourceFieldId = params.sourceHandle?.split('_')?.pop() ?? '';
@@ -304,37 +393,50 @@ export const Canvas: React.FC<CanvasProps> = ({ initialTables }) => {
sourceFieldId,
targetFieldId,
});
// return setEdges((edges) =>
// addEdge<TableEdgeType>(
// { ...params, data: { relationship }, id: relationship.id },
// edges
// )
// );
},
[createRelationship, getField, toast, databaseType]
[
createRelationship,
createDependency,
getField,
getTable,
toast,
databaseType,
]
);
const onEdgesChangeHandler: OnEdgesChange<TableEdgeType> = useCallback(
const onEdgesChangeHandler: OnEdgesChange<EdgeType> = useCallback(
(changes) => {
const removeChanges: NodeRemoveChange[] = changes.filter(
(change) => change.type === 'remove'
) as NodeRemoveChange[];
const relationshipsToRemove: string[] = removeChanges
.map(
(change) =>
(getEdge(change.id) as TableEdgeType)?.data
?.relationship?.id
)
.filter((id) => !!id) as string[];
const edgesToRemove = removeChanges
.map((change) => getEdge(change.id) as EdgeType | undefined)
.filter((edge) => !!edge);
const relationshipsToRemove: string[] = (
edgesToRemove.filter(
(edge) => edge?.type === 'relationship-edge'
) as RelationshipEdgeType[]
).map((edge) => edge?.data?.relationship?.id as string);
const dependenciesToRemove: string[] = (
edgesToRemove.filter(
(edge) => edge?.type === 'dependency-edge'
) as DependencyEdgeType[]
).map((edge) => edge?.data?.dependency?.id as string);
if (relationshipsToRemove.length > 0) {
removeRelationships(relationshipsToRemove);
}
if (dependenciesToRemove.length > 0) {
removeDependencies(dependenciesToRemove);
}
return onEdgesChange(changes);
},
[getEdge, onEdgesChange, removeRelationships]
[getEdge, onEdgesChange, removeRelationships, removeDependencies]
);
const updateOverlappingGraphOnChanges = useCallback(
@@ -589,7 +691,7 @@ export const Canvas: React.FC<CanvasProps> = ({ initialTables }) => {
edgeTypes={edgeTypes}
defaultEdgeOptions={{
animated: false,
type: 'table-edge',
type: 'relationship-edge',
}}
panOnScroll={scrollAction === 'pan'}
>

View File

@@ -0,0 +1,168 @@
import React, { useCallback, useMemo } from 'react';
import type { Edge, EdgeProps } from '@xyflow/react';
import { getSmoothStepPath, Position, useReactFlow } from '@xyflow/react';
import { useChartDB } from '@/hooks/use-chartdb';
import { cn } from '@/lib/utils';
import type { DBDependency } from '@/lib/domain/db-dependency';
import { useLayout } from '@/hooks/use-layout';
export type DependencyEdgeType = Edge<
{
dependency: DBDependency;
highlighted?: boolean;
},
'dependency-edge'
>;
export const DependencyEdge: React.FC<EdgeProps<DependencyEdgeType>> = ({
id,
sourceX,
sourceY,
targetX,
targetY,
source,
target,
selected,
// data,
}) => {
const { getInternalNode } = useReactFlow();
const { dependencies } = useChartDB();
const { openDependencyFromSidebar, selectSidebarSection } = useLayout();
const openDependencyInEditor = useCallback(() => {
selectSidebarSection('dependencies');
openDependencyFromSidebar(id);
}, [id, openDependencyFromSidebar, selectSidebarSection]);
const edgeNumber = useMemo(
() =>
dependencies
.filter(
(dep) =>
(dep.tableId === target &&
dep.dependentTableId === source) ||
(dep.tableId === source &&
dep.dependentTableId === target)
)
.findIndex((dep) => dep.id === id),
[dependencies, id, source, target]
);
const sourceNode = getInternalNode(source);
const targetNode = getInternalNode(target);
const { sourceTopY, sourceBottomY, targetTopY, targetBottomY } =
useMemo(() => {
const sourceHeight = sourceNode?.measured.height ?? 0;
const sourceTopY = sourceY + 5;
const sourceBottomY = sourceY + sourceHeight + 10;
const targetHeight = targetNode?.measured.height ?? 0;
const targetTopY = targetY + 1;
const targetBottomY = targetY + targetHeight + 5;
return { sourceTopY, sourceBottomY, targetTopY, targetBottomY };
}, [
sourceNode?.measured.height,
sourceY,
targetNode?.measured.height,
targetY,
]);
const { sourceSide, targetSide } = useMemo(() => {
const distances = {
topToTop: Math.abs(sourceTopY - targetTopY),
topToBottom: Math.abs(sourceTopY - targetBottomY),
bottomToTop: Math.abs(sourceBottomY - targetTopY),
bottomToBottom: Math.abs(sourceBottomY - targetBottomY),
};
const minDistance = Math.min(
distances.topToTop,
distances.topToBottom,
distances.bottomToTop,
distances.bottomToBottom
);
const minDistanceKey = Object.keys(distances).find(
(key) => distances[key as keyof typeof distances] === minDistance
) as keyof typeof distances;
switch (minDistanceKey) {
case 'topToBottom':
return { sourceSide: 'top', targetSide: 'bottom' };
case 'bottomToTop':
return { sourceSide: 'bottom', targetSide: 'top' };
case 'bottomToBottom':
return { sourceSide: 'bottom', targetSide: 'bottom' };
default:
return { sourceSide: 'top', targetSide: 'top' };
}
}, [sourceTopY, sourceBottomY, targetTopY, targetBottomY]);
const [edgePath] = useMemo(
() =>
getSmoothStepPath({
sourceX,
sourceY: sourceSide === 'top' ? sourceTopY : sourceBottomY,
targetX,
targetY: targetSide === 'top' ? targetTopY : targetBottomY,
borderRadius: 14,
sourcePosition:
sourceSide === 'top' ? Position.Top : Position.Bottom,
targetPosition:
targetSide === 'top' ? Position.Top : Position.Bottom,
offset: (edgeNumber + 1) * 14,
}),
[
edgeNumber,
sourceBottomY,
sourceSide,
sourceTopY,
sourceX,
targetBottomY,
targetSide,
targetTopY,
targetX,
]
);
return (
<>
<path
id={id}
d={edgePath}
fill="none"
className={cn([
'react-flow__edge-path',
`!stroke-2 ${selected ? '!stroke-pink-600' : '!stroke-blue-400'}`,
])}
onClick={(e) => {
if (e.detail === 2) {
openDependencyInEditor();
}
}}
/>
<path
d={edgePath}
fill="none"
strokeOpacity={0}
strokeWidth={20}
// eslint-disable-next-line tailwindcss/no-custom-classname
className="react-flow__edge-interaction"
onClick={(e) => {
if (e.detail === 2) {
openDependencyInEditor();
}
}}
/>
</>
// <BaseEdge
// id={id}
// path={edgePath}
// markerStart="url(#cardinality_one)"
// markerEnd="url(#cardinality_one)"
// className={`!stroke-2 ${selected ? '!stroke-slate-500' : '!stroke-slate-300'}`}
// />
);
};

View File

@@ -8,15 +8,15 @@ import { useLayout } from '@/hooks/use-layout';
import { cn } from '@/lib/utils';
import { getCardinalityMarkerId } from './canvas-utils';
export type TableEdgeType = Edge<
export type RelationshipEdgeType = Edge<
{
relationship: DBRelationship;
highlighted?: boolean;
},
'table-edge'
'relationship-edge'
>;
export const TableEdge: React.FC<EdgeProps<TableEdgeType>> = ({
export const RelationshipEdge: React.FC<EdgeProps<RelationshipEdgeType>> = ({
id,
sourceX,
sourceY,
@@ -44,8 +44,10 @@ export const TableEdge: React.FC<EdgeProps<TableEdgeType>> = ({
relationships
.filter(
(relationship) =>
relationship.targetTableId === target &&
relationship.sourceTableId === source
(relationship.targetTableId === target &&
relationship.sourceTableId === source) ||
(relationship.targetTableId === source &&
relationship.sourceTableId === target)
)
.findIndex((relationship) => relationship.id === id),
[relationships, id, source, target]
@@ -157,7 +159,7 @@ export const TableEdge: React.FC<EdgeProps<TableEdgeType>> = ({
fill="none"
className={cn([
'react-flow__edge-path',
`!stroke-2 ${selected ? '!stroke-pink-600' : '!stroke-slate-300'}`,
`!stroke-2 ${selected ? '!stroke-pink-600' : '!stroke-slate-400'}`,
])}
onClick={(e) => {
if (e.detail === 2) {

View File

@@ -0,0 +1,115 @@
import { useChartDB } from '@/hooks/use-chartdb';
import type { DBTable } from '@/lib/domain/db-table';
import {
Handle,
Position,
useConnection,
useUpdateNodeInternals,
} from '@xyflow/react';
import React, { useEffect, useMemo, useRef } from 'react';
export const TOP_SOURCE_HANDLE_ID_PREFIX = 'top_dep_';
export const BOTTOM_SOURCE_HANDLE_ID_PREFIX = 'bottom_dep_';
export const TARGET_DEP_PREFIX = 'target_dep_';
export interface TableNodeDependencyIndicatorProps {
table: DBTable;
focused: boolean;
}
export const TableNodeDependencyIndicator: React.FC<TableNodeDependencyIndicatorProps> =
React.memo(({ table, focused }) => {
const { getTable, dependencies } = useChartDB();
const updateNodeInternals = useUpdateNodeInternals();
const connection = useConnection();
const isTarget = useMemo(() => {
if (!connection.inProgress) {
return false;
}
const sourceTable = connection.fromNode?.id
? getTable(connection.fromNode.id)
: null;
if (!sourceTable) {
return false;
}
const isSourceTableView =
sourceTable.isView || sourceTable.isMaterializedView;
const isTableView = table.isView || table.isMaterializedView;
return (
((isSourceTableView && !isTableView) ||
(!isSourceTableView && isTableView)) &&
connection.fromNode.id !== table.id &&
(connection.fromHandle.id?.startsWith(
TOP_SOURCE_HANDLE_ID_PREFIX
) ||
connection.fromHandle.id?.startsWith(
BOTTOM_SOURCE_HANDLE_ID_PREFIX
))
);
}, [
connection,
table.id,
getTable,
table.isMaterializedView,
table.isView,
]);
const numberOfEdgesToTable = useMemo(
() =>
dependencies.filter(
(dependency) => dependency.tableId === table.id
).length,
[dependencies, table.id]
);
const previousNumberOfEdgesToTableRef = useRef(numberOfEdgesToTable);
useEffect(() => {
if (
previousNumberOfEdgesToTableRef.current !== numberOfEdgesToTable
) {
updateNodeInternals(table.id);
previousNumberOfEdgesToTableRef.current = numberOfEdgesToTable;
}
}, [table.id, updateNodeInternals, numberOfEdgesToTable]);
return (
<>
<Handle
id={`${TOP_SOURCE_HANDLE_ID_PREFIX}${table.id}`}
className={`!h-4 !w-4 !border-2 !bg-pink-600 ${!focused ? '!invisible' : ''}`}
position={Position.Top}
type="source"
/>
{Array.from(
{ length: numberOfEdgesToTable },
(_, index) => index
).map((index) => (
<Handle
id={`${TARGET_DEP_PREFIX}${index}_${table.id}`}
key={`${TARGET_DEP_PREFIX}${index}_${table.id}`}
className={`!invisible`}
position={Position.Top}
type="target"
/>
))}
{isTarget ? (
<Handle
id={`${TARGET_DEP_PREFIX}${numberOfEdgesToTable}_${table.id}`}
className={
isTarget
? '!absolute !left-0 !top-0 !h-full !w-full !transform-none !rounded-none !border-none !opacity-0'
: `!invisible`
}
position={Position.Top}
type="target"
/>
) : null}
</>
);
});
TableNodeDependencyIndicator.displayName = 'TableNodeDependencyIndicator';

View File

@@ -11,9 +11,9 @@ import { KeyRound, Trash2 } from 'lucide-react';
import type { DBField } from '@/lib/domain/db-field';
import { useChartDB } from '@/hooks/use-chartdb';
export const LEFT_HANDLE_ID_PREFIX = 'left_';
export const RIGHT_HANDLE_ID_PREFIX = 'right_';
export const TARGET_ID_PREFIX = 'target_';
export const LEFT_HANDLE_ID_PREFIX = 'left_rel_';
export const RIGHT_HANDLE_ID_PREFIX = 'right_rel_';
export const TARGET_ID_PREFIX = 'target_rel_';
export interface TableNodeFieldProps {
tableNodeId: string;
@@ -31,7 +31,12 @@ export const TableNodeField: React.FC<TableNodeFieldProps> = React.memo(
const connection = useConnection();
const isTarget = useMemo(
() =>
connection.inProgress && connection.fromNode.id !== tableNodeId,
connection.inProgress &&
connection.fromNode.id !== tableNodeId &&
(connection.fromHandle.id?.startsWith(RIGHT_HANDLE_ID_PREFIX) ||
connection.fromHandle.id?.startsWith(
LEFT_HANDLE_ID_PREFIX
)),
[connection, tableNodeId]
);
const numberOfEdgesToField = useMemo(

View File

@@ -15,11 +15,13 @@ import type { DBTable } from '@/lib/domain/db-table';
import { TableNodeField } from './table-node-field';
import { useLayout } from '@/hooks/use-layout';
import { useChartDB } from '@/hooks/use-chartdb';
import type { TableEdgeType } from '../table-edge';
import type { RelationshipEdgeType } from '../relationship-edge';
import type { DBField } from '@/lib/domain/db-field';
import { useTranslation } from 'react-i18next';
import { TableNodeContextMenu } from './table-node-context-menu';
import { cn } from '@/lib/utils';
import { TableNodeDependencyIndicator } from './table-node-dependency-indicator';
import type { EdgeType } from '../canvas';
export type TableNodeType = Node<
{
@@ -34,199 +36,203 @@ export const MID_TABLE_SIZE = 337;
export const MIN_TABLE_SIZE = 224;
export const TABLE_MINIMIZED_FIELDS = 10;
export const TableNode: React.FC<NodeProps<TableNodeType>> = ({
selected,
dragging,
id,
data: { table, isOverlapping },
}) => {
const { updateTable, relationships } = useChartDB();
const edges = useStore((store) => store.edges) as TableEdgeType[];
const { openTableFromSidebar, selectSidebarSection } = useLayout();
const [expanded, setExpanded] = useState(false);
const { t } = useTranslation();
export const TableNode: React.FC<NodeProps<TableNodeType>> = React.memo(
({ selected, dragging, id, data: { table, isOverlapping } }) => {
const { updateTable, relationships } = useChartDB();
const edges = useStore((store) => store.edges) as EdgeType[];
const { openTableFromSidebar, selectSidebarSection } = useLayout();
const [expanded, setExpanded] = useState(false);
const { t } = useTranslation();
const selectedEdges = edges.filter(
(edge) =>
(edge.source === id || edge.target === id) &&
(edge.selected || edge.data?.highlighted)
);
const selectedRelEdges = edges.filter(
(edge) =>
(edge.source === id || edge.target === id) &&
(edge.selected || edge.data?.highlighted) &&
edge.type === 'relationship-edge'
) as RelationshipEdgeType[];
const focused = !!selected && !dragging;
const focused = !!selected && !dragging;
const openTableInEditor = () => {
selectSidebarSection('tables');
openTableFromSidebar(table.id);
};
const openTableInEditor = () => {
selectSidebarSection('tables');
openTableFromSidebar(table.id);
};
const expandTable = useCallback(() => {
updateTable(table.id, {
width:
(table.width ?? MIN_TABLE_SIZE) < MID_TABLE_SIZE
? MID_TABLE_SIZE
: MAX_TABLE_SIZE,
});
}, [table.id, table.width, updateTable]);
const expandTable = useCallback(() => {
updateTable(table.id, {
width:
(table.width ?? MIN_TABLE_SIZE) < MID_TABLE_SIZE
? MID_TABLE_SIZE
: MAX_TABLE_SIZE,
});
}, [table.id, table.width, updateTable]);
const shrinkTable = useCallback(() => {
updateTable(table.id, {
width: MIN_TABLE_SIZE,
});
}, [table.id, updateTable]);
const shrinkTable = useCallback(() => {
updateTable(table.id, {
width: MIN_TABLE_SIZE,
});
}, [table.id, updateTable]);
const toggleExpand = () => {
setExpanded(!expanded);
};
const toggleExpand = () => {
setExpanded(!expanded);
};
const isMustDisplayedField = useCallback(
(field: DBField) => {
return (
relationships.some(
(relationship) =>
relationship.sourceFieldId === field.id ||
relationship.targetFieldId === field.id
) || field.primaryKey
const isMustDisplayedField = useCallback(
(field: DBField) => {
return (
relationships.some(
(relationship) =>
relationship.sourceFieldId === field.id ||
relationship.targetFieldId === field.id
) || field.primaryKey
);
},
[relationships]
);
const visibleFields = useMemo(() => {
if (expanded) {
return table.fields;
}
const mustDisplayedFields = table.fields.filter((field: DBField) =>
isMustDisplayedField(field)
);
const nonMustDisplayedFields = table.fields.filter(
(field: DBField) => !isMustDisplayedField(field)
);
},
[relationships]
);
const visibleFields = useMemo(() => {
if (expanded) {
return table.fields;
}
const visibleMustDisplayedFields = mustDisplayedFields.slice(
0,
TABLE_MINIMIZED_FIELDS
);
const remainingSlots =
TABLE_MINIMIZED_FIELDS - visibleMustDisplayedFields.length;
const visibleNonMustDisplayedFields = nonMustDisplayedFields.slice(
0,
remainingSlots
);
const mustDisplayedFields = table.fields.filter((field: DBField) =>
isMustDisplayedField(field)
);
const nonMustDisplayedFields = table.fields.filter(
(field: DBField) => !isMustDisplayedField(field)
);
return [
...visibleMustDisplayedFields,
...visibleNonMustDisplayedFields,
].sort((a, b) => table.fields.indexOf(a) - table.fields.indexOf(b));
}, [expanded, table.fields, isMustDisplayedField]);
const visibleMustDisplayedFields = mustDisplayedFields.slice(
0,
TABLE_MINIMIZED_FIELDS
);
const remainingSlots =
TABLE_MINIMIZED_FIELDS - visibleMustDisplayedFields.length;
const visibleNonMustDisplayedFields = nonMustDisplayedFields.slice(
0,
remainingSlots
);
return [
...visibleMustDisplayedFields,
...visibleNonMustDisplayedFields,
].sort((a, b) => table.fields.indexOf(a) - table.fields.indexOf(b));
}, [expanded, table.fields, isMustDisplayedField]);
return (
<TableNodeContextMenu table={table}>
<div
className={cn(
'flex w-full flex-col border-2 bg-slate-50 dark:bg-slate-950 rounded-lg shadow-sm transition-transform duration-300',
selected
? 'border-pink-600'
: 'border-slate-500 dark:border-slate-700',
isOverlapping
? 'ring-2 ring-offset-slate-50 dark:ring-offset-slate-900 ring-blue-500 ring-offset-2 animate-pulse-border animate-scale'
: ''
)}
onClick={(e) => {
if (e.detail === 2) {
openTableInEditor();
}
}}
>
<NodeResizer
isVisible={focused}
lineClassName="!border-none !w-2"
minWidth={MIN_TABLE_SIZE}
maxWidth={MAX_TABLE_SIZE}
shouldResize={(event) => event.dy === 0}
handleClassName="!hidden"
/>
return (
<TableNodeContextMenu table={table}>
<div
className="h-2 rounded-t-[6px]"
style={{ backgroundColor: table.color }}
></div>
<div className="group flex h-9 items-center justify-between bg-slate-200 px-2 dark:bg-slate-900">
<div className="flex min-w-0 flex-1 items-center gap-2">
<Table2 className="size-3.5 shrink-0 text-gray-600 dark:text-primary" />
<Label className="truncate text-sm font-bold">
{table.name}
</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>
<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={
table.width !== MAX_TABLE_SIZE
? expandTable
: shrinkTable
}
>
{table.width !== MAX_TABLE_SIZE ? (
<ChevronsLeftRight className="size-4" />
) : (
<ChevronsRightLeft className="size-4" />
)}
</Button>
</div>
</div>
<div
className="transition-[max-height] duration-200 ease-in-out"
style={{
maxHeight: expanded
? `${table.fields.length * 2}rem` // h-8 per field
: `${TABLE_MINIMIZED_FIELDS * 2}rem`, // h-8 per field
className={cn(
'flex w-full flex-col border-2 bg-slate-50 dark:bg-slate-950 rounded-lg shadow-sm transition-transform duration-300',
selected
? 'border-pink-600'
: 'border-slate-500 dark:border-slate-700',
isOverlapping
? 'ring-2 ring-offset-slate-50 dark:ring-offset-slate-900 ring-blue-500 ring-offset-2 animate-pulse-border animate-scale'
: ''
)}
onClick={(e) => {
if (e.detail === 2) {
openTableInEditor();
}
}}
>
{table.fields.map((field: DBField) => (
<TableNodeField
key={field.id}
focused={focused}
tableNodeId={id}
field={field}
highlighted={selectedEdges.some(
(edge) =>
edge.data?.relationship.sourceFieldId ===
field.id ||
edge.data?.relationship.targetFieldId ===
field.id
)}
visible={visibleFields.includes(field)}
isConnectable={!table.isView}
/>
))}
</div>
{table.fields.length > TABLE_MINIMIZED_FIELDS && (
<NodeResizer
isVisible={focused}
lineClassName="!border-none !w-2"
minWidth={MIN_TABLE_SIZE}
maxWidth={MAX_TABLE_SIZE}
shouldResize={(event) => event.dy === 0}
handleClassName="!hidden"
/>
<TableNodeDependencyIndicator
table={table}
focused={focused}
/>
<div
className="z-10 flex h-8 cursor-pointer items-center justify-center rounded-b-md border-t text-xs text-muted-foreground transition-colors duration-200 hover:bg-slate-100 dark:hover:bg-slate-800"
onClick={toggleExpand}
>
{expanded ? (
<>
<ChevronUp className="mr-1 size-3.5" />
{t('show_less')}
</>
) : (
<>
<ChevronDown className="mr-1 size-3.5" />
{t('show_more')}
</>
)}
className="h-2 rounded-t-[6px]"
style={{ backgroundColor: table.color }}
></div>
<div className="group flex h-9 items-center justify-between bg-slate-200 px-2 dark:bg-slate-900">
<div className="flex min-w-0 flex-1 items-center gap-2">
<Table2 className="size-3.5 shrink-0 text-gray-600 dark:text-primary" />
<Label className="truncate text-sm font-bold">
{table.name}
</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>
<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={
table.width !== MAX_TABLE_SIZE
? expandTable
: shrinkTable
}
>
{table.width !== MAX_TABLE_SIZE ? (
<ChevronsLeftRight className="size-4" />
) : (
<ChevronsRightLeft className="size-4" />
)}
</Button>
</div>
</div>
)}
</div>
</TableNodeContextMenu>
);
};
<div
className="transition-[max-height] duration-200 ease-in-out"
style={{
maxHeight: expanded
? `${table.fields.length * 2}rem` // h-8 per field
: `${TABLE_MINIMIZED_FIELDS * 2}rem`, // h-8 per field
}}
>
{table.fields.map((field: DBField) => (
<TableNodeField
key={field.id}
focused={focused}
tableNodeId={id}
field={field}
highlighted={selectedRelEdges.some(
(edge) =>
edge.data?.relationship
.sourceFieldId === field.id ||
edge.data?.relationship
.targetFieldId === field.id
)}
visible={visibleFields.includes(field)}
isConnectable={!table.isView}
/>
))}
</div>
{table.fields.length > TABLE_MINIMIZED_FIELDS && (
<div
className="z-10 flex h-8 cursor-pointer items-center justify-center rounded-b-md border-t text-xs text-muted-foreground transition-colors duration-200 hover:bg-slate-100 dark:hover:bg-slate-800"
onClick={toggleExpand}
>
{expanded ? (
<>
<ChevronUp className="mr-1 size-3.5" />
{t('show_less')}
</>
) : (
<>
<ChevronDown className="mr-1 size-3.5" />
{t('show_more')}
</>
)}
</div>
)}
</div>
</TableNodeContextMenu>
);
}
);
TableNode.displayName = 'TableNode';

View File

@@ -0,0 +1,106 @@
import React, { useMemo } from 'react';
import { Button } from '@/components/button/button';
import { ListCollapse } from 'lucide-react';
import { Input } from '@/components/input/input';
import { DependencyList } from './dependency-list/dependency-list';
import { useChartDB } from '@/hooks/use-chartdb';
import { EmptyState } from '@/components/empty-state/empty-state';
import { ScrollArea } from '@/components/scroll-area/scroll-area';
import { useTranslation } from 'react-i18next';
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from '@/components/tooltip/tooltip';
import type { DBDependency } from '@/lib/domain/db-dependency';
import { shouldShowDependencyBySchemaFilter } from '@/lib/domain/db-dependency';
import { useLayout } from '@/hooks/use-layout';
export interface DependenciesSectionProps {}
export const DependenciesSection: React.FC<DependenciesSectionProps> = () => {
const { dependencies, filteredSchemas, getTable } = useChartDB();
const [filterText, setFilterText] = React.useState('');
const { closeAllDependenciesInSidebar } = useLayout();
const { t } = useTranslation();
const filteredDependencies = useMemo(() => {
const filterName: (dependency: DBDependency) => boolean = (
dependency
) => {
if (!filterText?.trim?.()) {
return true;
}
const tableName = getTable(dependency.tableId)?.name ?? '';
const dependentTableName =
getTable(dependency.dependentTableId)?.name ?? '';
return (
tableName.toLowerCase().includes(filterText.toLowerCase()) ||
dependentTableName
.toLowerCase()
.includes(filterText.toLowerCase())
);
};
const filterSchema: (dependency: DBDependency) => boolean = (
dependency
) => shouldShowDependencyBySchemaFilter(dependency, filteredSchemas);
return dependencies.filter(filterSchema).filter(filterName);
}, [dependencies, filterText, filteredSchemas, getTable]);
return (
<section className="flex flex-1 flex-col overflow-hidden px-2">
<div className="flex items-center justify-between gap-4 py-1">
<div>
<Tooltip>
<TooltipTrigger asChild>
<span>
<Button
variant="ghost"
className="size-8 p-0"
onClick={closeAllDependenciesInSidebar}
>
<ListCollapse className="size-4" />
</Button>
</span>
</TooltipTrigger>
<TooltipContent>
{t('side_panel.dependencies_section.collapse')}
</TooltipContent>
</Tooltip>
</div>
<div className="flex-1">
<Input
type="text"
placeholder={t(
'side_panel.dependencies_section.filter'
)}
className="h-8 w-full focus-visible:ring-0"
value={filterText}
onChange={(e) => setFilterText(e.target.value)}
/>
</div>
</div>
<div className="flex flex-1 flex-col overflow-hidden">
<ScrollArea className="h-full">
{dependencies.length === 0 ? (
<EmptyState
title={t(
'side_panel.dependencies_section.empty_state.title'
)}
description={t(
'side_panel.dependencies_section.empty_state.description'
)}
className="mt-20"
/>
) : (
<DependencyList dependencies={filteredDependencies} />
)}
</ScrollArea>
</div>
</section>
);
};

View File

@@ -0,0 +1,95 @@
import { Button } from '@/components/button/button';
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from '@/components/tooltip/tooltip';
import { useChartDB } from '@/hooks/use-chartdb';
import type { DBDependency } from '@/lib/domain/db-dependency';
import { useReactFlow } from '@xyflow/react';
import { FileMinus2, FileOutput, Trash2 } from 'lucide-react';
import React, { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
export interface DependencyListItemContentProps {
dependency: DBDependency;
}
export const DependencyListItemContent: React.FC<
DependencyListItemContentProps
> = ({ dependency }) => {
const { getTable, removeDependency } = useChartDB();
const { deleteElements } = useReactFlow();
const { t } = useTranslation();
const table = getTable(dependency.tableId);
const dependentTable = getTable(dependency.dependentTableId);
const deleteDependencyHandler = useCallback(() => {
removeDependency(dependency.id);
deleteElements({
edges: [{ id: dependency.id }],
});
}, [dependency.id, removeDependency, deleteElements]);
return (
<div className="my-1 flex flex-col rounded-b-md px-1">
<div className="flex flex-col gap-6">
<div className="flex items-center justify-between gap-1 text-xs">
<div className="flex basis-1/2 flex-col gap-2 overflow-hidden text-xs">
<div className="flex flex-row items-center gap-1">
<FileOutput className="size-4 text-subtitle" />
<div className="font-bold text-subtitle">
{t(
'side_panel.dependencies_section.dependency.table'
)}
</div>
</div>
<Tooltip>
<TooltipTrigger>
<div className="truncate text-left text-sm">
{table?.name}
</div>
</TooltipTrigger>
<TooltipContent>{table?.name}</TooltipContent>
</Tooltip>
</div>
<div className="flex basis-1/2 flex-col gap-2 overflow-hidden text-xs">
<div className="flex flex-row items-center gap-1">
<FileMinus2 className="size-4 text-subtitle" />
<div className="font-bold text-subtitle">
{t(
'side_panel.dependencies_section.dependency.dependent_table'
)}
</div>
</div>
<Tooltip>
<TooltipTrigger>
<div className="truncate text-left text-sm ">
{dependentTable?.name}
</div>
</TooltipTrigger>
<TooltipContent>
{dependentTable?.name}
</TooltipContent>
</Tooltip>
</div>
</div>
</div>
<div className="flex flex-1 items-center justify-center pt-2">
<Button
variant="ghost"
className="h-8 p-2 text-xs"
onClick={deleteDependencyHandler}
>
<Trash2 className="mr-1 size-3.5 text-red-700" />
<div className="text-red-700">
{t(
'side_panel.dependencies_section.dependency.delete_dependency'
)}
</div>
</Button>
</div>
</div>
);
};

View File

@@ -0,0 +1,144 @@
import React, { useCallback, useMemo } from 'react';
import { EllipsisVertical, CircleDotDashed, Trash2 } from 'lucide-react';
import { ListItemHeaderButton } from '../../../../list-item-header-button/list-item-header-button';
import { useReactFlow } from '@xyflow/react';
import { useChartDB } from '@/hooks/use-chartdb';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from '@/components/dropdown-menu/dropdown-menu';
import { useBreakpoint } from '@/hooks/use-breakpoint';
import { useTranslation } from 'react-i18next';
import type { DBDependency } from '@/lib/domain/db-dependency';
import { useLayout } from '@/hooks/use-layout';
export interface DependencyListItemHeaderProps {
dependency: DBDependency;
}
export const DependencyListItemHeader: React.FC<
DependencyListItemHeaderProps
> = ({ dependency }) => {
const { removeDependency, getTable } = useChartDB();
const { fitView, deleteElements, setEdges } = useReactFlow();
const { t } = useTranslation();
const { hideSidePanel } = useLayout();
const { isMd: isDesktop } = useBreakpoint('md');
const dependencyName = useMemo(() => {
const table = getTable(dependency.tableId);
const dependentTable = getTable(dependency.dependentTableId);
// should not happen
if (!table || !dependentTable) {
return '';
}
return `${table.name} -> ${dependentTable.name}`;
}, [dependency.tableId, dependency.dependentTableId, getTable]);
const focusOnDependency = useCallback(
(event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
event.stopPropagation();
setEdges((edges) =>
edges.map((edge) =>
edge.id == dependency.id
? {
...edge,
selected: true,
}
: {
...edge,
selected: false,
}
)
);
fitView({
duration: 500,
maxZoom: 1,
minZoom: 1,
nodes: [
{
id: dependency.tableId,
},
{
id: dependency.dependentTableId,
},
],
});
if (!isDesktop) {
hideSidePanel();
}
},
[
fitView,
dependency.tableId,
dependency.dependentTableId,
setEdges,
dependency.id,
isDesktop,
hideSidePanel,
]
);
const deleteDependencyHandler = useCallback(() => {
removeDependency(dependency.id);
deleteElements({
edges: [{ id: dependency.id }],
});
}, [dependency.id, removeDependency, deleteElements]);
const renderDropDownMenu = useCallback(
() => (
<DropdownMenu>
<DropdownMenuTrigger>
<ListItemHeaderButton>
<EllipsisVertical />
</ListItemHeaderButton>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-40">
<DropdownMenuLabel>
{t(
'side_panel.dependencies_section.dependency.dependency_actions.title'
)}
</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuItem
onClick={deleteDependencyHandler}
className="flex justify-between !text-red-700"
>
{t(
'side_panel.dependencies_section.dependency.dependency_actions.delete_dependency'
)}
<Trash2 className="size-3.5 text-red-700" />
</DropdownMenuItem>
</DropdownMenuGroup>
</DropdownMenuContent>
</DropdownMenu>
),
[deleteDependencyHandler, t]
);
return (
<div className="group flex h-11 flex-1 items-center justify-between overflow-hidden">
<div className="flex min-w-0 flex-1">
<div className="truncate">{dependencyName}</div>
</div>
<div className="flex flex-row-reverse">
<div>{renderDropDownMenu()}</div>
<div className="flex flex-row-reverse md:hidden md:group-hover:flex">
<ListItemHeaderButton onClick={focusOnDependency}>
<CircleDotDashed />
</ListItemHeaderButton>
</div>
</div>
</div>
);
};

View File

@@ -0,0 +1,34 @@
import React from 'react';
import {
AccordionContent,
AccordionItem,
AccordionTrigger,
} from '@/components/accordion/accordion';
import { DependencyListItemHeader } from './dependency-list-item-header/dependency-list-item-header';
import { DependencyListItemContent } from './dependency-list-item-content/dependency-list-item-content';
import type { DBDependency } from '@/lib/domain/db-dependency';
export interface DependencyListItemProps {
dependency: DBDependency;
}
export const DependencyListItem = React.forwardRef<
React.ElementRef<typeof AccordionItem>,
DependencyListItemProps
>(({ dependency }, ref) => {
return (
<AccordionItem value={dependency.id} className="rounded-md" ref={ref}>
<AccordionTrigger
asChild
className="w-full rounded-md px-2 py-0 hover:bg-accent hover:no-underline data-[state=open]:rounded-b-none"
>
<DependencyListItemHeader dependency={dependency} />
</AccordionTrigger>
<AccordionContent className="p-1 pb-0">
<DependencyListItemContent dependency={dependency} />
</AccordionContent>
</AccordionItem>
);
});
DependencyListItem.displayName = 'DependencyListItem';

View File

@@ -0,0 +1,63 @@
import React, { useCallback } from 'react';
import { Accordion } from '@/components/accordion/accordion';
import { useLayout } from '@/hooks/use-layout';
import type { DBDependency } from '@/lib/domain/db-dependency';
import { DependencyListItem } from './dependency-list-item/dependency-list-item';
export interface DependencyListProps {
dependencies: DBDependency[];
}
export const DependencyList: React.FC<DependencyListProps> = ({
dependencies,
}) => {
const { openDependencyFromSidebar, openedDependencyInSidebar } =
useLayout();
const lastOpenedDependency = React.useRef<string | null>(null);
const refs = dependencies.reduce(
(acc, dependency) => {
acc[dependency.id] = React.createRef();
return acc;
},
{} as Record<string, React.RefObject<HTMLDivElement>>
);
const scrollToDependency = useCallback(
(id: string) =>
refs[id]?.current?.scrollIntoView({
behavior: 'smooth',
block: 'start',
}),
[refs]
);
const handleScrollToDependency = useCallback(() => {
if (
openedDependencyInSidebar &&
lastOpenedDependency.current !== openedDependencyInSidebar
) {
lastOpenedDependency.current = openedDependencyInSidebar;
scrollToDependency(openedDependencyInSidebar);
}
}, [scrollToDependency, openedDependencyInSidebar]);
return (
<Accordion
type="single"
collapsible
className="flex w-full flex-col gap-1"
value={openedDependencyInSidebar}
onValueChange={openDependencyFromSidebar}
onAnimationEnd={handleScrollToDependency}
>
{dependencies.map((dependency) => (
<DependencyListItem
key={dependency.id}
dependency={dependency}
ref={refs[dependency.id]}
/>
))}
</Accordion>
);
};

View File

@@ -4,11 +4,11 @@ import { RelationshipListItem } from './relationship-list-item/relationship-list
import type { DBRelationship } from '@/lib/domain/db-relationship';
import { useLayout } from '@/hooks/use-layout';
export interface TableListProps {
export interface RelationshipListProps {
relationships: DBRelationship[];
}
export const RelationshipList: React.FC<TableListProps> = ({
export const RelationshipList: React.FC<RelationshipListProps> = ({
relationships,
}) => {
const { openRelationshipFromSidebar, openedRelationshipInSidebar } =

View File

@@ -15,6 +15,7 @@ import { useTranslation } from 'react-i18next';
import type { SelectBoxOption } from '@/components/select-box/select-box';
import { SelectBox } from '@/components/select-box/select-box';
import { useChartDB } from '@/hooks/use-chartdb';
import { DependenciesSection } from './dependencies-section/dependencies-section';
export interface SidePanelProps {}
@@ -104,14 +105,21 @@ export const SidePanel: React.FC<SidePanelProps> = () => {
'side_panel.relationships_section.relationships'
)}
</SelectItem>
<SelectItem value="dependencies">
{t(
'side_panel.dependencies_section.dependencies'
)}
</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
</div>
{selectedSidebarSection === 'tables' ? (
<TablesSection />
) : (
) : selectedSidebarSection === 'relationships' ? (
<RelationshipsSection />
) : (
<DependenciesSection />
)}
</aside>
);

View File

@@ -76,6 +76,8 @@ export const TopNavbar: React.FC<TopNavbarProps> = () => {
setScrollAction,
setShowCardinality,
showCardinality,
setShowDependenciesOnCanvas,
showDependenciesOnCanvas,
} = useLocalConfig();
const { effectiveTheme } = useTheme();
const { t, i18n } = useTranslation();
@@ -299,6 +301,10 @@ export const TopNavbar: React.FC<TopNavbarProps> = () => {
setShowCardinality(!showCardinality);
}, [showCardinality, setShowCardinality]);
const showOrHideDependencies = useCallback(() => {
setShowDependenciesOnCanvas(!showDependenciesOnCanvas);
}, [showDependenciesOnCanvas, setShowDependenciesOnCanvas]);
const emojiAI = '✨';
const changeLanguage = useCallback(
@@ -604,11 +610,17 @@ export const TopNavbar: React.FC<TopNavbarProps> = () => {
? t('menu.view.hide_sidebar')
: t('menu.view.show_sidebar')}
</MenubarItem>
<MenubarSeparator />
<MenubarItem onClick={showOrHideCardinality}>
{showCardinality
? t('menu.view.hide_cardinality')
: t('menu.view.show_cardinality')}
</MenubarItem>
<MenubarItem onClick={showOrHideDependencies}>
{showDependenciesOnCanvas
? t('menu.view.hide_dependencies')
: t('menu.view.show_dependencies')}
</MenubarItem>
<MenubarSeparator />
<MenubarSub>
<MenubarSubTrigger>

View File

@@ -78,6 +78,7 @@ export const examples: Example[] = [
],
color: '#b067e9',
isView: false,
isMaterializedView: false,
createdAt: Date.now(),
},
{
@@ -150,6 +151,7 @@ export const examples: Example[] = [
],
color: '#8a61f5',
isView: false,
isMaterializedView: false,
createdAt: Date.now(),
},
{
@@ -222,6 +224,7 @@ export const examples: Example[] = [
],
color: '#ff6363',
isView: false,
isMaterializedView: false,
createdAt: Date.now(),
},
{
@@ -302,6 +305,7 @@ export const examples: Example[] = [
],
color: '#4dee8a',
isView: false,
isMaterializedView: false,
createdAt: Date.now(),
},
{
@@ -377,6 +381,7 @@ export const examples: Example[] = [
],
color: '#ff6b8a',
isView: false,
isMaterializedView: false,
createdAt: Date.now(),
},
{
@@ -450,6 +455,7 @@ export const examples: Example[] = [
],
color: '#b067e9',
isView: false,
isMaterializedView: false,
createdAt: Date.now(),
},
{
@@ -500,6 +506,7 @@ export const examples: Example[] = [
indexes: [],
color: '#b0b0b0',
isView: true,
isMaterializedView: false,
createdAt: Date.now(),
},
{
@@ -539,6 +546,7 @@ export const examples: Example[] = [
indexes: [],
color: '#b0b0b0',
isView: true,
isMaterializedView: false,
createdAt: Date.now(),
},
],
@@ -671,6 +679,7 @@ export const examples: Example[] = [
],
color: '#8a61f5',
isView: false,
isMaterializedView: false,
createdAt: Date.now(),
},
{
@@ -712,6 +721,7 @@ export const examples: Example[] = [
],
color: '#4dee8a',
isView: false,
isMaterializedView: false,
createdAt: Date.now(),
},
{
@@ -837,6 +847,7 @@ export const examples: Example[] = [
],
color: '#7175fa',
isView: false,
isMaterializedView: false,
createdAt: Date.now(),
},
{
@@ -923,6 +934,7 @@ export const examples: Example[] = [
],
color: '#9ef07a',
isView: false,
isMaterializedView: false,
createdAt: Date.now(),
},
{
@@ -1015,6 +1027,7 @@ export const examples: Example[] = [
],
color: '#4dee8a',
isView: false,
isMaterializedView: false,
createdAt: Date.now(),
},
{
@@ -1094,6 +1107,7 @@ export const examples: Example[] = [
],
color: '#4dee8a',
isView: false,
isMaterializedView: false,
createdAt: Date.now(),
},
{
@@ -1198,6 +1212,7 @@ export const examples: Example[] = [
],
color: '#7175fa',
isView: false,
isMaterializedView: false,
createdAt: Date.now(),
},
{
@@ -1252,6 +1267,7 @@ export const examples: Example[] = [
],
color: '#c05dcf',
isView: false,
isMaterializedView: false,
createdAt: Date.now(),
},
{
@@ -1365,6 +1381,7 @@ export const examples: Example[] = [
],
color: '#ff9f74',
isView: false,
isMaterializedView: false,
createdAt: Date.now(),
},
],
@@ -1580,6 +1597,7 @@ export const examples: Example[] = [
],
color: '#4dee8a',
isView: false,
isMaterializedView: false,
createdAt: Date.now(),
},
{
@@ -1705,6 +1723,7 @@ export const examples: Example[] = [
],
color: '#8a61f5',
isView: false,
isMaterializedView: false,
createdAt: Date.now(),
},
{
@@ -1762,6 +1781,7 @@ export const examples: Example[] = [
],
color: '#8eb7ff',
isView: false,
isMaterializedView: false,
createdAt: Date.now(),
},
{
@@ -1834,6 +1854,7 @@ export const examples: Example[] = [
],
color: '#42e0c0',
isView: false,
isMaterializedView: false,
createdAt: Date.now(),
},
{
@@ -1891,6 +1912,7 @@ export const examples: Example[] = [
],
color: '#c05dcf',
isView: false,
isMaterializedView: false,
createdAt: Date.now(),
},
{
@@ -2042,6 +2064,7 @@ export const examples: Example[] = [
],
color: '#8a61f5',
isView: false,
isMaterializedView: false,
createdAt: Date.now(),
},
{
@@ -2217,6 +2240,7 @@ export const examples: Example[] = [
],
color: '#7175fa',
isView: false,
isMaterializedView: false,
createdAt: Date.now(),
},
{
@@ -2282,6 +2306,7 @@ export const examples: Example[] = [
],
color: '#8a61f5',
isView: false,
isMaterializedView: false,
createdAt: Date.now(),
},
{
@@ -2340,6 +2365,7 @@ export const examples: Example[] = [
],
color: '#42e0c0',
isView: false,
isMaterializedView: false,
createdAt: Date.now(),
},
{
@@ -2416,6 +2442,7 @@ export const examples: Example[] = [
],
color: '#b067e9',
isView: false,
isMaterializedView: false,
createdAt: Date.now(),
},
{
@@ -2470,6 +2497,7 @@ export const examples: Example[] = [
],
color: '#7175fa',
isView: false,
isMaterializedView: false,
createdAt: Date.now(),
},
{
@@ -2572,6 +2600,7 @@ export const examples: Example[] = [
],
color: '#9ef07a',
isView: false,
isMaterializedView: false,
createdAt: Date.now(),
},
{
@@ -2695,6 +2724,7 @@ export const examples: Example[] = [
],
color: '#8a61f5',
isView: false,
isMaterializedView: false,
createdAt: Date.now(),
},
{
@@ -2840,6 +2870,7 @@ export const examples: Example[] = [
],
color: '#ff6b8a',
isView: false,
isMaterializedView: false,
createdAt: Date.now(),
},
{
@@ -2908,6 +2939,7 @@ export const examples: Example[] = [
],
color: '#8eb7ff',
isView: false,
isMaterializedView: false,
createdAt: Date.now(),
},
],