├── .DS_Store ├── .angular └── cache │ └── 16.2.4 │ └── angular-webpack │ └── be6301068d2975157428be5b39892fc84880bd8f │ ├── 0.pack │ └── index.pack ├── .eslintrc.json ├── .gitignore ├── .vscode ├── extensions.json ├── launch.json ├── settings.json └── tasks.json ├── .vscodeignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── assets ├── Angulens_3.png ├── collapsingFolders.gif ├── modal.gif ├── navigatefile.gif ├── services.gif └── startAnguLens.gif ├── package-lock.json ├── package.json ├── src ├── createViewAlgos │ └── populateAlgos.ts ├── extension.ts └── test │ ├── runTest.ts │ └── suite │ ├── extension.test.ts │ └── index.ts ├── tsconfig.json ├── vsc-extension-quickstart.md └── webview-ui ├── .editorconfig ├── .gitignore ├── .vscode ├── extensions.json ├── launch.json └── tasks.json ├── README.md ├── angular.json ├── package-lock.json ├── package.json ├── src ├── app │ ├── app-routing.module.ts │ ├── app.component.css │ ├── app.component.html │ ├── app.component.spec.ts │ ├── app.component.ts │ ├── app.module.ts │ ├── folder-file │ │ ├── folder-file.component.css │ │ ├── folder-file.component.html │ │ ├── folder-file.component.spec.ts │ │ └── folder-file.component.ts │ ├── modal │ │ ├── modal.component.css │ │ ├── modal.component.html │ │ ├── modal.component.spec.ts │ │ └── modal.component.ts │ ├── parent-child │ │ ├── parent-child.component.css │ │ ├── parent-child.component.html │ │ ├── parent-child.component.spec.ts │ │ └── parent-child.component.ts │ ├── services-view │ │ ├── services-view.component.css │ │ ├── services-view.component.html │ │ ├── services-view.component.spec.ts │ │ └── services-view.component.ts │ └── utilities │ │ └── vscode.ts ├── favicon.ico ├── index.html ├── main.ts ├── models │ ├── FileSystem.ts │ ├── message.ts │ └── uri.ts ├── services │ ├── FileSystemService.ts │ └── ParentChildServices.ts └── styles.css ├── tsconfig.app.json ├── tsconfig.json └── tsconfig.spec.json /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/AnguLens/6ed5f9dde429dd1cf82bbc577e5f2e85294e62a4/.DS_Store -------------------------------------------------------------------------------- /.angular/cache/16.2.4/angular-webpack/be6301068d2975157428be5b39892fc84880bd8f/0.pack: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/AnguLens/6ed5f9dde429dd1cf82bbc577e5f2e85294e62a4/.angular/cache/16.2.4/angular-webpack/be6301068d2975157428be5b39892fc84880bd8f/0.pack -------------------------------------------------------------------------------- /.angular/cache/16.2.4/angular-webpack/be6301068d2975157428be5b39892fc84880bd8f/index.pack: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/AnguLens/6ed5f9dde429dd1cf82bbc577e5f2e85294e62a4/.angular/cache/16.2.4/angular-webpack/be6301068d2975157428be5b39892fc84880bd8f/index.pack -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parser": "@typescript-eslint/parser", 4 | "parserOptions": { 5 | "ecmaVersion": 6, 6 | "sourceType": "module" 7 | }, 8 | "plugins": [ 9 | "@typescript-eslint" 10 | ], 11 | "rules": { 12 | "@typescript-eslint/naming-convention": "warn", 13 | "@typescript-eslint/semi": "warn", 14 | "curly": "warn", 15 | "eqeqeq": "warn", 16 | "no-throw-literal": "warn", 17 | "semi": "off" 18 | }, 19 | "ignorePatterns": [ 20 | "out", 21 | "dist", 22 | "**/*.d.ts" 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /out -------------------------------------------------------------------------------- /.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 | ] 7 | } 8 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | // A launch configuration that compiles the extension and then opens it 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": [ 13 | "--extensionDevelopmentPath=${workspaceFolder}" 14 | ], 15 | "outFiles": [ 16 | "${workspaceFolder}/out/**/*.js" 17 | ], 18 | "preLaunchTask": "${defaultBuildTask}" 19 | }, 20 | { 21 | "name": "Extension Tests", 22 | "type": "extensionHost", 23 | "request": "launch", 24 | "args": [ 25 | "--extensionDevelopmentPath=${workspaceFolder}", 26 | "--extensionTestsPath=${workspaceFolder}/out/test/suite/index" 27 | ], 28 | "outFiles": [ 29 | "${workspaceFolder}/out/test/**/*.js" 30 | ], 31 | "preLaunchTask": "${defaultBuildTask}" 32 | } 33 | ] 34 | } 35 | -------------------------------------------------------------------------------- /.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 | } -------------------------------------------------------------------------------- /.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 | src/** 4 | .gitignore 5 | .yarnrc 6 | vsc-extension-quickstart.md 7 | **/tsconfig.json 8 | **/.eslintrc.json 9 | **/*.map 10 | **/*.ts 11 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to the "angulens" extension will be documented in this file. 4 | 5 | Check [Keep a Changelog](http://keepachangelog.com/) for recommendations on how to structure this file. 6 | 7 | ## [Unreleased] 8 | 9 | - Initial release -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 OSLabs Beta 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. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | Magnifying glass AnguLens logo 3 |
4 | 5 |
6 | Version number: 1.0.0 7 | Contributors: 4 8 |
9 | 10 |
11 | angular badge 12 | typescript badge 13 |
14 | 15 | 16 | # 17 | 18 | ## AnguLens 19 | 20 | Welcome to AnguLens, a VSCode extension build to aid understanding of Angular based projects through visualization. 21 | 22 | ## Features 23 | 24 | Using AnguLens you can: 25 | - Choose a project directory 26 | - View folder-file hierarchy 27 | - Navigate to selected files in views via double click 28 | - View inputs / outputs of all components 29 | - View routers 30 | - View component hierarchy between parent and child components 31 | - View additional information of files on click via modal 32 | 33 | ## Getting started 34 | 35 | ### Starting source directory 36 | 37 | 1. Install AnguLens from extension marketplace 38 | 2. Open commands (cmd/ctrl + shift + p) and run "Start AnguLens" 39 | 3. Right click on "src" folder in Angular project 40 | 4. Click "Copy Path" 41 | 5. Paste path into text input and click generate 42 | 43 |

44 | 45 | # Folder-File view 46 | Displays file structure of selected directory 47 | - Click on folders to open or close them for a cleaner view 48 |

49 | - Double click files to navigate current VSCode window to clicked file 50 |

51 | 52 | # Component view 53 | Displays component hierarchy, including routers, with overlayed connections representing different connections 54 | - Filter component connections using dropdown menu 55 | - Inputs/Outputs 56 | - Component Hierarchy 57 | - Click on components to get additional information in modal 58 | -Inputs 59 | -Outputs 60 | -Services 61 |

62 | 63 | # Services view 64 | -Displays every service as a new network, with any components that utilize that service 65 |

66 | 67 | [contributors-url]: https://github.com/oslabs-beta/AnguLens/graphs/contributors 68 | [contributors-shield]: https://img.shields.io/badge/Contributors-4-darkred?logoColor=white 69 | [version-shield]: https://img.shields.io/badge/version-1.0.0-darkred?logoColor=white 70 | [angular-shield]: https://img.shields.io/badge/Angular-darkred?style=flat-square&logo=angular 71 | [angular-url]: https://angular.io/ 72 | [typescript-shield]: https://img.shields.io/badge/TypeScript-blue?style=flat-square&logo=typescript&logoColor=white 73 | [typescript-url]: https://www.typescriptlang.org/ 74 | 75 | # Contributing 76 | AnguLens is an open source project and we encourage iteration and or contribution. To coontribute fork the repo, make feature branches, and PR from your feature branch into AnguLen's dev. 77 | 78 | 79 | 80 | ## Current Roadmap 81 | - Removing use of retain context when hidden from VSCode API and use state instead 82 | - Support for Workspaces/monorepos 83 | - Mapping Signals 84 | - Expanding module support 85 | - Mapping Observabels 86 | -------------------------------------------------------------------------------- /assets/Angulens_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/AnguLens/6ed5f9dde429dd1cf82bbc577e5f2e85294e62a4/assets/Angulens_3.png -------------------------------------------------------------------------------- /assets/collapsingFolders.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/AnguLens/6ed5f9dde429dd1cf82bbc577e5f2e85294e62a4/assets/collapsingFolders.gif -------------------------------------------------------------------------------- /assets/modal.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/AnguLens/6ed5f9dde429dd1cf82bbc577e5f2e85294e62a4/assets/modal.gif -------------------------------------------------------------------------------- /assets/navigatefile.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/AnguLens/6ed5f9dde429dd1cf82bbc577e5f2e85294e62a4/assets/navigatefile.gif -------------------------------------------------------------------------------- /assets/services.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/AnguLens/6ed5f9dde429dd1cf82bbc577e5f2e85294e62a4/assets/services.gif -------------------------------------------------------------------------------- /assets/startAnguLens.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/AnguLens/6ed5f9dde429dd1cf82bbc577e5f2e85294e62a4/assets/startAnguLens.gif -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angulens", 3 | "displayName": "AnguLens", 4 | "description": "Angular Visualizer", 5 | "version": "0.0.1", 6 | "engines": { 7 | "vscode": "^1.83.0" 8 | }, 9 | "categories": [ 10 | "Other" 11 | ], 12 | "activationEvents": [ 13 | "onCommand: angulens.start" 14 | ], 15 | "main": "./out/extension.js", 16 | "contributes": { 17 | "commands": [ 18 | { 19 | "command": "angulens.start", 20 | "title": "Start AnguLens" 21 | } 22 | ] 23 | }, 24 | "scripts": { 25 | "vscode:prepublish": "npm run compile", 26 | "compile": "tsc -p ./", 27 | "watch": "tsc -watch -p ./", 28 | "pretest": "npm run compile && npm run lint", 29 | "lint": "eslint src --ext ts", 30 | "test": "node ./out/test/runTest.js" 31 | }, 32 | "devDependencies": { 33 | "@phenomnomnominal/tsquery": "^6.1.3", 34 | "@types/klaw": "^3.0.4", 35 | "@types/mocha": "^10.0.2", 36 | "@types/node": "18.x", 37 | "@types/vscode": "^1.83.0", 38 | "@typescript-eslint/eslint-plugin": "^6.7.3", 39 | "@typescript-eslint/parser": "^6.7.3", 40 | "@vscode/test-electron": "^2.3.4", 41 | "cheerio": "^1.0.0-rc.12", 42 | "eslint": "^8.50.0", 43 | "glob": "^10.3.3", 44 | "mocha": "^10.2.0" 45 | }, 46 | "dependencies": { 47 | "@types/vscode-webview": "^1.57.2", 48 | "klaw": "^4.1.0", 49 | "typescript": "^5.3.2" 50 | 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/createViewAlgos/populateAlgos.ts: -------------------------------------------------------------------------------- 1 | //@ts-nocheck 2 | 3 | import * as ts from "typescript"; 4 | import * as fs from "fs"; 5 | import { tsquery } from "@phenomnomnominal/tsquery"; 6 | import cheerio = require("cheerio"); 7 | 8 | 9 | export function populateStructure(array: any, selectorNames: object[], servicesList: object[], modulesList: object[]): object { 10 | const output = {}; 11 | let rootPath: string = ""; 12 | let omitIndeces; 13 | for (const item of array) { 14 | let pathArray = item.path.split("/"); 15 | let name = pathArray.pop(); 16 | 17 | if (rootPath.length === 0) { 18 | rootPath = name; //resetting our default, from rootpath = '', so that our else conditional on line 25 will hit 19 | output[name] = { 20 | type: "folder", 21 | path: item.path, 22 | }; 23 | omitIndeces = pathArray.length; 24 | } else { 25 | pathArray.splice(0, omitIndeces); 26 | let objTracker = output; 27 | for (const key of pathArray) { 28 | objTracker = objTracker[key]; 29 | } 30 | 31 | // type logic 32 | let type; 33 | let nameList = name.split(".") 34 | if (nameList.length > 1) { 35 | type = nameList.pop(); 36 | } else { 37 | type = "folder"; 38 | } 39 | 40 | if (type === "ts") { 41 | const filePath = item.path; 42 | let tempArray = filePath.split("/"); 43 | tempArray.pop(); 44 | const folderPath = tempArray.join("/"); 45 | const sourceFile = generateAST(filePath); 46 | 47 | // looking for "selectorName" (the variable name used to call components, in angular templates) 48 | const selectorProperties = tsquery( 49 | sourceFile, 50 | "PropertyAssignment > Identifier[name=selector]" 51 | ); 52 | 53 | // looking for name (the object class name that's exported from component.ts, and imported elsewhere) 54 | const exportClassName = tsquery( 55 | sourceFile, 56 | 'ClassDeclaration:has(Decorator[expression.expression.name="Component"]) > Identifier' 57 | ) 58 | let className; 59 | if (exportClassName.length > 0){ 60 | //we don't really need this if... any component.ts file we check will have a name (query results array will have length) 61 | className = exportClassName[0].getText(); 62 | }; 63 | 64 | //REFACTOR? why do we have the if statement below? --> selectorProperties should never be empty. It's just querying our AST to pull out data 65 | if (selectorProperties.length > 0) { 66 | // selectorProperties(result of our query) is an array. If it has length, access [0] element 67 | const selectorName = selectorProperties[0].parent.initializer.text; 68 | // results of our query (selectorPropertiez) is an array of NODE(s) from the AST 69 | const obj = { 70 | selectorName, 71 | folderPath, 72 | filePath, 73 | className, 74 | inputs: [], 75 | outputs: [], 76 | }; 77 | 78 | populateInputs(sourceFile, obj, folderPath); 79 | populateOutputs(sourceFile, obj, folderPath); 80 | inLineCheck(sourceFile, obj); 81 | selectorNames.push(obj); 82 | } 83 | } 84 | 85 | //checking for type ts, and file names that include "service" - this will work whether the string is event.service or eventService 86 | //REFACTOR all of this into a helper function? (services funcion) 87 | if (type === "ts" && name.toLowerCase().includes("service")){ 88 | //REFACTOR: instead of running generateAST twice on filepath, we can call it above and use a variable 89 | let servicePath = generateAST(item.path); 90 | 91 | //querying the value of the "providedIn" property, within the @injectable directive of our service file - the value of this property is the module where this service is scoped to 92 | const serviceProvidedIn = tsquery( 93 | servicePath, 94 | 'PropertyAssignment > Identifier[name=providedIn]' 95 | ); 96 | 97 | //The following if statement will only run if there was @injectable in the service file --> sometimes we might land on a service.spec.ts file without @injectable 98 | if (serviceProvidedIn.length){ 99 | let serviceObj = { 100 | path: item.path, 101 | fileName: nameList.join("."),//will result in "event.service" if naming conventions were followed (the .ts already removed on line 37's "nameList.pop()" call) --> we could set name differently here.... 102 | className: '', //need the exported className, to check against selectorNames imports (to see where the service is imported) 103 | injectionPoints: [], 104 | providedIn: serviceProvidedIn[0].parent.initializer.text 105 | }; 106 | 107 | //querying the className from our service file - what's exported / what's imported when you want to use the service in a component 108 | let serviceClassName = tsquery( 109 | servicePath, 110 | 'ClassDeclaration:has(Decorator[expression.expression.name="Injectable"]) > Identifier' 111 | ); 112 | 113 | serviceObj.className = serviceClassName[0].getText(); 114 | 115 | servicesList.push(serviceObj); //serviceList instantiated in extension.ts, passed in to populateStucture as argument 116 | } 117 | } 118 | 119 | objTracker[name] = { 120 | type, 121 | path: item.path, 122 | }; 123 | } 124 | } 125 | return output; 126 | } 127 | 128 | export function generateAST(filePath: string) { 129 | const fileContent = fs.readFileSync(filePath, "utf-8"); //get the string content / code of our file 130 | 131 | const sourceFile = ts.createSourceFile( 132 | //create the AST from our fileContent, store as sourceFile 133 | filePath, 134 | fileContent, 135 | ts.ScriptTarget.Latest, 136 | true 137 | ); 138 | return sourceFile; 139 | } 140 | 141 | function populateInputs(sourceFile, obj, folderPath) { 142 | const inputProperties = tsquery( 143 | sourceFile, 144 | "PropertyDeclaration:has(Identifier[name=Input])" 145 | ) as ts.PropertyDeclaration[]; 146 | 147 | inputProperties.forEach((variable) => { 148 | const variableName = (variable.name as ts.Identifier).text; 149 | const input = { 150 | name: variableName, 151 | pathTo: folderPath, 152 | }; 153 | obj.inputs.push(input); 154 | }); 155 | } 156 | 157 | function populateOutputs(sourceFile, obj, folderPath) { 158 | const outputProperties = tsquery( 159 | sourceFile, 160 | "PropertyDeclaration:has(Decorator > CallExpression > Identifier[name=Output])" 161 | ) as ts.PropertyDeclaration[]; 162 | 163 | outputProperties.forEach((variable) => { 164 | const variableName = (variable.name as ts.Identifier).text; 165 | const output = { 166 | name: variableName, 167 | pathFrom: folderPath, 168 | }; 169 | obj.outputs.push(output); 170 | }); 171 | } 172 | 173 | export function inLineCheck(sourceFile: ts.SourceFile, obj: object) { 174 | const templateProperties = tsquery( 175 | sourceFile, 176 | "NoSubstitutionTemplateLiteral" 177 | ); 178 | // if Component is using an inline template 179 | if (templateProperties.length > 0) { 180 | const temp = templateProperties[0] as ts.NoSubstitutionTemplateLiteral; //ts.StringLiteral; 181 | obj.template = temp.text.trim(); 182 | } 183 | //else --> the component is not using an inline template = we don't need to create an obj.template property, because there will be a template file we can just use our convertToHtml helper function on 184 | } 185 | 186 | 187 | export function populateServicesView(selectorNames: object[], servicesList: object[]): any { 188 | let appPath: string; 189 | 190 | //REFACTOR? --> we sort the array alphabetically so app comes first? we don't *need* to iterate here.... 191 | for (const selectorName of selectorNames) { 192 | if (selectorName.selectorName === "app-root") { 193 | appPath = selectorName.folderPath; 194 | } 195 | 196 | let ast = generateAST(selectorName.filePath); 197 | //Enriching our services list, by checking which components (each called a "selectorName" here) imports a given service 198 | populateServicesList(servicesList, selectorName, ast); 199 | } 200 | return servicesList; 201 | } 202 | 203 | 204 | // populates Parent Child object (pcObject) with all relevant data, to send to Angular App front end (in webviewUI folder) 205 | //also enriches our services list with all the components where that service is imported and used 206 | export function populatePCView(selectorNames: object[]): object { 207 | let appPath: string; 208 | 209 | //REFACTOR? --> we sort the array alphabetically so app comes first? we don't *need* to iterate here.... 210 | for (const selectorName of selectorNames) { 211 | if (selectorName.selectorName === "app-root") { 212 | appPath = selectorName.folderPath; 213 | } 214 | 215 | let ast = generateAST(selectorName.filePath); 216 | //Enriching our services list, by checking which components (each called a "selectorName" here) imports a given service 217 | //populateServicesList(servicesList, selectorName, ast); 218 | } 219 | 220 | const pcObject = { 221 | name: "app-root", 222 | path: appPath, 223 | children: [], 224 | router: {}, 225 | }; 226 | 227 | populateChildren(pcObject, selectorNames); 228 | handleRouter(appPath, selectorNames, pcObject); 229 | //could check for app-router.module? --> or do we want to do that in handleRouter? 230 | // check for any lazy loaded routes? --> if they have components that aren't otherwise tracked in pcObject 231 | return pcObject; 232 | } 233 | 234 | 235 | 236 | function populateServicesList(servicesList, selectorName, ast) { 237 | servicesList.forEach(service => { 238 | //Query the imports from each component (items in selectorNames) 239 | const componentImports = tsquery( 240 | ast, 241 | `ImportSpecifier > Identifier[name="${service.className}"]` 242 | ); 243 | if (componentImports.length){ 244 | service.injectionPoints.push(selectorName); 245 | } 246 | }); 247 | } 248 | 249 | 250 | 251 | function populateChildren(pcObject: object, selectorNames: object[]): object { 252 | let templateContent: string; 253 | 254 | for (const selectorName of selectorNames) { 255 | if (selectorName.folderPath === pcObject.path) { 256 | if (!selectorName.template) { 257 | const filePath = convertToHtml(pcObject.path); 258 | templateContent = fs.readFileSync(filePath, "utf-8"); 259 | } else { 260 | templateContent = selectorName.template; 261 | } 262 | } 263 | } 264 | 265 | for (const selectorName of selectorNames) { 266 | const obj = { 267 | name: "", 268 | path: "", 269 | inputs: [], 270 | children: [], 271 | outputs: [], 272 | }; 273 | 274 | if (selectorCheck(templateContent, selectorName.selectorName)) { 275 | obj.name = selectorName.selectorName; 276 | obj.path = selectorName.folderPath; 277 | selectorName.inputs.forEach((input) => { 278 | if (inputCheck(templateContent, input.name)) { 279 | input.pathFrom = pcObject.path; 280 | obj.inputs.push(input); 281 | } 282 | }); 283 | selectorName.outputs.forEach((output) => { 284 | if (outputCheck(templateContent, output.name)) { 285 | output.pathTo = pcObject.path; 286 | obj.outputs.push(output); 287 | } 288 | }); 289 | pcObject.children.push(obj); 290 | } 291 | } 292 | //Recursively call this function on each obj of children array 293 | pcObject.children.forEach((child) => populateChildren(child, selectorNames)); 294 | return pcObject; 295 | } 296 | 297 | //takes in a folder path for an angular component, and returns the component.html filepath for its template 298 | function convertToHtml(folderPath: string): string { 299 | let path = folderPath.split("/"); 300 | const component = path.pop(); 301 | const htmlFile = component + ".component.html"; 302 | return folderPath + "/" + htmlFile; 303 | } 304 | 305 | // checks if angular template (parsed = template content in string form) contains a given component (selectorName) 306 | function selectorCheck(parsed: string, selectorName: string): boolean { 307 | const $ = cheerio.load(parsed); 308 | // $ is variable name (?) --> using cheerio to ".load" our template content (parsed) in string format 309 | const foundElement = $(selectorName); 310 | //checking $ to see if it has the given selectorName (component being called, like an element, within our angular template's HTML) 311 | 312 | if (foundElement.length) { 313 | //if it found a match (variable foundElement has length) return true 314 | return true; 315 | } 316 | return false; 317 | } 318 | 319 | // checks an agular template (in string formate) to see if it contians an inputName 320 | function inputCheck(templateContent: string, inputName: string) { 321 | const regex = new RegExp(`\\[${inputName}\\]`, "g"); 322 | const matches = templateContent.match(regex); 323 | if (matches) { 324 | return true; 325 | } else { 326 | return false; 327 | } 328 | } 329 | 330 | // checks an agular template (in string formate) to see if it contians an outputName 331 | function outputCheck(templateContent: string, outputName: string) { 332 | const regex = new RegExp(`\\(${outputName}\\)`, "g"); 333 | const matches = templateContent.match(regex); 334 | if (matches) { 335 | return true; 336 | } else { 337 | return false; 338 | } 339 | } 340 | 341 | function handleRouter(appPath, selectorNames, pcObject) { 342 | const routerObject = { //generate new object (instead of pcObject) to represent components brought in from router outlet... 343 | name: 'router-outlet', 344 | path: 'router-outlet', 345 | children: [] 346 | }; 347 | const appComponent = appPath + "/app.component.ts"; 348 | //checking if app.component.ts is using an inline template, or a template URL 349 | inLineCheck(appComponent, routerObject); 350 | 351 | //if app component is not using inline template 352 | if (!routerObject.template) { 353 | let appTemplate: string = fs.readFileSync(convertToHtml(appPath), "utf-8"); 354 | const $ = cheerio.load(appTemplate); 355 | const foundRouter = $("router-outlet"); 356 | if (foundRouter.length) { 357 | populateRouterOutletComponents( 358 | appPath, 359 | selectorNames, 360 | routerObject, 361 | pcObject 362 | ); 363 | } 364 | } 365 | //if app component is using an inline template 366 | else { 367 | const $ = cheerio.load(routerObject.template); 368 | const foundRouter = $("router-outlet"); 369 | if (foundRouter.length) { 370 | populateRouterOutletComponents( 371 | appPath, 372 | selectorNames, 373 | routerObject, 374 | pcObject 375 | ); 376 | } 377 | } 378 | } 379 | 380 | function populateRouterOutletComponents( 381 | appPath, 382 | selectorNames, 383 | routerObject, 384 | pcObject 385 | ) { 386 | const modulePath = appPath + "/app.module.ts"; 387 | const moduleSource = generateAST(modulePath); 388 | 389 | const routerComponents = tsquery( 390 | moduleSource, 391 | 'PropertyAssignment Identifier[name="component"] ~ Identifier' //checking the AST to find the component names in the "routes" of app.module 392 | ); 393 | const componentNames = routerComponents.map((node) => node.text); 394 | 395 | const routerPaths = tsquery( 396 | moduleSource, 397 | 'PropertyAssignment Identifier[name="path"] ~ StringLiteral' //checking the URL path routing for the components in "routes" of app.module 398 | ); 399 | const componentPaths = routerPaths.map((node) => node.text); 400 | 401 | for (let i = 0; i < componentNames.length; i++) { 402 | let component = {}; 403 | component.name = componentNames[i]; 404 | component.path = "placeholder"; 405 | component.urlPath = componentPaths[i]; 406 | component.children = []; 407 | component.inputs = []; 408 | component.outputs = []; 409 | selectorNames.forEach(selector => {//SUPER inefficient, find a better way to grab the components.... an object with properties? Could you iterate over that / do everything we currently do with selectornames with that? 410 | if (selector.className === component.name){ 411 | component.path = selector.folderPath;//find the matching selector, in selectorNames, and grab it's folderpath, so generate children can access it when we pass in each component object 412 | } 413 | }); 414 | routerObject.children.push(component); 415 | } 416 | //run populate children on each component of that routerObject, to find any children components instantiated by those components 417 | routerObject.children.forEach((component) => { 418 | populateChildren(component, selectorNames); 419 | }); 420 | //add in our router-outlet components (aka routerObject) onto the router property of our larger pcObject 421 | pcObject.router = routerObject; 422 | } 423 | -------------------------------------------------------------------------------- /src/extension.ts: -------------------------------------------------------------------------------- 1 | // The module 'vscode' contains the VS Code extensibility API 2 | // Import the module and reference it with the alias vscode in your code below 3 | import * as vscode from "vscode"; 4 | import { Uri, Webview } from "vscode"; 5 | import * as path from "path"; 6 | import * as fs from "fs"; 7 | import { getVSCodeDownloadUrl } from "@vscode/test-electron/out/util"; 8 | import * as klaw from "klaw"; 9 | import { send } from "process"; 10 | import { 11 | populateStructure, 12 | populatePCView, 13 | populateServicesView, 14 | // inLineCheck, 15 | // generateAST 16 | } from "./createViewAlgos/populateAlgos"; 17 | 18 | // This method is called when your extension is activated 19 | // Your extension is activated the very first time the command is executed 20 | export function activate(context: vscode.ExtensionContext) { 21 | // Use the console to output diagnostic information (console.log) and errors (console.error) 22 | // This line of code will only be executed once when your extension is activated 23 | console.log('Congratulations, your extension "angulens" is now active!'); 24 | // const extensionPath = 25 | // vscode.extensions.getExtension("")?.extensionPath; 26 | // console.log("EXTENSION PATH", extensionPath); 27 | 28 | // The command has been defined in the package.json file 29 | // Now provide the implementation of the command with registerCommand 30 | // The commandId parameter must match the command field in package.json 31 | 32 | // Register the command for opening files in a new tab 33 | const openFileDisposable = vscode.commands.registerCommand( 34 | "angulens.openFile", 35 | (data) => { 36 | // Handle opening the file in a new tab 37 | vscode.workspace 38 | .openTextDocument(vscode.Uri.file(data.filePath)) 39 | .then((document) => { 40 | vscode.window.showTextDocument(document); 41 | }); 42 | } 43 | ); 44 | 45 | context.subscriptions.push(openFileDisposable); 46 | 47 | // create a webview panel and sets the html to the getWebViewContent function 48 | const runWebView = vscode.commands.registerCommand("angulens.start", () => { 49 | const panel = vscode.window.createWebviewPanel( 50 | "AnguLensPanel", // viewType, unique identifier 51 | "AnguLens", // name of tab in vsCode 52 | vscode.ViewColumn.One, // showOptions 53 | { 54 | enableScripts: true, 55 | retainContextWhenHidden: true, 56 | } // options 57 | ); 58 | 59 | const runtimeUri = panel.webview.asWebviewUri( 60 | vscode.Uri.file( 61 | path.join( 62 | __dirname, 63 | "../webview-ui/dist/webview-ui", 64 | "runtime.01fe1d460628a1d3.js" 65 | ) 66 | ) 67 | ); 68 | const polyfillsUri = panel.webview.asWebviewUri( 69 | vscode.Uri.file( 70 | path.join( 71 | __dirname, 72 | "../webview-ui/dist/webview-ui", 73 | "polyfills.ef3261c6791c905c.js" 74 | ) 75 | ) 76 | ); 77 | const scriptUri = panel.webview.asWebviewUri( 78 | vscode.Uri.file( 79 | path.join( 80 | __dirname, 81 | "../webview-ui/dist/webview-ui", 82 | "main.82b1c7506f973f5f.js" 83 | ) 84 | ) 85 | ); 86 | const stylesUri = panel.webview.asWebviewUri( 87 | vscode.Uri.file( 88 | path.join( 89 | __dirname, 90 | "../webview-ui/dist/webview-ui", 91 | "styles.8567a20a5369e76b.css" 92 | ) 93 | ) 94 | ); 95 | interface Message { 96 | command: string; 97 | data: any; 98 | } 99 | 100 | let items: any[] = []; 101 | let selectorNames: object[] = []; 102 | let servicesList: object[] = []; 103 | let modulesList: object[] = []; 104 | let currentFilePath: string = ""; 105 | let pcObject: object = {}; 106 | let fsObject: object = {}; 107 | let cachedServicesObject: object; 108 | let generatedServices: boolean = false; 109 | panel.webview.onDidReceiveMessage( 110 | (message: Message) => { 111 | switch (message.command) { 112 | case "loadNetwork": { 113 | items = []; 114 | selectorNames = []; 115 | servicesList = []; //Do we need to create this here AND instantiate the variable on line 115? or is that overkill? 116 | modulesList = []; // same a above 117 | generatedServices = false; 118 | const srcRootPath = message.data.filePath; 119 | currentFilePath = message.data.filePath; 120 | let rootPath: string = ""; 121 | if (Array.isArray(srcRootPath)) { 122 | rootPath = srcRootPath[0].uri.fsPath; 123 | } else if (typeof srcRootPath === "string") { 124 | rootPath = srcRootPath; 125 | } else { 126 | console.error("Invalid rootpath provided"); 127 | return; 128 | } 129 | klaw(rootPath) 130 | .on("data", (item) => items.push(item)) 131 | .on("end", () => { 132 | fsObject = populateStructure( 133 | items, 134 | selectorNames, 135 | servicesList, 136 | modulesList 137 | ); 138 | 139 | const sendNewPathObj: Message = { 140 | command: "generateFolderFile", 141 | data: fsObject, 142 | }; 143 | 144 | panel.webview.postMessage(sendNewPathObj); 145 | }); 146 | break; 147 | } 148 | 149 | case "loadServices": { 150 | if (!generatedServices) { 151 | cachedServicesObject = populateServicesView( 152 | selectorNames, 153 | servicesList 154 | ); 155 | generatedServices = true; 156 | } 157 | const serviceMessage: Message = { 158 | command: "updateServices", 159 | data: cachedServicesObject, 160 | }; 161 | panel.webview.postMessage(serviceMessage); 162 | break; 163 | } 164 | 165 | case "reloadServices": { 166 | const serviceMessage: Message = { 167 | command: "reloadServices", 168 | data: {}, 169 | }; 170 | panel.webview.postMessage(serviceMessage); 171 | } 172 | 173 | case "loadParentChild": { 174 | pcObject = populatePCView(selectorNames); 175 | const pcMessage: Message = { 176 | command: "updatePC", 177 | data: pcObject, 178 | }; 179 | panel.webview.postMessage(pcMessage); 180 | break; 181 | } 182 | 183 | case "reloadPC": { 184 | const pcMessage: Message = { 185 | command: "reloadPC", 186 | data: {}, 187 | }; 188 | panel.webview.postMessage(pcMessage); 189 | break; 190 | } 191 | 192 | case "reloadFolderFile": { 193 | panel.webview.postMessage({ 194 | command: "reloadFolderFile", 195 | data: {}, 196 | }); 197 | break; 198 | } 199 | 200 | case "openFile": { 201 | vscode.commands.executeCommand("angulens.openFile", message.data); 202 | break; 203 | } 204 | 205 | default: 206 | console.error("Unknown command", message.command); 207 | break; 208 | } 209 | }, 210 | undefined, 211 | context.subscriptions 212 | ); 213 | 214 | panel.webview.html = getWebViewContent( 215 | stylesUri, 216 | runtimeUri, 217 | polyfillsUri, 218 | scriptUri 219 | ); 220 | /* 221 | Leaving 222 | */ 223 | panel.onDidChangeViewState((e) => { 224 | if (e.webviewPanel.visible && e.webviewPanel.active) { 225 | panel.webview.postMessage({ 226 | command: "loadState", 227 | data: {}, 228 | }); 229 | } 230 | }); 231 | }); 232 | 233 | context.subscriptions.push(runWebView); 234 | } 235 | 236 | function getWebViewContent( 237 | stylesUri: any, 238 | runtimeUri: any, 239 | polyfillsUri: any, 240 | scriptUri: any 241 | ) { 242 | return ` 243 | 244 | 245 | 246 | 247 | 248 | Hello World 249 | 250 | 251 | 252 | 253 | 254 | 255 | `; 256 | } 257 | 258 | // This method is called when your extension is deactivated 259 | export function deactivate() {} 260 | -------------------------------------------------------------------------------- /src/test/runTest.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | 3 | import { runTests } from '@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 test runner 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 | -------------------------------------------------------------------------------- /src/test/suite/extension.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from '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 | import * as vscode from 'vscode'; 6 | // import * as myExtension from '../../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 | -------------------------------------------------------------------------------- /src/test/suite/index.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | import * as Mocha from 'mocha'; 3 | import * as glob from 'glob'; 4 | 5 | export function run(): Promise { 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 | mocha.addFile(path.resolve(testsRoot, file)); 20 | }); 21 | testFileStream.on("error", (err) => { 22 | e(err); 23 | }); 24 | testFileStream.on("end", () => { 25 | try { 26 | // Run the mocha test 27 | mocha.run(failures => { 28 | if (failures > 0) { 29 | e(new Error(`${failures} tests failed.`)); 30 | } else { 31 | c(); 32 | } 33 | }); 34 | } catch (err) { 35 | console.error(err); 36 | e(err); 37 | } 38 | }); 39 | }); 40 | } 41 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "ES2020", 5 | "outDir": "out", 6 | "lib": ["ES2020"], 7 | "sourceMap": true, 8 | "rootDir": "src", 9 | "strict": true /* enable all strict type-checking options */ 10 | /* Additional Checks */ 11 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 12 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 13 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 14 | }, 15 | "exclude": ["node_modules", ".vscode-test", "webview-ui"] 16 | } 17 | -------------------------------------------------------------------------------- /vsc-extension-quickstart.md: -------------------------------------------------------------------------------- 1 | # Welcome to your VS Code Extension 2 | 3 | ## What's in the folder 4 | 5 | * This folder contains all of the files necessary for your extension. 6 | * `package.json` - this is the manifest file in which you declare your extension and command. 7 | * The sample plugin registers a command and defines its title and command name. With this information VS Code can show the command in the command palette. It doesn’t yet need to load the plugin. 8 | * `src/extension.ts` - this is the main file where you will provide the implementation of your command. 9 | * The file exports one function, `activate`, which is called the very first time your extension is activated (in this case by executing the command). Inside the `activate` function we call `registerCommand`. 10 | * We pass the function containing the implementation of the command as the second parameter to `registerCommand`. 11 | 12 | ## Get up and running straight away 13 | 14 | * Press `F5` to open a new window with your extension loaded. 15 | * Run your command from the command palette by pressing (`Ctrl+Shift+P` or `Cmd+Shift+P` on Mac) and typing `Hello World`. 16 | * Set breakpoints in your code inside `src/extension.ts` to debug your extension. 17 | * Find output from your extension in the debug console. 18 | 19 | ## Make changes 20 | 21 | * You can relaunch the extension from the debug toolbar after changing code in `src/extension.ts`. 22 | * You can also reload (`Ctrl+R` or `Cmd+R` on Mac) the VS Code window with your extension to load your changes. 23 | 24 | ## Explore the API 25 | 26 | * You can open the full set of our API when you open the file `node_modules/@types/vscode/index.d.ts`. 27 | 28 | ## Run tests 29 | 30 | * Open the debug viewlet (`Ctrl+Shift+D` or `Cmd+Shift+D` on Mac) and from the launch configuration dropdown pick `Extension Tests`. 31 | * Press `F5` to run the tests in a new window with your extension loaded. 32 | * See the output of the test result in the debug console. 33 | * Make changes to `src/test/suite/extension.test.ts` or create new test files inside the `test/suite` folder. 34 | * The provided test runner will only consider files matching the name pattern `**.test.ts`. 35 | * You can create folders inside the `test` folder to structure your tests any way you want. 36 | 37 | ## Go further 38 | 39 | * [Follow UX guidelines](https://code.visualstudio.com/api/ux-guidelines/overview) to create extensions that seamlessly integrate with VS Code's native interface and patterns. 40 | * Reduce the extension size and improve the startup time by [bundling your extension](https://code.visualstudio.com/api/working-with-extensions/bundling-extension). 41 | * [Publish your extension](https://code.visualstudio.com/api/working-with-extensions/publishing-extension) on the VS Code extension marketplace. 42 | * Automate builds by setting up [Continuous Integration](https://code.visualstudio.com/api/working-with-extensions/continuous-integration). 43 | -------------------------------------------------------------------------------- /webview-ui/.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see https://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.ts] 12 | quote_type = single 13 | 14 | [*.md] 15 | max_line_length = off 16 | trim_trailing_whitespace = false 17 | -------------------------------------------------------------------------------- /webview-ui/.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # Compiled output 4 | /dist 5 | /tmp 6 | /out-tsc 7 | /bazel-out 8 | 9 | # Node 10 | /node_modules 11 | npm-debug.log 12 | yarn-error.log 13 | 14 | # IDEs and editors 15 | .idea/ 16 | .project 17 | .classpath 18 | .c9/ 19 | *.launch 20 | .settings/ 21 | *.sublime-workspace 22 | 23 | # Visual Studio Code 24 | .vscode/* 25 | !.vscode/settings.json 26 | !.vscode/tasks.json 27 | !.vscode/launch.json 28 | !.vscode/extensions.json 29 | .history/* 30 | 31 | # Miscellaneous 32 | /.angular/cache 33 | .sass-cache/ 34 | /connect.lock 35 | /coverage 36 | /libpeerconnection.log 37 | testem.log 38 | /typings 39 | 40 | # System files 41 | .DS_Store 42 | Thumbs.db 43 | -------------------------------------------------------------------------------- /webview-ui/.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=827846 3 | "recommendations": ["angular.ng-template"] 4 | } 5 | -------------------------------------------------------------------------------- /webview-ui/.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 3 | "version": "0.2.0", 4 | "configurations": [ 5 | { 6 | "name": "ng serve", 7 | "type": "chrome", 8 | "request": "launch", 9 | "preLaunchTask": "npm: start", 10 | "url": "http://localhost:4200/" 11 | }, 12 | { 13 | "name": "ng test", 14 | "type": "chrome", 15 | "request": "launch", 16 | "preLaunchTask": "npm: test", 17 | "url": "http://localhost:9876/debug.html" 18 | } 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /webview-ui/.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // For more information, visit: https://go.microsoft.com/fwlink/?LinkId=733558 3 | "version": "2.0.0", 4 | "tasks": [ 5 | { 6 | "type": "npm", 7 | "script": "start", 8 | "isBackground": true, 9 | "problemMatcher": { 10 | "owner": "typescript", 11 | "pattern": "$tsc", 12 | "background": { 13 | "activeOnStart": true, 14 | "beginsPattern": { 15 | "regexp": "(.*?)" 16 | }, 17 | "endsPattern": { 18 | "regexp": "bundle generation complete" 19 | } 20 | } 21 | } 22 | }, 23 | { 24 | "type": "npm", 25 | "script": "test", 26 | "isBackground": true, 27 | "problemMatcher": { 28 | "owner": "typescript", 29 | "pattern": "$tsc", 30 | "background": { 31 | "activeOnStart": true, 32 | "beginsPattern": { 33 | "regexp": "(.*?)" 34 | }, 35 | "endsPattern": { 36 | "regexp": "bundle generation complete" 37 | } 38 | } 39 | } 40 | } 41 | ] 42 | } 43 | -------------------------------------------------------------------------------- /webview-ui/README.md: -------------------------------------------------------------------------------- 1 | # WebviewUi 2 | 3 | This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 16.2.4. 4 | 5 | ## Development server 6 | 7 | Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The application will automatically reload if you change any of the source files. 8 | 9 | ## Code scaffolding 10 | 11 | Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`. 12 | 13 | ## Build 14 | 15 | Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. 16 | 17 | ## Running unit tests 18 | 19 | Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io). 20 | 21 | ## Running end-to-end tests 22 | 23 | Run `ng e2e` to execute the end-to-end tests via a platform of your choice. To use this command, you need to first add a package that implements end-to-end testing capabilities. 24 | 25 | ## Further help 26 | 27 | To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI Overview and Command Reference](https://angular.io/cli) page. 28 | -------------------------------------------------------------------------------- /webview-ui/angular.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "version": 1, 4 | "newProjectRoot": "projects", 5 | "projects": { 6 | "webview-ui": { 7 | "projectType": "application", 8 | "schematics": {}, 9 | "root": "", 10 | "sourceRoot": "src", 11 | "prefix": "app", 12 | "architect": { 13 | "build": { 14 | "builder": "@angular-devkit/build-angular:browser", 15 | "options": { 16 | "outputPath": "dist/webview-ui", 17 | "index": "src/index.html", 18 | "main": "src/main.ts", 19 | "polyfills": [ 20 | "zone.js" 21 | ], 22 | "tsConfig": "tsconfig.app.json", 23 | "assets": [ 24 | "src/favicon.ico", 25 | "src/assets" 26 | ], 27 | "styles": [ 28 | "src/styles.css" 29 | ], 30 | "scripts": [] 31 | }, 32 | "configurations": { 33 | "production": { 34 | "budgets": [ 35 | { 36 | "type": "initial", 37 | "maximumWarning": "500kb", 38 | "maximumError": "2mb" 39 | }, 40 | { 41 | "type": "anyComponentStyle", 42 | "maximumWarning": "2kb", 43 | "maximumError": "4kb" 44 | } 45 | ], 46 | "outputHashing": "all" 47 | }, 48 | "development": { 49 | "buildOptimizer": false, 50 | "optimization": false, 51 | "vendorChunk": true, 52 | "extractLicenses": false, 53 | "sourceMap": true, 54 | "namedChunks": true 55 | } 56 | }, 57 | "defaultConfiguration": "production" 58 | }, 59 | "serve": { 60 | "builder": "@angular-devkit/build-angular:dev-server", 61 | "configurations": { 62 | "production": { 63 | "browserTarget": "webview-ui:build:production" 64 | }, 65 | "development": { 66 | "browserTarget": "webview-ui:build:development" 67 | } 68 | }, 69 | "defaultConfiguration": "development" 70 | }, 71 | "extract-i18n": { 72 | "builder": "@angular-devkit/build-angular:extract-i18n", 73 | "options": { 74 | "browserTarget": "webview-ui:build" 75 | } 76 | }, 77 | "test": { 78 | "builder": "@angular-devkit/build-angular:karma", 79 | "options": { 80 | "polyfills": [ 81 | "zone.js", 82 | "zone.js/testing" 83 | ], 84 | "tsConfig": "tsconfig.spec.json", 85 | "assets": [ 86 | "src/favicon.ico", 87 | "src/assets" 88 | ], 89 | "styles": [ 90 | "src/styles.css" 91 | ], 92 | "scripts": [] 93 | } 94 | } 95 | } 96 | } 97 | }, 98 | "cli": { 99 | "analytics": "1ccaefed-6e97-49e6-8e0d-cf0544232f9f" 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /webview-ui/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "webview-ui", 3 | "version": "0.0.0", 4 | "scripts": { 5 | "ng": "ng", 6 | "start": "ng serve", 7 | "build": "ng build", 8 | "watch": "ng build --watch --configuration development", 9 | "test": "ng test" 10 | }, 11 | "private": true, 12 | "dependencies": { 13 | "@angular/animations": "^16.2.0", 14 | "@angular/common": "^16.2.0", 15 | "@angular/compiler": "^16.2.0", 16 | "@angular/core": "^16.2.0", 17 | "@angular/forms": "^16.2.0", 18 | "@angular/platform-browser": "^16.2.0", 19 | "@angular/platform-browser-dynamic": "^16.2.0", 20 | "@angular/router": "^16.2.0", 21 | "@ng-icons/core": "^25.4.0", 22 | "@ng-icons/material-file-icons": "^25.4.0", 23 | "@types/vscode-webview": "^1.57.2", 24 | "install": "^0.13.0", 25 | "npm": "^10.2.1", 26 | "rxjs": "~7.8.0", 27 | "tslib": "^2.3.0", 28 | "vis-network": "^9.1.7", 29 | "zone.js": "~0.13.0" 30 | }, 31 | "devDependencies": { 32 | "@angular-devkit/build-angular": "^16.2.4", 33 | "@angular/cli": "^16.2.4", 34 | "@angular/compiler-cli": "^16.2.0", 35 | "@types/jasmine": "~4.3.0", 36 | "jasmine-core": "~4.6.0", 37 | "karma": "~6.4.0", 38 | "karma-chrome-launcher": "~3.2.0", 39 | "karma-coverage": "~2.2.0", 40 | "karma-jasmine": "~5.1.0", 41 | "karma-jasmine-html-reporter": "~2.1.0", 42 | "typescript": "~5.1.3" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /webview-ui/src/app/app-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { RouterModule, Routes } from '@angular/router'; 3 | 4 | const routes: Routes = []; 5 | 6 | @NgModule({ 7 | imports: [RouterModule.forRoot(routes)], 8 | exports: [RouterModule] 9 | }) 10 | export class AppRoutingModule { } 11 | -------------------------------------------------------------------------------- /webview-ui/src/app/app.component.css: -------------------------------------------------------------------------------- 1 | @import url("https://fonts.googleapis.com/css2?family=Rubik&family=Work+Sans:wght@600&display=swap"); 2 | * { 3 | font-family: "Rubik", sans-serif; 4 | font-weight: 600; 5 | } 6 | 7 | .container { 8 | display: flex; 9 | flex-direction: column; 10 | align-items: center; /* Center items horizontally */ 11 | justify-content: center; /* Center items vertically */ 12 | background: #130c25; 13 | height: 100vh; /* Ensure the container takes the full viewport height */ 14 | } 15 | .mainContainer { 16 | margin-right: 150px; 17 | } 18 | .current-views-container { 19 | display: flex; 20 | flex-direction: row; 21 | } 22 | .containerOutsideButton { 23 | display: flex; 24 | justify-content: flex-start; 25 | position: absolute; 26 | width: max-content; 27 | } 28 | 29 | .buttonContainer { 30 | display: flex; 31 | justify-content: flex-start; 32 | height: max-content; 33 | width: max-content; 34 | border-radius: 2px; 35 | } 36 | 37 | /* CSS */ 38 | .button-82-pushable { 39 | position: relative; 40 | border: none; 41 | background: transparent; 42 | padding: 0; 43 | cursor: pointer; 44 | outline-offset: 4px; 45 | transition: filter 250ms; 46 | user-select: none; 47 | -webkit-user-select: none; 48 | touch-action: manipulation; 49 | margin-left: 10px; 50 | } 51 | 52 | .button-82-shadow { 53 | position: absolute; 54 | top: 0; 55 | left: 0; 56 | width: 100%; 57 | height: 100%; 58 | border-radius: 12px; 59 | background: hsl(0deg 0% 0% / 0.25); 60 | will-change: transform; 61 | transform: translateY(2px); 62 | transition: transform 600ms cubic-bezier(0.3, 0.7, 0.4, 1); 63 | } 64 | 65 | .button-82-edge { 66 | position: absolute; 67 | top: 0; 68 | left: 0; 69 | width: 100%; 70 | height: 100%; 71 | border-radius: 12px; 72 | background: linear-gradient( 73 | to left, 74 | hsl(340deg 100% 16%) 0%, 75 | hsl(340deg 100% 32%) 8%, 76 | hsl(340deg 100% 32%) 92%, 77 | hsl(340deg 100% 16%) 100% 78 | ); 79 | } 80 | 81 | .button-82-front { 82 | display: block; 83 | position: relative; 84 | padding: 12px 27px; 85 | border-radius: 12px; 86 | font-size: 15px; 87 | color: white; 88 | background: hsl(345, 100%, 37%); 89 | will-change: transform; 90 | transform: translateY(-4px); 91 | transition: transform 600ms cubic-bezier(0.3, 0.7, 0.4, 1); 92 | } 93 | 94 | @media (min-width: 768px) { 95 | .button-82-front { 96 | font-size: 15px; 97 | padding: 12px 25px; 98 | } 99 | } 100 | 101 | .button-82-pushable:hover { 102 | filter: brightness(110%); 103 | -webkit-filter: brightness(110%); 104 | } 105 | 106 | .button-82-pushable:hover .button-82-front { 107 | transform: translateY(-6px); 108 | transition: transform 250ms cubic-bezier(0.3, 0.7, 0.4, 1.5); 109 | } 110 | 111 | .button-82-pushable:active .button-82-front { 112 | transform: translateY(-2px); 113 | transition: transform 34ms; 114 | } 115 | 116 | .button-82-pushable:hover .button-82-shadow { 117 | transform: translateY(4px); 118 | transition: transform 250ms cubic-bezier(0.3, 0.7, 0.4, 1.5); 119 | } 120 | 121 | .button-82-pushable:active .button-82-shadow { 122 | transform: translateY(1px); 123 | transition: transform 34ms; 124 | } 125 | 126 | .button-82-pushable:focus:not(:focus-visible) { 127 | outline: none; 128 | } 129 | /* main { 130 | /* margin-left: 5rem; 131 | padding: 1em; } */ 132 | /* 133 | .navbar { 134 | padding-top: 5rem; 135 | width: 5rem; 136 | height: 100vh; 137 | position: fixed; 138 | background-color: #212121; 139 | } 140 | 141 | .navbar-nav { 142 | list-style: none; 143 | padding: 0; 144 | margin: 0; 145 | display: flex; 146 | flex-direction: column; 147 | align-items: center; 148 | padding-top: 1rem; 149 | } 150 | .nav-item { 151 | width: 100%; 152 | } 153 | 154 | .nav-item:last-child { 155 | margin-top: auto; 156 | } 157 | 158 | .nav-link { 159 | display: flex; 160 | align-items: center; 161 | height: 5rem; 162 | text-decoration: none; 163 | color: #fff; 164 | } 165 | 166 | .nav-link svg { 167 | min-width: 2rem; 168 | margin: 0 1.5rem; 169 | } */ 170 | 171 | .link-text { 172 | display: none; 173 | margin-left: 1rem; 174 | } 175 | 176 | .navbar:hover { 177 | width: 16rem; 178 | } 179 | 180 | .navbar:hover .link-text { 181 | display: block; 182 | } 183 | -------------------------------------------------------------------------------- /webview-ui/src/app/app.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 21 |
22 |
23 | 32 | 41 | 50 |
51 |
52 |
53 |
54 | 55 |
56 | 57 |
58 | 59 |
60 | 61 |
62 | 63 |
64 | 65 | 66 |
67 |
68 |
69 | -------------------------------------------------------------------------------- /webview-ui/src/app/app.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | import { RouterTestingModule } from '@angular/router/testing'; 3 | import { AppComponent } from './app.component'; 4 | 5 | describe('AppComponent', () => { 6 | beforeEach(() => TestBed.configureTestingModule({ 7 | imports: [RouterTestingModule], 8 | declarations: [AppComponent] 9 | })); 10 | 11 | it('should create the app', () => { 12 | const fixture = TestBed.createComponent(AppComponent); 13 | const app = fixture.componentInstance; 14 | expect(app).toBeTruthy(); 15 | }); 16 | 17 | it(`should have as title 'webview-ui'`, () => { 18 | const fixture = TestBed.createComponent(AppComponent); 19 | const app = fixture.componentInstance; 20 | // expect(app.title).toEqual('webview-ui'); 21 | }); 22 | 23 | it('should render title', () => { 24 | const fixture = TestBed.createComponent(AppComponent); 25 | fixture.detectChanges(); 26 | const compiled = fixture.nativeElement as HTMLElement; 27 | expect(compiled.querySelector('.content span')?.textContent).toContain('webview-ui app is running!'); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /webview-ui/src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Inject, OnInit } from '@angular/core'; 2 | import { vscode } from './utilities/vscode'; 3 | import { ExtensionMessage } from '../models/message'; 4 | import { FileSystemService } from '.././services/FileSystemService'; 5 | @Component({ 6 | selector: 'app-root', 7 | templateUrl: './app.component.html', 8 | styleUrls: ['./app.component.css'], 9 | }) 10 | export class AppComponent implements OnInit { 11 | currentView: string = ''; 12 | generatedPc: boolean = false; 13 | generatedServices: boolean = false; 14 | imageUrls: string[] = []; 15 | 16 | constructor(private fileSystemService: FileSystemService) {} 17 | ngOnInit() { 18 | //start view as the folder-file hierarchy graph 19 | this.currentView = 'folder-file'; 20 | } 21 | 22 | loadServices() { 23 | this.generatedServices = this.fileSystemService.getGeneratedServices(); 24 | if ( 25 | this.currentView === 'parent-child' || 26 | this.currentView === 'folder-file' 27 | ) { 28 | this.currentView = 'services'; 29 | if (this.generatedServices) { 30 | vscode.postMessage({ 31 | command: 'reloadServices', 32 | data: {}, 33 | }); 34 | console.log(this.generatedServices, 'RELOAD SERVICES'); 35 | } else { 36 | vscode.postMessage({ 37 | command: 'loadServices', 38 | data: {}, 39 | }); 40 | this.generatedServices = true; 41 | console.log('LOAD SERVICES'); 42 | this.fileSystemService.setGeneratedServices(this.generatedServices); 43 | } 44 | } 45 | } 46 | 47 | loadFolderFile() { 48 | if ( 49 | this.currentView === 'parent-child' || 50 | this.currentView === 'services' 51 | ) { 52 | this.currentView = 'folder-file'; 53 | vscode.postMessage({ 54 | command: 'reloadFolderFile', 55 | data: {}, 56 | }); 57 | } 58 | } 59 | 60 | loadParentChild() { 61 | this.generatedPc = this.fileSystemService.getGeneratedPC(); 62 | if (this.currentView === 'folder-file' || this.currentView === 'services') { 63 | this.currentView = 'parent-child'; 64 | if (!this.fileSystemService.generatedServices) { 65 | vscode.postMessage({ 66 | command: 'loadServices', 67 | data: {} 68 | }); 69 | } 70 | 71 | if (this.generatedPc === true) { 72 | vscode.postMessage({ 73 | command: 'reloadPC', 74 | data: {}, 75 | }); 76 | } else { 77 | vscode.postMessage({ 78 | command: 'loadParentChild', 79 | data: {}, 80 | }); 81 | 82 | this.generatedPc = true; 83 | this.fileSystemService.setGeneratedPC(this.generatedPc); // resetting file system service to true 84 | } 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /webview-ui/src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { BrowserModule } from '@angular/platform-browser'; 3 | 4 | import { AppRoutingModule } from './app-routing.module'; 5 | import { AppComponent } from './app.component'; 6 | 7 | import { FormsModule } from '@angular/forms'; 8 | import { FolderFileComponent } from './folder-file/folder-file.component'; 9 | import { ParentChildComponent } from './parent-child/parent-child.component'; 10 | 11 | import { FileSystemService } from 'src/services/FileSystemService'; 12 | import { ParentChildServices } from 'src/services/ParentChildServices'; 13 | import { ServicesViewComponent } from './services-view/services-view.component'; 14 | 15 | import { ModalComponent } from './modal/modal.component'; 16 | // import { NgIconsModule } from '@ng-icons/core'; 17 | // import matfFolderAnimationColored from '@ng-icons/material-file-icons'; 18 | 19 | @NgModule({ 20 | declarations: [ 21 | AppComponent, 22 | FolderFileComponent, 23 | ParentChildComponent, 24 | ModalComponent, 25 | ServicesViewComponent, 26 | ], 27 | //q: what is declarations used for? 28 | //a: declarations is used to make directives (including components and pipes) from the current module available to other directives in the current module. Selectors of directives, components or pipes are only matched against the HTML if they are declared or imported. 29 | imports: [BrowserModule, AppRoutingModule, FormsModule], 30 | providers: [FileSystemService, ParentChildServices], 31 | bootstrap: [AppComponent], 32 | }) 33 | export class AppModule {} 34 | -------------------------------------------------------------------------------- /webview-ui/src/app/folder-file/folder-file.component.css: -------------------------------------------------------------------------------- 1 | .center-container { 2 | display: flex; 3 | justify-content: end; 4 | flex-grow: 3; 5 | margin-bottom: 10px; 6 | } 7 | .custom-input { 8 | padding: 8px; 9 | margin-right: 10px; 10 | border: 1px solid #ccc; 11 | border-radius: 4px; 12 | font-size: 14px; 13 | } 14 | 15 | .custom-button { 16 | padding: 10px 15px; 17 | background-color: #a4161a; 18 | color: white; 19 | border: none; 20 | border-radius: 4px; 21 | cursor: pointer; 22 | font-size: 14px; 23 | } 24 | 25 | .network-container { 26 | width: 70vw; 27 | height: 70vh; 28 | background: #130c25; 29 | border-radius: 10px; 30 | border: 1px solid #444444 31 | } 32 | 33 | #mynetwork { 34 | width: 60vw; 35 | height: 60vh; 36 | border: 1px solid lightgray; 37 | } 38 | #loadingBar { 39 | position: absolute; 40 | top: 0px; 41 | left: 0px; 42 | width: 902px; 43 | height: 902px; 44 | background-color: rgba(200, 200, 200, 0.8); 45 | -webkit-transition: all 0.5s ease; 46 | -moz-transition: all 0.5s ease; 47 | -ms-transition: all 0.5s ease; 48 | -o-transition: all 0.5s ease; 49 | transition: all 0.5s ease; 50 | opacity: 1; 51 | } 52 | #wrapper { 53 | position: relative; 54 | width: 100%; 55 | height: 100%; 56 | } 57 | 58 | #text { 59 | position: absolute; 60 | top: 8px; 61 | left: 530px; 62 | width: 30px; 63 | height: 50px; 64 | margin: auto auto auto auto; 65 | font-size: 22px; 66 | color: #000000; 67 | } 68 | 69 | div.outerBorder { 70 | position: relative; 71 | top: 400px; 72 | width: 600px; 73 | height: 44px; 74 | margin: auto auto auto auto; 75 | border: 8px solid rgba(0, 0, 0, 0.1); 76 | background: rgb(252, 252, 252); /* Old browsers */ 77 | background: -moz-linear-gradient( 78 | top, 79 | rgba(252, 252, 252, 1) 0%, 80 | rgba(237, 237, 237, 1) 100% 81 | ); /* FF3.6+ */ 82 | background: -webkit-gradient( 83 | linear, 84 | left top, 85 | left bottom, 86 | color-stop(0%, rgba(252, 252, 252, 1)), 87 | color-stop(100%, rgba(237, 237, 237, 1)) 88 | ); /* Chrome,Safari4+ */ 89 | background: -webkit-linear-gradient( 90 | top, 91 | rgba(252, 252, 252, 1) 0%, 92 | rgba(237, 237, 237, 1) 100% 93 | ); /* Chrome10+,Safari5.1+ */ 94 | background: -o-linear-gradient( 95 | top, 96 | rgba(252, 252, 252, 1) 0%, 97 | rgba(237, 237, 237, 1) 100% 98 | ); /* Opera 11.10+ */ 99 | background: -ms-linear-gradient( 100 | top, 101 | rgba(252, 252, 252, 1) 0%, 102 | rgba(237, 237, 237, 1) 100% 103 | ); /* IE10+ */ 104 | background: linear-gradient( 105 | to bottom, 106 | rgba(252, 252, 252, 1) 0%, 107 | rgba(237, 237, 237, 1) 100% 108 | ); /* W3C */ 109 | filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#fcfcfc', endColorstr='#ededed',GradientType=0 ); /* IE6-9 */ 110 | border-radius: 72px; 111 | box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.2); 112 | } 113 | 114 | #border { 115 | position: absolute; 116 | top: 10px; 117 | left: 10px; 118 | width: 500px; 119 | height: 23px; 120 | margin: auto auto auto auto; 121 | box-shadow: 0px 0px 4px rgba(0, 0, 0, 0.2); 122 | border-radius: 10px; 123 | } 124 | 125 | #bar { 126 | position: absolute; 127 | top: 0px; 128 | left: 0px; 129 | width: 20px; 130 | height: 20px; 131 | margin: auto auto auto auto; 132 | border-radius: 11px; 133 | border: 2px solid rgba(30, 30, 30, 0.05); 134 | background: rgb(0, 173, 246); /* Old browsers */ 135 | box-shadow: 2px 0px 4px rgba(0, 0, 0, 0.4); 136 | } 137 | 138 | /* CSS */ 139 | .button-82-pushable { 140 | position: relative; 141 | border: none; 142 | background: transparent; 143 | padding: 0; 144 | cursor: pointer; 145 | outline-offset: 4px; 146 | transition: filter 250ms; 147 | user-select: none; 148 | -webkit-user-select: none; 149 | touch-action: manipulation; 150 | } 151 | 152 | .button-82-shadow { 153 | position: absolute; 154 | top: 0; 155 | left: 0; 156 | width: 100%; 157 | height: 100%; 158 | border-radius: 12px; 159 | background: hsl(0deg 0% 0% / 0.25); 160 | will-change: transform; 161 | transform: translateY(2px); 162 | transition: transform 600ms cubic-bezier(0.3, 0.7, 0.4, 1); 163 | } 164 | 165 | .button-82-edge { 166 | position: absolute; 167 | top: 0; 168 | left: 0; 169 | width: 100%; 170 | height: 100%; 171 | border-radius: 12px; 172 | background: linear-gradient( 173 | to left, 174 | hsl(340deg 100% 16%) 0%, 175 | hsl(340deg 100% 32%) 8%, 176 | hsl(340deg 100% 32%) 92%, 177 | hsl(340deg 100% 16%) 100% 178 | ); 179 | } 180 | 181 | .button-82-front { 182 | display: block; 183 | position: relative; 184 | padding: 12px 27px; 185 | border-radius: 12px; 186 | font-size: 15px; 187 | color: white; 188 | background: hsl(345, 100%, 37%); 189 | will-change: transform; 190 | transform: translateY(-4px); 191 | transition: transform 600ms cubic-bezier(0.3, 0.7, 0.4, 1); 192 | } 193 | 194 | @media (min-width: 768px) { 195 | .button-82-front { 196 | font-size: 15px; 197 | padding: 12px 25px; 198 | } 199 | } 200 | 201 | .button-82-pushable:hover { 202 | filter: brightness(110%); 203 | -webkit-filter: brightness(110%); 204 | } 205 | 206 | .button-82-pushable:hover .button-82-front { 207 | transform: translateY(-6px); 208 | transition: transform 250ms cubic-bezier(0.3, 0.7, 0.4, 1.5); 209 | } 210 | 211 | .button-82-pushable:active .button-82-front { 212 | transform: translateY(-2px); 213 | transition: transform 34ms; 214 | } 215 | 216 | .button-82-pushable:hover .button-82-shadow { 217 | transform: translateY(4px); 218 | transition: transform 250ms cubic-bezier(0.3, 0.7, 0.4, 1.5); 219 | } 220 | 221 | .button-82-pushable:active .button-82-shadow { 222 | transform: translateY(1px); 223 | transition: transform 34ms; 224 | } 225 | 226 | .button-82-pushable:focus:not(:focus-visible) { 227 | outline: none; 228 | } 229 | -------------------------------------------------------------------------------- /webview-ui/src/app/folder-file/folder-file.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | 11 | 18 |
19 |
20 | 21 | 22 | 23 | 24 |
25 | 26 |
27 |
32 |
33 |
{{ loadingBarText }}
34 |
35 |
36 |
37 |
38 |
39 |
40 | 41 | -------------------------------------------------------------------------------- /webview-ui/src/app/folder-file/folder-file.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { FolderFileComponent } from './folder-file.component'; 4 | 5 | describe('FolderFileComponent', () => { 6 | let component: FolderFileComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [FolderFileComponent] 12 | }); 13 | fixture = TestBed.createComponent(FolderFileComponent); 14 | component = fixture.componentInstance; 15 | fixture.detectChanges(); 16 | }); 17 | 18 | it('should create', () => { 19 | expect(component).toBeTruthy(); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /webview-ui/src/app/folder-file/folder-file.component.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Component, 3 | ChangeDetectionStrategy, 4 | ElementRef, 5 | OnInit, 6 | ViewChild, 7 | OnDestroy, 8 | ChangeDetectorRef, 9 | NgZone, 10 | AfterViewInit, 11 | } from '@angular/core'; 12 | import { DataSet } from 'vis-data'; 13 | import { Network } from 'vis-network/standalone'; 14 | import { 15 | FsItem, 16 | PcItem, 17 | Node, 18 | Edge, 19 | DataStore, 20 | ServiceItem, 21 | } from '../../models/FileSystem'; 22 | import { ExtensionMessage } from '../../models/message'; 23 | import { vscode } from '../utilities/vscode'; 24 | 25 | import { FileSystemService } from 'src/services/FileSystemService'; 26 | import { first } from 'rxjs'; 27 | // import matfFolderAnimationColored from '@ng-icons/material-file-icons'; 28 | 29 | type AppState = { 30 | networkData: any; // Use the appropriate data type for 'networkData' 31 | options: any; // Use the appropriate data type for 'options' 32 | }; 33 | interface Folder { 34 | type: 'folder'; 35 | path: string; 36 | [key: string]: Folder | string; 37 | } 38 | 39 | interface File { 40 | type: string; 41 | path: string; 42 | } 43 | 44 | @Component({ 45 | selector: 'folder-file', 46 | templateUrl: './folder-file.component.html', 47 | styleUrls: ['./folder-file.component.css'], 48 | changeDetection: ChangeDetectionStrategy.OnPush, 49 | }) 50 | export class FolderFileComponent implements OnInit, OnDestroy { 51 | @ViewChild('networkContainer') networkContainer!: ElementRef; 52 | 53 | constructor( 54 | private fileSystemService: FileSystemService, 55 | private cdr: ChangeDetectorRef, 56 | private zone: NgZone 57 | ) {} 58 | 59 | network: any; 60 | nodes: Node[] = []; 61 | renderedNodes: Node[] = []; 62 | edges: Edge[] = []; 63 | fsItems: FsItem[] = []; 64 | pcItems: PcItem[] = []; 65 | filePath: string = ''; 66 | reloadRequired: boolean = false; 67 | uriObj: object = { 68 | gitkeep: ``, 69 | ts: ` 70 | 71 | `, 72 | css: ` 73 | 74 | `, 75 | folder: ``, 76 | html: ``, 77 | file: ` 79 | `, 81 | }; 82 | options = { 83 | interaction: { 84 | navigationButtons: true, 85 | keyboard: true, 86 | }, 87 | layout: { 88 | improvedLayout: true, 89 | hierarchical: { 90 | direction: 'UD', // Up-Down direction 91 | nodeSpacing: 200, 92 | levelSeparation: 300, 93 | parentCentralization: true, 94 | edgeMinimization: true, 95 | shakeTowards: 'roots', // Tweak the layout algorithm to get better results 96 | sortMethod: 'directed', // Sort based on the hierarchical structure 97 | }, 98 | }, 99 | 100 | nodes: { 101 | shape: 'image', 102 | shapeProperties: { 103 | interpolation: false, 104 | }, 105 | shadow: { 106 | enabled: true, 107 | color: 'rgba(0,0,0,0.5)', 108 | size: 10, 109 | x: 5, 110 | y: 5, 111 | }, 112 | }, 113 | 114 | edges: { 115 | smooth: { 116 | enabled: true, 117 | type: 'cubicBezier', 118 | forceDirection: 'vertical', 119 | roundness: 0.4, 120 | }, 121 | }, 122 | 123 | physics: { 124 | enabled: false, 125 | }, 126 | }; 127 | canLoadBar: boolean = false; 128 | loadingBarDisplay: string = 'block'; 129 | loadingBarOpacity: number = 1; 130 | loadingBarWidth: any = '20px'; 131 | loadingBarText: any = '0%'; 132 | 133 | private handleLoadingBar(network?: any) { 134 | network.on('stabilizationProgress', (params: any) => { 135 | this.canLoadBar = true; 136 | const maxWidth = 496; 137 | const minWidth = 20; 138 | const widthFactor = params.iterations / params.total; 139 | const width = Math.max(minWidth, maxWidth * widthFactor); 140 | this.loadingBarWidth = `${width}px`; 141 | this.loadingBarText = `${Math.round(widthFactor * 100)}%`; 142 | this.cdr.detectChanges(); 143 | }); 144 | network.once('stabilizationIterationsDone', () => { 145 | this.loadingBarText = '100%'; 146 | this.loadingBarWidth = '496px'; 147 | this.loadingBarOpacity = 0; 148 | this.canLoadBar = false; 149 | this.loadingBarDisplay = 'none'; 150 | this.cdr.detectChanges(); 151 | }); 152 | } 153 | 154 | private handleMessageEvent = (event: MessageEvent) => { 155 | const message: ExtensionMessage = event.data; 156 | switch (message.command) { 157 | case 'loadState': { 158 | this.canLoadBar = false; 159 | 160 | const state = vscode.getState() as { 161 | pcData: DataStore | undefined; 162 | fsData: DataStore | undefined; 163 | fsNodes: Node[]; 164 | fsEdges: Edge[]; 165 | pcNodes: Node[]; 166 | pcEdges: Edge[]; 167 | pcItems: PcItem[]; 168 | servicesNodes: Node[]; 169 | servicesEdges: Edge[]; 170 | servicesData: ServiceItem[]; 171 | }; 172 | this.nodes = state.fsNodes; 173 | this.edges = state.fsEdges; 174 | this.renderedNodes = this.nodes.filter((node: Node) => !node.hidden); 175 | 176 | const data: { 177 | nodes: DataSet; 178 | edges: DataSet; 179 | } = { 180 | nodes: new DataSet(this.renderedNodes), 181 | edges: new DataSet(this.edges), 182 | }; 183 | const container = this.networkContainer.nativeElement; 184 | this.network = new Network(container, data, this.options); 185 | this.handleLoadingBar(this.network); 186 | //event listener for double click to open file 187 | this.network.on('doubleClick', (params: any) => { 188 | if (params.nodes.length > 0) { 189 | const nodeId = params.nodes[0]; 190 | if (nodeId) { 191 | // Send a message to your VS Code extension to open the file 192 | vscode.postMessage({ 193 | command: 'openFile', 194 | data: { filePath: nodeId }, 195 | }); 196 | } 197 | } 198 | }); 199 | this.network.on('click', (event: { nodes: string[] }) => { 200 | const { nodes: nodeIds } = event; 201 | if (nodeIds.length > 0) { 202 | this.hide(nodeIds); 203 | this.reRenderComponents(); 204 | } 205 | }); 206 | 207 | vscode.setState({ 208 | fsData: data, 209 | fsNodes: state.fsNodes, 210 | fsEdges: state.fsEdges, 211 | pcData: state.pcData, 212 | pcNodes: state.pcNodes, 213 | pcEdges: state.pcEdges, 214 | pcItems: state.pcItems, 215 | servicesNodes: state.servicesNodes, 216 | servicesEdges: state.servicesEdges, 217 | servicesData: state.servicesData, 218 | }); 219 | break; 220 | } 221 | 222 | //updatePath 223 | case 'generateFolderFile': { 224 | this.fsItems = this.populate(message.data.src); 225 | const state = vscode.getState() as { 226 | pcData?: DataStore; 227 | fsNodes?: Node[]; 228 | fsEdges?: Edge[]; 229 | pcNodes?: Node[]; 230 | pcEdges?: Edge[]; 231 | pcItems?: PcItem[]; 232 | servicesNodes?: Node[]; 233 | servicesEdges?: Edge[]; 234 | servicesData?: ServiceItem[]; 235 | }; 236 | 237 | const { nodes, edges } = this.createNodesAndEdges(this.fsItems); 238 | 239 | const edgesWithIds: Edge[] = edges.map((edge) => ({ 240 | ...edge, 241 | from: edge.from, 242 | to: edge.to, 243 | })); 244 | this.edges = edgesWithIds; 245 | this.nodes = nodes; 246 | 247 | const newNodes = new DataSet(nodes); 248 | const newEdges = new DataSet(edgesWithIds); 249 | // create a network 250 | const data: { 251 | nodes: DataSet; 252 | edges: DataSet; 253 | } = { 254 | nodes: newNodes, 255 | edges: newEdges, 256 | }; 257 | const container = this.networkContainer.nativeElement; 258 | this.network = new Network(container, data, this.options); 259 | 260 | this.handleLoadingBar(this.network); 261 | //event listener for double click to open file 262 | this.network.on('doubleClick', (params: any) => { 263 | if (params.nodes.length > 0) { 264 | const nodeId = params.nodes[0]; 265 | if (nodeId) { 266 | // Send a message to your VS Code extension to open the file 267 | vscode.postMessage({ 268 | command: 'openFile', 269 | data: { filePath: nodeId }, 270 | }); 271 | } 272 | } 273 | }); 274 | 275 | this.network.on('click', (event: { nodes: string[] }) => { 276 | const { nodes: nodeIds } = event; 277 | if (nodeIds.length > 0) { 278 | this.hide(nodeIds); 279 | this.reRenderComponents(); 280 | } 281 | }); 282 | 283 | vscode.setState({ 284 | fsData: data, 285 | fsNodes: this.nodes, 286 | fsEdges: this.edges, 287 | pcData: state.pcData, 288 | pcNodes: state.pcNodes, 289 | pcEdges: state.pcEdges, 290 | pcItems: state.pcItems, 291 | servicesNodes: state.servicesNodes, 292 | servicesEdges: state.servicesEdges, 293 | servicesData: state.servicesData, 294 | }); 295 | break; 296 | } 297 | 298 | // reupdate screen 299 | case 'reloadFolderFile': { 300 | this.canLoadBar = false; 301 | const state = vscode.getState() as { 302 | pcData: DataStore; 303 | fsData: DataStore; 304 | fsNodes: Node[]; 305 | fsEdges: Edge[]; 306 | }; 307 | this.nodes = state.fsNodes; 308 | this.edges = state.fsEdges; 309 | this.renderedNodes = this.nodes.filter((node: Node) => !node.hidden); 310 | 311 | const data: { 312 | nodes: DataSet; 313 | edges: DataSet; 314 | } = { 315 | nodes: new DataSet(this.renderedNodes), 316 | edges: new DataSet(this.edges), 317 | }; 318 | const container = this.networkContainer.nativeElement; 319 | this.network = new Network(container, data, this.options); 320 | //event listener for double click to open file 321 | this.network.on('doubleClick', (params: any) => { 322 | if (params.nodes.length > 0) { 323 | const nodeId = params.nodes[0]; 324 | if (nodeId) { 325 | // Send a message to your VS Code extension to open the file 326 | vscode.postMessage({ 327 | command: 'openFile', 328 | data: { filePath: nodeId }, 329 | }); 330 | } 331 | } 332 | }); 333 | 334 | this.network.on('click', (event: { nodes: string[] }) => { 335 | const { nodes: nodeIds } = event; 336 | if (nodeIds.length > 0) { 337 | this.hide(nodeIds); 338 | this.reRenderComponents(); 339 | } 340 | }); 341 | break; 342 | } 343 | default: 344 | console.log('unknown comand ->', message.command); 345 | break; 346 | } 347 | }; 348 | 349 | setupMessageListener(): void { 350 | window.addEventListener('message', this.handleMessageEvent); 351 | } 352 | 353 | ngOnInit(): void { 354 | this.canLoadBar = false; 355 | if (!vscode.getState()) vscode.setState({}); 356 | this.setupMessageListener(); 357 | } 358 | 359 | ngOnDestroy(): void { 360 | window.removeEventListener('message', this.handleMessageEvent); 361 | } 362 | 363 | /* 364 | After the User inputs a src path 365 | */ 366 | loadNetwork() { 367 | vscode.postMessage({ 368 | command: 'loadNetwork', 369 | data: { 370 | filePath: this.filePath, 371 | }, 372 | }); 373 | this.fileSystemService.setGeneratedPC(false); 374 | this.fileSystemService.setGeneratedServices(false); 375 | } 376 | 377 | reRenderComponents() { 378 | if (this.reloadRequired) { 379 | this.renderedNodes = this.nodes.filter((node: Node) => !node.hidden); 380 | this.network.setData({ 381 | nodes: this.renderedNodes, 382 | edges: this.edges, 383 | }); 384 | const state = vscode.getState(); 385 | vscode.setState({ 386 | ...(state as object), 387 | fsNodes: this.nodes, 388 | }); 389 | this.reloadRequired = false; 390 | } 391 | } 392 | 393 | hide(nodes: String[], firstRun: Boolean = true) { 394 | const clickedNodes = nodes.map((nodeId) => 395 | this.nodes.find((node) => node.id === nodeId) 396 | ); 397 | clickedNodes.forEach((clickedNode) => { 398 | if ( 399 | clickedNode && 400 | clickedNode.open !== undefined && 401 | (clickedNode.open || firstRun === true) 402 | ) { 403 | if (firstRun) { 404 | this.reloadRequired = true; 405 | clickedNode.open = !clickedNode.open; 406 | } 407 | const childrenArr: String[] = this.edges 408 | .filter((edge) => edge.from === clickedNode.id) 409 | .map((edge) => edge.to); 410 | this.hide(childrenArr, false); 411 | childrenArr.forEach((item) => { 412 | const currentNode: Node | undefined = this.nodes.find( 413 | (node) => node.id === item 414 | ); 415 | if (currentNode) { 416 | currentNode.hidden = !currentNode.hidden; 417 | } 418 | }); 419 | } 420 | }); 421 | } 422 | 423 | createNodesAndEdges(fsItems: FsItem[]): { nodes: Node[]; edges: Edge[] } { 424 | const nodes: Node[] = []; 425 | const edges: Edge[] = []; 426 | const uris: Record = this.uriObj as { 427 | git: '...'; 428 | ts: '...'; 429 | css: '...'; 430 | folder: '...'; 431 | html: '...'; 432 | file: '...'; 433 | }; 434 | function getSVGUri(uri: string): string { 435 | return 'data:image/svg+xml,' + encodeURIComponent(uri); 436 | } 437 | // Helper function to recursively add nodes and edges 438 | function addNodesAndEdges(item: FsItem, parentFolder?: string) { 439 | // Check if the node already exists to avoid duplicates 440 | const existingNode = nodes.find((node) => node.id === item.id); 441 | if (!existingNode) { 442 | // Add the current item as a node 443 | let fileImg: string = ''; 444 | if (uris[item.type]) fileImg = getSVGUri(uris[item.type]); 445 | else fileImg = getSVGUri(uris['file']); 446 | 447 | const newNode: Node = { 448 | id: item.id, 449 | label: item.label, 450 | image: { 451 | unselected: fileImg, 452 | selected: fileImg, 453 | }, 454 | hidden: false, 455 | 456 | font: { 457 | color: 'white', 458 | size: 14, 459 | }, 460 | }; 461 | 462 | if (item.type === 'folder') { 463 | newNode.open = true; 464 | newNode.onFolderClick = function () { 465 | this.open = !this.open; 466 | }; 467 | } 468 | 469 | nodes.push(newNode); 470 | 471 | // If the item has children (files or subfolders), add edges to them 472 | if (item.children && item.children.length > 0) { 473 | for (const childId of item.children) { 474 | const edge: Edge = { 475 | id: `${item.id}-${childId}`, 476 | from: item.id, 477 | to: childId, 478 | color: 'gold', 479 | }; 480 | edges.push(edge); 481 | const child = fsItems.find((fsItem) => fsItem.id === childId); 482 | if (child) { 483 | // Recursively add nodes and edges for children 484 | addNodesAndEdges(child, item.id); 485 | } 486 | } 487 | } 488 | } 489 | } 490 | 491 | // Iterate through the root items and start the process 492 | for (const rootItem of fsItems) { 493 | addNodesAndEdges(rootItem); 494 | } 495 | return { nodes, edges }; 496 | } 497 | 498 | populate(obj: Folder | File, items: FsItem[] = []): FsItem[] { 499 | // Helper function to recursively populate the file system hierarchy 500 | 501 | function isFolder(obj: Folder | File | unknown): obj is Folder { 502 | return (obj as Folder).type === 'folder'; 503 | } 504 | function populateGraph( 505 | obj: Folder | File, 506 | parentFolder?: string 507 | ): FsItem | undefined { 508 | if (isFolder(obj)) { 509 | const folder: FsItem = { 510 | id: obj.path, 511 | label: obj.path.split('/').pop() || '', 512 | type: obj.type, 513 | children: [], 514 | folderParent: parentFolder, 515 | }; 516 | 517 | // Extract known properties of Folder 518 | const { type, path, ...folderProps } = obj; 519 | 520 | for (const key in folderProps) { 521 | const currentKey = folderProps[key] as unknown; 522 | if (!isFolder(currentKey)) { 523 | const fileChild = populateGraph(currentKey as File, folder.id); 524 | if (fileChild) { 525 | folder.children.push(fileChild.id); 526 | fileChild.folderParent = folder.id; 527 | } 528 | } else { 529 | folder.children.push((currentKey as Folder).path); 530 | populateGraph(currentKey as Folder); 531 | } 532 | } 533 | 534 | items.push(folder); 535 | return folder; 536 | } else { 537 | const fsItem: FsItem = { 538 | id: obj.path, 539 | label: obj.path.split('/').pop() || '', 540 | type: obj.type, 541 | children: [], 542 | }; 543 | 544 | items.push(fsItem); 545 | return fsItem; 546 | } 547 | } 548 | 549 | populateGraph(obj); 550 | 551 | return items; 552 | } 553 | } 554 | -------------------------------------------------------------------------------- /webview-ui/src/app/modal/modal.component.css: -------------------------------------------------------------------------------- 1 | 2 | 3 | .sidebar { 4 | position: fixed; 5 | width: 17%; 6 | height: 40vh; 7 | background: #130c25; 8 | transition: all 0.25s ease-in-out; 9 | visibility: hidden; 10 | border-radius: 10px 10px 10px 0; 11 | box-sizing: border-box; 12 | transform: translateX(-100%); 13 | } 14 | .sidebar.show-sidebar { 15 | visibility: visible; 16 | transition: all 0.25s ease-in-out; 17 | opacity: 1; 18 | transform: translateX(0); 19 | } 20 | 21 | .nav-toggle { 22 | visibility: visible; 23 | right: -40px; 24 | top: 0; 25 | width: 40px; 26 | height: 40px; 27 | position: absolute; 28 | line-height: 50%; 29 | text-align: center; 30 | font-weight: 600; 31 | line-height: 40px; 32 | text-decoration: none; 33 | text-align: center; 34 | background-color: #3d2678; 35 | border-radius: 0 10px 10px 0; 36 | outline: none; 37 | border: none; 38 | } 39 | 40 | .nav-toggle:before { 41 | content: "\2192"; 42 | color: white; 43 | font-weight: 600; 44 | } 45 | 46 | .sidebar.show-sidebar .nav-toggle:before { 47 | content: "\2190"; 48 | } 49 | 50 | .sidebar header { 51 | font-size: 2em; 52 | color: white; 53 | /* text-align: center; */ 54 | background: #1d1238; 55 | padding: 0.5em; 56 | justify-content: center; 57 | border-top: 10px; 58 | border-radius: 10px 0px 10px 0px; 59 | } 60 | 61 | .modal-container { 62 | display: flex; 63 | margin-top: 60px; 64 | flex-direction: column; 65 | overflow: hidden; 66 | } 67 | 68 | .sidebar-content { 69 | display: flex; 70 | flex-direction: column; 71 | text-align: center; 72 | padding-left: 0; 73 | } 74 | 75 | .sublist { 76 | /* padding: 12px 16px; */ 77 | display: block; 78 | justify-content: start; 79 | padding-inline-start: 0px; 80 | margin-top: 0px; 81 | margin-block-end: 0px; 82 | margin-block-start: 0px; 83 | background: #26184a; 84 | } 85 | 86 | .subitem { 87 | display: block; 88 | font-size: 1.25em; 89 | background: #26184a; 90 | margin-top: 0px; 91 | justify-content: start; 92 | padding: 0.75em; 93 | } 94 | 95 | .subitem p { 96 | display: block; 97 | margin-block-start: 0px; 98 | margin-block-end: 0px; 99 | margin-inline-start: 0px; 100 | margin-inline-end: 0px; 101 | } 102 | 103 | .subitem li { 104 | margin-top: 0.25em; 105 | justify-content: start; 106 | } 107 | 108 | a { 109 | font-size: 1.5em; 110 | padding-bottom: 1em; 111 | text-decoration: none; 112 | line-height: 1.5em; 113 | padding: 0.5em; 114 | justify-content: start; 115 | outline: none; 116 | } 117 | 118 | .rotated .arrow { 119 | transform: rotate(-180deg); 120 | transition: transform 0.3s ease-in-out; 121 | } 122 | 123 | .arrow { 124 | color: white; 125 | margin-left: 3em; 126 | right: 10%; 127 | } 128 | 129 | .sidebar-content li { 130 | list-style: none; 131 | } 132 | 133 | .click-container { 134 | border-radius: 10px; /* Add rounded border */ 135 | border: 1px solid white; /* Add border color */ 136 | padding: 0.5em; 137 | margin-top: 0; 138 | margin-bottom: 0; 139 | display: flex; 140 | width: auto; 141 | justify-content: space-between; 142 | border-top: none; 143 | border-left: none; 144 | border-right: none; 145 | } 146 | 147 | code { 148 | font-family: monospace; 149 | background: rgba(135, 131, 120, 0.15); 150 | color: #eb5757; 151 | padding: 0.25em; 152 | border: 20px; 153 | } 154 | 155 | #a-inputs { 156 | color: #04bd6c; 157 | font-weight: 400; 158 | } 159 | 160 | #code-input { 161 | font-family: "SFMono-Regular", Menlo, Consolas, "PT Mono", "Liberation Mono", 162 | Courier, monospace; 163 | color: #04bd6c; 164 | padding: 0.25em; 165 | margin-top: 0.11em; 166 | border: 20px; 167 | } 168 | 169 | #a-services { 170 | color: white; 171 | font-weight: 400; 172 | } 173 | 174 | #code-output-parent { 175 | color: #ffcf40; 176 | font-family: monospace; 177 | background: rgba(135, 131, 120, 0.15); 178 | padding: 0.25em; 179 | border: 20px; 180 | margin-top: 0.11em; 181 | } 182 | -------------------------------------------------------------------------------- /webview-ui/src/app/modal/modal.component.html: -------------------------------------------------------------------------------- 1 | 130 | -------------------------------------------------------------------------------- /webview-ui/src/app/modal/modal.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { ModalComponent } from './modal.component'; 4 | 5 | describe('ModalComponent', () => { 6 | let component: ModalComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ModalComponent] 12 | }); 13 | fixture = TestBed.createComponent(ModalComponent); 14 | component = fixture.componentInstance; 15 | fixture.detectChanges(); 16 | }); 17 | 18 | it('should create', () => { 19 | expect(component).toBeTruthy(); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /webview-ui/src/app/modal/modal.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { PcItem } from 'src/models/FileSystem'; 3 | import { ParentChildServices } from 'src/services/ParentChildServices'; 4 | 5 | @Component({ 6 | selector: 'modal', 7 | templateUrl: './modal.component.html', 8 | styleUrls: ['./modal.component.css'], 9 | }) 10 | export class ModalComponent implements OnInit { 11 | modalItem: PcItem | null = null; 12 | inputs: any[] = []; // Array for selected node's inputs (Node receiving data from its parent) 13 | outputs: any[] = []; // Array for outputs (Node sending data to its child) 14 | services: string[] = []; // Array for injectables 15 | section: string = ''; 16 | showInputs = true; 17 | showOutputs = true; 18 | showServices = true; 19 | 20 | constructor(private pcService: ParentChildServices) {} 21 | 22 | ngOnInit(): void { 23 | this.pcService.openModal$.subscribe((deliverable) => { 24 | // Receive pcItem along with the event 25 | this.modalItem = deliverable.pcItem; 26 | this.inputs = deliverable.pcItem.inputs; 27 | console.log(this.inputs, 'INPUTS'); 28 | this.outputs = deliverable.pcItem.outputs; 29 | this.connectEdges(this.inputs, this.outputs); 30 | this.services = [...deliverable.services]; 31 | this.showInputs = true; 32 | this.showOutputs = true; 33 | this.openModal(); 34 | }); 35 | } 36 | 37 | isSidebarVisible = false; 38 | 39 | connectEdges(inputs: any[], outputs: any[]) { 40 | inputs.forEach((input) => { 41 | this.pcService.getItems().forEach((item) => { 42 | if (item.id === input.pathFrom) { 43 | input.pathFrom = item.label; 44 | } 45 | }); 46 | }); 47 | outputs.forEach((output) => { 48 | this.pcService.getItems().forEach((item) => { 49 | if (item.id === output.pathTo) { 50 | output.pathTo = item.label; 51 | } 52 | }); 53 | }); 54 | } 55 | 56 | toggleSidebar() { 57 | console.log('CLICKED TOGGLE'); 58 | this.isSidebarVisible = !this.isSidebarVisible; 59 | } 60 | 61 | openModal() { 62 | this.isSidebarVisible = true; 63 | } 64 | 65 | showSection(section: string) { 66 | switch (section) { 67 | case 'inputs': 68 | this.showInputs = !this.showInputs; 69 | this.inputs = this.modalItem?.inputs || []; 70 | break; 71 | case 'outputs': 72 | this.showOutputs = !this.showOutputs; 73 | this.outputs = this.modalItem?.outputs || []; 74 | break; 75 | case 'services': 76 | this.showServices = !this.showServices; 77 | break; 78 | default: 79 | // Handle unknown section 80 | break; 81 | } 82 | } 83 | } 84 | 85 | /* 86 | REGULAR COMPONENT 87 | Inputs (Receiving Data From Parent: ) 88 | Name of Input Variable 89 | 90 | Outputs (Sending data to Parent:) 91 | Name of Output Variable 92 | 93 | Any Injectables in this component 94 | */ 95 | 96 | /* 97 | ROUTER COMPONENT 98 | Children - 99 | 100 | */ 101 | -------------------------------------------------------------------------------- /webview-ui/src/app/parent-child/parent-child.component.css: -------------------------------------------------------------------------------- 1 | .network-container { 2 | width: 70vw; 3 | height: 70vh; 4 | background: #130c25; 5 | display: block; 6 | border-radius: 10px; 7 | border: 1px solid #444444 8 | } 9 | 10 | .center { 11 | display: flex; 12 | justify-content: flex-end; 13 | margin-bottom: 10px; 14 | } 15 | .dropbtn { 16 | background-color: #4caf50; 17 | color: white; 18 | padding: 16px; 19 | font-size: 16px; 20 | border: none; 21 | cursor: pointer; 22 | } 23 | 24 | .dropdown { 25 | position: relative; 26 | display: inline-block; 27 | } 28 | 29 | .dropdown-content { 30 | display: none; 31 | position: absolute; 32 | background-color: #f9f9f9; 33 | min-width: 160px; 34 | box-shadow: 0px 8px 16px 0px rgba(0, 0, 0, 0.2); 35 | z-index: 1; 36 | } 37 | 38 | .dropdown-content a { 39 | color: black; 40 | padding: 12px 16px; 41 | text-decoration: none; 42 | display: block; 43 | } 44 | 45 | .dropdown-content a:hover { 46 | background-color: #f1f1f1; 47 | } 48 | .dropdown:hover .dropdown-content { 49 | display: block; 50 | } 51 | 52 | .dropdown:hover .dropbtn { 53 | background-color: #3e8e41; 54 | } 55 | 56 | /* dropdown button new */ 57 | .button-1 { 58 | background: hsl(345, 100%, 37%); 59 | border-radius: 8px; 60 | border-style: none; 61 | box-sizing: border-box; 62 | color: #ffffff; 63 | cursor: pointer; 64 | display: inline-block; 65 | font-size: 14px; 66 | height: 45px; 67 | line-height: 20px; 68 | list-style: none; 69 | margin: 0; 70 | outline: none; 71 | padding: 10px 16px; 72 | position: relative; 73 | text-align: center; 74 | text-decoration: none; 75 | transition: color 100ms; 76 | vertical-align: baseline; 77 | user-select: none; 78 | -webkit-user-select: none; 79 | touch-action: manipulation; 80 | } 81 | 82 | .button-1:hover, 83 | .button-1:focus { 84 | background-color: #f082ac; 85 | } 86 | 87 | /* Legend styling */ 88 | 89 | .legendContainer { 90 | display: flex; 91 | justify-content: center; 92 | width: 70vw; 93 | height: max-content; 94 | } 95 | .nodesContainer { 96 | display: flex; 97 | justify-content: center; 98 | } 99 | 100 | .circleContainer { 101 | display: flex; 102 | align-items: center; 103 | } 104 | 105 | .legendCircle { 106 | height: 20px; 107 | width: 20px; 108 | box-shadow: 2px 0px 4px rgba(0, 0, 0, 0.4); 109 | border-radius: 50%; 110 | margin-right: 8px; 111 | } 112 | 113 | .nodeTitle { 114 | color: white; 115 | font-weight: 400; 116 | margin-right: 20px; 117 | } 118 | .edge { 119 | display: flex; 120 | align-items: center; 121 | } 122 | .arrow { 123 | font-size: 35px; 124 | margin-right: 10px; 125 | 126 | } 127 | 128 | .edgesContainer { 129 | display: flex; 130 | align-items: center; 131 | } 132 | 133 | .edgeTitle { 134 | color: white; 135 | margin-right: 10px; 136 | } 137 | -------------------------------------------------------------------------------- /webview-ui/src/app/parent-child/parent-child.component.html: -------------------------------------------------------------------------------- 1 |
2 | 10 |
11 | 12 |
22 |
23 |
24 |
25 |
26 |

Root Node

27 |
28 |
29 |
30 |

Router-Outlet Node

31 |
32 |
33 |
34 |

Component Node

35 |
36 |
37 |
38 |
39 |
40 |

Inputs

41 |
42 |
43 |
44 |

Outputs

45 |
46 |
47 |
48 | -------------------------------------------------------------------------------- /webview-ui/src/app/parent-child/parent-child.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { ParentChildComponent } from './parent-child.component'; 4 | 5 | describe('ParentChildComponent', () => { 6 | let component: ParentChildComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ParentChildComponent] 12 | }); 13 | fixture = TestBed.createComponent(ParentChildComponent); 14 | component = fixture.componentInstance; 15 | fixture.detectChanges(); 16 | }); 17 | 18 | it('should create', () => { 19 | expect(component).toBeTruthy(); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /webview-ui/src/app/parent-child/parent-child.component.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Component, 3 | ChangeDetectionStrategy, 4 | ElementRef, 5 | OnInit, 6 | ViewChild, 7 | OnDestroy, 8 | } from '@angular/core'; 9 | import { DataSet, DataView } from 'vis-data'; 10 | import { Network } from 'vis-network/standalone'; 11 | import { ExtensionMessage } from '../../models/message'; 12 | import { ParentChildServices } from 'src/services/ParentChildServices'; 13 | 14 | import { vscode } from '../utilities/vscode'; 15 | import { 16 | FsItem, 17 | PcItem, 18 | Node, 19 | Edge, 20 | Input, 21 | Output, 22 | RouterChildren, 23 | DataStore, 24 | ServiceItem, 25 | } from '../../models/FileSystem'; 26 | import { Router } from '@angular/router'; 27 | @Component({ 28 | selector: 'parent-child', 29 | templateUrl: './parent-child.component.html', 30 | styleUrls: ['./parent-child.component.css'], 31 | changeDetection: ChangeDetectionStrategy.OnPush, 32 | }) 33 | export class ParentChildComponent implements OnInit, OnDestroy { 34 | @ViewChild('networkContainer') networkContainer!: ElementRef; 35 | 36 | constructor(private pcService: ParentChildServices) {} 37 | 38 | nodes: Node[] = []; 39 | edges: Edge[] = []; 40 | pcItems: PcItem[] = []; 41 | servicesData: ServiceItem[] | undefined; 42 | private network: Network | undefined; 43 | 44 | handleClickModal(network: Network) { 45 | network.on('doubleClick', (params: any) => { 46 | if (params.nodes.length > 0) { 47 | const nodeId = params.nodes[0]; 48 | if (nodeId) { 49 | // Open Modal for specific node component 50 | let deliverPc: PcItem | null = null; 51 | for (const item of this.pcItems) { 52 | if (item.id === nodeId) { 53 | deliverPc = item; 54 | break; 55 | } 56 | } 57 | const nodeServices: string[] = []; 58 | // iterate through each service in array 59 | this.servicesData?.forEach((service: ServiceItem) => { 60 | // filter out node in injectionPoints key array 61 | const containsNode = service.injectionPoints.filter( 62 | (node) => node.folderPath === nodeId 63 | ); 64 | 65 | if (containsNode.length > 0) { 66 | console.log('NODE ID', nodeId); 67 | console.log('CONTAINSNODE', containsNode); 68 | console.log('CONTAINS NODE SERVICE', service); 69 | nodeServices.push(service.className); 70 | } 71 | }); 72 | 73 | const edgeRelations = this.getEdgesOfNode(nodeId); 74 | 75 | if (deliverPc && edgeRelations && nodeServices) { 76 | console.log('nodeServices', nodeServices); 77 | this.pcService.openModal({ 78 | pcItem: deliverPc as PcItem, 79 | edges: edgeRelations as Object, 80 | services: nodeServices as string[], 81 | }); 82 | } 83 | } 84 | } 85 | }); 86 | } 87 | 88 | getEdgesOfNode(nodeId: string) { 89 | // Item.relation === output, Sending Data from node to parent 90 | const outputEdges = this.edges.filter( 91 | (edge) => edge.from === nodeId && edge.relation === 'output' 92 | ); 93 | // Item.relation === input, Receiving Data from parent to node 94 | const inputEdges = this.edges.filter( 95 | (edge) => edge.to === nodeId && edge.relation === 'input' 96 | ); 97 | return { inputs: inputEdges, outputs: outputEdges }; 98 | } 99 | 100 | private handleMessageEvent = (event: MessageEvent) => { 101 | const message: ExtensionMessage = event.data; 102 | console.log('caught message?', message); 103 | 104 | switch (message.command) { 105 | case 'loadState': { 106 | const state = vscode.getState() as { 107 | pcData: DataStore | undefined; 108 | fsData: DataStore | undefined; 109 | fsNodes: Node[]; 110 | fsEdges: Edge[]; 111 | pcNodes: Node[]; 112 | pcEdges: Edge[]; 113 | servicesNodes: Node[]; 114 | servicesEdges: Edge[]; 115 | servicesData: ServiceItem[]; 116 | pcItems: PcItem[]; 117 | }; 118 | this.nodes = state.pcNodes; 119 | this.edges = state.pcEdges; 120 | this.servicesData = state.servicesData; 121 | const newNodes = new DataSet(state.pcNodes); 122 | const newEdges = new DataSet(state.pcEdges); 123 | const data: { 124 | nodes: DataSet; 125 | edges: DataSet; 126 | } = { 127 | nodes: newNodes, 128 | edges: newEdges, 129 | }; 130 | const container = this.networkContainer.nativeElement; 131 | this.network = new Network(container, data, this.options); 132 | this.pcItems = state.pcItems; 133 | vscode.setState({ 134 | pcItems: this.pcItems, 135 | pcData: data, 136 | fsData: state.fsData, 137 | fsNodes: state.fsNodes, 138 | fsEdges: state.fsEdges, 139 | pcNodes: state.pcNodes, 140 | pcEdges: state.pcEdges, 141 | servicesNodes: state.servicesNodes, 142 | servicesEdges: state.servicesEdges, 143 | servicesData: state.servicesData, 144 | }); 145 | this.handleClickModal(this.network); 146 | break; 147 | } 148 | 149 | case 'updatePC': { 150 | this.pcItems = this.populate(message.data); 151 | console.log('PC MESSAGE DATA', message.data); 152 | this.pcService.setItems(this.pcItems); 153 | const state = vscode.getState() as { 154 | pcData: DataStore | undefined; 155 | fsData: DataStore | undefined; 156 | fsNodes: Node[]; 157 | fsEdges: Edge[]; 158 | pcNodes: Node[]; 159 | pcEdges: Edge[]; 160 | servicesNodes: Node[]; 161 | servicesEdges: Edge[]; 162 | servicesData: ServiceItem[]; 163 | }; 164 | 165 | const { nodes, edges } = this.createNodesAndEdges(this.pcItems); 166 | this.nodes = nodes; 167 | this.edges = edges; 168 | this.servicesData = state.servicesData; 169 | 170 | const newNodes = new DataSet(nodes); 171 | const newEdges = new DataSet(edges); 172 | 173 | // create a network 174 | const container = this.networkContainer.nativeElement; 175 | // const data = { newNodes, newEdges }; 176 | const data: { 177 | nodes: DataSet; 178 | edges: DataSet; 179 | } = { 180 | nodes: newNodes, 181 | edges: newEdges, 182 | }; 183 | //update state 184 | 185 | vscode.setState({ 186 | pcItems: this.pcItems, 187 | pcData: data, 188 | fsData: state.fsData, 189 | fsNodes: state.fsNodes, 190 | fsEdges: state.fsEdges, 191 | pcNodes: this.nodes, 192 | pcEdges: this.edges, 193 | servicesNodes: state.servicesNodes, 194 | servicesEdges: state.servicesEdges, 195 | servicesData: state.servicesData, 196 | }); 197 | this.network = new Network(container, data, this.options); 198 | this.handleClickModal(this.network); 199 | break; 200 | } 201 | 202 | case 'reloadPC': { 203 | const state = vscode.getState() as { 204 | pcData: DataStore; 205 | fsData: DataStore; 206 | fsNodes: Node[]; 207 | fsEdges: Edge[]; 208 | pcNodes: Node[]; 209 | pcEdges: Edge[]; 210 | pcItems: PcItem[]; 211 | servicesData: ServiceItem[]; 212 | }; 213 | this.nodes = state.pcNodes; 214 | this.edges = state.pcEdges; 215 | this.pcItems = state.pcItems; 216 | this.servicesData = state.servicesData; 217 | 218 | console.log('pcItems', this.pcItems); 219 | 220 | const container = this.networkContainer.nativeElement; 221 | this.network = new Network(container, state.pcData, this.options); 222 | this.handleClickModal(this.network); 223 | break; 224 | } 225 | 226 | case 'updateServices': { 227 | this.servicesData = message.data; 228 | const state = vscode.getState() as { 229 | pcData: DataStore | undefined; 230 | fsData: DataStore | undefined; 231 | fsNodes: Node[]; 232 | fsEdges: Edge[]; 233 | pcNodes: Node[]; 234 | pcEdges: Edge[]; 235 | pcItems: PcItem[]; 236 | servicesNodes: Node[]; 237 | servicesEdges: Edge[]; 238 | servicesData: ServiceItem[]; 239 | }; 240 | 241 | vscode.setState({ 242 | pcData: state.pcData, 243 | fsData: state.fsData, 244 | fsNodes: state.fsNodes, 245 | fsEdges: state.fsEdges, 246 | pcNodes: state.pcNodes, 247 | pcEdges: state.pcEdges, 248 | pcItems: state.pcItems, 249 | servicesNodes: state.servicesNodes, 250 | servicesEdges: state.servicesEdges, 251 | servicesData: this.servicesData, 252 | }); 253 | console.log('SERVICE DATA', this.servicesData); 254 | break; 255 | } 256 | 257 | default: 258 | console.log('PC DEFAULT CASE unknown command ->', message.command); 259 | break; 260 | } 261 | }; 262 | 263 | options = { 264 | interaction: { 265 | navigationButtons: true, 266 | keyboard: true, 267 | }, 268 | layout: { 269 | hierarchical: { 270 | direction: 'UD', // Up-Down direction 271 | // nodeSpacing: 1000, 272 | // levelSeparation: 300, 273 | parentCentralization: true, 274 | edgeMinimization: true, 275 | shakeTowards: 'roots', // Tweak the layout algorithm to get better results 276 | // sortMethod: 'directed', // Sort based on the hierarchical structure 277 | }, 278 | }, 279 | 280 | nodes: { 281 | shape: 'circle', 282 | shadow: { 283 | enabled: true, 284 | color: 'rgba(0,0,0,0.5)', 285 | size: 10, 286 | x: 5, 287 | y: 5, 288 | }, 289 | }, 290 | 291 | edges: { 292 | color: 'cyan', 293 | smooth: { 294 | enabled: true, 295 | type: 'cubicBezier', 296 | forceDirection: 'vertical', 297 | roundness: 0.4, 298 | }, 299 | }, 300 | 301 | physics: { 302 | hierarchicalRepulsion: { 303 | avoidOverlap: 1, 304 | nodeDistance: 145, 305 | }, 306 | }, 307 | }; 308 | setupMessageListener(): void { 309 | window.addEventListener('message', this.handleMessageEvent); 310 | } 311 | ngOnInit(): void { 312 | this.setupMessageListener(); 313 | } 314 | 315 | ngOnDestroy(): void { 316 | console.log('VIEW DESTROYED'); 317 | window.removeEventListener('message', this.handleMessageEvent); 318 | } 319 | 320 | selectedFilter: string = 'all'; 321 | edgesDataSet: DataSet = new DataSet(this.edges); // Initialize as empty DataSet object 322 | edgesView = new DataView(this.edgesDataSet); 323 | 324 | edgesFilter(edgesData: DataSet) { 325 | switch (this.selectedFilter) { 326 | case 'input': 327 | const inputEdges = edgesData.get({ 328 | filter: (edge) => edge.relation !== 'output', 329 | }); 330 | const inputDataSet = new DataSet(inputEdges); 331 | return inputDataSet; 332 | // return item.relation === this.selectedFilter; 333 | case 'output': 334 | const outputEdges = edgesData.get({ 335 | filter: (edge) => edge.relation !== 'input', 336 | }); 337 | const outputDataSet = new DataSet(outputEdges); 338 | return outputDataSet; 339 | case 'all': 340 | return edgesData; 341 | default: 342 | return edgesData; 343 | } 344 | } 345 | 346 | updateFilters(filterType: string) { 347 | this.selectedFilter = filterType; 348 | this.edgesDataSet.clear(); 349 | this.edgesDataSet.add(this.edges); 350 | const result = this.edgesFilter(this.edgesDataSet); 351 | this.edgesView = new DataView(result); 352 | 353 | if (this.network) { 354 | const data: { 355 | nodes: DataSet; 356 | edges: DataView; 357 | } = { 358 | nodes: new DataSet(this.nodes), 359 | edges: this.edgesView, 360 | }; 361 | const container = this.networkContainer.nativeElement; 362 | this.network = new Network(container, data, this.options); 363 | this.handleClickModal(this.network); 364 | } 365 | } 366 | 367 | createNodesAndEdges(pcItems: PcItem[]): { nodes: Node[]; edges: Edge[] } { 368 | const nodes: Node[] = []; 369 | const edges: Edge[] = []; 370 | 371 | let hasAddedRootNode = false; 372 | // Helper function to recursively add nodes and edges 373 | function addNodesAndEdges(item: PcItem, parentFolder?: string) { 374 | // Check if the node already exists to avoid duplicates 375 | const existingNode = nodes.find((node) => node.id === item.id); 376 | if (!existingNode) { 377 | // Add the current item as a node 378 | let fileImg: string = ''; 379 | let selectedImg: string = ''; 380 | 381 | if (!hasAddedRootNode) { 382 | nodes.push({ 383 | id: item.id, 384 | label: item.label, 385 | color: '#ff6961', 386 | }); 387 | hasAddedRootNode = true; 388 | } else { 389 | nodes.push({ 390 | id: item.id, 391 | label: item.label, 392 | color: { background: 'cyan', border: 'black' }, 393 | }); 394 | } 395 | if (item.inputs.length > 0) { 396 | // iterate through inputs array 397 | for (let inputItem in item.inputs) { 398 | const edge: Edge = { 399 | id: `${item.id}-${item.inputs[inputItem].pathFrom}`, 400 | from: item.inputs[inputItem].pathFrom, 401 | to: item.id, 402 | relation: 'input', 403 | color: { color: 'green', highlight: 'green' }, 404 | smooth: { type: 'curvedCCW', roundness: 0.3 }, 405 | arrows: { 406 | to: { 407 | enabled: true, 408 | type: 'arrow', 409 | }, 410 | middle: { 411 | type: 'arrow', 412 | }, 413 | }, 414 | label: 'Input', 415 | font: { align: 'middle' }, 416 | }; 417 | edges.push(edge); 418 | } 419 | } 420 | 421 | if (item.outputs.length > 0) { 422 | for (let outputItem in item.outputs) { 423 | const edge: Edge = { 424 | id: `${item.id}-${item.outputs[outputItem].pathTo}`, 425 | from: item.id, 426 | to: item.outputs[outputItem].pathTo, 427 | color: { color: 'red', highlight: 'red' }, 428 | relation: 'output', 429 | arrows: { 430 | to: { 431 | enabled: true, 432 | type: 'arrow', 433 | }, 434 | middle: { 435 | type: 'arrow', 436 | }, 437 | }, 438 | smooth: { type: 'curvedCCW', roundness: 0.2 }, 439 | label: 'Output', 440 | font: { align: 'middle' }, 441 | }; 442 | edges.push(edge); 443 | } 444 | } 445 | 446 | // If the item has children (files or subfolders), add edges to them 447 | if (item.children && item.children.length > 0) { 448 | for (const childId of item.children) { 449 | const edge: Edge = { 450 | id: `${item.id}-${childId}`, 451 | from: item.id, 452 | to: childId, 453 | relation: 'all', 454 | smooth: false, 455 | }; 456 | edges.push(edge); 457 | const child = pcItems.find((pcItem) => pcItem.id === childId); 458 | if (child) { 459 | // Recursively add nodes and edges for children 460 | addNodesAndEdges(child, item.id); 461 | } 462 | } 463 | } 464 | } 465 | 466 | //add router and roputer children nodes and edges 467 | if (item.router !== undefined && item.router.children) { 468 | // Create the "router-outlet" node 469 | const routerOutletNode: Node = { 470 | id: 'router-outlet', 471 | label: 'router-outlet', 472 | color: '#CBC3E3', 473 | }; 474 | nodes.push(routerOutletNode); 475 | const edge: Edge = { 476 | id: `${item.id}-router-outlet`, 477 | from: routerOutletNode.id, 478 | to: item.id, // Connect to the "router-outlet" node 479 | relation: 'router-outlet', 480 | smooth: true, 481 | color: { color: 'purple', highlight: 'purple' }, 482 | }; 483 | edges.push(edge); 484 | //recursively add children 485 | for (const routerChild of item.router.children) { 486 | // Add the router component as a node 487 | nodes.push({ 488 | id: routerChild.path, 489 | label: routerChild.name, 490 | color: { color: 'purple', highlight: 'purple' }, 491 | }); 492 | 493 | // Create an edge from the router component to the "router-outlet" 494 | const edge: Edge = { 495 | id: `${routerChild.name}-router-outlet`, 496 | from: routerChild.path, 497 | to: 'router-outlet', // Connect to the "router-outlet" node 498 | relation: 'router', 499 | smooth: true, 500 | color: { color: 'purple', highlight: 'purple' }, 501 | }; 502 | edges.push(edge); 503 | 504 | // Recursively add nodes and edges for children of router children 505 | if (routerChild.children && routerChild.children.length > 0) { 506 | for (const innerRouterChild of routerChild.children) { 507 | routerChildrenHelper(innerRouterChild, routerChild.path); 508 | } 509 | } 510 | } 511 | } 512 | // Helper function for adding nodes and edges for children of router children 513 | function routerChildrenHelper( 514 | innerRouterChild: RouterChildren, 515 | parentId: string 516 | ) { 517 | // Check if the node already exists to avoid duplicates 518 | const existingNode = nodes.find( 519 | (node) => node.id === innerRouterChild.path 520 | ); 521 | if (!existingNode) { 522 | // Add the router child as a node 523 | nodes.push({ 524 | id: innerRouterChild.path, 525 | label: innerRouterChild.name, 526 | color: '#CBC3E3', 527 | }); 528 | 529 | // Create an edge from the router child to its parent router 530 | const edge: Edge = { 531 | id: `${innerRouterChild.path}-${parentId}`, 532 | from: innerRouterChild.path, 533 | to: parentId, 534 | relation: 'router-outlet', 535 | smooth: true, 536 | color: { color: 'purple', highlight: 'purple' }, 537 | }; 538 | edges.push(edge); 539 | 540 | // Recursively add nodes and edges for children of router children 541 | if ( 542 | innerRouterChild.children && 543 | innerRouterChild.children.length > 0 544 | ) { 545 | for (const childOfInnerChild of innerRouterChild.children) { 546 | routerChildrenHelper(childOfInnerChild, innerRouterChild.path); 547 | } 548 | } 549 | } 550 | } 551 | } 552 | // Iterate through the root items and start the process 553 | for (const rootItem of pcItems) { 554 | addNodesAndEdges(rootItem); 555 | } 556 | 557 | return { nodes, edges }; 558 | } 559 | 560 | populate(obj: any, items: PcItem[] = []): PcItem[] { 561 | function populateGraph(obj: any, parentComponent?: string): PcItem | void { 562 | // if current object has a name -> create a node for it 563 | if (obj.hasOwnProperty('name')) { 564 | const currentNode: PcItem = { 565 | id: obj.path, 566 | label: obj.name, 567 | type: 'component', 568 | inputs: [], 569 | outputs: [], 570 | children: [], 571 | router: obj.router, 572 | }; 573 | items.push(currentNode); 574 | 575 | if (obj.inputs) currentNode.inputs = obj.inputs; 576 | if (obj.outputs) currentNode.outputs = obj.outputs; 577 | 578 | // check if object has children 579 | if (obj.children) { 580 | for (const child of obj.children) { 581 | // if it does, run populate graph on each child, declare parentComponent 582 | const childNode = populateGraph(child, currentNode.id); 583 | if (childNode) { 584 | // add child to parent node's children array 585 | currentNode.children.push(childNode.id); 586 | } 587 | } 588 | if (parentComponent) { 589 | return currentNode; 590 | } 591 | } 592 | return currentNode; 593 | } 594 | } 595 | populateGraph(obj); 596 | return items; 597 | } 598 | } 599 | -------------------------------------------------------------------------------- /webview-ui/src/app/services-view/services-view.component.css: -------------------------------------------------------------------------------- 1 | .network-container { 2 | width: 70vw; 3 | height: 70vh; 4 | background: #130c25; 5 | display: block; 6 | border-radius: 10px; 7 | border: 2px solid white; 8 | } 9 | -------------------------------------------------------------------------------- /webview-ui/src/app/services-view/services-view.component.html: -------------------------------------------------------------------------------- 1 |
12 | 13 | 23 | -------------------------------------------------------------------------------- /webview-ui/src/app/services-view/services-view.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { ServicesViewComponent } from './services-view.component'; 4 | 5 | describe('ServicesViewComponent', () => { 6 | let component: ServicesViewComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ServicesViewComponent] 12 | }); 13 | fixture = TestBed.createComponent(ServicesViewComponent); 14 | component = fixture.componentInstance; 15 | fixture.detectChanges(); 16 | }); 17 | 18 | it('should create', () => { 19 | expect(component).toBeTruthy(); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /webview-ui/src/app/services-view/services-view.component.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Component, 3 | ChangeDetectionStrategy, 4 | ElementRef, 5 | OnInit, 6 | ViewChild, 7 | OnDestroy, 8 | } from '@angular/core'; 9 | import { DataSet, DataView } from 'vis-data'; 10 | import { Network } from 'vis-network/standalone'; 11 | import { ExtensionMessage } from '../../models/message'; 12 | import { vscode } from '../utilities/vscode'; 13 | import { 14 | FsItem, 15 | PcItem, 16 | Node, 17 | Edge, 18 | Input, 19 | Output, 20 | RouterChildren, 21 | ServiceItem, 22 | InjectionPoint, 23 | DataStore, 24 | } from '../../models/FileSystem'; 25 | import { Router } from '@angular/router'; 26 | 27 | @Component({ 28 | selector: 'services-view', 29 | templateUrl: './services-view.component.html', 30 | styleUrls: ['./services-view.component.css'], 31 | }) 32 | export class ServicesViewComponent implements OnInit, OnDestroy { 33 | @ViewChild('networkContainer') networkContainer!: ElementRef; 34 | constructor() {} 35 | services: ServiceItem[] = []; 36 | network: Network | undefined; 37 | nodes: Node[] = []; 38 | edges: Edge[] = []; 39 | options = { 40 | interaction: { 41 | navigationButtons: true, 42 | keyboard: true, 43 | }, 44 | layout: { 45 | hierarchical: { 46 | direction: 'UD', // Up-Down direction 47 | sortMethod: 'directed', 48 | // nodeSpacing: 1000, 49 | // levelSeparation: 300, 50 | parentCentralization: false, 51 | edgeMinimization: true, 52 | }, 53 | }, 54 | 55 | nodes: { 56 | shape: 'circle', 57 | shadow: { 58 | enabled: true, 59 | color: 'rgba(0,0,0,0.5)', 60 | size: 10, 61 | x: 5, 62 | y: 5, 63 | }, 64 | }, 65 | 66 | edges: { 67 | color: 'cyan', 68 | smooth: { 69 | enabled: true, 70 | type: 'cubicBezier', 71 | forceDirection: 'vertical', 72 | roundness: 0.4, 73 | }, 74 | }, 75 | }; 76 | private handleMessageEvent = (event: MessageEvent) => { 77 | const message: ExtensionMessage = event.data; 78 | 79 | switch (message.command) { 80 | case 'updateServices': { 81 | const serviceObj = message.data; 82 | const state = vscode.getState() as { 83 | pcData: any; 84 | fsData: any; 85 | fsNodes: Node[]; 86 | fsEdges: Edge[]; 87 | pcNodes: Node[]; 88 | pcEdges: Edge[]; 89 | pcItems: PcItem[]; 90 | servicesData: ServiceItem[]; 91 | }; 92 | 93 | //run message.data through helper functions 94 | this.services = this.populate(serviceObj); 95 | 96 | //turn into nodes and edges 97 | const { nodes, edges } = this.createNodesEdges(this.services); 98 | 99 | const newNodes = new DataSet(nodes); 100 | const newEdges = new DataSet(edges); 101 | // create a network 102 | const data: { 103 | nodes: DataSet; 104 | edges: DataSet; 105 | } = { 106 | nodes: newNodes, 107 | edges: newEdges, 108 | }; 109 | const container = this.networkContainer.nativeElement; 110 | this.network = new Network(container, data, this.options); 111 | 112 | vscode.setState({ 113 | pcData: state.pcData, 114 | fsData: state.fsData, 115 | fsNodes: state.fsNodes, 116 | fsEdges: state.fsEdges, 117 | pcNodes: state.pcNodes, 118 | pcEdges: state.pcEdges, 119 | servicesNodes: nodes, 120 | servicesEdges: edges, 121 | pcItems: state.pcItems, 122 | servicesData: state.servicesData, 123 | }); 124 | break; 125 | } 126 | 127 | case 'reloadServices': { 128 | const state = vscode.getState() as { 129 | pcData: DataStore | undefined; 130 | fsData: DataStore | undefined; 131 | fsNodes: Node[]; 132 | fsEdges: Edge[]; 133 | pcNodes: Node[]; 134 | pcEdges: Edge[]; 135 | servicesNodes: Node[]; 136 | servicesEdges: Edge[]; 137 | }; 138 | this.nodes = state.servicesNodes; 139 | this.edges = state.servicesEdges; 140 | const newNodes = new DataSet(state.servicesNodes); 141 | const newEdges = new DataSet(state.servicesEdges); 142 | const data: { 143 | nodes: DataSet; 144 | edges: DataSet; 145 | } = { 146 | nodes: newNodes, 147 | edges: newEdges, 148 | }; 149 | const container = this.networkContainer.nativeElement; 150 | this.network = new Network(container, data, this.options); 151 | break; 152 | } 153 | 154 | default: 155 | console.log( 156 | 'Services DEFAULT CASE unknown command ->', 157 | message.command 158 | ); 159 | break; 160 | } 161 | }; 162 | 163 | setupMessageListener(): void { 164 | window.addEventListener('message', this.handleMessageEvent); 165 | } 166 | 167 | ngOnInit(): void { 168 | this.setupMessageListener(); 169 | } 170 | 171 | ngOnDestroy(): void { 172 | console.log('VIEW DESTROYED'); 173 | window.removeEventListener('message', this.handleMessageEvent); 174 | } 175 | 176 | createNodesEdges(serviceItems: ServiceItem[]): { 177 | nodes: Node[]; 178 | edges: Edge[]; 179 | } { 180 | const nodes: Node[] = []; 181 | const edges: Edge[] = []; 182 | let idCounter = 0; 183 | serviceItems.forEach((item: ServiceItem) => { 184 | const newServiceNode: Node = { 185 | id: item.path, 186 | label: item.className, 187 | color: '#ff6961', 188 | }; 189 | nodes.push(newServiceNode); 190 | if (item.injectionPoints.length > 0) { 191 | item.injectionPoints.forEach((injectItem: InjectionPoint) => { 192 | const newInjectNode: Node = { 193 | id: `${injectItem.folderPath}-${idCounter}`, 194 | label: injectItem.selectorName, 195 | color: 'cyan', 196 | }; 197 | nodes.push(newInjectNode); 198 | const newEdge: Edge = { 199 | id: `${item.path}-${injectItem.folderPath}-${idCounter}`, 200 | from: item.path, 201 | to: newInjectNode.id, 202 | }; 203 | edges.push(newEdge); 204 | }); 205 | } 206 | idCounter++; 207 | }); 208 | 209 | return { nodes, edges }; 210 | } 211 | 212 | populate(servicesItems: ServiceItem[] = []): ServiceItem[] { 213 | const serviceArray: ServiceItem[] = []; 214 | servicesItems.forEach((item: ServiceItem) => { 215 | const newServiceItem: ServiceItem = { 216 | className: item.className, 217 | fileName: item.fileName, 218 | injectionPoints: item.injectionPoints, 219 | path: item.path, 220 | providedIn: item.providedIn, 221 | }; 222 | serviceArray.push(newServiceItem); 223 | }); 224 | return serviceArray; 225 | } 226 | } 227 | -------------------------------------------------------------------------------- /webview-ui/src/app/utilities/vscode.ts: -------------------------------------------------------------------------------- 1 | import type { WebviewApi } from 'vscode-webview'; 2 | 3 | /** 4 | * A utility wrapper around the acquireVsCodeApi() function, which enables 5 | * message passing and state management between the webview and extension 6 | * contexts. 7 | * 8 | * This utility also enables webview code to be run in a web browser-based 9 | * dev server by using native web browser features that mock the functionality 10 | * enabled by acquireVsCodeApi. 11 | */ 12 | class VSCodeAPIWrapper { 13 | private readonly vsCodeApi: WebviewApi | undefined; 14 | 15 | constructor() { 16 | // Check if the acquireVsCodeApi function exists in the current development 17 | // context (i.e. VS Code development window or web browser) 18 | if (typeof acquireVsCodeApi === 'function') { 19 | this.vsCodeApi = acquireVsCodeApi(); 20 | } 21 | } 22 | 23 | /** 24 | * Post a message (i.e. send arbitrary data) to the owner of the webview. 25 | * 26 | * @remarks When running webview code inside a web browser, postMessage will instead 27 | * log the given message to the console. 28 | * 29 | * @param message Abitrary data (must be JSON serializable) to send to the extension context. 30 | */ 31 | public postMessage(message: unknown) { 32 | if (this.vsCodeApi) { 33 | this.vsCodeApi.postMessage(message); 34 | } else { 35 | console.log(message); 36 | } 37 | } 38 | 39 | /** 40 | * Get the persistent state stored for this webview. 41 | * 42 | * @remarks When running webview source code inside a web browser, getState will retrieve state 43 | * from local storage (https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage). 44 | * 45 | * @return The current state or `undefined` if no state has been set. 46 | */ 47 | public getState(): unknown | undefined { 48 | if (this.vsCodeApi) { 49 | return this.vsCodeApi.getState(); 50 | } else { 51 | const state = localStorage.getItem('vscodeState'); 52 | return state ? JSON.parse(state) : undefined; 53 | } 54 | } 55 | 56 | /** 57 | * Set the persistent state stored for this webview. 58 | * 59 | * @remarks When running webview source code inside a web browser, setState will set the given 60 | * state using local storage (https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage). 61 | * 62 | * @param newState New persisted state. This must be a JSON serializable object. Can be retrieved 63 | * using {@link getState}. 64 | * 65 | * @return The new state. 66 | */ 67 | public setState(newState: T): T { 68 | if (this.vsCodeApi) { 69 | return this.vsCodeApi.setState(newState); 70 | } else { 71 | localStorage.setItem('vscodeState', JSON.stringify(newState)); 72 | return newState; 73 | } 74 | } 75 | } 76 | 77 | // Exports class singleton to prevent multiple invocations of acquireVsCodeApi. 78 | export const vscode = new VSCodeAPIWrapper(); 79 | -------------------------------------------------------------------------------- /webview-ui/src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/AnguLens/6ed5f9dde429dd1cf82bbc577e5f2e85294e62a4/webview-ui/src/favicon.ico -------------------------------------------------------------------------------- /webview-ui/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | WebviewUi 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /webview-ui/src/main.ts: -------------------------------------------------------------------------------- 1 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 2 | 3 | import { AppModule } from './app/app.module'; 4 | 5 | 6 | platformBrowserDynamic().bootstrapModule(AppModule) 7 | .catch(err => console.error(err)); 8 | 9 | 10 | -------------------------------------------------------------------------------- /webview-ui/src/models/FileSystem.ts: -------------------------------------------------------------------------------- 1 | import { DataSet } from 'vis-data'; 2 | 3 | export class FsItem { 4 | constructor( 5 | public id: string, 6 | public label: string, 7 | public type: string, 8 | public folderParent?: string, 9 | public children: string[] = [] 10 | ) {} 11 | } 12 | 13 | export class PcItem { 14 | constructor( 15 | public id: string, 16 | public label: string, 17 | public type: string, 18 | public inputs: Input[] = [], 19 | public outputs: Output[] = [], 20 | public children: string[] = [], 21 | public router?: { 22 | name: string; 23 | path: string; 24 | children: RouterChildren[]; // Make sure children is an array of PcItem 25 | } 26 | ) {} 27 | } 28 | 29 | export interface DataStore { 30 | nodes: DataSet; 31 | edges: DataSet; 32 | } 33 | 34 | export class ServiceItem { 35 | constructor( 36 | public className: string, 37 | public fileName: string, 38 | public injectionPoints: InjectionPoint[], 39 | public path: string, 40 | public providedIn: string 41 | ) {} 42 | } 43 | 44 | export interface InjectionPoint { 45 | selectorName: string; 46 | folderPath: string; 47 | } 48 | 49 | export interface RouterChildren { 50 | name: string; 51 | children: RouterChildren[]; 52 | inputs: Input[]; 53 | outputs: Output[]; 54 | path: string; 55 | urlPath: string; 56 | } 57 | 58 | export interface Input { 59 | name: string; 60 | pathTo: string; 61 | pathFrom: string; 62 | } 63 | 64 | export interface Output { 65 | name: string; 66 | pathTo: string; 67 | pathFrom: string; 68 | } 69 | export interface Node { 70 | id: string; 71 | label: string; 72 | image?: { 73 | unselected?: string; 74 | selected?: string; 75 | }; 76 | hidden?: boolean; 77 | open?: boolean; 78 | color?: string | {}; 79 | font?: {}; 80 | onFolderClick?: () => void; 81 | } 82 | 83 | export interface Edge { 84 | id: string; 85 | from: string; 86 | to: string; 87 | color?: {}; 88 | relation?: string; 89 | endPointOffset?: { 90 | to: number; 91 | from: number; 92 | }; 93 | arrowStrikethrough?: boolean; 94 | smooth?: { type: string; roundness: number } | boolean; 95 | arrows?: { 96 | to: object; 97 | middle?: object; 98 | from?: object; 99 | }; 100 | group?: object; 101 | label?: string; 102 | font?: object; 103 | } 104 | // 105 | -------------------------------------------------------------------------------- /webview-ui/src/models/message.ts: -------------------------------------------------------------------------------- 1 | export interface ExtensionMessage { 2 | command: string; 3 | data: any; 4 | } -------------------------------------------------------------------------------- /webview-ui/src/models/uri.ts: -------------------------------------------------------------------------------- 1 | export interface URIObj { 2 | path: string 3 | } -------------------------------------------------------------------------------- /webview-ui/src/services/FileSystemService.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { FsItem } from '../models/FileSystem'; 3 | 4 | @Injectable({ 5 | providedIn: 'root', 6 | }) 7 | export class FileSystemService { 8 | nodes: any[] = []; 9 | edges: any[] = []; 10 | uris: any[] = []; 11 | fsItems: FsItem[] = []; 12 | filePath: object = {}; 13 | generatedPc: boolean = false; 14 | generatedServices: boolean = false; 15 | // Add any other state variables as needed 16 | 17 | updateState(fsItems: FsItem[], uris: any[], filePath: object) { 18 | this.fsItems = fsItems; 19 | this.uris = uris; 20 | this.filePath = filePath; 21 | } 22 | 23 | setGeneratedServices(generatedServices: boolean) { 24 | this.generatedServices = generatedServices; 25 | } 26 | 27 | getGeneratedServices() { 28 | return this.generatedServices; 29 | } 30 | 31 | 32 | setGeneratedPC(generatedPc: boolean) { 33 | this.generatedPc = generatedPc; 34 | } 35 | 36 | getGeneratedPC() { 37 | return this.generatedPc; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /webview-ui/src/services/ParentChildServices.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { PcItem, Node } from '../models/FileSystem'; 3 | import { Subject } from 'rxjs'; 4 | 5 | @Injectable({ 6 | providedIn: 'root', 7 | }) 8 | export class ParentChildServices { 9 | nodes: any[] = []; 10 | edges: any[] = []; 11 | uris: any[] = []; 12 | pcItems: PcItem[] = []; 13 | filePath: object = {}; 14 | 15 | // subject to open modal 16 | private openModalSource = new Subject<{ 17 | pcItem: PcItem; 18 | edges: object; 19 | services: string[]; 20 | }>(); 21 | openModal$ = this.openModalSource.asObservable(); 22 | 23 | // Add any other state variables as needed 24 | setItems(pcItems: PcItem[]) { 25 | this.pcItems = pcItems; 26 | } 27 | 28 | getItems() { 29 | return this.pcItems; 30 | } 31 | 32 | openModal(deliverables: { 33 | pcItem: PcItem; 34 | edges: object; 35 | services: string[]; 36 | }) { 37 | this.openModalSource.next(deliverables); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /webview-ui/src/styles.css: -------------------------------------------------------------------------------- 1 | /* You can add global styles to this file, and also import other style files */ 2 | @import url('https://fonts.googleapis.com/css2?family=Rubik&family=Work+Sans:wght@600&display=swap'); 3 | * { 4 | font-family: 'Rubik', sans-serif; 5 | font-weight: 600; 6 | } 7 | 8 | -------------------------------------------------------------------------------- /webview-ui/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "./tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "./out-tsc/app", 6 | "types": [] 7 | }, 8 | "files": [ 9 | "src/main.ts" 10 | ], 11 | "include": [ 12 | "src/**/*.d.ts" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /webview-ui/tsconfig.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "compileOnSave": false, 4 | "compilerOptions": { 5 | "baseUrl": "./", 6 | "outDir": "./dist/out-tsc", 7 | "forceConsistentCasingInFileNames": true, 8 | "strict": true, 9 | "noImplicitOverride": true, 10 | "noPropertyAccessFromIndexSignature": true, 11 | "noImplicitReturns": true, 12 | "noFallthroughCasesInSwitch": true, 13 | "sourceMap": true, 14 | "declaration": false, 15 | "downlevelIteration": true, 16 | "experimentalDecorators": true, 17 | "moduleResolution": "node", 18 | "importHelpers": true, 19 | "target": "ES2022", 20 | "module": "ES2022", 21 | "useDefineForClassFields": false, 22 | "lib": [ 23 | "ES2022", 24 | "dom" 25 | ] 26 | }, 27 | "angularCompilerOptions": { 28 | "enableI18nLegacyMessageIdFormat": false, 29 | "strictInjectionParameters": true, 30 | "strictInputAccessModifiers": true, 31 | "strictTemplates": true 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /webview-ui/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "./tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "./out-tsc/spec", 6 | "types": [ 7 | "jasmine" 8 | ] 9 | }, 10 | "include": [ 11 | "src/**/*.spec.ts", 12 | "src/**/*.d.ts" 13 | ] 14 | } 15 | --------------------------------------------------------------------------------