├── .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 |

3 |
4 |
5 |
6 |

7 |

8 |
9 |
10 |
11 |

12 |

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 |
52 |
53 |
54 |
55 |
56 |
57 |
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 |
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: ``,
72 | css: ``,
75 | folder: ``,
76 | html: ``,
77 | file: ``,
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 |
2 |
129 |
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 |
3 |
4 |
9 |
10 |
11 |
12 |
22 |
23 |
24 |
28 |
29 |
30 |
Router-Outlet Node
31 |
32 |
33 |
34 |
Component Node
35 |
36 |
37 |
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 |
--------------------------------------------------------------------------------