├── .gitignore
├── .vscode
├── extensions.json
├── launch.json
├── settings.json
└── tasks.json
├── .vscodeignore
├── LICENSE
├── README.md
├── biome.json
├── media
├── logo_dark.svg
├── logo_light.svg
├── readme
│ ├── banner.jpg
│ ├── example.gif
│ ├── logo.png
│ └── usage.jpg
└── uml.svg
├── package.json
├── src
├── core
│ └── render.ts
├── extension.ts
├── panels
│ └── prisma-uml-panel.ts
└── utilities
│ ├── getNonce.ts
│ └── getUri.ts
├── test
├── run-test.js
└── suite
│ ├── extension.test.js
│ └── index.js
├── tsconfig.json
└── webview-ui
├── .gitignore
├── README.md
├── index.html
├── package.json
├── postcss.config.js
├── src
├── App.tsx
├── components
│ ├── EnumNode.tsx
│ ├── ModelNode.tsx
│ ├── SchemaVisualizer.tsx
│ └── icons
│ │ ├── IDownload.tsx
│ │ └── props.ts
├── globals.css
├── index.tsx
├── lib
│ ├── contexts
│ │ └── theme.tsx
│ ├── hooks
│ │ └── useGraph.ts
│ ├── types
│ │ └── schema.ts
│ └── utils
│ │ ├── colots.ts
│ │ ├── layout-utils.ts
│ │ └── screnshot.ts
└── vite-env.d.ts
├── tailwind.config.js
├── tsconfig.app.json
├── tsconfig.app.tsbuildinfo
├── tsconfig.json
├── tsconfig.node.json
├── tsconfig.node.tsbuildinfo
└── vite.config.ts
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | .vscode-test/
3 | *.vsix
4 |
5 | package-lock.json
6 | dist
--------------------------------------------------------------------------------
/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 | // See http://go.microsoft.com/fwlink/?LinkId=827846
3 | // for the documentation about the extensions.json format
4 | "recommendations": [
5 | "dbaeumer.vscode-eslint",
6 | "ms-vscode.extension-test-runner"
7 | ]
8 | }
9 |
--------------------------------------------------------------------------------
/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | // A launch configuration that launches the extension inside a new window
2 | // Use IntelliSense to learn about possible attributes.
3 | // Hover to view descriptions of existing attributes.
4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
5 | {
6 | "version": "0.2.0",
7 | "configurations": [
8 | {
9 | "name": "Run Extension",
10 | "type": "extensionHost",
11 | "request": "launch",
12 | "args": ["--extensionDevelopmentPath=${workspaceFolder}"],
13 | "outFiles": ["${workspaceFolder}/dist/**/*.js"],
14 | "preLaunchTask": "${defaultBuildTask}"
15 | },
16 | {
17 | "name": "Extension Tests",
18 | "type": "extensionHost",
19 | "request": "launch",
20 | "args": [
21 | "--extensionDevelopmentPath=${workspaceFolder}",
22 | "--extensionTestsPath=${workspaceFolder}/test/suite/index"
23 | ]
24 | }
25 | ]
26 | }
27 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | // Place your settings in this file to overwrite default and user settings.
2 | {
3 | "files.exclude": {
4 | "out": false // set this to true to hide the "out" folder with the compiled JS files
5 | },
6 | "search.exclude": {
7 | "out": true // set this to false to include "out" folder in search results
8 | },
9 | // Turn off tsc task auto detection since we have the necessary tasks as npm scripts
10 | "typescript.tsc.autoDetect": "off",
11 | "editor.defaultFormatter": "biomejs.biome",
12 | "editor.formatOnSave": true
13 | }
14 |
--------------------------------------------------------------------------------
/.vscode/tasks.json:
--------------------------------------------------------------------------------
1 | // See https://go.microsoft.com/fwlink/?LinkId=733558
2 | // for the documentation about the tasks.json format
3 | {
4 | "version": "2.0.0",
5 | "tasks": [
6 | {
7 | "type": "npm",
8 | "script": "watch",
9 | "problemMatcher": "$tsc-watch",
10 | "isBackground": true,
11 | "presentation": {
12 | "reveal": "never"
13 | },
14 | "group": {
15 | "kind": "build",
16 | "isDefault": true
17 | }
18 | }
19 | ]
20 | }
21 |
--------------------------------------------------------------------------------
/.vscodeignore:
--------------------------------------------------------------------------------
1 | .vscode/**
2 | .vscode-test/**
3 | test/**
4 | .gitignore
5 | .yarnrc
6 | vsc-extension-quickstart.md
7 | **/jsconfig.json
8 | **/*.map
9 | **/.eslintrc.json
10 |
11 | .prettierrc
12 | node_modules
13 | package-lock.json
14 | pnpm-lock.yaml
15 | src/**
16 |
17 | webview-ui/**
18 | !webview-ui/build/
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 Abian Suarez
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Prisma Generate UML is a VSCode extension that quickly creates UML diagrams from Prisma schemas with a single click, offering easy visualization.
7 |
8 |
9 |
10 |
11 | > _You can download final bundles from the [Releases](https://github.com/AbianS/prisma-generate-uml/releases) section._
12 |
13 |       
14 |
15 | > [!NOTE]
16 | > 🚧
17 | > **Prisma Generate UML** is currently under development. Stay tuned for more updates!
18 |
19 | ## ✨ Features
20 |
21 | - 🔥 **Instant UML Diagrams**: Generate UML diagrams from Prisma schemas with a single click.
22 | - 🖼 **Easy Visualization**: Simplify data architecture visualization in an exciting way.
23 | - 🛠 **Seamless Integration**: Works seamlessly within VSCode, no extra configuration required.
24 | - 📂 **Multi-file Prisma Schema Support**: We fully support Prisma's `prismaSchemaFolder` feature, allowing you to split your schema into multiple files while still generating a complete UML diagram of your entire database.
25 | - 🔃 **Automatic Updates**: We'll keep your UML diagrams up-to-date with the latest changes to your Prisma schema.
26 |
27 | ## 🔍 What It Does
28 |
29 | Get ready to breathe life into your data models! ✨ With our extension, creating UML diagrams from your Prisma files is as easy as it gets.
30 |
31 | 🚀 When you open your Prisma schema, the UML icon at the top of the editor becomes your magic wand. A simple click, and presto! Your UML model springs to life in an instant.
32 |
33 | Say goodbye to boring documentation and hello to the dazzling representation of your database architecture.
34 |
35 | Transform your Prisma definitions into a stunning UML diagram with ease and dive into the excitement of data visualization! 🪄💎
36 |
37 | 
38 |
39 | ## 🚀 How to Use
40 |
41 | Generate UML diagrams with a single click:
42 |
43 | 1. Open your Prisma file.
44 | 2. Look for the UML icon at the top of the editor.
45 | 3. Click it, and you're done! Your UML diagram will be created instantly.
46 |
47 | Simplify data architecture visualization in an exciting way! 🚀
48 |
49 | 
50 |
--------------------------------------------------------------------------------
/biome.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://biomejs.dev/schemas/1.9.4/schema.json",
3 | "formatter": {
4 | "ignore": [
5 | "node_modules",
6 | "dist",
7 | ".next",
8 | "logs",
9 | "postgres",
10 | ".turbo",
11 | "build"
12 | ],
13 | "enabled": true,
14 | "formatWithErrors": false,
15 | "indentStyle": "space",
16 | "indentWidth": 2,
17 | "lineEnding": "lf",
18 | "lineWidth": 80,
19 | "attributePosition": "auto"
20 | },
21 | "organizeImports": {
22 | "enabled": true,
23 | "ignore": [
24 | "node_modules",
25 | "dist",
26 | ".next",
27 | "logs",
28 | "postgres",
29 | ".turbo",
30 | "build"
31 | ]
32 | },
33 | "linter": {
34 | "enabled": true,
35 | "rules": {
36 | "recommended": true,
37 | "correctness": {
38 | "noChildrenProp": "off",
39 | "useExhaustiveDependencies": "off",
40 | "noUnsafeOptionalChaining": "off",
41 | "noSwitchDeclarations": "off",
42 | "useJsxKeyInIterable": "off",
43 | "noUnusedImports": "warn"
44 | },
45 | "performance": {
46 | "noAccumulatingSpread": "off"
47 | },
48 | "style": {
49 | "useFilenamingConvention": {
50 | "level": "error",
51 | "options": {
52 | "strictCase": true,
53 | "filenameCases": ["kebab-case", "export"],
54 | "requireAscii": true
55 | }
56 | },
57 | "noNonNullAssertion": "off",
58 | "noParameterAssign": "off",
59 | "useTemplate": "off",
60 | "useImportType": "off",
61 | "noUselessElse": "off",
62 | "useShorthandFunctionType": "off",
63 | "useEnumInitializers": "off",
64 | "useDefaultParameterLast": "off"
65 | },
66 | "complexity": {
67 | "noStaticOnlyClass": "off",
68 | "noBannedTypes": "off",
69 | "noForEach": "off",
70 | "useLiteralKeys": "off"
71 | },
72 | "a11y": {
73 | "useButtonType": "off",
74 | "useKeyWithClickEvents": "off",
75 | "noSvgWithoutTitle": "off"
76 | },
77 | "suspicious": {
78 | "noExplicitAny": "off",
79 | "noArrayIndexKey": "off",
80 | "noPrototypeBuiltins": "off"
81 | }
82 | },
83 | "ignore": [
84 | "node_modules",
85 | "dist",
86 | ".next",
87 | "logs",
88 | "postgres",
89 | ".turbo",
90 | "build"
91 | ]
92 | },
93 | "javascript": {
94 | "parser": {
95 | "unsafeParameterDecoratorsEnabled": true
96 | },
97 | "formatter": {
98 | "jsxQuoteStyle": "double",
99 | "quoteProperties": "asNeeded",
100 | "trailingCommas": "all",
101 | "semicolons": "always",
102 | "arrowParentheses": "always",
103 | "bracketSpacing": true,
104 | "bracketSameLine": false,
105 | "quoteStyle": "single",
106 | "attributePosition": "auto"
107 | }
108 | }
109 | }
110 |
--------------------------------------------------------------------------------
/media/logo_dark.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/media/logo_light.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/media/readme/banner.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AbianS/prisma-generate-uml/5bab480b7835c90c153f1283d1a9a9579d55a47c/media/readme/banner.jpg
--------------------------------------------------------------------------------
/media/readme/example.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AbianS/prisma-generate-uml/5bab480b7835c90c153f1283d1a9a9579d55a47c/media/readme/example.gif
--------------------------------------------------------------------------------
/media/readme/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AbianS/prisma-generate-uml/5bab480b7835c90c153f1283d1a9a9579d55a47c/media/readme/logo.png
--------------------------------------------------------------------------------
/media/readme/usage.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AbianS/prisma-generate-uml/5bab480b7835c90c153f1283d1a9a9579d55a47c/media/readme/usage.jpg
--------------------------------------------------------------------------------
/media/uml.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "prisma-generate-uml",
3 | "displayName": "Prisma Generate UML",
4 | "description": "Generate UML Diagram from prisma schema",
5 | "version": "3.4.0",
6 | "icon": "media/readme/logo.png",
7 | "repository": "https://github.com/AbianS/prisma-generate-uml",
8 | "publisher": "AbianS",
9 | "engines": {
10 | "vscode": "^1.83.0"
11 | },
12 | "categories": ["Other"],
13 | "activationEvents": ["onStartupFinished"],
14 | "main": "./dist/extension.js",
15 | "browser": "./dist/extension.js",
16 | "contributes": {
17 | "commands": [
18 | {
19 | "command": "prisma-generate-uml.generateUML",
20 | "title": "Generate Prisma UML",
21 | "icon": {
22 | "light": "./media/logo_light.svg",
23 | "dark": "./media/logo_dark.svg"
24 | }
25 | },
26 | {
27 | "command": "hello-world.showHelloWorld",
28 | "title": "Hello World (React + Vite): Show"
29 | }
30 | ],
31 | "menus": {
32 | "editor/title": [
33 | {
34 | "when": "editorLangId == prisma",
35 | "command": "prisma-generate-uml.generateUML",
36 | "group": "navigation"
37 | }
38 | ]
39 | }
40 | },
41 | "scripts": {
42 | "install:all": "npm install && cd webview-ui && npm install",
43 | "start:webview": "cd webview-ui && npm run dev",
44 | "build:webview": "cd webview-ui && npm run build",
45 | "vscode:prepublish": "npm run esbuild -- --minify && npm run copy",
46 | "copy": "shx cp node_modules/@prisma/prisma-schema-wasm/src/prisma_schema_build_bg.wasm dist/",
47 | "esbuild": "esbuild ./src/extension.ts --bundle --outfile=dist/extension.js --external:vscode --format=cjs --platform=node",
48 | "compile": "tsc -p ./",
49 | "watch": "tsc -watch -p ./",
50 | "pretest": "npm run compile && npm run lint",
51 | "lint": "biome lint .",
52 | "lint:fix": "biome check . --write",
53 | "test": "vscode-test"
54 | },
55 | "devDependencies": {
56 | "@biomejs/biome": "1.9.4",
57 | "@types/mocha": "^10.0.10",
58 | "@types/node": "22.x",
59 | "@types/vscode": "^1.83.0",
60 | "@vscode/test-cli": "^0.0.10",
61 | "@vscode/test-electron": "^2.4.1",
62 | "esbuild": "^0.24.2",
63 | "shx": "^0.3.4",
64 | "typescript": "^5.7.2"
65 | },
66 | "dependencies": {
67 | "@microsoft/vscode-file-downloader-api": "^1.0.1",
68 | "@prisma/internals": "^6.1.0"
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/src/core/render.ts:
--------------------------------------------------------------------------------
1 | import { DMMF } from '@prisma/generator-helper';
2 |
3 | export type Model = {
4 | name: string;
5 | fields: {
6 | name: string;
7 | type: string;
8 | hasConnections?: boolean;
9 | }[];
10 | isChild?: boolean;
11 | };
12 |
13 | export type Enum = {
14 | name: string;
15 | values: string[];
16 | };
17 |
18 | export type ModelConnection = {
19 | target: string;
20 | source: string;
21 | name: string;
22 | };
23 |
24 | /**
25 | * Transforms the Prisma DMMF (Data Model Meta Format) into a structure that can be used by the React application.
26 | * This function generates a list of models, enums, and connections based on the DMMF document.
27 | *
28 | * @param {DMMF.Document} dmmf - The Prisma DMMF document containing the schema information.
29 | * @returns {{ models: Model[], enums: Enum[], connections: ModelConnection[] }} An object containing the transformed models, enums, and connections.
30 | */
31 | export function transformDmmfToModelsAndConnections(dmmf: DMMF.Document): {
32 | models: Model[];
33 | enums: Enum[];
34 | connections: ModelConnection[];
35 | } {
36 | const models = generateModels(dmmf.datamodel.models);
37 | const enums = generateEnums(dmmf.datamodel.enums);
38 | const connections = generateModelConnections(dmmf.datamodel.models);
39 |
40 | return { models, enums, connections };
41 | }
42 |
43 | /**
44 | * Generates an array of `Model` objects based on the models defined in the DMMF.
45 | * Each model includes its fields and a flag indicating whether it has relationships with other models.
46 | *
47 | * @param {readonly DMMF.Model[]} models - The list of models from the DMMF document.
48 | * @returns {Model[]} An array of `Model` objects containing their respective fields and relationship data.
49 | */
50 | export function generateModels(models: readonly DMMF.Model[]): Model[] {
51 | return models.map((model) => ({
52 | name: model.name,
53 | fields: model.fields.map((field) => ({
54 | name: field.name,
55 | type: field.isList ? `${field.type}[]` : field.type,
56 | hasConnections:
57 | field.kind === 'object' || (field.relationFromFields?.length ?? 0) > 0,
58 | })),
59 | isChild: model.fields.some(
60 | (field) => (field.relationFromFields?.length ?? 0) > 0,
61 | ),
62 | }));
63 | }
64 |
65 | /**
66 | * Generates an array of `Enum` objects based on the enums defined in the DMMF.
67 | * Each enum includes its name and its possible values.
68 | *
69 | * @param {readonly DMMF.DatamodelEnum[]} enums - The list of enums from the DMMF document.
70 | * @returns {Enum[]} An array of `Enum` objects with their respective values.
71 | */
72 | export function generateEnums(enums: readonly DMMF.DatamodelEnum[]): Enum[] {
73 | return enums.map((enumItem) => ({
74 | name: enumItem.name,
75 | values: enumItem.values.map((v) => v.name),
76 | }));
77 | }
78 |
79 | /**
80 | * Generates connections between models based on relationships defined in the DMMF.
81 | * Each connection represents a relationship between two models, identified by source and target handles.
82 | *
83 | * @param {readonly DMMF.Model[]} models - The list of models from the DMMF document.
84 | * @returns {ModelConnection[]} An array of `ModelConnection` objects representing relationships between models.
85 | */
86 | export function generateModelConnections(
87 | models: readonly DMMF.Model[],
88 | ): ModelConnection[] {
89 | const connections: ModelConnection[] = [];
90 |
91 | models.forEach((model) => {
92 | model.fields.forEach((field) => {
93 | const targetModelName = field.type;
94 | const connectionName = field.relationName || field.name;
95 |
96 | // If the field type is another model, create a connection
97 | const isConnectedToOtherModel = models.some(
98 | (m) => m.name === targetModelName,
99 | );
100 |
101 | if (isConnectedToOtherModel) {
102 | connections.push({
103 | source: `${model.name}-${field.name}-source`,
104 | target: `${targetModelName}-target`,
105 | name: connectionName,
106 | });
107 | }
108 | });
109 | });
110 |
111 | return connections;
112 | }
113 |
--------------------------------------------------------------------------------
/src/extension.ts:
--------------------------------------------------------------------------------
1 | import { getDMMF, getSchemaWithPath } from '@prisma/internals';
2 | import * as vscode from 'vscode';
3 | import { transformDmmfToModelsAndConnections } from './core/render';
4 | import { PrismaUMLPanel } from './panels/prisma-uml-panel';
5 | let outputChannel: vscode.OutputChannel;
6 |
7 | export function activate(context: vscode.ExtensionContext) {
8 | outputChannel = vscode.window.createOutputChannel('Prisma Generate UML');
9 | outputChannel.appendLine('Prisma Generate UML extension activated');
10 |
11 | const disposable = vscode.commands.registerCommand(
12 | 'prisma-generate-uml.generateUML',
13 | async () => {
14 | const editor = vscode.window.activeTextEditor;
15 |
16 | if (editor && editor.document.languageId === 'prisma') {
17 | const currentFileUri = editor.document.uri;
18 |
19 | await generateUMLForPrismaFile(context, currentFileUri);
20 | } else {
21 | vscode.window.showInformationMessage(
22 | 'Open a .prisma file to use this command',
23 | );
24 | }
25 | },
26 | );
27 |
28 | const onDidSaveDisposable = vscode.workspace.onDidSaveTextDocument(
29 | async (document) => {
30 | if (document.languageId === 'prisma' && PrismaUMLPanel.currentPanel) {
31 | await generateUMLForPrismaFile(context, document.uri);
32 | }
33 | },
34 | );
35 |
36 | context.subscriptions.push(disposable);
37 | context.subscriptions.push(onDidSaveDisposable);
38 | }
39 |
40 | async function generateUMLForPrismaFile(
41 | context: vscode.ExtensionContext,
42 | fileUri: vscode.Uri,
43 | ) {
44 | const folderUri = vscode.Uri.joinPath(fileUri, '..');
45 |
46 | let response: Awaited> | null = null;
47 |
48 | try {
49 | const schemaResultFromFile = await getSchemaWithPath(fileUri.fsPath);
50 | response = await getDMMF({ datamodel: schemaResultFromFile.schemas });
51 | } catch (err) {
52 | console.error(
53 | `[prisma-generate-uml] Tried reading schema from file: ${err}`,
54 | );
55 | }
56 |
57 | if (!response) {
58 | try {
59 | const schemaResultFromDir = await getSchemaWithPath(folderUri.fsPath);
60 | response = await getDMMF({ datamodel: schemaResultFromDir.schemas });
61 | } catch (err) {
62 | console.error(
63 | `[prisma-generate-uml] Tried reading schema from directory: ${err}`,
64 | );
65 | }
66 | }
67 |
68 | if (!response) {
69 | throw new Error('no schema found');
70 | }
71 |
72 | const { models, connections, enums } =
73 | transformDmmfToModelsAndConnections(response);
74 | PrismaUMLPanel.render(
75 | context.extensionUri,
76 | models,
77 | connections,
78 | enums,
79 | fileUri,
80 | );
81 | }
82 |
83 | export function deactivate() {}
84 |
--------------------------------------------------------------------------------
/src/panels/prisma-uml-panel.ts:
--------------------------------------------------------------------------------
1 | import * as vscode from 'vscode';
2 | import { Enum, Model, ModelConnection } from '../core/render';
3 | import { getNonce } from '../utilities/getNonce';
4 | import { getUri } from '../utilities/getUri';
5 |
6 | export class PrismaUMLPanel {
7 | public static currentPanel: PrismaUMLPanel | undefined;
8 | public static readonly viewType = 'prismaUML';
9 | private readonly _panel: vscode.WebviewPanel;
10 | private _disposables: vscode.Disposable[] = [];
11 |
12 | private constructor(
13 | panel: vscode.WebviewPanel,
14 | private readonly _extensionUri: vscode.Uri,
15 | private readonly _currentFileUri: vscode.Uri,
16 | models: Model[],
17 | connections: ModelConnection[],
18 | enums: Enum[],
19 | ) {
20 | this._panel = panel;
21 |
22 | this._panel.onDidDispose(() => this.dispose(), null, this._disposables);
23 |
24 | this._panel.webview.html = this._getWebviewContent(this._panel.webview);
25 |
26 | this._panel.iconPath = vscode.Uri.joinPath(
27 | this._extensionUri,
28 | 'media/uml.svg',
29 | );
30 |
31 | this._panel.webview.postMessage({
32 | command: 'setData',
33 | models,
34 | connections,
35 | enums,
36 | });
37 |
38 | this._panel.webview.postMessage({
39 | command: 'setTheme',
40 | theme: vscode.window.activeColorTheme.kind,
41 | });
42 |
43 | this._panel.webview.onDidReceiveMessage(
44 | async (message) => {
45 | switch (message.command) {
46 | case 'saveImage':
47 | await this._saveImage(message.data);
48 | return;
49 | }
50 | },
51 | null,
52 | this._disposables,
53 | );
54 | }
55 |
56 | public static render(
57 | extensionUri: vscode.Uri,
58 | models: Model[],
59 | connections: ModelConnection[],
60 | enums: Enum[],
61 | currentFileUri: vscode.Uri,
62 | ) {
63 | if (PrismaUMLPanel.currentPanel) {
64 | PrismaUMLPanel.currentPanel.updateData(models, connections, enums);
65 | } else {
66 | const panel = vscode.window.createWebviewPanel(
67 | PrismaUMLPanel.viewType,
68 | 'Prisma Schema UML',
69 | vscode.ViewColumn.Two,
70 | {
71 | enableScripts: true,
72 | retainContextWhenHidden: true,
73 | localResourceRoots: [
74 | vscode.Uri.joinPath(extensionUri, 'out'),
75 | vscode.Uri.joinPath(extensionUri, 'webview-ui/build'),
76 | ],
77 | },
78 | );
79 | PrismaUMLPanel.currentPanel = new PrismaUMLPanel(
80 | panel,
81 | extensionUri,
82 | currentFileUri,
83 | models,
84 | connections,
85 | enums,
86 | );
87 | }
88 | }
89 |
90 | private async _saveImage(data: { format: string; dataUrl: string }) {
91 | const base64Data = data.dataUrl.replace(/^data:image\/\w+;base64,/, '');
92 | const buffer = Buffer.from(base64Data, 'base64');
93 |
94 | const uri = await vscode.window.showSaveDialog({
95 | filters: { Images: [data.format] },
96 | defaultUri: vscode.Uri.file(`prisma-uml.${data.format}`),
97 | });
98 |
99 | if (uri) {
100 | try {
101 | await vscode.workspace.fs.writeFile(uri, buffer);
102 | vscode.window.showInformationMessage(`Image saved to ${uri.fsPath}`);
103 | } catch (error) {
104 | vscode.window.showErrorMessage(`Failed to save image: ${error}`);
105 | }
106 | }
107 | }
108 |
109 | public updateData(
110 | models: Model[],
111 | connections: ModelConnection[],
112 | enums: Enum[],
113 | ) {
114 | this._panel.webview.postMessage({
115 | command: 'setData',
116 | models,
117 | connections,
118 | enums,
119 | });
120 | }
121 |
122 | public dispose() {
123 | PrismaUMLPanel.currentPanel = undefined;
124 | this._panel.dispose();
125 | while (this._disposables.length) {
126 | const disposable = this._disposables.pop();
127 | if (disposable) {
128 | disposable.dispose();
129 | }
130 | }
131 | }
132 |
133 | private _getWebviewContent(webview: vscode.Webview) {
134 | const stylesUri = getUri(webview, this._extensionUri, [
135 | 'webview-ui',
136 | 'build',
137 | 'assets',
138 | 'index.css',
139 | ]);
140 | const scriptUri = getUri(webview, this._extensionUri, [
141 | 'webview-ui',
142 | 'build',
143 | 'assets',
144 | 'index.js',
145 | ]);
146 | const nonce = getNonce();
147 |
148 | return /*html*/ `
149 |
150 |
151 |
152 |
153 |
154 |
155 |
156 | Prisma UML
157 |
158 |
159 |
160 |
161 |
162 |
163 | `;
164 | }
165 | }
166 |
--------------------------------------------------------------------------------
/src/utilities/getNonce.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * A helper function that returns a unique alphanumeric identifier called a nonce.
3 | *
4 | * @remarks This function is primarily used to help enforce content security
5 | * policies for resources/scripts being executed in a webview context.
6 | *
7 | * @returns A nonce
8 | */
9 | export function getNonce() {
10 | let text = '';
11 | const possible =
12 | 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
13 | for (let i = 0; i < 32; i++) {
14 | text += possible.charAt(Math.floor(Math.random() * possible.length));
15 | }
16 | return text;
17 | }
18 |
--------------------------------------------------------------------------------
/src/utilities/getUri.ts:
--------------------------------------------------------------------------------
1 | import { Uri, Webview } from 'vscode';
2 |
3 | /**
4 | * A helper function which will get the webview URI of a given file or resource.
5 | *
6 | * @remarks This URI can be used within a webview's HTML as a link to the
7 | * given file/resource.
8 | *
9 | * @param webview A reference to the extension webview
10 | * @param extensionUri The URI of the directory containing the extension
11 | * @param pathList An array of strings representing the path to a file/resource
12 | * @returns A URI pointing to the file/resource
13 | */
14 | export function getUri(
15 | webview: Webview,
16 | extensionUri: Uri,
17 | pathList: string[],
18 | ) {
19 | return webview.asWebviewUri(Uri.joinPath(extensionUri, ...pathList));
20 | }
21 |
--------------------------------------------------------------------------------
/test/run-test.js:
--------------------------------------------------------------------------------
1 | const path = require('node:path');
2 |
3 | const { runTests } = require('@vscode/test-electron');
4 |
5 | async function main() {
6 | try {
7 | // The folder containing the Extension Manifest package.json
8 | // Passed to `--extensionDevelopmentPath`
9 | const extensionDevelopmentPath = path.resolve(__dirname, '../');
10 |
11 | // The path to the extension test script
12 | // Passed to --extensionTestsPath
13 | const extensionTestsPath = path.resolve(__dirname, './suite/index');
14 |
15 | // Download VS Code, unzip it and run the integration test
16 | await runTests({ extensionDevelopmentPath, extensionTestsPath });
17 | } catch (err) {
18 | console.error('Failed to run tests', err);
19 | process.exit(1);
20 | }
21 | }
22 |
23 | main();
24 |
--------------------------------------------------------------------------------
/test/suite/extension.test.js:
--------------------------------------------------------------------------------
1 | const assert = require('node:assert');
2 |
3 | // You can import and use all API from the 'vscode' module
4 | // as well as import your extension to test it
5 | const vscode = require('vscode');
6 | // const myExtension = require('../extension');
7 |
8 | suite('Extension Test Suite', () => {
9 | vscode.window.showInformationMessage('Start all tests.');
10 |
11 | test('Sample test', () => {
12 | assert.strictEqual(-1, [1, 2, 3].indexOf(5));
13 | assert.strictEqual(-1, [1, 2, 3].indexOf(0));
14 | });
15 | });
16 |
--------------------------------------------------------------------------------
/test/suite/index.js:
--------------------------------------------------------------------------------
1 | const path = require('node:path');
2 | const Mocha = require('mocha');
3 | const glob = require('glob');
4 |
5 | function run() {
6 | // Create the mocha test
7 | const mocha = new Mocha({
8 | ui: 'tdd',
9 | color: true,
10 | });
11 |
12 | const testsRoot = path.resolve(__dirname, '..');
13 |
14 | return new Promise((c, e) => {
15 | const testFiles = new glob.Glob('**/**.test.js', { cwd: testsRoot });
16 | const testFileStream = testFiles.stream();
17 |
18 | testFileStream.on('data', (file) => {
19 | // Add files to the test suite
20 | mocha.addFile(path.resolve(testsRoot, file));
21 | });
22 | testFileStream.on('error', (err) => {
23 | e(err);
24 | });
25 | testFileStream.on('end', () => {
26 | try {
27 | // Run the mocha test
28 | mocha.run((failures) => {
29 | if (failures > 0) {
30 | e(new Error(`${failures} tests failed.`));
31 | } else {
32 | c();
33 | }
34 | });
35 | } catch (err) {
36 | console.error(err);
37 | e(err);
38 | }
39 | });
40 | });
41 | }
42 |
43 | module.exports = {
44 | run,
45 | };
46 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "module": "Node16",
4 | "target": "ES2020",
5 | "outDir": "dist",
6 | "lib": ["ES2020"],
7 | "sourceMap": true,
8 | "rootDir": "src",
9 | "strict": true /* enable all strict type-checking options */,
10 | "skipLibCheck": true /* Skip type checking of declaration files. */,
11 | "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
12 | /* Additional Checks */
13 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
14 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
15 | // "noUnusedParameters": true, /* Report errors on unused parameters. */
16 | },
17 | "exclude": ["webview-ui"]
18 | }
19 |
--------------------------------------------------------------------------------
/webview-ui/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | pnpm-debug.log*
8 | lerna-debug.log*
9 |
10 | node_modules
11 | dist
12 | dist-ssr
13 | *.local
14 |
15 | # Editor directories and files
16 | .vscode/*
17 | !.vscode/extensions.json
18 | .idea
19 | .DS_Store
20 | *.suo
21 | *.ntvs*
22 | *.njsproj
23 | *.sln
24 | *.sw?
25 |
26 | build
--------------------------------------------------------------------------------
/webview-ui/README.md:
--------------------------------------------------------------------------------
1 | # React + TypeScript + Vite
2 |
3 | This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
4 |
5 | Currently, two official plugins are available:
6 |
7 | - [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh
8 | - [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
9 |
10 | ## Expanding the ESLint configuration
11 |
12 | If you are developing a production application, we recommend updating the configuration to enable type aware lint rules:
13 |
14 | - Configure the top-level `parserOptions` property like this:
15 |
16 | ```js
17 | export default tseslint.config({
18 | languageOptions: {
19 | // other options...
20 | parserOptions: {
21 | project: ['./tsconfig.node.json', './tsconfig.app.json'],
22 | tsconfigRootDir: import.meta.dirname,
23 | },
24 | },
25 | })
26 | ```
27 |
28 | - Replace `tseslint.configs.recommended` to `tseslint.configs.recommendedTypeChecked` or `tseslint.configs.strictTypeChecked`
29 | - Optionally add `...tseslint.configs.stylisticTypeChecked`
30 | - Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and update the config:
31 |
32 | ```js
33 | // eslint.config.js
34 | import react from 'eslint-plugin-react'
35 |
36 | export default tseslint.config({
37 | // Set the react version
38 | settings: { react: { version: '18.3' } },
39 | plugins: {
40 | // Add the react plugin
41 | react,
42 | },
43 | rules: {
44 | // other rules...
45 | // Enable its recommended rules
46 | ...react.configs.recommended.rules,
47 | ...react.configs['jsx-runtime'].rules,
48 | },
49 | })
50 | ```
51 |
--------------------------------------------------------------------------------
/webview-ui/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Vite + React + TS
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/webview-ui/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "webview-ui",
3 | "private": true,
4 | "version": "0.0.0",
5 | "type": "module",
6 | "scripts": {
7 | "dev": "vite",
8 | "build": "tsc -b && vite build",
9 | "preview": "vite preview"
10 | },
11 | "dependencies": {
12 | "@dagrejs/dagre": "^1.1.4",
13 | "@xyflow/react": "^12.3.6",
14 | "fast-deep-equal": "^3.1.3",
15 | "html-to-image": "^1.11.11",
16 | "lucide-react": "^0.469.0",
17 | "react": "^19.0.0",
18 | "react-dom": "^19.0.0"
19 | },
20 | "devDependencies": {
21 | "@types/react": "^19.0.2",
22 | "@types/react-dom": "^19.0.2",
23 | "@vitejs/plugin-react": "^4.3.4",
24 | "autoprefixer": "^10.4.20",
25 | "globals": "^15.14.0",
26 | "postcss": "^8.4.49",
27 | "tailwindcss": "^3.4.17",
28 | "typescript": "^5.7.2",
29 | "vite": "^6.0.6"
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/webview-ui/postcss.config.js:
--------------------------------------------------------------------------------
1 | export default {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | };
7 |
--------------------------------------------------------------------------------
/webview-ui/src/App.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from 'react';
2 | import { SchemaVisualizer } from './components/SchemaVisualizer';
3 | import { ThemeProvider } from './lib/contexts/theme';
4 | import {
5 | ColorThemeKind,
6 | Enum,
7 | Model,
8 | ModelConnection,
9 | } from './lib/types/schema';
10 | import { ReactFlowProvider } from '@xyflow/react';
11 |
12 | function App() {
13 | const [models, setModels] = useState([]);
14 | const [enums, setEnums] = useState([]);
15 | const [theme, setTheme] = useState(ColorThemeKind.Dark);
16 | const [connections, setConnections] = useState([]);
17 |
18 | useEffect(() => {
19 | function handleMessage(event: MessageEvent) {
20 | const message = event.data;
21 |
22 | if (message.command === 'setData') {
23 | setModels(message.models);
24 | setConnections(message.connections);
25 | setEnums(message.enums);
26 | }
27 |
28 | if (message.command === 'setTheme') {
29 | setTheme(message.theme);
30 | }
31 | }
32 |
33 | window.addEventListener('message', handleMessage);
34 |
35 | return () => {
36 | window.removeEventListener('message', handleMessage);
37 | };
38 | }, []);
39 |
40 | return (
41 | models.length > 0 &&
42 | connections.length > 0 && (
43 |
44 |
45 |
50 |
51 |
52 | )
53 | );
54 | }
55 |
56 | export default App;
57 |
--------------------------------------------------------------------------------
/webview-ui/src/components/EnumNode.tsx:
--------------------------------------------------------------------------------
1 | import { NodeProps } from '@xyflow/react';
2 | import { memo } from 'react';
3 | import { useTheme } from '../lib/contexts/theme';
4 | import { EnumNodeTye } from '../lib/types/schema';
5 |
6 | export const EnumNode = memo(({ data }: NodeProps) => {
7 | const { isDarkMode } = useTheme();
8 |
9 | return (
10 |
23 |
33 |
40 |
{data.name}
41 |
42 |
43 |
44 |
45 | {data.values.map((value, index) => (
46 |
66 | ))}
67 |
68 |
69 | );
70 | });
71 |
--------------------------------------------------------------------------------
/webview-ui/src/components/ModelNode.tsx:
--------------------------------------------------------------------------------
1 | import { Handle, NodeProps, Position } from '@xyflow/react';
2 | import { JSX, memo } from 'react';
3 | import { useTheme } from '../lib/contexts/theme';
4 | import { ModelNodeTye } from '../lib/types/schema';
5 |
6 | import {
7 | Calculator,
8 | Calendar,
9 | CheckSquare,
10 | File,
11 | FileText,
12 | Hash,
13 | Link2,
14 | List,
15 | Type,
16 | } from 'lucide-react';
17 |
18 | const typeIcons: Record = {
19 | string: ,
20 | int: ,
21 | float: ,
22 | double: ,
23 | date: ,
24 | datetime: ,
25 | boolean: ,
26 | text: ,
27 | file: ,
28 | enum:
,
29 | };
30 |
31 | const getIconForType = (type: string) => {
32 | return typeIcons[type.toLowerCase()] || ;
33 | };
34 |
35 | export const ModelNode = memo(({ data }: NodeProps) => {
36 | const { isDarkMode } = useTheme();
37 |
38 | return (
39 |
52 | {data.isChild && (
53 |
58 | )}
59 |
60 |
70 |
77 |
{data.name}
78 |
79 |
80 |
81 |
82 | {data.fields.map(({ type, name, hasConnections }, index) => (
83 |
103 |
104 | {getIconForType(type)}
105 | {name}
106 |
107 |
110 |
111 | {hasConnections && (
112 |
120 | )}
121 |
122 | ))}
123 |
124 |
125 | );
126 | });
127 |
--------------------------------------------------------------------------------
/webview-ui/src/components/SchemaVisualizer.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | Background,
3 | BackgroundVariant,
4 | ConnectionLineType,
5 | ControlButton,
6 | Controls,
7 | Edge,
8 | MiniMap,
9 | Panel,
10 | ReactFlow,
11 | useReactFlow,
12 | } from '@xyflow/react';
13 | import { useTheme } from '../lib/contexts/theme';
14 | import { useGraph } from '../lib/hooks/useGraph';
15 | import { Enum, Model, ModelConnection } from '../lib/types/schema';
16 | import {
17 | getButtonStyle,
18 | maskColor,
19 | nodeColor,
20 | nodeStrokeColor,
21 | } from '../lib/utils/colots';
22 | import { screenshot } from '../lib/utils/screnshot';
23 | import { EnumNode } from './EnumNode';
24 | import { ModelNode } from './ModelNode';
25 | import { IDownload } from './icons/IDownload';
26 | import { useMemo } from 'react';
27 |
28 | interface Props {
29 | models: Model[];
30 | connections: ModelConnection[];
31 | enums: Enum[];
32 | }
33 |
34 | export const SchemaVisualizer = ({ connections, models, enums }: Props) => {
35 | const { isDarkMode } = useTheme();
36 | const { getNodes } = useReactFlow();
37 |
38 | const modelNodes = useMemo(() => {
39 | return models.map((model) => ({
40 | id: model.name,
41 | data: model,
42 | type: 'model',
43 | position: { x: 0, y: 0 },
44 | }));
45 | }, [models]);
46 |
47 | const enumNodes = useMemo(() => {
48 | return enums.map((enumItem) => ({
49 | id: enumItem.name,
50 | data: enumItem,
51 | type: 'enum',
52 | position: { x: 0, y: 0 },
53 | }));
54 | }, [enums]);
55 |
56 | const edges: Edge[] = useMemo(() => {
57 | return connections.map((connection) => ({
58 | id: `${connection.source}-${connection.target}`,
59 | source: connection.source.split('-')[0],
60 | target: connection.target.split('-')[0],
61 | sourceHandle: connection.source,
62 | targetHandle: connection.target,
63 | animated: false,
64 |
65 | style: {
66 | stroke: isDarkMode ? '#ffffff' : '#000000',
67 | strokeWidth: 2,
68 | strokeOpacity: 0.5,
69 | strokeLinejoin: 'round',
70 | strokeLinecap: 'round',
71 | strokeDasharray: '5',
72 | strokeDashoffset: 0,
73 | fill: 'none',
74 | },
75 | }));
76 | }, [connections]);
77 |
78 | const {
79 | nodes,
80 | edges: edgesState,
81 | onNodesChange,
82 | onEdgesChange,
83 | onConnect,
84 | onLayout,
85 | selectedLayout,
86 | } = useGraph([...modelNodes, ...enumNodes], edges);
87 |
88 | return (
89 |
94 |
107 |
108 | screenshot(getNodes as any)}
111 | >
112 |
113 |
114 |
115 |
124 |
128 |
129 |
135 |
141 |
142 |
143 |
144 | );
145 | };
146 |
--------------------------------------------------------------------------------
/webview-ui/src/components/icons/IDownload.tsx:
--------------------------------------------------------------------------------
1 | import { IconProps } from './props';
2 |
3 | export const IDownload = ({
4 | color = 'black',
5 | size = 16,
6 | ...attributes
7 | }: IconProps) => (
8 |
21 | );
22 |
--------------------------------------------------------------------------------
/webview-ui/src/components/icons/props.ts:
--------------------------------------------------------------------------------
1 | export type IconProps = {
2 | color?: string;
3 | size?: string | number;
4 | } & React.SVGAttributes;
5 |
--------------------------------------------------------------------------------
/webview-ui/src/globals.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | body {
6 | @apply p-0;
7 | }
8 |
--------------------------------------------------------------------------------
/webview-ui/src/index.tsx:
--------------------------------------------------------------------------------
1 | import { StrictMode } from 'react';
2 | import '@xyflow/react/dist/style.css';
3 | import { createRoot } from 'react-dom/client';
4 | import App from './App';
5 | import './globals.css';
6 |
7 | createRoot(document.getElementById('root')!).render(
8 |
9 |
10 | ,
11 | );
12 |
--------------------------------------------------------------------------------
/webview-ui/src/lib/contexts/theme.tsx:
--------------------------------------------------------------------------------
1 | import { ReactNode, createContext, useContext } from 'react';
2 | import { ColorThemeKind } from '../types/schema';
3 |
4 | type ThemeContextType = {
5 | theme: ColorThemeKind;
6 | isDarkMode: boolean;
7 | };
8 |
9 | const ThemeContext = createContext(undefined);
10 |
11 | export const useTheme = () => {
12 | const context = useContext(ThemeContext);
13 | if (!context) {
14 | throw new Error('useTheme debe ser usado dentro de un ThemeProvider');
15 | }
16 | return context;
17 | };
18 |
19 | export const ThemeProvider = ({
20 | children,
21 | theme,
22 | }: {
23 | children: ReactNode;
24 | theme: ColorThemeKind;
25 | }) => {
26 | const isDarkMode =
27 | theme === ColorThemeKind.Dark || theme === ColorThemeKind.HighContrast;
28 |
29 | return (
30 |
31 | {children}
32 |
33 | );
34 | };
35 |
--------------------------------------------------------------------------------
/webview-ui/src/lib/hooks/useGraph.ts:
--------------------------------------------------------------------------------
1 | import equal from 'fast-deep-equal';
2 | import { useCallback, useEffect, useRef, useState } from 'react';
3 | import { getLayoutedElements } from '../utils/layout-utils';
4 | import {
5 | addEdge,
6 | Connection,
7 | ConnectionLineType,
8 | Edge,
9 | useEdgesState,
10 | useNodesState,
11 | useReactFlow,
12 | } from '@xyflow/react';
13 | import { MyNode } from '../types/schema';
14 |
15 | const DEFAULT_LAYOUT = 'TB';
16 |
17 | export const useGraph = (initialNodes: MyNode[], initialEdges: Edge[]) => {
18 | const { fitView } = useReactFlow();
19 |
20 | const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
21 | const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
22 |
23 | const [selectedLayout, setSelectedLayout] = useState(DEFAULT_LAYOUT);
24 |
25 | const [shouldFitView, setShouldFitView] = useState(false);
26 |
27 | const isFirstRender = useRef(true);
28 |
29 | const applyLayout = useCallback(
30 | (layoutDirection: string, fromNodes = nodes, fromEdges = edges) => {
31 | const { nodes: layoutedNodes, edges: layoutedEdges } =
32 | getLayoutedElements(fromNodes, fromEdges, layoutDirection);
33 | setNodes(layoutedNodes);
34 | setEdges(layoutedEdges);
35 | setShouldFitView(true);
36 | },
37 | [nodes, edges, setNodes, setEdges],
38 | );
39 |
40 | const onLayout = useCallback(
41 | (direction: string) => {
42 | applyLayout(direction);
43 | setSelectedLayout(direction);
44 | },
45 | [applyLayout],
46 | );
47 |
48 | const onConnect = useCallback(
49 | (params: Connection) => {
50 | setEdges((eds) =>
51 | addEdge(
52 | {
53 | ...params,
54 | type: ConnectionLineType.SmoothStep,
55 | animated: true,
56 | },
57 | eds,
58 | ),
59 | );
60 | },
61 | [setEdges],
62 | );
63 |
64 | useEffect(() => {
65 | if (!initialNodes?.length && !initialEdges?.length) return;
66 |
67 | if (isFirstRender.current) {
68 | isFirstRender.current = false;
69 | applyLayout(DEFAULT_LAYOUT, initialNodes, initialEdges);
70 | return;
71 | }
72 |
73 | const nodesChanged = !equal(
74 | initialNodes.map((n) => n.data),
75 | nodes.map((n) => n.data),
76 | );
77 | const edgesChanged = !equal(
78 | initialEdges.map((e) => ({ source: e.source, target: e.target })),
79 | edges.map((e) => ({ source: e.source, target: e.target })),
80 | );
81 |
82 | if (nodesChanged || edgesChanged) {
83 | applyLayout(selectedLayout, initialNodes, initialEdges);
84 | }
85 | }, [initialNodes, initialEdges, applyLayout, selectedLayout, nodes, edges]);
86 |
87 | useEffect(() => {
88 | if (shouldFitView) {
89 | fitView();
90 | setShouldFitView(false);
91 | }
92 | }, [shouldFitView, fitView]);
93 |
94 | useEffect(() => {
95 | const deleteDiv = document.getElementsByClassName(
96 | 'react-flow__panel react-flow__attribution bottom right',
97 | );
98 |
99 | if (deleteDiv.length > 0) {
100 | deleteDiv[0].remove();
101 | }
102 | }, []);
103 |
104 | return {
105 | nodes,
106 | edges,
107 | onNodesChange,
108 | onEdgesChange,
109 | onConnect,
110 | onLayout,
111 | setNodes,
112 | setEdges,
113 | selectedLayout,
114 | };
115 | };
116 |
--------------------------------------------------------------------------------
/webview-ui/src/lib/types/schema.ts:
--------------------------------------------------------------------------------
1 | import { Node } from '@xyflow/react';
2 |
3 | export type Model = {
4 | name: string;
5 | fields: {
6 | name: string;
7 | type: string;
8 | hasConnections?: boolean;
9 | }[];
10 | isChild?: boolean;
11 | };
12 |
13 | export type ModelConnection = {
14 | target: string;
15 | source: string;
16 | name: string;
17 | };
18 |
19 | export type Enum = {
20 | name: string;
21 | values: string[];
22 | };
23 |
24 | export enum ColorThemeKind {
25 | Light = 1,
26 | Dark = 2,
27 | HighContrast = 3,
28 | HighContrastLight = 4,
29 | }
30 |
31 | export type EnumNodeTye = Node;
32 | export type ModelNodeTye = Node;
33 |
34 | type NodeData = Model | Enum;
35 | export type MyNode = Node;
36 |
--------------------------------------------------------------------------------
/webview-ui/src/lib/utils/colots.ts:
--------------------------------------------------------------------------------
1 | export const nodeColor = (isDarkMode: boolean) =>
2 | isDarkMode ? '#3d5797' : '#8b9dc3';
3 |
4 | export const nodeStrokeColor = (isDarkMode: boolean) =>
5 | isDarkMode ? '#282828' : '#e0e0e0';
6 |
7 | export const maskColor = (isDarkMode: boolean) =>
8 | isDarkMode ? 'rgba(0, 0, 0, 0.2)' : 'rgba(255, 255, 255, 0.5)';
9 |
10 | export const getButtonStyle = (selectedLayout: string, layout: string) => {
11 | return selectedLayout === layout
12 | ? 'bg-blue-500 text-white px-4 py-2 rounded'
13 | : 'bg-gray-300 text-black px-4 py-2 rounded';
14 | };
15 |
--------------------------------------------------------------------------------
/webview-ui/src/lib/utils/layout-utils.ts:
--------------------------------------------------------------------------------
1 | import dagre from '@dagrejs/dagre';
2 | import { Edge, Position } from '@xyflow/react';
3 | import { MyNode } from '../types/schema';
4 |
5 | const dagreGraph = new dagre.graphlib.Graph();
6 | dagreGraph.setDefaultEdgeLabel(() => ({}));
7 |
8 | const nodeWidth = 250;
9 | const nodeHeight = 400;
10 |
11 | export const getLayoutedElements = (
12 | nodes: MyNode[],
13 | edges: Edge[],
14 | direction = 'TB',
15 | ) => {
16 | const isHorizontal = direction === 'LR';
17 | dagreGraph.setGraph({ rankdir: direction });
18 |
19 | nodes.forEach((node) => {
20 | dagreGraph.setNode(node.id, { width: nodeWidth, height: nodeHeight });
21 | });
22 |
23 | edges.forEach((edge) => {
24 | dagreGraph.setEdge(edge.source, edge.target);
25 | });
26 |
27 | dagre.layout(dagreGraph);
28 |
29 | const newNodes = nodes.map((node) => {
30 | const nodeWithPosition = dagreGraph.node(node.id);
31 | return {
32 | ...node,
33 | targetPosition: isHorizontal ? Position.Left : Position.Top,
34 | sourcePosition: isHorizontal ? Position.Right : Position.Bottom,
35 | position: {
36 | x: nodeWithPosition.x - nodeWidth / 2,
37 | y: nodeWithPosition.y - nodeHeight / 2,
38 | },
39 | };
40 | });
41 |
42 | return { nodes: newNodes, edges };
43 | };
44 |
--------------------------------------------------------------------------------
/webview-ui/src/lib/utils/screnshot.ts:
--------------------------------------------------------------------------------
1 | import { getNodesBounds, getViewportForBounds } from '@xyflow/react';
2 | import { toPng } from 'html-to-image';
3 | import { MyNode } from '../types/schema';
4 |
5 | interface VSCodeAPI {
6 | postMessage(message: SaveImageMessage): void;
7 | getState(): unknown;
8 | setState(state: unknown): void;
9 | }
10 |
11 | interface SaveImageMessage {
12 | command: 'saveImage';
13 | data: {
14 | format: 'png';
15 | dataUrl: string;
16 | };
17 | }
18 |
19 | declare function acquireVsCodeApi(): VSCodeAPI;
20 |
21 | const vscode = acquireVsCodeApi();
22 |
23 | export const screenshot = (getNodes: () => MyNode[]) => {
24 | const nodesBounds = getNodesBounds(getNodes());
25 |
26 | // 8k resolution
27 | const imageWidth = 7680;
28 | const imageHeight = 4320;
29 |
30 | const transform = getViewportForBounds(
31 | nodesBounds,
32 | imageWidth,
33 | imageHeight,
34 | 0,
35 | 2,
36 | 0,
37 | );
38 |
39 | toPng(document.querySelector('.react-flow__viewport') as HTMLElement, {
40 | filter: (node) => {
41 | const exclude = ['react-flow__minimap', 'react-flow__controls'];
42 | return !exclude.some((className) => node.classList?.contains(className));
43 | },
44 | backgroundColor: 'transparent',
45 | width: imageWidth,
46 | height: imageHeight,
47 | style: {
48 | transform: `translate(${transform.x}px, ${transform.y}px) scale(${transform.zoom})`,
49 | },
50 | })
51 | .then((dataUrl) => {
52 | vscode.postMessage({
53 | command: 'saveImage',
54 | data: { format: 'png', dataUrl },
55 | });
56 | })
57 | .catch((error) => {
58 | console.error('Error generating image:', error);
59 | });
60 | };
61 |
--------------------------------------------------------------------------------
/webview-ui/src/vite-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/webview-ui/tailwind.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('tailwindcss').Config} */
2 | export default {
3 | content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'],
4 | theme: {
5 | extend: {},
6 | },
7 | plugins: [],
8 | };
9 |
--------------------------------------------------------------------------------
/webview-ui/tsconfig.app.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES2020",
4 | "useDefineForClassFields": true,
5 | "lib": ["ES2020", "DOM", "DOM.Iterable"],
6 | "module": "ESNext",
7 | "skipLibCheck": true,
8 |
9 | "moduleResolution": "bundler",
10 | "allowImportingTsExtensions": true,
11 | "isolatedModules": true,
12 | "moduleDetection": "force",
13 | "noEmit": true,
14 | "jsx": "react-jsx",
15 |
16 | "strict": true,
17 | "noUnusedLocals": true,
18 | "noUnusedParameters": true,
19 | "noFallthroughCasesInSwitch": true
20 | },
21 | "include": ["src"]
22 | }
23 |
--------------------------------------------------------------------------------
/webview-ui/tsconfig.app.tsbuildinfo:
--------------------------------------------------------------------------------
1 | {"root":["./src/app.tsx","./src/index.tsx","./src/vite-env.d.ts","./src/components/enumnode.tsx","./src/components/modelnode.tsx","./src/components/schemavisualizer.tsx","./src/components/icons/idownload.tsx","./src/components/icons/props.ts","./src/lib/contexts/theme.tsx","./src/lib/hooks/usegraph.ts","./src/lib/types/schema.ts","./src/lib/utils/colots.ts","./src/lib/utils/layout-utils.ts","./src/lib/utils/screnshot.ts"],"version":"5.7.2"}
--------------------------------------------------------------------------------
/webview-ui/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "files": [],
3 | "references": [
4 | { "path": "./tsconfig.app.json" },
5 | { "path": "./tsconfig.node.json" }
6 | ]
7 | }
8 |
--------------------------------------------------------------------------------
/webview-ui/tsconfig.node.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES2022",
4 | "lib": ["ES2023"],
5 | "module": "ESNext",
6 | "skipLibCheck": true,
7 |
8 | "moduleResolution": "bundler",
9 | "allowImportingTsExtensions": true,
10 | "isolatedModules": true,
11 | "moduleDetection": "force",
12 | "noEmit": true,
13 |
14 | "strict": true,
15 | "noUnusedLocals": true,
16 | "noUnusedParameters": true,
17 | "noFallthroughCasesInSwitch": true
18 | },
19 | "include": ["vite.config.ts"]
20 | }
21 |
--------------------------------------------------------------------------------
/webview-ui/tsconfig.node.tsbuildinfo:
--------------------------------------------------------------------------------
1 | {"root":["./vite.config.ts"],"version":"5.7.2"}
--------------------------------------------------------------------------------
/webview-ui/vite.config.ts:
--------------------------------------------------------------------------------
1 | import react from '@vitejs/plugin-react';
2 | import { defineConfig } from 'vite';
3 |
4 | // https://vitejs.dev/config/
5 | export default defineConfig({
6 | plugins: [react()],
7 | build: {
8 | outDir: 'build',
9 | rollupOptions: {
10 | output: {
11 | entryFileNames: 'assets/[name].js',
12 | chunkFileNames: 'assets/[name].js',
13 | assetFileNames: 'assets/[name].[ext]',
14 | },
15 | },
16 | },
17 | });
18 |
--------------------------------------------------------------------------------