mirror of
https://github.com/chartdb/chartdb.git
synced 2025-10-23 07:11:56 +00:00
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:
15140
package-lock.json
generated
15140
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -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",
|
||||
|
@@ -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,
|
||||
});
|
||||
|
@@ -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}
|
||||
|
@@ -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,
|
||||
]
|
||||
);
|
||||
|
||||
|
@@ -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,
|
||||
|
@@ -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,
|
||||
|
@@ -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}
|
||||
|
@@ -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,
|
||||
});
|
||||
|
@@ -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}
|
||||
|
@@ -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,
|
||||
});
|
||||
|
@@ -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}
|
||||
|
@@ -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]);
|
||||
|
||||
|
@@ -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: {
|
||||
|
@@ -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: {
|
||||
|
@@ -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: {
|
||||
|
@@ -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: {
|
||||
|
@@ -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: {
|
||||
|
@@ -17,3 +17,4 @@ export const randomColor = () => {
|
||||
};
|
||||
|
||||
export const greyColor = '#b0b0b0'; // A Cloudy Grey.
|
||||
export const blackColor = '#000'; // A Blacky black.
|
||||
|
@@ -1,4 +1,5 @@
|
||||
export interface ViewInfo {
|
||||
schema: string;
|
||||
view_name: string;
|
||||
view_definition?: string;
|
||||
}
|
||||
|
209
src/lib/domain/db-dependency.ts
Normal file
209
src/lib/domain/db-dependency.ts
Normal 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());
|
||||
}
|
@@ -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,
|
||||
};
|
||||
|
@@ -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(),
|
||||
};
|
||||
|
@@ -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'}
|
||||
>
|
||||
|
168
src/pages/editor-page/canvas/dependency-edge.tsx
Normal file
168
src/pages/editor-page/canvas/dependency-edge.tsx
Normal 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'}`}
|
||||
// />
|
||||
);
|
||||
};
|
@@ -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) {
|
@@ -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';
|
@@ -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(
|
||||
|
@@ -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';
|
||||
|
@@ -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>
|
||||
);
|
||||
};
|
@@ -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>
|
||||
);
|
||||
};
|
@@ -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>
|
||||
);
|
||||
};
|
@@ -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';
|
@@ -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>
|
||||
);
|
||||
};
|
@@ -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 } =
|
||||
|
@@ -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>
|
||||
);
|
||||
|
@@ -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>
|
||||
|
@@ -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(),
|
||||
},
|
||||
],
|
||||
|
Reference in New Issue
Block a user