Browse Source

feat/Added basic erd support

pull/3612/head
Muhammed Mustafa 2 years ago
parent
commit
63aceb7e9c
  1. 107
      packages/nc-gui/components/dashboard/settings/Erd.vue
  2. 84
      packages/nc-gui/components/dashboard/settings/erd/RelationEdge.vue
  3. 40
      packages/nc-gui/components/dashboard/settings/erd/TableNode.vue

107
packages/nc-gui/components/dashboard/settings/Erd.vue

@ -1,29 +1,29 @@
<script setup lang="ts">
import { useI18n } from 'vue-i18n'
import type { Edge, Node } from '@braks/vue-flow'
import { Background, MarkerType, VueFlow, isNode, useVueFlow } from '@braks/vue-flow'
import type { Edge } from '@braks/vue-flow'
import { Background, Controls, VueFlow, useVueFlow } from '@braks/vue-flow'
import { ref } from 'vue'
import { ColumnType, UITypes } from 'nocodb-sdk'
import { UITypes } from 'nocodb-sdk'
import dagre from 'dagre'
import TableNode from './erd/TableNode.vue'
import RelationEdge from './erd/RelationEdge.vue'
import { useNuxtApp, useProject } from '#imports'
import { useProject } from '#imports'
const dagreGraph = new dagre.graphlib.Graph()
dagreGraph.setDefaultEdgeLabel(() => ({}))
const { updateNodeInternals } = useVueFlow()
const { $api } = useNuxtApp()
const { project, tables } = useProject()
const { t } = useI18n()
const { updateNodeInternals, removeNodes, removeEdges, $reset } = useVueFlow()
const { tables } = useProject()
const { metas, getMeta, metasWithId } = useMetas()
const nodes = ref<Node[]>([])
const nodes = ref<any[]>([])
const edges = ref<Edge[]>([])
let isLoading = $ref(true)
const isLayouting = ref(false)
const config = ref({
showPkAndFk: true,
showViews: false,
})
const loadMetasOfTablesNotInMetas = async () => {
await Promise.all(
@ -36,24 +36,26 @@ const loadMetasOfTablesNotInMetas = async () => {
}
const populateTables = () => {
Object.keys(metasWithId.value).forEach((tableId) => {
tables.value.forEach((table) => {
if (!table?.id) return
nodes.value.push({
id: tableId,
data: metasWithId.value[tableId],
id: table.id,
data: { ...metasWithId.value[table.id], showPkAndFk: config.value.showPkAndFk },
type: 'custom',
})
dagreGraph.setNode(tableId, { width: 250, height: 30 * metasWithId.value[tableId].columns.length })
dagreGraph.setNode(table.id, { width: 250, height: 30 * metasWithId.value[table.id].columns.length })
})
dagreGraph.setGraph({ rankdir: 'LR' })
}
const populateRelations = () => {
const ltarColumns = Object.keys(metasWithId.value).reduce((acc, tableId) => {
const ltarColumns = Object.keys(metasWithId.value).reduce((acc: any[], tableId) => {
const table = metasWithId.value[tableId]
const ltarColumns = table.columns.filter((column) => column.uidt === UITypes.LinkToAnotherRecord)
const ltarColumns = table.columns.filter((column: any) => column.uidt === UITypes.LinkToAnotherRecord)
ltarColumns.forEach((column) => {
ltarColumns.forEach((column: any) => {
if (column.colOptions.type === 'hm') {
acc.push(column)
}
@ -73,6 +75,7 @@ const populateRelations = () => {
edges.value = ltarColumns.map((column: any) => {
const source = column.fk_model_id
const target = column.colOptions.fk_related_model_id
const targetColumnId = column.colOptions.fk_related_column_id
dagreGraph.setEdge(source, target)
@ -81,10 +84,9 @@ const populateRelations = () => {
source: `${source}`,
target: `${target}`,
sourceHandle: `s-${column.id}-${source}`,
targetHandle: `d-${column.id}-${target}`,
targetHandle: `d-${targetColumnId}-${target}`,
type: 'custom',
data: { column, table: metasWithId.value[source], relatedTable: metasWithId.value[target] },
markerEnd: MarkerType.ArrowClosed,
}
})
@ -107,27 +109,54 @@ const populateElements = () => {
populateTables()
}
const populateErd = () => {
isLayouting.value = true
populateElements()
populateRelations()
layoutNodes()
updateNodeInternals(nodes.value as any)
isLayouting.value = false
}
const resetElements = () => {
$reset()
updateNodeInternals(nodes.value as any)
removeNodes(nodes.value)
removeEdges(edges.value)
nodes.value = []
edges.value = []
dagreGraph.nodes().forEach((node: any) => dagreGraph.removeNode(node))
dagreGraph.edges().forEach((edge: any) => dagreGraph.removeEdge(edge))
}
onMounted(async () => {
if (isLoading) {
await loadMetasOfTablesNotInMetas()
isLoading = true
resetElements()
populateElements()
populateRelations()
layoutNodes()
await loadMetasOfTablesNotInMetas()
console.log('nodes', nodes.value)
console.log('edges', edges.value)
isLoading = false
populateErd()
})
updateNodeInternals(nodes.value as any)
isLoading = false
}
watch(config.value, () => {
populateErd()
})
</script>
<template>
<div style="height: 650px">
<div v-if="isLoading"></div>
<VueFlow v-else :nodes="nodes" :edges="edges" :fit-view-on-init="true" :default-edge-options="{ type: 'step' }">
<div v-if="isLoading" style="height: 650px"></div>
<div v-else class="relative" style="height: 650px">
<VueFlow
:key="config.toString()"
:nodes="nodes"
:edges="edges"
:fit-view-on-init="true"
:default-edge-options="{ type: 'step' }"
:elevate-edges-on-select="true"
>
<Controls :show-fit-view="false" :show-interactive="false" />
<template #node-custom="props">
<TableNode :data="props.data" />
</template>
@ -136,5 +165,15 @@ onMounted(async () => {
</template>
<Background />
</VueFlow>
<div class="absolute top-4 right-4 flex-col bg-white py-2 px-4 border-1 border-gray-100 rounded-md z-50 space-y-1">
<div class="flex flex-row items-center">
<a-checkbox v-model:checked="config.showPkAndFk" />
<span class="ml-2" style="font-size: 0.65rem">Show PK and FK</span>
</div>
<div class="flex flex-row items-center">
<a-checkbox v-model:checked="config.showViews" />
<span class="ml-2" style="font-size: 0.65rem">Show views</span>
</div>
</div>
</div>
</template>

84
packages/nc-gui/components/dashboard/settings/erd/RelationEdge.vue

@ -1,5 +1,5 @@
<script setup>
import { EdgeText, getBezierCenter, getBezierPath, getEdgeCenter } from '@braks/vue-flow'
import { getBezierPath } from '@braks/vue-flow'
import { computed } from 'vue'
const props = defineProps({
@ -53,8 +53,9 @@ const props = defineProps({
},
})
const isHovered = ref(false)
const { column, relatedTable, table } = props.data
const { column } = toRefs(props.data)
const isManyToMany = computed(() => column.value?.colOptions?.type === 'mm')
const edgePath = computed(() =>
getBezierPath({
@ -66,24 +67,6 @@ const edgePath = computed(() =>
targetPosition: props.targetPosition,
}),
)
const center = computed(() =>
getEdgeCenter({
sourceX: props.sourceX,
sourceY: props.sourceY,
sourcePosition: props.sourcePosition,
targetX: props.targetX,
targetY: props.targetY,
targetPosition: props.targetPosition,
}),
)
watch(
() => isHovered.value,
(val) => {
console.log(val)
},
)
</script>
<script>
@ -93,28 +76,55 @@ export default {
</script>
<template>
<circle :cx="sourceX" :cy="sourceY" fill="#fff" :r="5" stroke="#6F3381" :stroke-width="1.5" />
<path
:id="id"
:style="style"
class="stroke-gray-500 p-4 hover:stroke-green-500 hover:cursor-pointer"
:class="{ 'stroke-green-500': isHovered }"
:stroke-width="2.5"
class="path-wrapper p-4 hover:cursor-pointer"
:stroke-width="8"
fill="none"
:d="edgePath"
:marker-end="markerEnd"
onmouseover="isHovered = true"
onmouseout="isHovered = false"
/>
<EdgeText
:x="center[0]"
:y="center[1]"
label="Text"
:label-style="{ fill: 'white' }"
:label-show-bg="true"
:label-bg-style="{ fill: '#10b981' }"
:label-bg-padding="[2, 4]"
:label-bg-border-radius="2"
<path
:id="id"
:style="style"
class="path stroke-gray-500 hover:stroke-green-500 hover:cursor-pointer"
:stroke-width="1.5"
fill="none"
:d="edgePath"
:marker-end="markerEnd"
/>
<circle :cx="targetX" :cy="targetY" fill="#fff" :r="5" stroke="#6F3381" :stroke-width="1.5" />
<rect
:x="sourceX"
:y="sourceY - 4"
width="8"
height="8"
fill="#fff"
stroke="#6F3381"
:stroke-width="1.5"
:transform="`rotate(45,${sourceX + 2},${sourceY - 4})`"
/>
<rect
v-if="isManyToMany"
:x="targetX"
:y="targetY - 4"
width="8"
height="8"
fill="#fff"
stroke="#6F3381"
:stroke-width="1.5"
:transform="`rotate(45,${targetX + 2},${targetY - 4})`"
/>
<circle v-else :cx="targetX" :cy="targetY" fill="#fff" :r="5" stroke="#6F3381" :stroke-width="1.5" />
</template>
<style scoped lang="scss">
.path-wrapper:hover + .path {
@apply stroke-green-500;
stroke-width: 2;
}
.path:hover {
stroke-width: 2;
}
</style>

40
packages/nc-gui/components/dashboard/settings/erd/TableNode.vue

@ -9,12 +9,14 @@ const props = defineProps({
},
})
const { data: table } = props
const columns = table.columns
// console.log(table)
const { data } = toRefs(props)
const pkColumn = computed(() => {
return columns.find((col) => col.pk)
provide(MetaInj, data)
const columns = data.value.columns
const pkAndFkColumns = computed(() => {
return columns.filter(() => data.value.showPkAndFk).filter((col) => col.pk || col.uidt === UITypes.ForeignKey)
})
const nonPkColumns = computed(() => {
@ -25,23 +27,19 @@ const nonPkColumns = computed(() => {
<template>
<div class="h-full flex flex-col min-w-16 bg-gray-50 rounded-lg border-1">
<div class="text-gray-600 text-md py-2 border-b-2 border-gray-100 w-full pl-3 bg-gray-100 font-semibold">
{{ table.title }}
{{ data.title }}
</div>
<div class="mx-1">
<div class="w-full border-b-1 py-2 border-gray-100">
<SmartsheetHeaderCell v-if="pkColumn" :column="pkColumn" :hide-menu="true" />
<div>
<div class="keys mb-1">
<div v-for="col in pkAndFkColumns" :key="col.title" class="w-full border-b-1 py-2 border-gray-100">
<SmartsheetHeaderCell v-if="col" :column="col" :hide-menu="true" />
</div>
</div>
<div v-for="col in nonPkColumns" :key="col.title">
<div class="w-full h-full flex items-center min-w-32 border-b-1 border-gray-100 py-2">
<div class="w-full h-full flex items-center min-w-32 border-b-1 border-gray-100 py-2 px-1">
<div v-if="col.uidt === UITypes.LinkToAnotherRecord" class="flex relative w-full">
<Handle :id="`s-${col.id}-${table.id}`" class="-right-4" type="source" :position="Position.Right" :hidden="false" />
<Handle
:id="`d-${col.id}-${table.id}`"
class="-left-1"
type="destination"
:position="Position.Left"
:hidden="false"
/>
<Handle :id="`s-${col.id}-${data.id}`" class="-right-4 opacity-0" type="source" :position="Position.Right" />
<Handle :id="`d-${col.id}-${data.id}`" class="-left-1 opacity-0" type="target" :position="Position.Left" />
<SmartsheetHeaderVirtualCell :column="col" :hide-menu="true" />
</div>
<SmartsheetHeaderVirtualCell v-else-if="isVirtualCol(col)" :column="col" :hide-menu="true" />
@ -52,3 +50,9 @@ const nonPkColumns = computed(() => {
</div>
</div>
</template>
<style scoped lang="scss">
.keys {
background-color: #f6f6f6;
}
</style>

Loading…
Cancel
Save