mirror of https://github.com/nocodb/nocodb
Raju Udava
2 years ago
committed by
GitHub
25 changed files with 2250 additions and 37 deletions
@ -0,0 +1,5 @@
|
||||
<template> |
||||
<div class="w-full h-full !py-0 !px-2" style="height: 70vh"> |
||||
<ErdView /> |
||||
</div> |
||||
</template> |
@ -0,0 +1,227 @@
|
||||
<script setup lang="ts"> |
||||
import type { Edge, Node } from '@braks/vue-flow' |
||||
import { Background, Controls, VueFlow, useVueFlow } from '@braks/vue-flow' |
||||
import type { ColumnType, FormulaType, LinkToAnotherRecordType, LookupType, RollupType } from 'nocodb-sdk' |
||||
import { UITypes } from 'nocodb-sdk' |
||||
import dagre from 'dagre' |
||||
import TableNode from './TableNode.vue' |
||||
import RelationEdge from './RelationEdge.vue' |
||||
|
||||
interface Props { |
||||
tables: any[] |
||||
config: { |
||||
showPkAndFk: boolean |
||||
showViews: boolean |
||||
showAllColumns: boolean |
||||
singleTableMode: boolean |
||||
showJunctionTableNames: boolean |
||||
} |
||||
} |
||||
|
||||
const { tables, config } = defineProps<Props>() |
||||
|
||||
const { metasWithIdAsKey } = useMetas() |
||||
|
||||
const { $destroy, fitView } = useVueFlow() |
||||
|
||||
const nodes = ref<Node[]>([]) |
||||
const edges = ref<Edge[]>([]) |
||||
|
||||
let dagreGraph: dagre.graphlib.Graph |
||||
|
||||
const initDagre = () => { |
||||
dagreGraph = new dagre.graphlib.Graph() |
||||
dagreGraph.setDefaultEdgeLabel(() => ({})) |
||||
dagreGraph.setGraph({ rankdir: 'LR' }) |
||||
} |
||||
|
||||
const populateInitialNodes = () => { |
||||
nodes.value = tables.flatMap((table) => { |
||||
if (!table.id) return [] |
||||
|
||||
const columns = |
||||
metasWithIdAsKey.value[table.id].columns?.filter( |
||||
(col) => config.showAllColumns || (!config.showAllColumns && col.uidt === UITypes.LinkToAnotherRecord), |
||||
) || [] |
||||
|
||||
dagreGraph.setNode(table.id, { width: 250, height: 50 * columns.length }) |
||||
|
||||
return [ |
||||
{ |
||||
id: table.id, |
||||
data: { ...metasWithIdAsKey.value[table.id], showPkAndFk: config.showPkAndFk, showAllColumns: config.showAllColumns }, |
||||
type: 'custom', |
||||
position: { x: 0, y: 0 }, |
||||
}, |
||||
] |
||||
}) |
||||
} |
||||
|
||||
const populateEdges = () => { |
||||
const ltarColumns = tables.reduce<ColumnType[]>((acc, table) => { |
||||
const meta = metasWithIdAsKey.value[table.id!] |
||||
const columns = meta.columns?.filter( |
||||
(column: ColumnType) => column.uidt === UITypes.LinkToAnotherRecord && column.system !== 1, |
||||
) |
||||
|
||||
columns?.forEach((column: ColumnType) => { |
||||
if ((column.colOptions as LinkToAnotherRecordType)?.type === 'hm') { |
||||
acc.push(column) |
||||
} |
||||
|
||||
if ((column.colOptions as LinkToAnotherRecordType).type === 'mm') { |
||||
// Avoid duplicate mm connections |
||||
const correspondingColumn = acc.find( |
||||
(c) => |
||||
(c.colOptions as LinkToAnotherRecordType | FormulaType | RollupType | LookupType).type === 'mm' && |
||||
(c.colOptions as LinkToAnotherRecordType).fk_parent_column_id === |
||||
(column.colOptions as LinkToAnotherRecordType).fk_child_column_id && |
||||
(c.colOptions as LinkToAnotherRecordType).fk_child_column_id === |
||||
(column.colOptions as LinkToAnotherRecordType).fk_parent_column_id, |
||||
) |
||||
if (!correspondingColumn) { |
||||
acc.push(column) |
||||
} |
||||
} |
||||
}) |
||||
|
||||
return acc |
||||
}, [] as ColumnType[]) |
||||
|
||||
const edgeMMTableLabel = (modelId: string) => { |
||||
const mmModel = metasWithIdAsKey.value[modelId] |
||||
if (mmModel.title !== mmModel.table_name) { |
||||
return `${mmModel.title} (${mmModel.table_name})` |
||||
} |
||||
return mmModel.title |
||||
} |
||||
|
||||
edges.value = ltarColumns.map((column) => { |
||||
const source = column.fk_model_id! |
||||
const target = (column.colOptions as LinkToAnotherRecordType).fk_related_model_id! |
||||
|
||||
let sourceColumnId, targetColumnId |
||||
let edgeLabel = '' |
||||
|
||||
if ((column.colOptions as LinkToAnotherRecordType).type === 'hm') { |
||||
sourceColumnId = (column.colOptions as LinkToAnotherRecordType).fk_child_column_id |
||||
targetColumnId = (column.colOptions as LinkToAnotherRecordType).fk_child_column_id |
||||
} |
||||
|
||||
if ((column.colOptions as LinkToAnotherRecordType).type === 'mm') { |
||||
sourceColumnId = (column.colOptions as LinkToAnotherRecordType).fk_parent_column_id |
||||
targetColumnId = (column.colOptions as LinkToAnotherRecordType).fk_child_column_id |
||||
edgeLabel = config.showJunctionTableNames |
||||
? edgeMMTableLabel((column.colOptions as LinkToAnotherRecordType).fk_mm_model_id!) |
||||
: '' |
||||
} |
||||
|
||||
if (source !== target) dagreGraph.setEdge(source, target) |
||||
|
||||
return { |
||||
id: `e-${sourceColumnId}-${source}-${targetColumnId}-${target}-#${edgeLabel}`, |
||||
source: `${source}`, |
||||
target: `${target}`, |
||||
sourceHandle: `s-${sourceColumnId}-${source}`, |
||||
targetHandle: `d-${targetColumnId}-${target}`, |
||||
type: 'custom', |
||||
data: { |
||||
column, |
||||
isSelfRelation: source === target && sourceColumnId === targetColumnId, |
||||
label: edgeLabel, |
||||
}, |
||||
} |
||||
}) |
||||
} |
||||
|
||||
const connectNonConnectedNodes = () => { |
||||
const connectedNodes = new Set<string>() |
||||
|
||||
edges.value.forEach((edge) => { |
||||
connectedNodes.add(edge.source) |
||||
connectedNodes.add(edge.target) |
||||
}) |
||||
|
||||
const nonConnectedNodes = tables.filter((table) => !connectedNodes.has(table.id!)) |
||||
|
||||
if (nonConnectedNodes.length === 0) return |
||||
|
||||
if (nonConnectedNodes.length === 1) { |
||||
const firstTable = tables.find((table) => table.type === 'table' && table.id !== nonConnectedNodes[0].id) |
||||
if (!firstTable) return |
||||
|
||||
dagreGraph.setEdge(nonConnectedNodes[0].id, firstTable.id) |
||||
return |
||||
} |
||||
|
||||
const firstNode = nonConnectedNodes[0] |
||||
nonConnectedNodes.forEach((node, index) => { |
||||
if (index === 0) return |
||||
|
||||
const source = firstNode.id |
||||
const target = node.id |
||||
|
||||
dagreGraph.setEdge(source, target) |
||||
}) |
||||
} |
||||
|
||||
const layoutNodes = () => { |
||||
if (!config.singleTableMode) connectNonConnectedNodes() |
||||
|
||||
dagre.layout(dagreGraph) |
||||
|
||||
nodes.value = nodes.value.flatMap((node) => { |
||||
const nodeWithPosition = dagreGraph.node(node.id) |
||||
|
||||
if (!nodeWithPosition) return [] |
||||
|
||||
return [{ ...node, position: { x: nodeWithPosition.x, y: nodeWithPosition.y } } as Node] |
||||
}) |
||||
} |
||||
|
||||
const init = () => { |
||||
initDagre() |
||||
populateInitialNodes() |
||||
populateEdges() |
||||
layoutNodes() |
||||
|
||||
setTimeout(() => fitView({ duration: 300 })) |
||||
} |
||||
|
||||
init() |
||||
|
||||
onScopeDispose($destroy) |
||||
|
||||
watch([() => tables, () => config], init, { deep: true, flush: 'pre' }) |
||||
</script> |
||||
|
||||
<template> |
||||
<VueFlow :nodes="nodes" :edges="edges" elevate-edges-on-select> |
||||
<Controls class="!left-auto right-2 !top-3.5 !bottom-auto" :show-fit-view="false" :show-interactive="false" /> |
||||
|
||||
<template #node-custom="props"> |
||||
<TableNode :data="props.data" /> |
||||
</template> |
||||
|
||||
<template #edge-custom="props"> |
||||
<RelationEdge v-bind="props" /> |
||||
</template> |
||||
|
||||
<Background /> |
||||
|
||||
<div |
||||
v-if="!config.singleTableMode" |
||||
class="absolute bottom-0 right-0 flex flex-col text-xs bg-white px-2 py-1 border-1 rounded-md border-gray-200 z-50 nc-erd-histogram" |
||||
style="font-size: 0.6rem" |
||||
> |
||||
<div class="flex flex-row items-center space-x-1 border-b-1 pb-1 border-gray-100"> |
||||
<MdiTableLarge class="text-primary" /> |
||||
<div>{{ $t('objects.table') }}</div> |
||||
</div> |
||||
<div class="flex flex-row items-center space-x-1 pt-1"> |
||||
<MdiEyeCircleOutline class="text-primary" /> |
||||
<div>{{ $t('objects.sqlVIew') }}</div> |
||||
</div> |
||||
</div> |
||||
</VueFlow> |
||||
</template> |
@ -0,0 +1,161 @@
|
||||
<script setup> |
||||
import { EdgeText, getBezierPath, getEdgeCenter } from '@braks/vue-flow' |
||||
import { computed } from 'vue' |
||||
|
||||
const props = defineProps({ |
||||
id: { |
||||
type: String, |
||||
required: true, |
||||
}, |
||||
sourceX: { |
||||
type: Number, |
||||
required: true, |
||||
}, |
||||
sourceY: { |
||||
type: Number, |
||||
required: true, |
||||
}, |
||||
targetX: { |
||||
type: Number, |
||||
required: true, |
||||
}, |
||||
targetY: { |
||||
type: Number, |
||||
required: true, |
||||
}, |
||||
sourcePosition: { |
||||
type: String, |
||||
required: true, |
||||
}, |
||||
targetPosition: { |
||||
type: String, |
||||
required: true, |
||||
}, |
||||
data: { |
||||
type: Object, |
||||
required: false, |
||||
}, |
||||
markerEnd: { |
||||
type: String, |
||||
required: false, |
||||
}, |
||||
style: { |
||||
type: Object, |
||||
required: false, |
||||
}, |
||||
sourceHandleId: { |
||||
type: String, |
||||
required: false, |
||||
}, |
||||
targetHandleId: { |
||||
type: String, |
||||
required: false, |
||||
}, |
||||
}) |
||||
|
||||
const data = toRef(props, 'data') |
||||
|
||||
const isManyToMany = computed(() => data.value.column?.colOptions?.type === 'mm') |
||||
|
||||
const edgePath = computed(() => { |
||||
if (data.value.isSelfRelation) { |
||||
const { sourceX, sourceY, targetX, targetY } = props |
||||
const radiusX = (sourceX - targetX) * 0.6 |
||||
const radiusY = 50 |
||||
return `M ${sourceX} ${sourceY} A ${radiusX} ${radiusY} 0 1 0 ${targetX} ${targetY}` |
||||
} |
||||
|
||||
return getBezierPath({ |
||||
sourceX: props.sourceX, |
||||
sourceY: props.sourceY, |
||||
sourcePosition: props.sourcePosition, |
||||
targetX: props.targetX, |
||||
targetY: props.targetY, |
||||
targetPosition: props.targetPosition, |
||||
}) |
||||
}) |
||||
|
||||
const center = computed(() => |
||||
getEdgeCenter({ |
||||
sourceX: props.sourceX, |
||||
sourceY: props.sourceY, |
||||
targetX: props.targetX, |
||||
targetY: props.targetY, |
||||
}), |
||||
) |
||||
</script> |
||||
|
||||
<script> |
||||
export default { |
||||
inheritAttrs: false, |
||||
} |
||||
</script> |
||||
|
||||
<template> |
||||
<path |
||||
:id="id" |
||||
:style="style" |
||||
class="path-wrapper p-4 hover:cursor-pointer" |
||||
:stroke-width="8" |
||||
fill="none" |
||||
:d="edgePath" |
||||
:marker-end="markerEnd" |
||||
/> |
||||
<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" |
||||
/> |
||||
|
||||
<EdgeText |
||||
v-if="data.label?.length > 0" |
||||
:class="`nc-erd-table-label-${data.label.toLowerCase().replace(' ', '-').replace('\(', '').replace(')', '')}`" |
||||
:x="center[0]" |
||||
:y="center[1]" |
||||
:label="data.label" |
||||
:label-style="{ fill: 'white' }" |
||||
:label-show-bg="true" |
||||
:label-bg-style="{ fill: '#10b981' }" |
||||
:label-bg-padding="[2, 4]" |
||||
:label-bg-border-radius="2" |
||||
/> |
||||
|
||||
<rect |
||||
class="nc-erd-edge-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" |
||||
class="nc-erd-edge-rect" |
||||
: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 class="nc-erd-edge-circle" :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> |
@ -0,0 +1,121 @@
|
||||
<script lang="ts" setup> |
||||
import type { NodeProps } from '@braks/vue-flow' |
||||
import { Handle, Position } from '@braks/vue-flow' |
||||
import type { ColumnType, TableType } from 'nocodb-sdk' |
||||
import { UITypes, isVirtualCol } from 'nocodb-sdk' |
||||
import type { Ref } from 'vue' |
||||
|
||||
interface Props extends NodeProps { |
||||
data: TableType & { showPkAndFk: boolean; showAllColumns: boolean } |
||||
} |
||||
|
||||
const props = defineProps<Props>() |
||||
|
||||
const { data } = toRefs(props) |
||||
|
||||
provide(MetaInj, data as Ref<TableType>) |
||||
|
||||
const { $e } = useNuxtApp() |
||||
|
||||
const columns = computed(() => { |
||||
// Hide hm ltar created for `mm` relations |
||||
return data.value.columns?.filter((col) => !(col.uidt === UITypes.LinkToAnotherRecord && col.system === 1)) |
||||
}) |
||||
|
||||
const pkAndFkColumns = computed(() => { |
||||
return columns.value |
||||
?.filter(() => data.value.showPkAndFk && data.value.showAllColumns) |
||||
.filter((col) => col.pk || col.uidt === UITypes.ForeignKey) |
||||
}) |
||||
|
||||
const nonPkColumns = computed(() => { |
||||
return columns.value |
||||
?.filter( |
||||
(col: ColumnType) => data.value.showAllColumns || (!data.value.showAllColumns && col.uidt === UITypes.LinkToAnotherRecord), |
||||
) |
||||
.filter((col: ColumnType) => !col.pk && col.uidt !== UITypes.ForeignKey) |
||||
}) |
||||
|
||||
const relatedColumnId = (col: Record<string, any>) => |
||||
col.colOptions.type === 'mm' ? col.colOptions.fk_parent_column_id : col.colOptions.fk_child_column_id |
||||
</script> |
||||
|
||||
<template> |
||||
<div |
||||
class="h-full flex flex-col min-w-16 bg-gray-50 rounded-lg border-1 nc-erd-table-node" |
||||
:class="`nc-erd-table-node-${data.table_name}`" |
||||
@click="$e('c:erd:node-click')" |
||||
> |
||||
<GeneralTooltip modifier-key="Alt"> |
||||
<template #title> {{ data.table_name }} </template> |
||||
<div |
||||
class="text-gray-600 text-md py-2 border-b-1 border-gray-200 rounded-t-lg w-full pr-3 pl-2 bg-gray-100 font-semibold flex flex-row items-center" |
||||
> |
||||
<MdiTableLarge v-if="data.type === 'table'" class="text-primary" /> |
||||
<MdiEyeCircleOutline v-else class="text-primary" /> |
||||
<div class="flex pl-1.5"> |
||||
{{ data.title }} |
||||
</div> |
||||
</div> |
||||
</GeneralTooltip> |
||||
<div> |
||||
<div |
||||
v-for="col in pkAndFkColumns" |
||||
:key="col.title" |
||||
class="w-full border-b-1 py-2 border-gray-100 keys" |
||||
:class="`nc-erd-table-node-${data.table_name}-column-${col.column_name}`" |
||||
> |
||||
<SmartsheetHeaderCell v-if="col" :column="col" :hide-menu="true" /> |
||||
</div> |
||||
|
||||
<div class="w-full mb-1"></div> |
||||
|
||||
<div v-for="(col, index) in nonPkColumns" :key="col.title"> |
||||
<div |
||||
class="w-full h-full flex items-center min-w-32 border-gray-100 py-2 px-1" |
||||
:class="index + 1 === nonPkColumns.length ? 'rounded-b-lg' : 'border-b-1'" |
||||
> |
||||
<div |
||||
v-if="col.uidt === UITypes.LinkToAnotherRecord" |
||||
class="flex relative w-full" |
||||
:class="`nc-erd-table-node-${data.table_name}-column-${col.title?.toLowerCase().replace(' ', '_')}`" |
||||
> |
||||
<Handle |
||||
:id="`s-${relatedColumnId(col)}-${data.id}`" |
||||
class="-right-4 opacity-0" |
||||
type="source" |
||||
:position="Position.Right" |
||||
/> |
||||
|
||||
<Handle |
||||
:id="`d-${relatedColumnId(col)}-${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" |
||||
:class="`nc-erd-table-node-${data.table_name}-column-${col.column_name}`" |
||||
/> |
||||
|
||||
<SmartsheetHeaderCell |
||||
v-else |
||||
:column="col" |
||||
:hide-menu="true" |
||||
:class="`nc-erd-table-node-${data.table_name}-column-${col.column_name}`" |
||||
/> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
</template> |
||||
|
||||
<style scoped lang="scss"> |
||||
.keys { |
||||
background-color: #f6f6f6; |
||||
} |
||||
</style> |
@ -0,0 +1,162 @@
|
||||
<script setup lang="ts"> |
||||
import type { LinkToAnotherRecordType, TableType } from 'nocodb-sdk' |
||||
import { UITypes } from 'nocodb-sdk' |
||||
|
||||
const { table } = defineProps<{ table?: TableType }>() |
||||
|
||||
const { includeM2M } = useGlobal() |
||||
|
||||
const { tables: projectTables } = useProject() |
||||
const tables = ref<TableType[]>([]) |
||||
const { metas, getMeta } = useMetas() |
||||
|
||||
let isLoading = $ref(true) |
||||
const showAdvancedOptions = ref(false) |
||||
|
||||
const config = ref({ |
||||
showPkAndFk: true, |
||||
showViews: false, |
||||
showAllColumns: true, |
||||
singleTableMode: !!table, |
||||
showMMTables: false, |
||||
showJunctionTableNames: false, |
||||
}) |
||||
|
||||
const loadMetaOfTablesNotInMetas = async (localTables: TableType[]) => { |
||||
await Promise.all( |
||||
localTables |
||||
.filter((table) => !metas.value[table.id!]) |
||||
.map(async (table) => { |
||||
await getMeta(table.id!) |
||||
}), |
||||
) |
||||
} |
||||
|
||||
const populateTables = async () => { |
||||
let localTables: TableType[] = [] |
||||
if (table) { |
||||
// if table is provided only get the table and its related tables |
||||
localTables = projectTables.value.filter( |
||||
(t) => |
||||
t.id === table.id || |
||||
table.columns?.find( |
||||
(column) => |
||||
column.uidt === UITypes.LinkToAnotherRecord && |
||||
(column.colOptions as LinkToAnotherRecordType)?.fk_related_model_id === t.id, |
||||
), |
||||
) |
||||
} else { |
||||
localTables = projectTables.value |
||||
} |
||||
|
||||
await loadMetaOfTablesNotInMetas(localTables) |
||||
|
||||
tables.value = localTables |
||||
.filter( |
||||
(t) => |
||||
// todo: table type is missing mm property in type definition |
||||
config.value.showMMTables || |
||||
(!config.value.showMMTables && !t.mm) || |
||||
// Show mm table if it's the selected table |
||||
t.id === table?.id, |
||||
) |
||||
.filter((t) => (!config.value.showViews && t.type !== 'view') || config.value.showViews) |
||||
|
||||
isLoading = false |
||||
} |
||||
|
||||
watch( |
||||
[config, metas], |
||||
async () => { |
||||
await populateTables() |
||||
}, |
||||
{ |
||||
deep: true, |
||||
}, |
||||
) |
||||
|
||||
watch( |
||||
[projectTables], |
||||
async () => { |
||||
await populateTables() |
||||
}, |
||||
{ immediate: true }, |
||||
) |
||||
|
||||
watch( |
||||
() => config.value.showAllColumns, |
||||
() => { |
||||
config.value.showPkAndFk = config.value.showAllColumns |
||||
}, |
||||
) |
||||
</script> |
||||
|
||||
<template> |
||||
<div |
||||
class="w-full" |
||||
style="height: inherit" |
||||
:class="{ |
||||
'nc-erd-vue-flow': !config.singleTableMode, |
||||
'nc-erd-vue-flow-single-table': config.singleTableMode, |
||||
}" |
||||
> |
||||
<div v-if="isLoading" class="h-full w-full flex flex-col justify-center items-center"> |
||||
<div class="flex flex-row justify-center"> |
||||
<a-spin size="large" /> |
||||
</div> |
||||
</div> |
||||
<div v-else class="relative h-full"> |
||||
<ErdFlow :tables="tables" :config="config" /> |
||||
|
||||
<div |
||||
class="absolute top-2 right-10 flex-col bg-white py-2 px-4 border-1 border-gray-100 rounded-md z-50 space-y-1 nc-erd-context-menu z-50" |
||||
> |
||||
<div class="flex flex-row items-center"> |
||||
<a-checkbox |
||||
v-model:checked="config.showAllColumns" |
||||
v-e="['c:erd:showAllColumns']" |
||||
class="nc-erd-showColumns-checkbox" |
||||
/> |
||||
<span |
||||
class="ml-2 select-none nc-erd-showColumns-label" |
||||
style="font-size: 0.65rem" |
||||
@dblclick="showAdvancedOptions = true" |
||||
> |
||||
{{ $t('activity.erd.showColumns') }} |
||||
</span> |
||||
</div> |
||||
<div class="flex flex-row items-center"> |
||||
<a-checkbox |
||||
v-model:checked="config.showPkAndFk" |
||||
v-e="['c:erd:showPkAndFk']" |
||||
class="nc-erd-showPkAndFk-checkbox" |
||||
:class="{ |
||||
'nc-erd-showPkAndFk-checkbox-enabled': config.showAllColumns, |
||||
'nc-erd-showPkAndFk-checkbox-disabled': !config.showAllColumns, |
||||
'nc-erd-showPkAndFk-checkbox-checked': config.showPkAndFk, |
||||
'nc-erd-showPkAndFk-checkbox-unchecked': !config.showPkAndFk, |
||||
}" |
||||
:disabled="!config.showAllColumns" |
||||
/> |
||||
<span class="ml-2 select-none text-[0.65rem]">{{ $t('activity.erd.showPkAndFk') }}</span> |
||||
</div> |
||||
<div v-if="!table" class="flex flex-row items-center"> |
||||
<a-checkbox v-model:checked="config.showViews" v-e="['c:erd:showViews']" class="nc-erd-showViews-checkbox" /> |
||||
<span class="ml-2 select-none text-[0.65rem]">{{ $t('activity.erd.showSqlViews') }}</span> |
||||
</div> |
||||
<div v-if="!table && showAdvancedOptions && includeM2M" class="flex flex-row items-center"> |
||||
<a-checkbox v-model:checked="config.showMMTables" v-e="['c:erd:showMMTables']" class="nc-erd-showMMTables-checkbox" /> |
||||
<span class="ml-2 select-none text-[0.65rem]">{{ $t('activity.erd.showMMTables') }}</span> |
||||
</div> |
||||
<div v-if="showAdvancedOptions && includeM2M" class="flex flex-row items-center"> |
||||
<a-checkbox |
||||
v-model:checked="config.showJunctionTableNames" |
||||
v-e="['c:erd:showJunctionTableNames']" |
||||
class="nc-erd-showJunctionTableNames-checkbox" |
||||
/> |
||||
<span class="ml-2 select-none text-[0.65rem]">{{ $t('activity.erd.showJunctionTableNames') }}</span> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
</template> |
@ -0,0 +1,42 @@
|
||||
<script lang="ts" setup> |
||||
const props = defineProps<Props>() |
||||
|
||||
const emits = defineEmits(['update:modelValue']) |
||||
|
||||
const meta = inject(MetaInj) |
||||
|
||||
interface Props { |
||||
modelValue: boolean |
||||
} |
||||
|
||||
const vModel = useVModel(props, 'modelValue', emits) |
||||
|
||||
const selectedView = inject(ActiveViewInj) |
||||
</script> |
||||
|
||||
<template> |
||||
<a-modal |
||||
v-model:visible="vModel" |
||||
size="small" |
||||
:footer="null" |
||||
width="max(900px,60vw)" |
||||
:closable="false" |
||||
wrap-class-name="erd-single-table-modal" |
||||
transition-name="fade" |
||||
> |
||||
<div class="flex flex-row justify-between w-full items-center mb-1"> |
||||
<a-typography-title class="ml-4 select-none" type="secondary" :level="5"> |
||||
{{ `${$t('title.erdView')}: ${selectedView?.title}` }} |
||||
</a-typography-title> |
||||
|
||||
<a-button type="text" class="!rounded-md border-none -mt-1.5 -mr-1" @click="vModel = false"> |
||||
<template #icon> |
||||
<MdiClose class="cursor-pointer mt-1 nc-modal-close" /> |
||||
</template> |
||||
</a-button> |
||||
</div> |
||||
<div class="w-full h-full !py-0 !px-2" style="height: 70vh"> |
||||
<ErdView :table="meta" /> |
||||
</div> |
||||
</a-modal> |
||||
</template> |
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,373 @@
|
||||
import { mainPage, settingsPage } from "../../support/page_objects/mainPage"; |
||||
import {loginPage, projectsPage} from "../../support/page_objects/navigation"; |
||||
import { isTestSuiteActive, sakilaSqlViews, sakilaTables } from "../../support/page_objects/projectConstants"; |
||||
|
||||
export const genTest = (apiType, dbType) => { |
||||
if (!isTestSuiteActive(apiType, dbType)) return; |
||||
|
||||
describe(`${apiType.toUpperCase()} ERD`, () => { |
||||
// before(() => {
|
||||
// loginPage.loginAndOpenProject(apiType, dbType);
|
||||
// cy.openTableTab("Country", 25);
|
||||
// cy.saveLocalStorage();
|
||||
// });
|
||||
|
||||
beforeEach(() => { |
||||
cy.restoreLocalStorage(); |
||||
}) |
||||
|
||||
afterEach(() => { |
||||
cy.saveLocalStorage(); |
||||
}) |
||||
|
||||
after(() => { |
||||
cy.restoreLocalStorage(); |
||||
cy.closeTableTab("Country"); |
||||
cy.saveLocalStorage(); |
||||
}); |
||||
|
||||
// Test cases
|
||||
|
||||
it(`Enable MM setting Open Table ERD`, () => { |
||||
cy.openTableTab("Country", 25); |
||||
mainPage.toggleShowMMSetting(); |
||||
|
||||
mainPage.openErdTab(); |
||||
mainPage.closeMetaTab(); |
||||
}); |
||||
|
||||
it(`Verify ERD Context menu in all table view`, () => { |
||||
mainPage.openErdTab(); |
||||
cy.get('.nc-erd-context-menu').should('be.visible'); |
||||
cy.get('.nc-erd-context-menu').get('.nc-erd-histogram').should('be.visible'); |
||||
cy.get('.nc-erd-context-menu').find('.ant-checkbox').should('have.length', 3); |
||||
cy.get('.nc-erd-context-menu').find('.ant-checkbox').eq(0).should('have.class', 'ant-checkbox-checked'); |
||||
cy.get('.nc-erd-context-menu').find('.ant-checkbox').eq(1).should('have.class', 'ant-checkbox-checked'); |
||||
cy.get('.nc-erd-context-menu').find('.ant-checkbox').eq(2).should('not.have.class', 'ant-checkbox-checked'); |
||||
|
||||
cy.get('.nc-erd-context-menu').find('.nc-erd-showColumns-label').dblclick(); |
||||
cy.get('.nc-erd-context-menu').find('.ant-checkbox').should('have.length', 5); |
||||
}); |
||||
|
||||
it("Verify ERD of all tables view and verify columns of actor and payment with default config", () => { |
||||
cy.get('.nc-erd-vue-flow').find('.nc-erd-table-node').should('have.length', 12) |
||||
cy.get('.nc-erd-vue-flow').find('.vue-flow__edge').should('have.length', 14) |
||||
cy.get('.nc-erd-vue-flow').find('.nc-erd-edge-circle').should('have.length', 11) |
||||
cy.get('.nc-erd-vue-flow').find('.nc-erd-edge-rect').should('have.length', 17) |
||||
|
||||
for(const tableName of sakilaTables) { |
||||
cy.get('.nc-erd-vue-flow').find(`.nc-erd-table-node-${tableName}`).should('exist'); |
||||
} |
||||
|
||||
// Actor table
|
||||
[ |
||||
'actor_id', |
||||
'first_name', |
||||
'last_name', |
||||
'last_update', |
||||
'film_list' |
||||
].forEach((colTitle) => { |
||||
cy.get('.nc-erd-vue-flow').find(`.nc-erd-table-node-actor`).find(`.nc-erd-table-node-actor-column-${colTitle}`).should('exist'); |
||||
}); |
||||
|
||||
// Payment table
|
||||
[ |
||||
'payment_id', |
||||
'customer_id', |
||||
'staff_id', |
||||
'rental_id', |
||||
'amount', |
||||
'payment_date', |
||||
'last_update', |
||||
'customer', |
||||
'rental', |
||||
'staff' |
||||
].forEach((colTitle) => { |
||||
cy.get('.nc-erd-vue-flow').find(`.nc-erd-table-node-payment`).find(`.nc-erd-table-node-payment-column-${colTitle}`).should('exist'); |
||||
}); |
||||
}); |
||||
|
||||
it("Verify ERD of all tables view and verify columns of actor and payment with default config with showAllColumn disabled", () => { |
||||
cy.get('.nc-erd-context-menu').get('.nc-erd-showColumns-checkbox').click(); |
||||
cy.get('.nc-erd-showPkAndFk-checkbox-disabled').should('exist'); |
||||
cy.get('.nc-erd-showPkAndFk-checkbox-unchecked').should('exist'); |
||||
|
||||
// Actor table
|
||||
[ |
||||
'film_list' |
||||
].forEach((colTitle) => { |
||||
cy.get('.nc-erd-vue-flow').find(`.nc-erd-table-node-actor`).find(`.nc-erd-table-node-actor-column-${colTitle}`).should('exist'); |
||||
}); |
||||
|
||||
// Payment table
|
||||
[ |
||||
'customer', |
||||
'rental', |
||||
'staff' |
||||
].forEach((colTitle) => { |
||||
cy.get('.nc-erd-vue-flow').find(`.nc-erd-table-node-payment`).find(`.nc-erd-table-node-payment-column-${colTitle}`).should('exist'); |
||||
}); |
||||
}); |
||||
|
||||
it("Verify ERD of all tables view and verify columns of actor and payment with default config with showPkAndFk disabled", () => { |
||||
// enable showAllColumn
|
||||
cy.get('.nc-erd-context-menu').get('.nc-erd-showColumns-checkbox').click(); |
||||
cy.get('.nc-erd-context-menu').get('.nc-erd-showPkAndFk-checkbox').click(); |
||||
|
||||
// Actor table
|
||||
[ |
||||
'last_name', |
||||
'last_update', |
||||
'film_list' |
||||
].forEach((colTitle) => { |
||||
cy.get('.nc-erd-vue-flow').find(`.nc-erd-table-node-actor`).find(`.nc-erd-table-node-actor-column-${colTitle}`).should('exist'); |
||||
}); |
||||
|
||||
// Payment table
|
||||
[ |
||||
'amount', |
||||
'payment_date', |
||||
'last_update', |
||||
'customer', |
||||
'rental', |
||||
'staff' |
||||
].forEach((colTitle) => { |
||||
cy.get('.nc-erd-vue-flow').find(`.nc-erd-table-node-payment`).find(`.nc-erd-table-node-payment-column-${colTitle}`).should('exist'); |
||||
}); |
||||
}); |
||||
|
||||
it("Verify ERD of all tables view with sql grid on and verify columns of ActorInfo", () => { |
||||
cy.get('.nc-erd-context-menu').get('.nc-erd-showViews-checkbox').click(); |
||||
|
||||
cy.get('.nc-erd-vue-flow').find('.nc-erd-table-node').should('have.length', 19) |
||||
cy.get('.nc-erd-vue-flow').find('.vue-flow__edge').should('have.length', 14) |
||||
cy.get('.nc-erd-vue-flow').find('.nc-erd-edge-circle').should('have.length', 11) |
||||
cy.get('.nc-erd-vue-flow').find('.nc-erd-edge-rect').should('have.length', 17) |
||||
|
||||
for(const tableName of sakilaTables) { |
||||
cy.get('.nc-erd-vue-flow').find(`.nc-erd-table-node-${tableName}`).should('exist'); |
||||
} |
||||
|
||||
for(const tableName of sakilaSqlViews) { |
||||
cy.get('.nc-erd-vue-flow').find(`.nc-erd-table-node-${tableName}`).should('exist'); |
||||
} |
||||
|
||||
// ActorInfo SQL View
|
||||
[ |
||||
'actor_id', |
||||
'first_name', |
||||
'last_name', |
||||
'film_info' |
||||
].forEach((colTitle) => { |
||||
cy.get('.nc-erd-vue-flow').find(`.nc-erd-table-node-actor_info`).find(`.nc-erd-table-node-actor_info-column-${colTitle}`).should('exist'); |
||||
}) |
||||
|
||||
}); |
||||
|
||||
it("Verify show MM tables", () => { |
||||
cy.get('.nc-erd-vue-flow').find(`.nc-erd-table-node-store`).should('not.exist'); |
||||
// disable showViews
|
||||
cy.get('.nc-erd-context-menu').get('.nc-erd-showViews-checkbox').click(); |
||||
cy.get('.nc-erd-context-menu').get('.nc-erd-showMMTables-checkbox').click(); |
||||
|
||||
cy.get('.nc-erd-vue-flow').find('.nc-erd-table-node').should('have.length', 16) |
||||
cy.get('.nc-erd-vue-flow').find('.vue-flow__edge').should('have.length', 26) |
||||
cy.get('.nc-erd-vue-flow').find('.nc-erd-edge-circle').should('have.length', 22) |
||||
cy.get('.nc-erd-vue-flow').find('.nc-erd-edge-rect').should('have.length', 30) |
||||
|
||||
// Check if store table is present
|
||||
cy.get('.nc-erd-vue-flow').find(`.nc-erd-table-node-store`).should('exist'); |
||||
}) |
||||
|
||||
it("Verify show junction table names", () => { |
||||
// disable showViews
|
||||
cy.get('.nc-erd-context-menu').get('.nc-erd-showJunctionTableNames-checkbox').click(); |
||||
|
||||
cy.get('.nc-erd-vue-flow').get('.nc-erd-table-label-filmactor-film_actor').should('exist'); |
||||
mainPage.closeMetaTab(); |
||||
}) |
||||
|
||||
it('Verify table ERD view of country', () => { |
||||
mainPage.openTableErdView(); |
||||
|
||||
cy.get('.nc-erd-vue-flow-single-table').find('.nc-erd-table-node').should('have.length', 2) |
||||
cy.get('.nc-erd-vue-flow-single-table').find('.vue-flow__edge').should('have.length', 1) |
||||
cy.get('.nc-erd-vue-flow-single-table').find('.nc-erd-edge-circle').should('have.length', 1) |
||||
cy.get('.nc-erd-vue-flow-single-table').find('.nc-erd-edge-rect').should('have.length', 1) |
||||
|
||||
const countryColumns = [ |
||||
'country_id', |
||||
'country', |
||||
'last_update', |
||||
'city_list' |
||||
] |
||||
|
||||
// Country table
|
||||
countryColumns.forEach((colTitle) => { |
||||
cy.get('.nc-erd-vue-flow-single-table').find(`.nc-erd-table-node-country`).find(`.nc-erd-table-node-country-column-${colTitle}`).should('exist'); |
||||
}); |
||||
|
||||
const cityColumns = [ |
||||
'city_id', |
||||
'city', |
||||
'last_update', |
||||
'country', |
||||
'address_list' |
||||
] |
||||
|
||||
// City table
|
||||
cityColumns.forEach((colTitle) => { |
||||
cy.get('.nc-erd-vue-flow-single-table').find(`.nc-erd-table-node-city`).find(`.nc-erd-table-node-city-column-${colTitle}`).should('exist'); |
||||
}); |
||||
}) |
||||
|
||||
it('Verify table ERD view of country showAllColumn disabled', () => { |
||||
cy.get('.nc-erd-vue-flow-single-table').within(() => { |
||||
cy.get('.nc-erd-context-menu').get('.nc-erd-showColumns-checkbox').click(); |
||||
cy.get('.nc-erd-showPkAndFk-checkbox-disabled').should('exist'); |
||||
cy.get('.nc-erd-showPkAndFk-checkbox-unchecked').should('exist'); |
||||
|
||||
const countryColumns = [ |
||||
'city_list' |
||||
] |
||||
|
||||
// Country table
|
||||
countryColumns.forEach((colTitle) => { |
||||
cy.get(`.nc-erd-table-node-country`).find(`.nc-erd-table-node-country-column-${colTitle}`).should('exist'); |
||||
}); |
||||
|
||||
const cityColumns = [ |
||||
'country', |
||||
'address_list' |
||||
] |
||||
|
||||
// City table
|
||||
cityColumns.forEach((colTitle) => { |
||||
cy.get(`.nc-erd-table-node-city`).find(`.nc-erd-table-node-city-column-${colTitle}`).should('exist'); |
||||
}); |
||||
|
||||
cy.get('.nc-erd-context-menu').get('.nc-erd-showColumns-checkbox').click(); |
||||
}) |
||||
}) |
||||
|
||||
it('Verify table ERD view of country show PK AND FK disabled', () => { |
||||
cy.get('.nc-erd-vue-flow-single-table').within(() => { |
||||
cy.get('.nc-erd-context-menu').get('.nc-erd-showPkAndFk-checkbox').click(); |
||||
|
||||
const countryColumns = [ |
||||
'country', |
||||
'last_update', |
||||
'city_list' |
||||
] |
||||
|
||||
// Country table
|
||||
countryColumns.forEach((colTitle) => { |
||||
cy.get(`.nc-erd-table-node-country`).find(`.nc-erd-table-node-country-column-${colTitle}`).should('exist'); |
||||
}); |
||||
|
||||
const cityColumns = [ |
||||
'city', |
||||
'last_update', |
||||
'country', |
||||
'address_list' |
||||
] |
||||
|
||||
// City table
|
||||
cityColumns.forEach((colTitle) => { |
||||
cy.get(`.nc-erd-table-node-city`).find(`.nc-erd-table-node-city-column-${colTitle}`).should('exist'); |
||||
}); |
||||
|
||||
cy.get('.nc-erd-context-menu').get('.nc-erd-showPkAndFk-checkbox').click(); |
||||
|
||||
}) |
||||
cy.getActiveModal().find('.nc-modal-close').click({ force: true }); |
||||
}) |
||||
|
||||
it('create column and check if the change is in the schema', () => { |
||||
mainPage.addColumn('test_column', 'country') |
||||
|
||||
// table view
|
||||
mainPage.openTableErdView(); |
||||
cy.get('.nc-erd-vue-flow-single-table').within(() => { |
||||
cy.get('.nc-erd-table-node-country').find('.nc-erd-table-node-country-column-test_column').should('exist'); |
||||
}) |
||||
cy.getActiveModal().find('.nc-modal-close').click({ force: true }); |
||||
|
||||
// All table view
|
||||
mainPage.openErdTab(); |
||||
cy.get('.nc-erd-vue-flow').within(() => { |
||||
cy.get('.nc-erd-table-node-country').find('.nc-erd-table-node-country-column-test_column').should('exist'); |
||||
}) |
||||
mainPage.closeMetaTab(); |
||||
|
||||
|
||||
mainPage.deleteColumn('test_column') |
||||
|
||||
// table view
|
||||
mainPage.openTableErdView(); |
||||
cy.get('.nc-erd-vue-flow-single-table').within(() => { |
||||
cy.get('.nc-erd-table-node-country').find('.nc-erd-table-node-country-column-test_column').should('not.exist'); |
||||
}) |
||||
cy.getActiveModal().find('.nc-modal-close').click({ force: true }); |
||||
|
||||
// All table view
|
||||
mainPage.openErdTab(); |
||||
cy.get('.nc-erd-vue-flow').within(() => { |
||||
cy.get('.nc-erd-table-node-country').find('.nc-erd-table-node-country-column-test_column').should('not.exist'); |
||||
}) |
||||
mainPage.closeMetaTab(); |
||||
}) |
||||
|
||||
it('Create table should reflected in ERD', () => { |
||||
cy.createTable('new') |
||||
|
||||
mainPage.openErdTab(); |
||||
cy.get('.nc-erd-vue-flow').within(() => { |
||||
cy.get('.nc-erd-table-node-new').should('exist'); |
||||
}) |
||||
mainPage.closeMetaTab(); |
||||
|
||||
cy.deleteTable('new') |
||||
|
||||
mainPage.openErdTab(); |
||||
cy.get('.nc-erd-vue-flow').within(() => { |
||||
cy.get('.nc-erd-table-node-new').should('not.exist'); |
||||
}) |
||||
mainPage.closeMetaTab(); |
||||
}) |
||||
|
||||
it(`Disable MM setting Open Table ERD and check easter egg should not work`, () => { |
||||
mainPage.toggleShowMMSetting(); |
||||
|
||||
mainPage.openErdTab(); |
||||
cy.get('.nc-erd-vue-flow').within(() => { |
||||
cy.get('.nc-erd-context-menu').find('.nc-erd-showColumns-label').dblclick(); |
||||
cy.get('.nc-erd-context-menu').find('.ant-checkbox').should('have.length', 3); |
||||
}) |
||||
|
||||
mainPage.closeMetaTab(); |
||||
}); |
||||
}); |
||||
}; |
||||
|
||||
/** |
||||
* @copyright Copyright (c) 2021, Xgene Cloud Ltd |
||||
* |
||||
* @author Raju Udava <sivadstala@gmail.com> |
||||
* |
||||
* @license GNU AGPL version 3 or any later version |
||||
* |
||||
* This program is free software: you can redistribute it and/or modify |
||||
* it under the terms of the GNU Affero General Public License as |
||||
* published by the Free Software Foundation, either version 3 of the |
||||
* License, or (at your option) any later version. |
||||
* |
||||
* This program is distributed in the hope that it will be useful, |
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||
* GNU Affero General Public License for more details. |
||||
* |
||||
* You should have received a copy of the GNU Affero General Public License |
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
* |
||||
*/ |
Loading…
Reference in new issue