1
1
<script setup lang="ts">
2
- import { ref , onMounted , onUnmounted } from ' vue'
2
+ import { ref , onMounted , onUnmounted , computed , watch } from ' vue'
3
3
import { useHead } from ' @vueuse/head'
4
4
import * as d3 from ' d3'
5
5
@@ -311,6 +311,71 @@ let simulation: d3.Simulation<ModelNode, undefined> | null = null
311
311
const downloadFormat = ref <' svg' | ' png' >(' svg' )
312
312
const isDownloading = ref (false )
313
313
314
+ // Add filter and search functionality
315
+ const searchQuery = ref (' ' )
316
+ const selectedModelType = ref (' All' )
317
+ const modelTypes = computed (() => {
318
+ const types = [' All' ]
319
+ const uniqueColors = new Set (models .map (model => model .color ))
320
+ uniqueColors .forEach (color => {
321
+ const modelsWithColor = models .filter (model => model .color === color )
322
+ if (modelsWithColor .length > 0 ) {
323
+ // Get the first model with this color to determine the type
324
+ const modelType = getModelTypeByColor (color )
325
+ if (modelType ) types .push (modelType )
326
+ }
327
+ })
328
+ return types
329
+ })
330
+
331
+ // Filter models based on search and type
332
+ const filteredModels = computed (() => {
333
+ return models .filter (model => {
334
+ const matchesSearch = searchQuery .value === ' ' ||
335
+ model .name .toLowerCase ().includes (searchQuery .value .toLowerCase ()) ||
336
+ model .properties .some (prop => prop .name .toLowerCase ().includes (searchQuery .value .toLowerCase ()))
337
+
338
+ const matchesType = selectedModelType .value === ' All' ||
339
+ getModelTypeByColor (model .color ) === selectedModelType .value
340
+
341
+ return matchesSearch && matchesType
342
+ })
343
+ })
344
+
345
+ // Get model type by color
346
+ function getModelTypeByColor(color : string ): string {
347
+ switch (color ) {
348
+ case colorPalette .primary :
349
+ return ' Authentication'
350
+ case colorPalette .secondary :
351
+ return ' Content'
352
+ case colorPalette .tertiary :
353
+ return ' Communication'
354
+ case colorPalette .quaternary :
355
+ return ' Commerce'
356
+ default :
357
+ return ' Other'
358
+ }
359
+ }
360
+
361
+ // Selected model for details panel
362
+ const selectedModel = ref <ModelNode | null >(null )
363
+
364
+ // Model statistics
365
+ const modelStats = computed (() => {
366
+ return {
367
+ totalModels: models .length ,
368
+ totalRelationships: relationships .length ,
369
+ totalProperties: models .reduce ((sum , model ) => sum + model .properties .length , 0 ),
370
+ modelsByType: {
371
+ Authentication: models .filter (m => m .color === colorPalette .primary ).length ,
372
+ Content: models .filter (m => m .color === colorPalette .secondary ).length ,
373
+ Communication: models .filter (m => m .color === colorPalette .tertiary ).length ,
374
+ Commerce: models .filter (m => m .color === colorPalette .quaternary ).length
375
+ }
376
+ }
377
+ })
378
+
314
379
// Update download function to support both formats
315
380
const downloadDiagram = async () => {
316
381
if (! diagramContainer .value ) {
@@ -487,7 +552,7 @@ const createDiagram = () => {
487
552
const g = svg .append (' g' )
488
553
489
554
// Apply initial zoom to see all content
490
- const initialScale = 0.6 // Increased zoom by 10%
555
+ const initialScale = 0.6
491
556
svg .call (zoom .transform , d3 .zoomIdentity .translate (width / 2 - width * initialScale / 2 , 10 ).scale (initialScale ))
492
557
493
558
// Set initial positions for models based on the reference image layout
@@ -498,7 +563,7 @@ const createDiagram = () => {
498
563
' post' : { x: width * 0.9 , y: - 200 },
499
564
500
565
// Second row - better distributed
501
- ' accessToken' : { x: width * 0.2 , y: - 200 }, // Further moved down to avoid overlapping
566
+ ' accessToken' : { x: width * 0.2 , y: - 200 },
502
567
' subscriber' : { x: width * 0.8 , y: 450 },
503
568
504
569
// Third row - more evenly spaced
@@ -537,7 +602,7 @@ const createDiagram = () => {
537
602
// Create nodes
538
603
const node = nodeGroup
539
604
.selectAll (' g' )
540
- .data (models )
605
+ .data (filteredModels . value )
541
606
.join (' g' )
542
607
.attr (' transform' , d => {
543
608
const x = d .posX || width / 2
@@ -579,6 +644,11 @@ const createDiagram = () => {
579
644
delete event .subject .dragOffsetX ;
580
645
delete event .subject .dragOffsetY ;
581
646
}))
647
+ .on (' click' , (event , d ) => {
648
+ // Set the selected model when clicked
649
+ event .stopPropagation () // Prevent bubbling
650
+ selectedModel .value = d
651
+ })
582
652
583
653
// Add shadow effect to nodes
584
654
node .append (' rect' )
@@ -773,8 +843,9 @@ const createDiagram = () => {
773
843
const sourceId = typeof rel .source === ' string' ? rel .source : rel .source .id
774
844
const targetId = typeof rel .target === ' string' ? rel .target : rel .target .id
775
845
776
- const sourceModel = models .find (m => m .id === sourceId )
777
- const targetModel = models .find (m => m .id === targetId )
846
+ // Only draw links for filtered models
847
+ const sourceModel = filteredModels .value .find (m => m .id === sourceId )
848
+ const targetModel = filteredModels .value .find (m => m .id === targetId )
778
849
779
850
if (sourceModel && targetModel && sourceModel .posX && sourceModel .posY && targetModel .posX && targetModel .posY ) {
780
851
// Calculate control points for the curve
@@ -917,8 +988,13 @@ const createDiagram = () => {
917
988
})
918
989
919
990
// Create force simulation with fixed positions
920
- simulation = d3 .forceSimulation <ModelNode >(models )
991
+ simulation = d3 .forceSimulation <ModelNode >(filteredModels . value )
921
992
.alphaDecay (0.02 ) // Slower decay for smoother animation
993
+
994
+ // Add click handler to clear selection when clicking on the background
995
+ svg .on (' click' , () => {
996
+ selectedModel .value = null
997
+ })
922
998
}
923
999
924
1000
// Helper function to determine arc sweep direction
@@ -962,8 +1038,9 @@ function updateLinks() {
962
1038
const sourceId = typeof rel .source === ' string' ? rel .source : rel .source .id
963
1039
const targetId = typeof rel .target === ' string' ? rel .target : rel .target .id
964
1040
965
- const sourceModel = models .find (m => m .id === sourceId )
966
- const targetModel = models .find (m => m .id === targetId )
1041
+ // Only draw links for filtered models
1042
+ const sourceModel = filteredModels .value .find (m => m .id === sourceId )
1043
+ const targetModel = filteredModels .value .find (m => m .id === targetId )
967
1044
968
1045
if (sourceModel && targetModel && sourceModel .posX && sourceModel .posY && targetModel .posX && targetModel .posY ) {
969
1046
// Calculate control points for the curve
@@ -997,6 +1074,16 @@ function updateLinks() {
997
1074
})
998
1075
}
999
1076
1077
+ // Watch for changes in filters and search to update diagram
1078
+ watch ([searchQuery , selectedModelType ], () => {
1079
+ // Use setTimeout to debounce the diagram update
1080
+ const timer = setTimeout (() => {
1081
+ createDiagram ()
1082
+ }, 300 )
1083
+
1084
+ return () => clearTimeout (timer )
1085
+ })
1086
+
1000
1087
// Initialize visualization on mount
1001
1088
onMounted (() => {
1002
1089
createDiagram ()
@@ -1015,20 +1102,68 @@ onUnmounted(() => {
1015
1102
<template >
1016
1103
<div class =" min-h-screen py-4 dark:bg-blue-gray-800 lg:py-8" >
1017
1104
<div class =" px-4 lg:px-8 sm:px-6" >
1018
- <!-- Header -->
1019
- <div class =" mb-8" >
1020
- <div class =" flex justify-between items-center" >
1021
- <div class =" flex items-center gap-3" >
1022
- <div class =" i-hugeicons-dashboard-speed-02 w-8 h-8 text-blue-500" />
1023
- <div >
1024
- <h3 class =" text-base text-gray-900 dark:text-gray-100 font-semibold leading-6" >
1025
- Data Models
1026
- </h3 >
1027
- <p class =" mt-2 text-sm text-gray-700 dark:text-gray-400" >
1028
- Visualize your application's data models and relationships.
1029
- </p >
1030
- </div >
1105
+ <!-- Stats Cards -->
1106
+ <dl class =" mt-5 grid grid-cols-1 gap-5 sm:grid-cols-2 lg:grid-cols-4 mb-8" >
1107
+ <div class =" overflow-hidden rounded-lg bg-white px-4 py-5 shadow sm:p-6 dark:bg-blue-gray-800" >
1108
+ <dt class =" truncate text-sm font-medium text-gray-500 dark:text-gray-300" >Total Models</dt >
1109
+ <dd class =" mt-1 text-3xl font-semibold tracking-tight text-gray-900 dark:text-white" >{{ modelStats.totalModels }}</dd >
1110
+ <dd class =" mt-2 flex items-center text-sm text-blue-600 dark:text-blue-400" >
1111
+ <div class =" i-hugeicons-database h-4 w-4 mr-1" ></div >
1112
+ <span >Application entities</span >
1113
+ </dd >
1114
+ </div >
1115
+
1116
+ <div class =" overflow-hidden rounded-lg bg-white px-4 py-5 shadow sm:p-6 dark:bg-blue-gray-800" >
1117
+ <dt class =" truncate text-sm font-medium text-gray-500 dark:text-gray-300" >Total Properties</dt >
1118
+ <dd class =" mt-1 text-3xl font-semibold tracking-tight text-gray-900 dark:text-white" >{{ modelStats.totalProperties }}</dd >
1119
+ <dd class =" mt-2 flex items-center text-sm text-green-600 dark:text-green-400" >
1120
+ <div class =" i-hugeicons-check-list h-4 w-4 mr-1" ></div >
1121
+ <span >Model attributes</span >
1122
+ </dd >
1123
+ </div >
1124
+
1125
+ <div class =" overflow-hidden rounded-lg bg-white px-4 py-5 shadow sm:p-6 dark:bg-blue-gray-800" >
1126
+ <dt class =" truncate text-sm font-medium text-gray-500 dark:text-gray-300" >Total Relationships</dt >
1127
+ <dd class =" mt-1 text-3xl font-semibold tracking-tight text-gray-900 dark:text-white" >{{ modelStats.totalRelationships }}</dd >
1128
+ <dd class =" mt-2 flex items-center text-sm text-purple-600 dark:text-purple-400" >
1129
+ <div class =" i-hugeicons-link-01 h-4 w-4 mr-1" ></div >
1130
+ <span >Model connections</span >
1131
+ </dd >
1132
+ </div >
1133
+
1134
+ <div class =" overflow-hidden rounded-lg bg-white px-4 py-5 shadow sm:p-6 dark:bg-blue-gray-800" >
1135
+ <dt class =" truncate text-sm font-medium text-gray-500 dark:text-gray-300" >Model Categories</dt >
1136
+ <dd class =" mt-1 text-3xl font-semibold tracking-tight text-gray-900 dark:text-white" >{{ modelTypes.length - 1 }}</dd >
1137
+ <dd class =" mt-2 flex items-center text-sm text-orange-600 dark:text-orange-400" >
1138
+ <div class =" i-hugeicons-tag-01 h-4 w-4 mr-1" ></div >
1139
+ <span >Functional groups</span >
1140
+ </dd >
1141
+ </div >
1142
+ </dl >
1143
+
1144
+ <!-- Search and Filter Controls -->
1145
+ <div class =" mb-6 flex flex-col sm:flex-row gap-4 items-center justify-between" >
1146
+ <div class =" relative w-full sm:w-64" >
1147
+ <div class =" absolute inset-y-0 left-0 flex items-center pl-3 pointer-events-none" >
1148
+ <div class =" i-hugeicons-search-01 w-5 h-5 text-gray-400" ></div >
1031
1149
</div >
1150
+ <input
1151
+ v-model =" searchQuery"
1152
+ type =" text"
1153
+ class =" block w-full pl-10 pr-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md leading-5 bg-white dark:bg-blue-gray-700 placeholder-gray-500 dark:placeholder-gray-400 focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm text-gray-900 dark:text-white"
1154
+ placeholder =" Search models or properties..."
1155
+ />
1156
+ </div >
1157
+
1158
+ <div class =" flex items-center gap-2 w-full sm:w-auto" >
1159
+ <label for =" model-type" class =" text-sm font-medium text-gray-700 dark:text-gray-300" >Filter by type:</label >
1160
+ <select
1161
+ id =" model-type"
1162
+ v-model =" selectedModelType"
1163
+ class =" block w-full rounded-md border-0 py-1.5 pl-3 pr-10 text-gray-900 dark:text-white ring-1 ring-inset ring-gray-300 dark:ring-gray-600 focus:ring-2 focus:ring-blue-600 sm:text-sm sm:leading-6 dark:bg-blue-gray-700"
1164
+ >
1165
+ <option v-for =" type in modelTypes" :key =" type" :value =" type" >{{ type }}</option >
1166
+ </select >
1032
1167
</div >
1033
1168
</div >
1034
1169
@@ -1037,7 +1172,7 @@ onUnmounted(() => {
1037
1172
<div class =" p-6" >
1038
1173
<div class =" flex items-center justify-between mb-4" >
1039
1174
<div class =" flex items-center gap-2" >
1040
- <h4 class =" text-base font-medium text-gray-900 dark:text-gray-100 " >Entity Relationship Diagram</h4 >
1175
+ <h4 class =" text-base font-medium text-gray-900 dark:text-white " >Entity Relationship Diagram</h4 >
1041
1176
<span class =" text-sm text-gray-500 dark:text-gray-400" >
1042
1177
(Drag nodes to rearrange)
1043
1178
</span >
@@ -1064,6 +1199,84 @@ onUnmounted(() => {
1064
1199
<div ref =" diagramContainer" class =" w-full h-[1200px] bg-gray-50 dark:bg-blue-gray-800 rounded-lg" ></div >
1065
1200
</div >
1066
1201
</div >
1202
+
1203
+ <!-- Selected Model Details Panel -->
1204
+ <div v-if =" selectedModel" class =" mb-8 bg-white dark:bg-blue-gray-700 rounded-lg shadow" >
1205
+ <div class =" px-4 py-5 sm:px-6 border-b border-gray-200 dark:border-gray-700" >
1206
+ <div class =" flex items-center justify-between" >
1207
+ <div class =" flex items-center gap-2" >
1208
+ <span class =" text-2xl" >{{ selectedModel.emoji }}</span >
1209
+ <h3 class =" text-lg font-medium text-gray-900 dark:text-white" >{{ selectedModel.name }} Details</h3 >
1210
+ </div >
1211
+ <button
1212
+ @click =" selectedModel = null"
1213
+ class =" text-gray-400 hover:text-gray-500 dark:hover:text-gray-300"
1214
+ >
1215
+ <div class =" i-hugeicons-x-mark w-5 h-5" ></div >
1216
+ </button >
1217
+ </div >
1218
+ </div >
1219
+ <div class =" px-4 py-5 sm:p-6" >
1220
+ <div class =" grid grid-cols-1 md:grid-cols-2 gap-6" >
1221
+ <!-- Properties Section -->
1222
+ <div >
1223
+ <h4 class =" text-base font-medium text-gray-900 dark:text-white mb-4" >Properties</h4 >
1224
+ <div class =" bg-gray-50 dark:bg-blue-gray-800 rounded-md p-4" >
1225
+ <table class =" min-w-full divide-y divide-gray-200 dark:divide-gray-700" >
1226
+ <thead >
1227
+ <tr >
1228
+ <th scope =" col" class =" px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider dark:text-gray-400" >Name</th >
1229
+ <th scope =" col" class =" px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider dark:text-gray-400" >Type</th >
1230
+ <th scope =" col" class =" px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider dark:text-gray-400" >Nullable</th >
1231
+ </tr >
1232
+ </thead >
1233
+ <tbody class =" divide-y divide-gray-200 dark:divide-gray-700" >
1234
+ <tr v-for =" prop in selectedModel.properties" :key =" prop.name" >
1235
+ <td class =" px-3 py-2 whitespace-nowrap text-sm font-medium" :class =" prop.name === 'id' ? 'text-yellow-600 dark:text-yellow-400' : 'text-gray-900 dark:text-white'" >{{ prop.name }}</td >
1236
+ <td class =" px-3 py-2 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400" >{{ prop.type }}</td >
1237
+ <td class =" px-3 py-2 whitespace-nowrap text-sm" >
1238
+ <span :class =" prop.nullable ? 'text-red-600 dark:text-red-400' : 'text-green-600 dark:text-green-400'" >
1239
+ {{ prop.nullable ? 'Yes' : 'No' }}
1240
+ </span >
1241
+ </td >
1242
+ </tr >
1243
+ </tbody >
1244
+ </table >
1245
+ </div >
1246
+ </div >
1247
+
1248
+ <!-- Relationships Section -->
1249
+ <div >
1250
+ <h4 class =" text-base font-medium text-gray-900 dark:text-white mb-4" >Relationships</h4 >
1251
+ <div class =" bg-gray-50 dark:bg-blue-gray-800 rounded-md p-4" >
1252
+ <table class =" min-w-full divide-y divide-gray-200 dark:divide-gray-700" >
1253
+ <thead >
1254
+ <tr >
1255
+ <th scope =" col" class =" px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider dark:text-gray-400" >Type</th >
1256
+ <th scope =" col" class =" px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider dark:text-gray-400" >Related Model</th >
1257
+ </tr >
1258
+ </thead >
1259
+ <tbody class =" divide-y divide-gray-200 dark:divide-gray-700" >
1260
+ <tr v-for =" (rel, index) in selectedModel.relationships" :key =" index" >
1261
+ <td class =" px-3 py-2 whitespace-nowrap text-sm font-medium" >
1262
+ <span :class =" {
1263
+ 'text-red-600 dark:text-red-400': rel.type === 'belongsTo',
1264
+ 'text-blue-600 dark:text-blue-400': rel.type === 'hasMany',
1265
+ 'text-green-600 dark:text-green-400': rel.type === 'hasOne',
1266
+ 'text-purple-600 dark:text-purple-400': rel.type === 'belongsToMany'
1267
+ }" >
1268
+ {{ rel.type }}
1269
+ </span >
1270
+ </td >
1271
+ <td class =" px-3 py-2 whitespace-nowrap text-sm text-gray-900 dark:text-white" >{{ rel.model }}</td >
1272
+ </tr >
1273
+ </tbody >
1274
+ </table >
1275
+ </div >
1276
+ </div >
1277
+ </div >
1278
+ </div >
1279
+ </div >
1067
1280
</div >
1068
1281
</div >
1069
1282
</template >
0 commit comments