├── .gitignore
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── __tests__
└── render
│ └── src
│ └── components
│ └── Main.unit.test.jsx
├── app
├── index.js
├── main
│ ├── io.js
│ └── notification.js
├── render
│ ├── assets
│ │ ├── delete.svg
│ │ ├── document.svg
│ │ ├── hummingbird-loading.gif
│ │ ├── open.svg
│ │ └── upload.svg
│ ├── html
│ │ └── c62a32a6b6474ba6d4c1c812edf945b9.gif
│ ├── js
│ │ ├── dom.js
│ │ └── renderer.js
│ └── src
│ │ ├── App.jsx
│ │ ├── components
│ │ ├── BundleHistory.jsx
│ │ ├── ControlPanel.jsx
│ │ ├── Dashboard
│ │ │ ├── Bar.jsx
│ │ │ ├── BundleSelector.jsx
│ │ │ ├── Chart.jsx
│ │ │ ├── Dashboard.jsx
│ │ │ ├── Files.jsx
│ │ │ ├── MFESelector.jsx
│ │ │ └── PercentBar.jsx
│ │ ├── Diagram.jsx
│ │ ├── Display.jsx
│ │ ├── Loading.jsx
│ │ ├── Main.jsx
│ │ ├── Menu.jsx
│ │ └── NavBar.jsx
│ │ ├── index.html
│ │ ├── index.js
│ │ ├── node-handling
│ │ ├── configs.js
│ │ ├── mapping.js
│ │ ├── reposition.js
│ │ └── styling.js
│ │ └── stylesheets
│ │ └── style.css
└── resources
│ └── paper.png
├── assets
└── images
│ ├── DisplayPanel.png
│ └── table-of-contents.png
├── babel.config.js
├── package.json
└── webpack.config.js
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | lerna-debug.log*
8 |
9 | # Diagnostic reports (https://nodejs.org/api/report.html)
10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
11 |
12 | # Runtime data
13 | pids
14 | *.pid
15 | *.seed
16 | *.pid.lock
17 |
18 | # Directory for instrumented libs generated by jscoverage/JSCover
19 | lib-cov
20 |
21 | # Coverage directory used by tools like istanbul
22 | coverage
23 | *.lcov
24 |
25 | # nyc test coverage
26 | .nyc_output
27 |
28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
29 | .grunt
30 |
31 | # Bower dependency directory (https://bower.io/)
32 | bower_components
33 |
34 | # node-waf configuration
35 | .lock-wscript
36 |
37 | # Compiled binary addons (https://nodejs.org/api/addons.html)
38 | build/Release
39 |
40 | # Dependency directories
41 | node_modules/
42 | jspm_packages/
43 |
44 | # TypeScript v1 declaration files
45 | typings/
46 |
47 | # TypeScript cache
48 | *.tsbuildinfo
49 |
50 | # Optional npm cache directory
51 | .npm
52 |
53 | # Optional eslint cache
54 | .eslintcache
55 |
56 | # Microbundle cache
57 | .rpt2_cache/
58 | .rts2_cache_cjs/
59 | .rts2_cache_es/
60 | .rts2_cache_umd/
61 |
62 | # Optional REPL history
63 | .node_repl_history
64 |
65 | # Output of 'npm pack'
66 | *.tgz
67 |
68 | # Yarn Integrity file
69 | .yarn-integrity
70 |
71 | # dotenv environment variables file
72 | .env
73 | .env.test
74 |
75 | # parcel-bundler cache (https://parceljs.org/)
76 | .cache
77 |
78 | # Next.js build output
79 | .next
80 |
81 | # Nuxt.js build / generate output
82 | .nuxt
83 | dist
84 |
85 | # Gatsby files
86 | .cache/
87 | # Comment in the public line in if your project uses Gatsby and *not* Next.js
88 | # https://nextjs.org/blog/next-9-1#public-directory-support
89 | # public
90 |
91 | # vuepress build output
92 | .vuepress/dist
93 |
94 | # Serverless directories
95 | .serverless/
96 |
97 | # FuseBox cache
98 | .fusebox/
99 |
100 | # DynamoDB Local files
101 | .dynamodb/
102 |
103 | # TernJS port file
104 | .tern-port
105 |
106 | # backup files
107 | *.BAK
108 |
109 | # bundles
110 | app/render/html/
111 |
112 | #ignore out files
113 | out/
114 | release/
115 |
116 | # results
117 | results.json
118 | dependency-graph.svg
119 | stats.json
120 |
121 | # lock files
122 | package-lock.json
123 | yarn.lock
124 |
125 | # electron builder
126 | electron-builder.json
127 |
128 | # project files
129 | BEV-project-files/
130 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # bev Contribution Guide
2 | Welcome to *bev*'s guide to help those interested in contributing to this excellent open source product.
3 |
4 | For fast navigation, use the table of contents icon
on the top-left corner of this document to get to a specific section of this guide more efficiently.
5 |
6 | ## Getting Started - Familiarizing Yourself With bev's Directories
7 | Before outlining current issues and conceptualized new features for this product, I would like to provide a brief run-down of the features contained within this application.
8 |
9 | ### app/main/
10 | Contains a few small files which leverage electron and others to interact directly with the user's native OS.
11 |
12 | * `io.js` contains the logic which allows a user to select or remove folders, as well as the logic to analyze the dependencies of provided folder paths.
13 | * `notification.js` houses minor logic to natively render notifications to the user's machine.
14 |
15 | ### render/assets/
16 | Contains all assets displayed in the application.
17 |
18 | ### render/html/
19 | This is the folder the application is where webpack will build to.
20 | As such, the entirety of its contents are in the gitignore.
21 |
22 | ### render/js/
23 | Content intended to communicate with the backend (app/main/) and render content from it to the frontend.
24 | Though this intent could seem in contradiction with the next section's description, bearing in mind that this is to specifically be an intermediary between front and back end features will help understand its place in this application's architecture.
25 |
26 | ### render/src/
27 | All front end rendering via React is housed here.
28 | Internal components rendered through `App.jsx` can be found in the components directory.
29 |
30 | ## Conceptualized New Features & Existing Bugs
31 | ### The Bounty Board (Bugs)
32 | * Nodes reposition when selected (position should only change on drag)
33 | * Cannot unselect a node until it has moved (selecting again, regardless of position, should unselect)
34 |
35 | ### Features
36 | * Improved Testing
37 | * CI/CD which automatically builds and deploys latest to `bev.dev` when all tests pass successfully on any merge to `main`
38 | * Improved performance by creating a bash file which pairs down what is currently done by the webpack-cli in `io.js`
39 | * Ability to delete Nodes from the dependency graph
40 | * Drag/Create edges in the dependency graph
41 | * Only for first party files (no node modules)
42 | * This should also programmatically update imports in the respective first party files the action is performed on
43 | * Create a right-hand panel which displays information on the selected Node in the Dependency Chart
44 | *
45 | * If the right-hand display panel is implemented, functionality to edit first party files directly is desirable
46 | * Implement GitHub Releases
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 course-one
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 | ## Introducing bev
2 |
3 | bev (Bird’s Eye View) is a microservice migration helper and a dependency manager. Developed under tech accelerator, OS Labs, bev is a multi-platform desktop application that allows you to analyze and visualize dependencies. bev alleviates the pain of migrating to microservices.
4 |
5 | ### Getting Started With bev
6 |
7 | We designed bev with simplicity in mind, so all you need to do is select the root directory of your microservice app and press “Analyze Dependencies.” We’ll take it from here!
8 |
9 | Click on the "Open Folders" button and select the folder(s) containing your project.
10 |
11 | 
12 |
13 | ## Features
14 | #### Dependency Visualizer
15 |
16 | bev parses through your project’s file structure to generate an interactive dependency graph. The dependency graph is a nice quick way to see how your dependencies *interflamingle*. The blue nodes represent local dependencies whereas the red nodes represent third-party dependencies. The tan nodes always represent the root directory.
17 |
18 | Click on any node to see which children components rely on it.
19 |
20 | 
21 |
22 | #### Bundle Sizer
23 |
24 | bev also finds and analyzes your bundle files. bev gives you a detailed breakdown of what’s being bundled.
25 |
26 | 
27 |
28 | Select the version history of which bundle file to view to see how the bundle has changed since updating dependencies.
29 |
30 | 
31 |
32 | ## How to Contribute
33 |
34 | If you would like to contribute, clone the repo.
35 |
36 | ```
37 | git clone https://github.com/oslabs-beta/bev.git
38 | ```
39 |
40 | ### Build Steps
41 |
42 | 1. Install dependencies by using the following command:
43 | ```
44 | npm i
45 | ```
46 | 2. To bundle all the files together, use:
47 | ```
48 | npm run build
49 | ```
50 | 3. To run the development build, use:
51 | ```
52 | npm start
53 | ```
54 | 4. To build the Electron app, use:
55 | ```
56 | npm run pack
57 | ```
58 |
59 | Electron-builder is configured to build for Mac, Windows and Linux. To configure which platform to build for, go into `package.json` and edit the scripts.
60 | ```
61 | "scripts": {
62 | "start": "electron .",
63 | "build": "cross-env NODE_ENV=development webpack",
64 | "postinstall": "electron-builder install-app-deps",
65 | "pack": "electron-builder -mwl"
66 | },
67 | ```
68 | In the `pack` script, append `m`, `w`, or `l` after the `-` to specify which platforms to build for.
69 | e.g.: To build for mac only, edit the pack script to:
70 | ```
71 | "pack": "electron-builder -m"
72 | ```
73 |
74 | #### Features we’d like to implement
75 |
76 | Please see our `CONTRIBUTING.md` for a detailed list of current bugs and conceptualized new features to implement.
77 |
78 | Of course, if you have a new feature which is not on this list, you are also welcome to submit and present it.
79 |
80 | ## Built With
81 | - Electron
82 | - React
83 | - React Router
84 | - React Testing Library
85 | - Dagre
86 | - Dependency Cruiser
87 | - Webpack CLI
88 | - React Flow
89 | - Jest
90 |
91 | ## Contributors
92 |
93 | Tu Pham | [Linkedin](https://www.linkedin.com/in/toopham/) | [Github](https://github.com/toopham)
94 |
95 | Ryan Lee | [Linkedin](https://www.linkedin.com/in/ryan-lee-dev/) | [Github](https://github.com/savoy1211)
96 |
97 | Michael Pay | [Linkedin](https://www.linkedin.com/in/michael-edward-pay/) | [Github](https://github.com/airpick)
98 |
99 | Ian Tran | [Linkedin](https://www.linkedin.com/in/ictran/) | [Github](https://github.com/eienTran)
100 |
--------------------------------------------------------------------------------
/__tests__/render/src/components/Main.unit.test.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {
3 | HashRouter,
4 | Routes,
5 | Route,
6 | Link
7 | } from 'react-router-dom';
8 | import { render, screen } from '@testing-library/react';
9 | import Main from '../../../../app/render/src/components/Main';
10 |
11 |
12 | describe('Existential Tests', () => {
13 | test('Renders a main \'container\' div', () => {
14 | render(
15 |
16 |
17 | }/>
18 |
19 |
20 | );
21 | const container = screen.getByTestId('container');
22 | expect(container).toBeInTheDocument;
23 | });
24 |
25 | test('Uploader button exists', () => {
26 | render(
27 |
28 |
29 | }/>
30 |
31 |
32 | );
33 | const uploader = screen.getByTestId('uploader-button');
34 | expect(uploader).toBeInTheDocument;
35 | });
36 |
37 | test('Folder list exists', () => {
38 | render(
39 |
40 |
41 | }/>
42 |
43 |
44 | );
45 | const folderlist = screen.getByTestId('folder-list');
46 | expect(folderlist).toBeInTheDocument;
47 | });
48 |
49 | test('Trigger event exists', () => {
50 | render(
51 |
52 |
53 | }/>
54 |
55 |
56 | );
57 | const trigger = screen.getByTestId('trigger-event');
58 | expect(trigger).toBeInTheDocument;
59 | });
60 | });
61 |
--------------------------------------------------------------------------------
/app/index.js:
--------------------------------------------------------------------------------
1 | const { app, BrowserWindow, ipcMain, dialog } = require('electron');
2 | const path = require('path');
3 | // Local dependencies
4 | const io = require('./main/io');
5 |
6 | // Open a window
7 | const openWindow = () => {
8 | const win = new BrowserWindow({
9 | width: 1000,
10 | height: 700,
11 | webPreferences: {
12 | /*
13 | By default, electron's renderer process does not have access to node.js
14 | (and file system) methods as a security measure;
15 | In order to be able to access files from the renderer process,
16 | like how we grabbed the user's folders, we need to set nodeIntegration to true;
17 | Newer builds of electron also require contextIsolation to be set to false for nodeIntegration to work;
18 | */
19 | nodeIntegration: true,
20 | devTools: true,
21 | contextIsolation: false,
22 | },
23 | });
24 |
25 | // Load `index.html` file
26 | win.loadFile(path.resolve(__dirname, 'render/html/index.html'));
27 |
28 | return win; // Return window
29 | };
30 |
31 | // When app is ready, open a window
32 | app.on('ready', () => {
33 | const win = openWindow();
34 | });
35 |
36 | // When all windows are closed, quit the app
37 | app.on('window-all-closed', () => {
38 | if (process.platform !== 'darwin') {
39 | app.quit();
40 | }
41 | });
42 |
43 | // When app activates, open a window
44 | app.on('activate', () => {
45 | if (BrowserWindow.getAllWindows().length === 0) {
46 | openWindow();
47 | }
48 | });
49 |
50 | /*
51 | The following ipcMain are analogous to controllers in express to handle "API" calls from frontend;
52 | They listen for a "route" sent from the ipcRenderer process and call the corresponding middleware from io;
53 | */
54 |
55 | // Return list of folders
56 | ipcMain.handle('app:get-folders', () => {
57 | return io.getFolders();
58 | });
59 |
60 | // Open filesystem dialog to choose files
61 | ipcMain.handle('app:on-fs-dialog-open', async (event) => {
62 | const folder = dialog.showOpenDialogSync({
63 | properties: ['openDirectory', 'multiSelections'],
64 | });
65 | if (folder) io.addFolders(folder);
66 | return io.getFolders();
67 | });
68 |
69 | // Listen to folder delete event
70 | ipcMain.handle('app:on-folder-delete', (event, folder) => {
71 | io.deleteFolder(folder.folderpath);
72 | const folders = io.getFolders();
73 | console.log('FOLDERS AFTER DELETE IN HANDLE : ', folders);
74 | return folders;
75 | });
76 |
77 | // Listen to folder open event
78 | ipcMain.on('app:on-folder-open', (event, folder) => {
79 | io.openFolder(folder.folderpath);
80 | });
81 |
82 | // Listen to analyze dependencies event
83 | ipcMain.handle('app:on-analyze', async (event, folders) => {
84 | //check for webpack in each folder, alert error to frontend
85 | let dependencyResults, bundleResults;
86 | try {
87 | dependencyResults = io.generateDependencyObject(folders);
88 | bundleResults = await io.generateBundleInfoObject(folders);
89 | } catch (err) {
90 | return { error: true, msg: err };
91 | }
92 |
93 | return {
94 | dependencyResults,
95 | bundleResults,
96 | };
97 | });
98 |
--------------------------------------------------------------------------------
/app/main/io.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const fs = require('fs-extra');
3 | const os = require('os');
4 | const open = require('open');
5 | const { cruise } = require('dependency-cruiser');
6 | const util = require('util');
7 | const exec = util.promisify(require('child_process').exec);
8 |
9 | // Local dependencies
10 | const notification = require('./notification');
11 | const appDir = path.resolve(os.homedir(), 'BEV-project-files');
12 | const folderName = 'folders.json';
13 |
14 | //Helper function
15 | const cleanStats = (stats, folder) => {
16 | const { assets, modules } = stats;
17 | let totalSize = 0;
18 | outputObj = {};
19 |
20 | // Set folder property
21 | outputObj['folder'] = folder;
22 | outputObj['date'] = new Date()
23 | .toISOString()
24 | .slice(0, 19)
25 | .replace('T', '')
26 | .replaceAll(':', '-')
27 | .replaceAll('-', '');
28 |
29 | // Fetch assets
30 | outputObj['assets'] = {};
31 | assets.forEach((asset) => {
32 | const { name, size } = asset;
33 | const type = name.split('.').pop();
34 | totalSize += size;
35 | if (outputObj['assets'].hasOwnProperty(type)) {
36 | outputObj['assets'][type].push({ name: name, size: size });
37 | } else {
38 | outputObj['assets'][type] = [{ name: name, size: size }];
39 | }
40 | });
41 |
42 | // Fetch modules
43 | outputObj['modules'] = [];
44 | modules.forEach((module) => {
45 | const { size, name } = module;
46 | outputObj['modules'].push({
47 | name: name,
48 | size: size,
49 | });
50 | });
51 |
52 | // Calculate total asset sizes
53 | outputObj['sizes'] = {};
54 | for (type in outputObj['assets']) {
55 | outputObj['sizes'][type] = 0;
56 | outputObj['assets'][type].forEach((asset) => {
57 | outputObj['sizes'][type] += asset.size;
58 | });
59 | }
60 |
61 | // Set total bundle size
62 | outputObj['sizes']['total'] = totalSize;
63 | return outputObj;
64 | };
65 |
66 | // Get list of Folders
67 | exports.getFolders = () => {
68 | // Ensure `appDir` exists
69 | fs.ensureDirSync(appDir);
70 |
71 | // If folder does not exist then create folder
72 | if (!fs.existsSync(path.resolve(appDir, folderName))) {
73 | fs.writeFileSync(path.resolve(appDir, folderName), JSON.stringify([]));
74 | }
75 |
76 | const foldersRaw = fs.readFileSync(path.resolve(appDir, folderName));
77 | const folders = JSON.parse(foldersRaw);
78 | return folders;
79 | };
80 |
81 | // Add folders to folders.json
82 | exports.addFolders = (foldersArr = []) => {
83 | // Ensure `appDir` exists, if not then create appDir --> BEV-project-files
84 | fs.ensureDirSync(appDir);
85 |
86 | // Get the json obj from folders.json
87 | const foldersRaw = fs.readFileSync(path.resolve(appDir, folderName));
88 |
89 | // Parse to turn json obj into an array of folders
90 | const folders = JSON.parse(foldersRaw);
91 |
92 | //Check for any exisiting.
93 | folders.forEach((folder) => {
94 | if (foldersArr.indexOf(folder) > -1)
95 | foldersArr.splice(foldersArr.indexOf(folder), 1);
96 | });
97 |
98 | // Write into folders.json with the foldersArr concat to the original array of folders
99 | fs.writeFileSync(
100 | path.resolve(appDir, folderName),
101 | JSON.stringify(folders.concat(foldersArr))
102 | );
103 | };
104 |
105 | // Delete folder from folders.json
106 | exports.deleteFolder = (folderpath) => {
107 | const folders = exports.getFolders();
108 | const index = folders.indexOf(folderpath);
109 |
110 | if (index != -1) folders.splice(index, 1);
111 |
112 | fs.writeFileSync(path.resolve(appDir, folderName), JSON.stringify(folders), {
113 | encoding: 'utf8',
114 | flag: 'w',
115 | });
116 | };
117 |
118 | // Open a folder
119 | exports.openFolder = (folderpath) => {
120 | console.log('INSIDE OPEN FOLDER OF PATH: ', folderpath);
121 |
122 | // Open a folder using default application
123 | if (fs.existsSync(folderpath)) {
124 | open(folderpath);
125 | }
126 | };
127 |
128 | // Configures and leverages dependency-cruiser to map dependencies
129 | exports.generateDependencyObject = (folderArr) => {
130 | const ARRAY_OF_FILES_AND_DIRS_TO_CRUISE = folderArr;
131 | const cruiseOptions = {
132 | doNotFollow: {
133 | path: 'node_modules',
134 | },
135 | reporterOptions: {
136 | dot: {
137 | theme: {
138 | graph: { rankdir: 'TD' },
139 | },
140 | },
141 | },
142 | };
143 |
144 | let json;
145 |
146 | try {
147 | const cruiseResult = cruise(
148 | ARRAY_OF_FILES_AND_DIRS_TO_CRUISE,
149 | cruiseOptions
150 | );
151 |
152 | json = cruiseResult.output;
153 |
154 | notification.resultsAdded(folderArr.length);
155 |
156 | fs.writeFile('results.json', JSON.stringify(json), 'utf8', () =>
157 | console.log('JSON generated complete')
158 | );
159 | } catch (error) {
160 | console.error(error);
161 | }
162 |
163 | return json;
164 | };
165 |
166 | exports.generateDependencyObject = (folderArr) => {
167 | const ARRAY_OF_FILES_AND_DIRS_TO_CRUISE = folderArr;
168 | const cruiseOptions = {
169 | includeOnly: ['src', 'assets', 'node_modules'],
170 | exclude: {
171 | path: ['release', 'public', 'out', 'dist', '__tests__'],
172 | },
173 | doNotFollow: {
174 | path: 'node_modules',
175 | },
176 | reporterOptions: {
177 | dot: {
178 | theme: {
179 | graph: { rankdir: 'TD' },
180 | },
181 | },
182 | },
183 | moduleSystems: ['amd', 'es6', 'tsd'],
184 | };
185 | let json;
186 | try {
187 | const cruiseResult = cruise(
188 | ARRAY_OF_FILES_AND_DIRS_TO_CRUISE,
189 | cruiseOptions
190 | );
191 |
192 | json = cruiseResult.output;
193 |
194 | notification.resultsAdded(folderArr.length);
195 |
196 | fs.writeFile('results.json', JSON.stringify(json), 'utf8', () =>
197 | console.log('JSON generated complete')
198 | );
199 | } catch (error) {
200 | console.error(error);
201 | }
202 |
203 | return json;
204 | };
205 |
206 | exports.generateBundleInfoObject = async (folders) => {
207 | // Generate stats.json
208 | let fileName;
209 | const outputBundleObjectsArray = [];
210 | for (let folder of folders) {
211 | //initialize statsArr to store history of stats for folder
212 | let statsArr = [];
213 | fileName = folder.replaceAll(':', '');
214 | fileName = fileName.split('').includes('\\')
215 | ? `stats-${fileName.replaceAll('\\', '-')}`
216 | : `stats-${fileName.replaceAll('/', '-')}`;
217 | console.log(
218 | "stats-folder.replaceAll('\\','-') :",
219 | fileName.replaceAll('\\', '-')
220 | );
221 | const filepath = path.resolve(appDir, fileName);
222 | const statspath = path.resolve(folder, 'bev-generated-stats.json');
223 |
224 | const { stdout, stderr } = await exec(
225 | `webpack --profile --json > bev-generated-stats.json`,
226 | { cwd: folder }
227 | );
228 |
229 | if (stderr) {
230 | console.log('stderr: ', stderr);
231 | } else {
232 | console.log('stdout: ', stdout);
233 | }
234 |
235 | //Read from stats file and store in outputBundleObjectsArray
236 | const rawStats = fs.readFileSync(statspath);
237 | const stats = JSON.parse(rawStats);
238 |
239 | //delete file after we are done
240 | fs.unlinkSync(statspath);
241 |
242 | //Clean up stats and retrieve only what we need
243 | const outputObj = cleanStats(stats, folder);
244 |
245 | //if stats history for the folder does not exist then create file
246 | if (!fs.existsSync(`${filepath}.json`)) {
247 | statsArr.push(outputObj);
248 | fs.writeFile(`${filepath}.json`, JSON.stringify(statsArr), 'utf8', () =>
249 | console.log('New stats file created successfully')
250 | );
251 | }
252 | //else if it already exist, then read from file, append to it the new outputObj.
253 | else {
254 | // Get the json obj from folders.json
255 | const statsRaw = fs.readFileSync(`${filepath}.json`);
256 |
257 | // Parse to turn json obj into an array of stats history
258 | statsArr.push(outputObj);
259 |
260 | //Latest stats version is located at index 0
261 | statsArr = statsArr.concat(JSON.parse(statsRaw));
262 | fs.writeFile(`${filepath}.json`, JSON.stringify(statsArr), 'utf8', () =>
263 | console.log('New stats history appended.')
264 | );
265 | }
266 |
267 | outputBundleObjectsArray.push(statsArr);
268 | }
269 |
270 | return outputBundleObjectsArray;
271 | };
272 |
273 | /*
274 | `depCruiserResults` is an Object
275 | `statsResults` is an Array of Objects
276 | */
277 | exports.modifyDependencyObject = (depCruiserResults, statsResults, folders) => {
278 | // Preprocess dirs into Arrays
279 | let onPC;
280 | if (appDir.split('').includes('\\')) onPC = true;
281 | console.log('onPC', onPC);
282 |
283 | let bevRootPath, folderPath;
284 | if (onPC) {
285 | bevRootPath = appDir.split('\\');
286 | bevRootPath = bevRootPath.slice(0, bevRootPath.length - 1);
287 | folderPath = folders[0].split('\\');
288 | } else {
289 | bevRootPath = appDir.split('/');
290 | bevRootPath = bevRootPath.slice(0, bevRootPath.length - 1);
291 | folderPath = folders[0].split('/');
292 | }
293 |
294 | let backLog = [];
295 | let lastIndex;
296 | for (let i = 0; i < bevRootPath.length; i += 1) {
297 | if (bevRootPath[i] !== folderPath[i]) {
298 | lastIndex = i;
299 | break;
300 | }
301 | }
302 | backLog = folderPath.slice(lastIndex);
303 | for (let i = 0; i < bevRootPath.length - lastIndex; i += 1) {
304 | backLog.unshift('..');
305 | }
306 | backLog = onPC ? backLog.join('\\') : backLog.join('/');
307 | console.log('backLog', backLog);
308 |
309 | // Check if backLog is the same as bevRootPath
310 | let modifyFilePath = true;
311 | if (backLog === bevRootPath.join('\\') || backLog === bevRootPath.join('/'))
312 | modifyFilePath = false;
313 |
314 | const newSources = {}; // source: ..., newSource:...
315 |
316 | // Traverse `dependencies` array in `depCruiserResults.modules`
317 | depCruiserResults.modules.map((m) => {
318 |
319 | // Add `dependencies[n].resolved` to `targetNodeNames`
320 | const source = modifyFilePath
321 | ? m.source.slice(backLog.length + 1)
322 | : m.source;
323 | console.log('source', source);
324 | m.dependencies.map((d) => {
325 | let target = modifyFilePath
326 | ? d.resolved.slice(backLog.length + 1)
327 | : d.resolved;
328 |
329 | let moduleName;
330 | if (d.dependencyTypes[0] === 'npm') moduleName = d.module;
331 |
332 | for (let i = 0; i < statsResults.length; i += 1) {
333 | // Trigger `target` update
334 | if (moduleName) {
335 | const info = statsResults[i].modules.filter(
336 | (module) =>
337 | module.hasOwnProperty('issuerName') &&
338 | module.issuerName.slice(2) === source
339 | );
340 | if (info.length > 0) {
341 | for (let j = 0; j < info.length; j += 1) {
342 | info[j].reasons.forEach((r) => {
343 | if (r.userRequest === moduleName) {
344 | newTarget = info[j].name.slice(2);
345 | newSources[target] = newTarget;
346 | target = newTarget;
347 | }
348 | });
349 | }
350 | }
351 | }
352 | }
353 |
354 | d.resolved = target;
355 | return d;
356 | });
357 |
358 | return m;
359 | });
360 |
361 | depCruiserResults.modules.map((m) => {
362 |
363 | // Add `dependencies[n].resolved` to `targetNodeNames`
364 | const source = modifyFilePath
365 | ? m.source.slice(backLog.length + 1)
366 | : m.source;
367 | m.dependencies.map((d) => {
368 | const target = d.resolved;
369 | console.log('target', target);
370 | for (let i = 0; i < statsResults.length; i += 1) {
371 | // Set `active` property
372 | const statsArrayTargetInfo = statsResults[i].modules.filter(
373 | (module) => module.name.slice(2) === target
374 | );
375 | if (statsArrayTargetInfo.length > 0) {
376 | statsArrayTargetInfo[0].reasons.forEach((r) => {
377 | const statsArraySource = r.resolvedModule.slice(2);
378 | if (!d.hasOwnProperty('active')) {
379 | if (
380 | statsArraySource === source &&
381 | r.type === 'harmony import specifier'
382 | ) {
383 | const { active } = r;
384 | d.active = active ?? false;
385 | }
386 | }
387 | });
388 | }
389 | }
390 |
391 | return d;
392 | });
393 |
394 | return m;
395 | });
396 |
397 | // Update module source names
398 | depCruiserResults.modules.map((m) => {
399 | const source = modifyFilePath
400 | ? m.source.slice(backLog.length + 1)
401 | : m.source;
402 | if (newSources.hasOwnProperty(source) && newSources[source] !== '')
403 | m.source = newSources[source];
404 | else
405 | m.source = modifyFilePath ? m.source.slice(backLog.length + 1) : m.source;
406 | return m;
407 | });
408 |
409 | // Return mutated depCruiserResults Object
410 | return depCruiserResults;
411 | };
412 |
--------------------------------------------------------------------------------
/app/main/notification.js:
--------------------------------------------------------------------------------
1 | const { Notification } = require( 'electron' );
2 |
3 | /*
4 | Using native electron methods to send user notifications about completed processes
5 | */
6 |
7 | // Display files added notification
8 | exports.foldersAdded = ( size ) => {
9 | const notif = new Notification( {
10 | title: 'Folders added',
11 | body: `${ size } folders(s) has been successfully added.`
12 | } );
13 |
14 | notif.show();
15 | };
16 |
17 | // Display files added notification
18 | exports.resultsAdded = ( size ) => {
19 | const notif = new Notification( {
20 | title: 'Analyzed Successful',
21 | body: `${ size } folders(s) has been successfully analyzed for dependencies.`
22 | } );
23 |
24 | notif.show();
25 | };
26 |
--------------------------------------------------------------------------------
/app/render/assets/delete.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/app/render/assets/document.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
45 |
--------------------------------------------------------------------------------
/app/render/assets/hummingbird-loading.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/bev/4fed75141012eb7bbe4c6bbe200133a594b6f97c/app/render/assets/hummingbird-loading.gif
--------------------------------------------------------------------------------
/app/render/assets/open.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/app/render/assets/upload.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/app/render/html/c62a32a6b6474ba6d4c1c812edf945b9.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/bev/4fed75141012eb7bbe4c6bbe200133a594b6f97c/app/render/html/c62a32a6b6474ba6d4c1c812edf945b9.gif
--------------------------------------------------------------------------------
/app/render/js/dom.js:
--------------------------------------------------------------------------------
1 | const { ipcRenderer } = require('electron');
2 |
3 | // Open folder
4 | window.openFolder = function (itemId) {
5 | // Get path of the file
6 | const itemNode = document.getElementById(itemId);
7 | const folderpath = itemNode.getAttribute('data-folderpath');
8 |
9 | // Send event to the main thread
10 | ipcRenderer.send('app:on-folder-open', { id: itemId, folderpath });
11 | };
12 |
13 | // Delete folder from folders.json
14 | window.deleteFolder = function (itemId) {
15 | // Get path of the file
16 | const itemNode = document.getElementById(itemId);
17 | const folderpath = itemNode.getAttribute('data-folderpath');
18 | const removeNode = document.getElementById(itemId);
19 | removeNode.remove();
20 |
21 | // Send event to the main thread
22 | ipcRenderer
23 | .invoke('app:on-folder-delete', { id: itemId, folderpath })
24 | .then((folders) => {
25 | const projectButton = document.getElementById('create-project');
26 | folders[0]
27 | ? (projectButton.disabled = false)
28 | : (projectButton.disabled = true);
29 | });
30 | };
31 |
32 | // Display folders
33 | exports.displayFolders = (folders = []) => {
34 | // Clear the folder list
35 | const folderListElem = document.getElementById('folderlist');
36 | folderListElem.innerHTML = '';
37 |
38 | // Repopulate the folder list w/ needed attributes
39 | folders.forEach((folder, index) => {
40 | const itemDomElem = document.createElement('div');
41 | itemDomElem.setAttribute('id', index);
42 | itemDomElem.setAttribute('class', 'app__folders__item');
43 | itemDomElem.setAttribute('data-folderpath', folder);
44 | itemDomElem.innerHTML = `${folder}
45 |
46 |
47 |
48 | `;
49 |
50 | folderListElem.appendChild(itemDomElem);
51 | });
52 |
53 | const projectButton = document.getElementById('create-project');
54 |
55 | // Logic to tone up or down the 'Analyze Dependencies' button
56 | // based on if folders have been selected.
57 | !folders[0]
58 | ? (projectButton.disabled = true)
59 | : (projectButton.disabled = false);
60 | };
61 |
62 | // Functionality for dependency analysis
63 | window.analyzeDep = function () {
64 | const folderNodes = document.getElementsByClassName('app__folders__item');
65 | const folderObjs = Array.from(folderNodes);
66 | const folders = folderObjs.map((node) =>
67 | node.getAttribute('data-folderpath')
68 | );
69 |
70 | const loadProject = document.getElementById('loading');
71 | loadProject.innerText = 'Loading...';
72 | loadProject.click();
73 |
74 | ipcRenderer.invoke( 'app:on-analyze', folders).then( results => {
75 | console.log('results', results)
76 | if (results.error) {
77 | const loadingContent = document.querySelector('#loading-content');
78 | loadingContent.parentElement.removeChild(loadingContent);
79 | const startButton = document.querySelector('#create-project');
80 | startButton.innerText = `${results.msg} \n Reload app and choose another project`;
81 | startButton.appendChild(errorText);
82 | return;
83 | }
84 | //change the value of a dom element.
85 | const startProject = document.getElementById('start-project');
86 | startProject.value = JSON.stringify(results);
87 | startProject.click();
88 | });
89 |
90 | // Navigates the user to a dependency graphy once analysis is complete
91 | ipcRenderer.invoke('app:on-analyze', folders).then((results) => {
92 | const startProject = document.getElementById('start-project');
93 | startProject.value = JSON.stringify(results);
94 | startProject.click();
95 | });
96 | };
97 |
--------------------------------------------------------------------------------
/app/render/js/renderer.js:
--------------------------------------------------------------------------------
1 | //const dragDrop = require( 'drag-drop' );
2 |
3 | document.addEventListener('DOMContentLoaded', () => {
4 | const { ipcRenderer } = require( 'electron' );
5 | const dom = require( '../js/dom.js' );
6 |
7 |
8 | // Get list of folders from the `main` process when app start
9 | ipcRenderer.invoke( 'app:get-folders' ).then( ( folders = [] ) => {
10 | dom.displayFolders( folders );
11 | } );
12 |
13 |
14 | // Handle file delete event
15 | // ipcRenderer.on( 'app:delete-file', ( event, filename ) => {
16 | // document.getElementById( filename ).remove();
17 | // } );
18 |
19 |
20 | // Add files drop listener
21 | // dragDrop( '#uploader', ( files ) => {
22 | // const _files = files.map( file => {
23 | // return {
24 | // name: file.name,
25 | // path: file.path,
26 | // };
27 | // } );
28 |
29 | // // Send file(s) add event to the `main` process
30 | // ipcRenderer.invoke( 'app:on-file-add', _files ).then( () => {
31 | // ipcRenderer.invoke( 'app:get-files' ).then( ( files = [] ) => {
32 | // dom.displayFiles( files );
33 | // } );
34 | // } );
35 | // } );
36 |
37 | // Open filesystem dialog
38 | window.openDialog = () => {
39 | ipcRenderer.invoke( 'app:on-fs-dialog-open' ).then( (folders) => {
40 | dom.displayFolders(folders);
41 | } );
42 | }
43 |
44 |
45 | //function to generate buttons
46 | const setMainButtons = () => {
47 | const uploaderButton = document.getElementById('uploader-button');
48 | if(uploaderButton){
49 | uploaderButton.addEventListener('click', (e)=> {
50 | openDialog();
51 | });
52 | }
53 |
54 | // Create an analyze button which has access to the analyzeDep function
55 | // Append it below the folder display area (see app/src/components/Main.jsx)
56 | // const analyzeButton = document.createElement('button');
57 | const analyzeButton = document.querySelector('#create-project');
58 | // analyzeButton.setAttribute('id', 'create-project');
59 | analyzeButton.setAttribute('onclick', 'analyzeDep()');
60 |
61 | // Disabled by default, but will be removed if displayFolders is invoked and populates its array
62 | analyzeButton.disabled = true;
63 | // const analyzeDiv = document.getElementById('analyze-button');
64 | // if(analyzeDiv) analyzeDiv.appendChild(analyzeButton);
65 | }
66 |
67 | setMainButtons();
68 |
69 | // identify an element to observe
70 | const elementToObserve = window.document.getElementById('app').children[0];
71 |
72 | // create a new instance of 'MutationObserver' named 'observer',
73 | // Listen for changes(mutations) in the app div which would trigger the callback to rerender
74 | observer = new MutationObserver(function(mutationsList, observer) {
75 |
76 | setMainButtons();
77 |
78 | //display folders
79 | // Get list of folders from the `main` process when app start
80 | const folderDiv = document.getElementById('folderlist');
81 | if(folderDiv){
82 | ipcRenderer.invoke( 'app:get-folders' ).then( ( folders = [] ) => {
83 | dom.displayFolders( folders );
84 | } );
85 | }
86 |
87 | });
88 |
89 | // call 'observe' on that MutationObserver instance,
90 | // passing it the element to observe, and the options object
91 | observer.observe(elementToObserve, {characterData: false, childList: true, attributes: false});
92 |
93 |
94 |
95 | });
96 |
97 |
98 |
99 |
100 |
101 |
102 |
--------------------------------------------------------------------------------
/app/render/src/App.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component, useState } from "react";
2 | import {
3 | HashRouter,
4 | Routes,
5 | Route
6 | } from 'react-router-dom';
7 | import './stylesheets/style.css';
8 | import Main from './components/Main';
9 | import Diagram from './components/Diagram';
10 | import NavBar from './components/NavBar';
11 | import ControlPanel from './components/ControlPanel';
12 | import Loading from './components/Loading';
13 | import ReactBootstrap from 'react-bootstrap';
14 | import Vue from 'vue';
15 |
16 | /*
17 | Implemented react-router based on react-router v6 which introduced braking changes
18 | */
19 | const App = () => {
20 | const [state, setState] = useState({default: true});
21 | const [bundleHistory, setBH] = useState([])
22 | const [bundleInfo, setBundleInfo] = useState([]);
23 | const [initialDiagramLoad, setInitialDiagramLoad] = useState(false);
24 | return (
25 |
26 |
27 |
28 |
29 | } />
30 | } />
31 | } />
32 |
33 |
34 |
35 | );
36 | }
37 |
38 | export default App;
--------------------------------------------------------------------------------
/app/render/src/components/BundleHistory.jsx:
--------------------------------------------------------------------------------
1 | import React, { PureComponent, useState } from 'react';
2 | import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } from 'recharts';
3 |
4 |
5 |
6 | const BundleHistory = (props) =>{
7 | const data = [];
8 | const colors = { js: '#ffadad', css: '#ffd6a5', sass: '#fdffb6', ts: '#caffbf', html: '#9bf6ff', vue: '#a0c4ff', img: '#bdb2ff', svg: '#ffc6ff', jpg: '#bdb2ff', jpeg: '#bdb2ff', png: '#bdb2ff', gif: '#bdb2ff'};
9 |
10 | //Select MFE bundle history
11 | const [MFEBundle, setMFEBundle] = useState(props.bundleHistory[0]);
12 | //get maximum length of version history.
13 | let n = 0;
14 |
15 | props.bundleHistory.forEach(bundle => {if(bundle.length > n){n = bundle.length}});
16 |
17 |
18 | for(let version of MFEBundle){
19 | const kbSizes= {};
20 | for(let key in version.sizes){
21 | kbSizes[key] = (version.sizes[key]/1024).toFixed(2);
22 | }
23 | const point = {name: version.date, ...kbSizes};
24 | console.log('POINT IS = ', point);
25 | //since bundlehistory lastest version is at index 0, when we graph we want last version to be last in array
26 | //hence we use unshift to get the lastest version to be at last index in data array
27 | //so that when we graph, history is from left to right
28 | data.unshift(point);
29 | }
30 |
31 | //Retrieve latest version of bundle which is at index 0
32 | const lastVersion = MFEBundle[0];
33 | const Lines = [];
34 | for(let key in lastVersion.sizes){
35 | if(lastVersion.sizes[key]==='total') Lines.push();
36 | else Lines.push();
37 | }
38 |
39 | //create options in the select which MFE Bundle History
40 | const options = [];
41 | props.bundleHistory.forEach( bundle =>{
42 | options.push();
43 | });
44 |
45 |
46 | const bundleHandler = (e) =>{
47 | for(let bundle of props.bundleHistory){
48 | if(bundle[0].folder === e.target.value){
49 | setMFEBundle(bundle);
50 | console.log('SET MFE HISTORY BUNDLE: ', bundle);
51 | return
52 | }
53 | }
54 | }
55 |
56 | return (
57 |
58 |
59 |
62 |
63 |
64 |
65 |
76 |
77 |
78 |
79 |
80 |
81 | {Lines}
82 |
83 |
84 |
85 |
86 | );
87 | };
88 |
89 |
90 | export default BundleHistory;
--------------------------------------------------------------------------------
/app/render/src/components/ControlPanel.jsx:
--------------------------------------------------------------------------------
1 | import React, {useState} from 'react';
2 | import Menu from './Menu';
3 | import Display from './Display';
4 | import { Link } from 'react-router-dom';
5 |
6 | const ControlPanel = (props) =>{
7 |
8 | const [tab, setTab] = useState('dashboard');
9 |
10 |
11 | return (
12 |
13 |
14 |
15 |
16 |
17 | );
18 | };
19 |
20 |
21 | export default ControlPanel;
--------------------------------------------------------------------------------
/app/render/src/components/Dashboard/Bar.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Link } from 'react-router-dom';
3 |
4 | const Bar = (props) =>{
5 |
6 | const colors = { js: '#ffadad', css: '#ffd6a5', sass: '#fdffb6', ts: '#caffbf', html: '#9bf6ff', vue: '#a0c4ff', img: '#bdb2ff', svg: '#ffc6ff', jpg: '#bdb2ff', jpeg: '#bdb2ff', png: '#bdb2ff', gif: '#bdb2ff'};
7 |
8 |
9 | return (
10 | (props.name in colors) ? (
11 |
12 | {props.name} : {Math.round(props.weightPercent*100)/100}%
13 |
14 | ) : (
15 |
16 | other : {Math.round(props.weightPercent*100)/100}%
17 |
18 | )
19 |
20 | );
21 | };
22 |
23 |
24 | export default Bar;
--------------------------------------------------------------------------------
/app/render/src/components/Dashboard/BundleSelector.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const BundleSelector = (props) =>{
4 |
5 | const options = [];
6 |
7 | props.bundles.forEach( version => {
8 | options.push();
9 | });
10 |
11 | return (
12 |
15 |
);
16 | };
17 |
18 |
19 | export default BundleSelector;
--------------------------------------------------------------------------------
/app/render/src/components/Dashboard/Chart.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import CakeChart from 'cake-chart';
3 |
4 | const Chart = (props) =>{
5 |
6 |
7 |
8 | return (
9 |
10 |
11 | );
12 | };
13 |
14 |
15 | export default Chart;
--------------------------------------------------------------------------------
/app/render/src/components/Dashboard/Dashboard.jsx:
--------------------------------------------------------------------------------
1 | import React, {useState} from 'react';
2 | import PercentBar from './PercentBar';
3 | import BundleSelector from './BundleSelector';
4 | import Files from './Files';
5 |
6 | const Dashboard = (props) =>{
7 | // props.bundleInfo is an array
8 | //const [bundle, setBundle] = useState(props.mfe);
9 | // const [weights , setWeights] = useState({JS: 400, CSS: 200, HTML: 150, IMG: 80, TOTAL: 830})
10 | //const [weights , setWeights] = useState(props.mfe.sizes)
11 | const [bundles, setBundles] = useState(['bundle-v1', 'bundle-v2', 'bundle-v3']);
12 |
13 | return (
14 |
15 |
16 |
17 |
18 |
19 | {/*
20 |
21 | Bundle Version Control
22 |
23 |
24 |
25 |
26 |
27 | Footer
28 |
29 |
*/}
30 |
31 | );
32 | };
33 |
34 |
35 | export default Dashboard;
--------------------------------------------------------------------------------
/app/render/src/components/Dashboard/Files.jsx:
--------------------------------------------------------------------------------
1 | import React, {useState} from 'react';
2 |
3 | const Files = (props) =>{
4 |
5 | const bundle = props.mfe;
6 | const totalSizePerAsset = {};
7 | const showFileDetailsObj = {};
8 | const hoverObj = {};
9 | Object.entries(bundle.assets).forEach(([k,v]) => {
10 | let totalSize = 0;
11 | v.forEach(e => totalSize += e.size)
12 | totalSizePerAsset[k] = totalSize;
13 | showFileDetailsObj[k] = false;
14 | hoverObj[k] = false;
15 | })
16 | const [showFileDetails, setShowFileDetails] = useState(showFileDetailsObj);
17 | const [hover, setHover] = useState(hoverObj);
18 |
19 | const handleClick = (e) => {
20 | console.log('e', e);
21 | const fileType = e.target.id;
22 | const newProperty = {};
23 | newProperty[fileType] = showFileDetails[fileType] === true ? false : true;
24 | setShowFileDetails({...showFileDetails, ...newProperty});
25 | console.log('showFileDetails', showFileDetails);
26 | }
27 |
28 | const fileTypeStyle = {};
29 | Object.keys(hover).forEach(fileType => {
30 | fileTypeStyle[fileType] = {
31 | display: 'flex',
32 | flexDirection: 'row',
33 | padding: '10px',
34 | borderStyle: 'ridge',
35 | justifyContent: 'space-between',
36 | backgroundColor: hover[fileType] === true ? 'aliceblue' : ''
37 | }
38 | })
39 |
40 | const handleHover = (e) => {
41 | console.log('hover before change', hover)
42 | const fileType = e.target.id;
43 | hover[fileType] = hover[fileType] ? false : true;
44 | setHover({...hover});
45 | console.log('hover after change', hover)
46 | }
47 |
48 | return (
49 | <>
50 |
51 | Bundle Assets
52 |
53 | {Object.entries(bundle.assets).map(([k,v], index) => (
54 | <>
55 | handleClick(e)}
59 | onMouseEnter={(e) => handleHover(e)}
60 | onMouseLeave={(e) => handleHover(e)}
61 | style={fileTypeStyle[k]}
62 | >
63 |
64 | {k}
65 |
66 |
67 | {totalSizePerAsset[k]} bytes
68 |
69 |
70 |
71 | {v.map((e, i) => (showFileDetails[k] ? (
) : <>>))}
81 |
82 | >
83 | ))}
84 | >
85 | )
86 | }
87 |
88 | export default Files;
--------------------------------------------------------------------------------
/app/render/src/components/Dashboard/MFESelector.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 |
4 | const MFESelector = ( props ) =>{
5 | console.log('bundle folders', props.bundleInfo.map(e => e.folder));
6 | const folders = props.bundleInfo.map(e => e.folder);
7 | const options = [];
8 | folders.forEach( folder =>{
9 | options.push();
10 | });
11 |
12 | const mfeHandler = (e) =>{
13 | console.log('SELECTED FOLDER : ', e.target.value);
14 | for(let bundle of props.bundleInfo){
15 | if(bundle.folder === e.target.value){
16 | props.setMFE(bundle);
17 | console.log('SET MFE : ', bundle);
18 | return
19 | }
20 | }
21 | console.log('CANNOT SET MFE ');
22 | return
23 | }
24 |
25 | return (
26 |
27 |
30 |
31 | );
32 | };
33 |
34 |
35 |
36 | {/* {
37 | console.log('dropdown event', e.target.value);
38 | props.setMFE(e.target.value);
39 | }} options={props.bundleInfo.map(e => e.folder)} value={'Select MFE'} placeholder="Select an option" /> */}
40 |
41 | export default MFESelector;
--------------------------------------------------------------------------------
/app/render/src/components/Dashboard/PercentBar.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Bar from './Bar';
3 | import MFESelector from './MFESelector';
4 |
5 | const PercentBar = (props) =>{
6 |
7 | const bars = [];
8 | const totalWeight = props.mfe.sizes.total;
9 | for(let key in props.mfe.sizes){
10 | if(key!='total') bars.push();
11 |
12 | }
13 |
14 | let displayMFESelector = '';
15 | if(props.bundleInfo.length > 1) displayMFESelector = ;
16 |
17 |
18 | return (
19 |
20 | {displayMFESelector}
21 |
Total size: {totalWeight} bytes
22 |
23 | {bars}
24 |
25 |
26 | );
27 | };
28 |
29 |
30 | export default PercentBar;
--------------------------------------------------------------------------------
/app/render/src/components/Diagram.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect, useCallback } from 'react';
2 | import ReactFlow, {
3 | removeElements,
4 | addEdge,
5 | MiniMap,
6 | Controls,
7 | Background,
8 | } from 'react-flow-renderer';
9 | import { LocalNodeComponent, DefaultNodeComponent } from '../node-handling/styling';
10 | import { mapToElements } from '../node-handling/reposition'
11 | // import Legend from './Legend';
12 | import dagre from 'dagre';
13 |
14 | // React flow props, independent of Diagram hooks
15 | const onLoad = (reactFlowInstance) => {
16 | console.log('flow loaded:', reactFlowInstance);
17 | reactFlowInstance.fitView();
18 | };
19 |
20 | const nodeTypes = {
21 | local: LocalNodeComponent,
22 | default: DefaultNodeComponent
23 | }
24 |
25 | // let nodesAndEdges = allNodesAndEdges;
26 | let nodesAndEdges;
27 |
28 | const Diagram = ({ resultElements, bundleInfo, initialDiagramLoad, setInitialDiagramLoad }) => {
29 | // On first load, reinitialize dagre graph
30 | if (!initialDiagramLoad) {
31 | const dagreGraph = new dagre.graphlib.Graph();
32 | dagreGraph.setDefaultEdgeLabel(() => ({}));
33 | nodesAndEdges = mapToElements(resultElements);
34 | }
35 |
36 | // Diagram hooks
37 | const [elements, setElements]= useState(!initialDiagramLoad ? [...nodesAndEdges.localNodes, ...nodesAndEdges.thirdPartyNodes, ...nodesAndEdges.edges] : []);
38 | const [clickedElement, setClickedElement] = useState({});
39 |
40 |
41 | // React Flow props, contingent on hooks
42 | const onElementsRemove = (elementsToRemove) => setElements((els) => removeElements(elementsToRemove, els));
43 | const onConnect = (params) => setElements((els) => addEdge(params, els));
44 |
45 | // Toggle edge animation on node click
46 | useEffect(() => {
47 | if (clickedElement.hasOwnProperty('id')) {
48 | setInitialDiagramLoad(true);
49 | console.log('if block triggered')
50 | const id = clickedElement.id;
51 | const newEdges = [];
52 | nodesAndEdges.edges.forEach(edge => {
53 | if (edge.source === id) edge.animated = !edge.animated;
54 | if (edge.target === id) edge.animated = !edge.animated;
55 | newEdges.push(edge);
56 | })
57 | console.log('click', clickedElement);
58 | setElements([...nodesAndEdges.localNodes, ...nodesAndEdges.thirdPartyNodes, ...newEdges]);
59 |
60 | } else {
61 | console.log('else block triggered');
62 | setElements([...nodesAndEdges.localNodes, ...nodesAndEdges.thirdPartyNodes, ...nodesAndEdges.edges]);
63 | }
64 | }, [clickedElement, resultElements])
65 |
66 | return (
67 | <>
68 |
69 |
70 | setClickedElement(emt)}
73 | onElementsRemove={onElementsRemove}
74 | onConnect={onConnect}
75 | onLoad={onLoad}
76 | snapToGrid={true}
77 | snapGrid={[15, 15]}
78 | className="react-flow-fix"
79 | nodeTypes={nodeTypes}
80 | >
81 | {/* */}
82 | {
84 | if (n.style?.background) return n.style.background;
85 | if (n.type === 'input') return '#0041d0';
86 | if (n.type === 'output') return '#ff0072';
87 | if (n.type === 'default') return '#1a192b';
88 | return '#eee';
89 | }}
90 | nodeColor={(n) => {
91 | if (n.style?.background) return n.style.background;
92 | return '#fff';
93 | }}
94 | nodeBorderRadius={2}
95 | />
96 |
97 |
98 |
99 | >
100 | );
101 | };
102 |
103 | export default Diagram;
--------------------------------------------------------------------------------
/app/render/src/components/Display.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import Dashboard from './Dashboard/Dashboard';
3 | import Diagram from './Diagram';
4 | import BundleHistory from './BundleHistory';
5 |
6 | const Display = (props) =>{
7 | console.log('props BUNDLEINFO : ', props.bundleInfo);
8 | const [mfe, setMFE] = useState(props.bundleInfo[0])
9 |
10 |
11 | let displayTab = ;
12 | if(props.tab === 'dashboard'){
13 | displayTab = ;
14 | }
15 | else if(props.tab == 'tree'){
16 | displayTab = ;
17 | }
18 | else if(props.tab === 'history'){
19 | displayTab = ;
20 | }
21 |
22 | return (
23 |
24 | {displayTab}
25 |
26 | );
27 | };
28 |
29 |
30 | export default Display;
--------------------------------------------------------------------------------
/app/render/src/components/Loading.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 | import { Link } from 'react-router-dom';
3 | import ControlPanel from "./ControlPanel";
4 | import loading from '../../assets/hummingbird-loading.gif';
5 |
6 | /*
7 | Implemented react-router based on react-router v6 which introduced braking changes
8 | */
9 | const Loading = (props) => {
10 | console.log('bundleInfo from Loading.jsx', props.bundleInfo)
11 |
12 | return (
13 | <>
14 |
15 |

16 |
Loading... This may take a while.
17 |
18 | >
19 | );
20 | }
21 |
22 |
23 | export default Loading;
--------------------------------------------------------------------------------
/app/render/src/components/Main.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import { Link } from 'react-router-dom';
3 | import Loading from './Loading';
4 |
5 | const Main = (props) =>{
6 |
7 | const [renderLoader, setRenderLoader] = useState(false);
8 | const updateState = (e) => {
9 | // Access json generated
10 | console.log('bundleResults : ', JSON.parse(e.target.value).bundleResults)
11 | props.setState(JSON.parse(e.target.value).dependencyResults);
12 | const bundleHistory = JSON.parse(e.target.value).bundleResults;
13 | props.setBH(bundleHistory);
14 | const bundleLatest = bundleHistory.map(bundle => bundle[0]);
15 | props.setBundleInfo(bundleLatest);
16 |
17 | };
18 |
19 | return (
20 | <>
21 |
22 |
23 |
24 |
25 |
(Make sure the uploaded projects contain a webpack config file in their root directories!)
26 |
27 |
28 |
29 |
30 |
31 |
32 | setRenderLoader(true)} />
33 |
34 | {updateState(e); }} />
35 |
36 |
37 | {(renderLoader) ? (
) : (<> >) }
38 |
39 | >
40 | );
41 | };
42 |
43 | export default Main;
--------------------------------------------------------------------------------
/app/render/src/components/Menu.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Link } from 'react-router-dom';
3 |
4 | const Menu = (props) =>{
5 |
6 | return (
7 |
23 | );
24 | };
25 |
26 |
27 | export default Menu;
--------------------------------------------------------------------------------
/app/render/src/components/NavBar.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Routes, Route, useNavigate } from 'react-router-dom';
3 | const NavBar = (props) =>{
4 |
5 | const navigate = useNavigate();
6 | const setDiagramLoad = () => {
7 | props.setInitialDiagramLoad(false);
8 | }
9 | return (
10 |
23 | );
24 | }
25 |
26 | export default NavBar;
--------------------------------------------------------------------------------
/app/render/src/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Bird's Eye View
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/app/render/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { render } from 'react-dom';
3 | import App from './App';
4 |
5 | render(,
6 | document.getElementById('app'),
7 | );
8 |
--------------------------------------------------------------------------------
/app/render/src/node-handling/configs.js:
--------------------------------------------------------------------------------
1 | export const handleNodeColor = (depType) => {
2 | if (depType === 'local') return 'lightblue';
3 | else if (depType === 'root') return '#FFF8DC';
4 | else return 'darkseagreen';
5 | }
6 |
7 | export const handleAnimated = (depType, active=true) => {
8 | if (active === true) return false
9 | return true;
10 | }
11 |
12 | export const handleEdgeType = (depType) => {
13 | if (depType === 'local' || depType === "root") return "straight";
14 | return 'custom';
15 | }
16 |
17 | export const handleEdgeStyle = (depType, active=true) => {
18 | if (active) {
19 | // if (depType === 'local' || depType === "root") return {'strokeWidth': 2.5, 'stroke': '#7070f5'};
20 | // return {'strokeWidth': 0.7, 'stroke': '#d37ef2'};
21 | if (depType === 'local' || depType === "root") return {'strokeWidth': 3.5, 'stroke': 'lightblue'};
22 | return {'strokeWidth': 0.5, 'stroke': 'darkseagreen'};
23 | } else {
24 | return {'strokeWidth': 3, 'stroke': '#E54B4B'};
25 | }
26 | }
--------------------------------------------------------------------------------
/app/render/src/node-handling/mapping.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { handleAnimated, handleNodeColor, handleEdgeType, handleEdgeStyle } from './configs';
3 |
4 | const refactorNodesForSharedDeps = (elementsObj) => {
5 | console.log('elementsObj', elementsObj)
6 | const {localDependencies, thirdPartyDependencies, edges} = elementsObj;
7 | const thirdPartyDuplicatesCatcher = {} // moduleName: [ids]
8 | let thirdPartiesToDeleteIds = [];
9 | let thirdPartiesToKeepIds = [];
10 |
11 | // Get third-party dependency duplicates (or more than 1)
12 | thirdPartyDependencies.forEach(dep => {
13 | const name = dep.data.label;
14 | console.log('name', name.props.children);
15 | thirdPartyDuplicatesCatcher[name.props.children] ? thirdPartyDuplicatesCatcher[name.props.children].push(dep.id) : thirdPartyDuplicatesCatcher[name.props.children] = [dep.id];
16 | })
17 | console.log('thirdPartyDuplicatesCatcher', thirdPartyDuplicatesCatcher)
18 | let tempDepsArray = Object.entries(thirdPartyDuplicatesCatcher).filter(([k,v]) => v.length > 1);
19 | const thirdPartyDuplicates = {};
20 | tempDepsArray.forEach(pair => thirdPartyDuplicates[pair[0]] = pair[1]);
21 | console.log('thirdPartyDuplicates', thirdPartyDuplicates);
22 | console.log('thirdPartyDependencies', thirdPartyDependencies);
23 |
24 | // Update edges
25 | edges.map(edge => {
26 | console.log('traversing edges', edge)
27 | const {target, source, id} = edge;
28 | let newTarget;
29 | // for (let i = 0; i < thirdPartyDependencies.length; i += 1) {
30 | // const currentDep = thirdPartyDependencies[i];
31 | // currentDep.data.label
32 | // }
33 | const temp = thirdPartyDependencies.filter( e => e.id === target);
34 | console.log('temp', temp)
35 |
36 | const currentEdgeTargetName = (temp.length > 0) ? temp[0].data.label.props.children : '';
37 |
38 | if (currentEdgeTargetName in thirdPartyDuplicates) {
39 | console.log('old edge', edge)
40 | if (target !== thirdPartyDuplicates[currentEdgeTargetName][0]) thirdPartiesToDeleteIds.push(target);
41 | newTarget = thirdPartyDuplicates[currentEdgeTargetName][0];
42 | const newEdge = {...edge, target: newTarget}
43 | console.log('newEdge', newEdge);
44 | return newEdge;
45 | }
46 | return edge;
47 |
48 | })
49 |
50 | // Delete shared deps in thirdPartyDependencies
51 | thirdPartiesToDeleteIds = [...new Set(thirdPartiesToDeleteIds)]
52 | const newThirdPartyDeps = thirdPartyDependencies.filter(obj => !thirdPartiesToDeleteIds.includes(obj.id));
53 | console.log('thirdPartyDespToDelete', thirdPartiesToDeleteIds)
54 | console.log('old thirdPartyDependencies', thirdPartyDependencies)
55 | console.log('newThirdPartyDeps', newThirdPartyDeps);
56 | return {
57 | localDependencies: localDependencies,
58 | thirdPartyDependencies: newThirdPartyDeps,
59 | edges: edges
60 | }
61 | }
62 |
63 | export const mapDepCruiserJSONToReactFlowElements = (input) => {
64 | if (input.default === true) return [];
65 | const arrayOfModules = input.modules;
66 | const nodes = [];
67 | const thirds = [];
68 | const edges = [];
69 | const sources = [];
70 | const position = {x: 0, y: 0};
71 | const modules = {}; // sourceName: moduleName;
72 |
73 | arrayOfModules.forEach(module => {
74 | const {source} = module;
75 | sources.push(source);
76 | modules[source] = {module: source, dependencyType: 'root'}
77 | })
78 |
79 | // Get edges
80 | for (let i = 0; i < arrayOfModules.length; i += 1) {
81 | arrayOfModules[i].dependencies.forEach((dep, j) => {
82 | const { resolved, moduleSystem, dependencyTypes, module, active } = dep;
83 | modules[resolved] = {module: module, dependencyType: dependencyTypes[0]};
84 | const newEdge = {
85 | id: `e${i}-${sources.indexOf(resolved)}`,
86 | source: String(i),
87 | target: String(sources.indexOf(resolved)),
88 | arrowHeadType: 'arrowclosed',
89 | // animated: handleAnimated(modules[resolved].dependencyType),
90 | animated: handleAnimated(dependencyTypes[0]),
91 | style: handleEdgeStyle(dependencyTypes[0]),
92 | // style: handleEdgeStyle(dependencyTypes[0]),
93 | // type: handleEdgeType(dependencyTypes[0]),
94 | }
95 | // if (newEdge.type === "straight") edges.push(newEdge);
96 | edges.push(newEdge);
97 | })
98 | }
99 |
100 | // Get nodes
101 | arrayOfModules.forEach((mod,i) => {
102 | const {source, dependencies} = mod;
103 | const newNode = {
104 | id: String(i),
105 | type: modules[source].dependencyType === 'local' ? 'local' : 'default',
106 | data: {
107 | label: (
108 | <>
109 | {/* {`${i} -- ${modules[source].module}`} */}
110 | {`${modules[source].module}`}
111 | >
112 | ),
113 | onChange: console.log('hello'),
114 | text: modules[source].module,
115 | dependencyType: modules[source].dependencyType
116 | },
117 | style: {background: handleNodeColor(modules[source].dependencyType), 'border-color': 'darkslategrey'},
118 | position: position,
119 | sourcePosition: modules[source].dependencyType === 'root' || modules[source].dependencyType === 'local' ? 'right' : 'left',
120 | targetPosition: modules[source].dependencyType === 'root' ? 'right' : 'left',
121 | dependencyType: modules[source].dependencyType
122 | };
123 | modules[source].dependencyType === 'root' || modules[source].dependencyType === 'local' ? nodes.push(newNode) : thirds.push(newNode);
124 | })
125 |
126 | let elementsObj = {
127 | localDependencies: nodes,
128 | thirdPartyDependencies: thirds,
129 | edges: edges
130 | }
131 |
132 | return elementsObj;
133 | }
134 |
--------------------------------------------------------------------------------
/app/render/src/node-handling/reposition.js:
--------------------------------------------------------------------------------
1 | import dagre from 'dagre';
2 | import ReactFlow, { isNode } from 'react-flow-renderer';
3 | import { mapDepCruiserJSONToReactFlowElements } from './mapping';
4 |
5 | let dagreGraph = new dagre.graphlib.Graph();
6 | dagreGraph.setDefaultEdgeLabel(() => ({}));
7 |
8 | const nodeWidth = 150;
9 | const nodeHeight = 50;
10 | let lowestLocalYPosition = 0;
11 | let maxLocalXPosition = 0;
12 |
13 | const repositionLocalNodes = (elements, direction = 'LR') => {
14 | dagreGraph.setGraph({ rankdir: direction });
15 | const localElements = [...elements.localDependencies, ...elements.edges];
16 |
17 | // Set up for dagreGraph object (setting nodes and edges)
18 | localElements.forEach((el) => {
19 | if (isNode(el)) {
20 | dagreGraph.setNode(el.id, { width: nodeWidth, height: nodeHeight });
21 | } else {
22 | dagreGraph.setEdge(el.source, el.target);
23 | }
24 | });
25 |
26 | dagre.layout(dagreGraph);
27 | const output = [];
28 | localElements.forEach((el, index) => {
29 | if (isNode(el)) {
30 | const nodeWithPosition = dagreGraph.node(el.id);
31 | el.position = {
32 | x: nodeWithPosition.x - nodeWidth / 1.5 + Math.random() / 1000,
33 | y: nodeWithPosition.y - nodeHeight / 1.5,
34 | };
35 | if (index === 0) lowestLocalYPosition = el.position.y, maxLocalXPosition = el.position.x;
36 | if (lowestLocalYPosition < el.position.y) lowestLocalYPosition = el.position.y;
37 | if (maxLocalXPosition < el.position.x) maxLocalXPosition = el.position.x;
38 | output.push(el);
39 | }
40 | });
41 | return output;
42 | };
43 |
44 | // Returning third party nodes ONLY
45 | const setThirdPartyDepPositions = (elements) => {
46 | const thirdPartyNodes = elements.thirdPartyDependencies;
47 | thirdPartyNodes.reverse();
48 | return thirdPartyNodes.map((el, index)=> {
49 | if (isNode(el)) {
50 | dagreGraph.setNode(el.id, { width: nodeWidth, height: nodeHeight });
51 | const nodeWithPosition = dagreGraph.node(el.id);
52 | el.targetPosition = 'right';
53 | el.sourcePosition = 'left';
54 | el.position = {
55 | y: nodeWithPosition.width - (nodeWidth * (index+1))/1.5 + Math.random() / 1000 + lowestLocalYPosition,
56 | x: nodeWithPosition.height - nodeHeight + maxLocalXPosition + (2*nodeHeight) + nodeWidth,
57 | };
58 | }
59 | return el;
60 | })
61 | }
62 |
63 | let allNodesAndEdges;
64 | const mapToElements = (resultElements) => {
65 | const reactFlowElements = mapDepCruiserJSONToReactFlowElements(resultElements);
66 | const nodes = repositionLocalNodes(reactFlowElements); // setting local node positions, returning local nodes
67 | const thirdPartyDepNodes = setThirdPartyDepPositions(reactFlowElements); // setting 3rd party node positions, returning 3rd party nodes
68 | return {
69 | localNodes: nodes,
70 | thirdPartyNodes: thirdPartyDepNodes,
71 | edges: reactFlowElements.edges
72 | }
73 | }
74 |
75 | export { allNodesAndEdges, mapToElements };
--------------------------------------------------------------------------------
/app/render/src/node-handling/styling.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Handle } from 'react-flow-renderer';
3 |
4 | export const LocalNodeComponent = ({data}) => {
5 | return (
6 |
16 |
17 |
{data.text}
18 |
19 |
20 | )
21 | }
22 |
23 | export const DefaultNodeComponent = ({data}) => {
24 | return (
25 |
32 | {(data.dependencyType === 'root') ?
: <>>}
33 |
{data.text}
34 | {(data.dependencyType !== 'root') ?
: <>>}
35 |
36 | )
37 | }
--------------------------------------------------------------------------------
/app/render/src/stylesheets/style.css:
--------------------------------------------------------------------------------
1 | /* @import url('https://fonts.googleapis.com/css2?family=Architects+Daughter&family=Josefin+Sans:wght@700&family=Oswald:wght@500&family=Roboto:wght@300&display=swap'); */
2 | @import url('https://fonts.googleapis.com/css2?family=Varela+Round&display=swap');
3 | /* @import url('https://fonts.googleapis.com/css2?family=Jua&display=swap'); */
4 |
5 | * {
6 | font-family: Roboto, sans-serif;
7 | font-size: 14px;
8 | box-sizing: border-box;
9 | }
10 |
11 | /* Major global changes */
12 | body {
13 | background-color: #F7EBE8;
14 | width: 100%;
15 | height: 100%;
16 | padding: 0;
17 | margin: 0;
18 | overflow: hidden;
19 | }
20 |
21 | #app {
22 | position: absolute;
23 | width: 100%;
24 | height: 100%;
25 | }
26 | ul {
27 | list-style: none;
28 | padding-left: 0px;
29 | }
30 |
31 | button {
32 | border: none;
33 | outline: none;
34 | padding: 6px 12px;
35 | font-size: 12px;
36 | text-transform: uppercase;
37 | background-color: #FFA987;
38 | color: #1E1E24;
39 | border-radius: 0.5em;
40 | overflow: hidden;
41 | cursor: pointer;
42 | margin: 2em 0.5em;
43 | opacity: 80%;
44 | }
45 |
46 | button:hover{
47 | background-color: #E54B4B;
48 | color: #F7EBE8;
49 | opacity: 100%;
50 | }
51 |
52 | /* NavBar features */
53 | nav {
54 | display: grid;
55 | grid-row: 1;
56 | grid-gap: 1em;
57 | z-index: 7;
58 | width: 100%;
59 | height: 5em;
60 | background: #1D3557;
61 | color: white;
62 | font-family: 'Roboto', sans-serif;
63 | font-weight: bold;
64 | top: 0;
65 | left: 0;
66 | margin: 0;
67 | position: absolute;
68 | }
69 |
70 | h1 {
71 | display: grid;
72 | grid-row: inherit;
73 | font-size: 3em;
74 | justify-content: center;
75 | align-items: center;
76 | /* font-family: 'Josefin Sans', sans-serif; */
77 | font-family: 'Varela Round', sans-serif;
78 | /* font-family: 'Jua', sans-serif; */
79 | color: #fff;
80 | margin: 0.2em;
81 | }
82 |
83 | /* NavBar settings */
84 | nav ul {
85 | top: 0em;
86 | position: absolute;
87 | list-style-type: none;
88 | display: flex;
89 | flex-direction: row;
90 | justify-content: flex-end;
91 | align-items: flex-end;
92 | }
93 |
94 | nav ul li {
95 | margin: 0.7em;
96 | width: 5em;
97 | height: 2em;
98 | background: #FFA987;
99 | color: #1E1E24;
100 | display: flex;
101 | justify-content: center;
102 | align-items: center;
103 | text-decoration: none;
104 | box-sizing: border-box;
105 | border-radius: 0.5em;
106 | font-family: Roboto, sans-serif;
107 | font-size: 12px;
108 | text-transform: uppercase;
109 | }
110 |
111 | nav ul li:hover{
112 | font-weight: bold;
113 | background-color: #E54B4B;
114 | color: #F7EBE8;
115 | box-sizing: border-box;
116 | cursor: pointer;
117 | }
118 |
119 | nav ul li a:active{
120 | color: rgb(254, 251, 204);
121 | text-decoration: none;
122 | }
123 |
124 | /* Class-specific styles */
125 |
126 |
127 | /* main div which all other items render within */
128 | .container {
129 | top: 5em;
130 | position: absolute;
131 | width: 100%;
132 | min-height: 100%;
133 | display: flex;
134 | flex-direction: column;
135 | align-items: center;
136 | }
137 |
138 | /* click to add folders uploader button */
139 | .uploader__icon-area {
140 | flex: 1 1 auto;
141 | display: flex;
142 | flex-direction: column;
143 | justify-content: center;
144 | align-items: center;
145 | }
146 |
147 | .uploader__icon {
148 | flex: 0 0 auto;
149 | width: 90px;
150 | }
151 |
152 | .uploader__area__text {
153 | font-size: 14px;
154 | text-transform: uppercase;
155 | letter-spacing: 0.5px;
156 | font-weight: bold;
157 | }
158 |
159 | .uploader__button-area {
160 | flex: 0 0 auto;
161 | text-align: center;
162 | }
163 |
164 | .uploader__button {
165 | border: none;
166 | outline: none;
167 | padding: 6px 12px;
168 | font-size: 12px;
169 | text-transform: uppercase;
170 | overflow: hidden;
171 | cursor: pointer;
172 | }
173 |
174 | /* folders display */
175 | .folders {
176 | flex: 0 0 auto;
177 | width: 50vw;
178 | background-color: #cde5e6;
179 | color: #1D3557;
180 | padding: 20px;
181 | overflow-x: hidden;
182 | overflow-y: auto;
183 | }
184 |
185 | .app__folders__item {
186 | display: flex;
187 | align-items: center;
188 | overflow: hidden;
189 | margin: .5em 0;
190 | padding: .2em .5em;
191 | }
192 |
193 | .app__files__item__info {
194 | flex: 1 1 auto;
195 | margin: 0 20px;
196 | overflow: hidden;
197 | }
198 |
199 | .app__files__item__info__name {
200 | overflow: hidden;
201 | white-space: nowrap;
202 | text-overflow: ellipsis;
203 | margin: 0; }
204 | .app__files__item__info__size {
205 | font-size: 12px;
206 | color: #1D3557;
207 | margin: .2em 0;
208 | }
209 |
210 | img {
211 | flex: 0 0 auto;
212 | width: 16px;
213 | margin: 0 .2em;
214 | cursor: pointer;
215 | }
216 |
217 | .app__files__item:not(:last-child) {
218 | margin-bottom: 20px;
219 | }
220 |
221 | /* diagram features */
222 | .react-flow {
223 | overflow-wrap: anywhere;
224 | }
225 |
226 | .react-flow-fix {
227 | position: unset !important;
228 | }
229 |
230 | .react-flow__renderer {
231 | top: 5em !important;
232 | }
233 |
234 | .react-flow__controls {
235 | opacity: 80%;
236 | box-shadow: 0 0 0.2em darkgrey;
237 | }
238 | .react-flow__controls:hover {
239 | opacity: 100%;
240 | }
241 |
242 | .react-flow__minimap {
243 | opacity: 80%;
244 | box-shadow: 0 0 0.2em darkgrey;
245 | border-radius: 0.5em;
246 | }
247 | .react-flow__minimap:hover {
248 | opacity: 100%;
249 | }
250 |
251 | .controls {
252 | justify-content: center;
253 | margin-top: 5em;
254 | z-index: 6;
255 | position: absolute;
256 | width: 100%;
257 | align-items: center;
258 | display: inline-flex;
259 | }
260 |
261 | .main-container {
262 | width: 100%;
263 | height: 100%;
264 | display: flex;
265 | flex-direction: column;
266 | align-items: flex-start;
267 | }
268 |
269 | .top-container {
270 | width: 100%;
271 | height: 5em;
272 | }
273 |
274 | .body-container {
275 | height: 100%;
276 | width: 100%;
277 | display: flex;
278 | flex-direction: row;
279 | align-items: flex-start;
280 | }
281 |
282 | #menu {
283 | display: flex;
284 | flex-direction: column;
285 | height: 100%;
286 | width: 200px;
287 | color:rgb(214, 214, 214);
288 | background-color: #1D3557;
289 | z-index: 5;
290 | }
291 |
292 | .control-menu {
293 | display: flex;
294 | flex-direction: column;
295 | margin-top: 35px;
296 | }
297 |
298 | ul.control-menu li {
299 | width: 100%;
300 | padding: 12px 15px;
301 | color:rgb(214, 214, 214);
302 | }
303 |
304 | ul.control-menu li:hover {
305 | background:rgba(0, 0, 0, 0.4);
306 | color: white;
307 | }
308 |
309 | .active-li {
310 | background: rgba(0, 0, 0, 0.4) !important;
311 | font-weight: bold;
312 | }
313 |
314 | .notactive-li{
315 | font-weight: none;
316 | }
317 |
318 | #display {
319 | width: 100%;
320 | height: 100%;
321 | background-color: #F7EBE8;
322 | flex-direction: column;
323 | align-items: center;
324 | }
325 |
326 |
327 | .dashboard {
328 | padding: 20px;
329 | width: 100%;
330 | height: 100%;
331 | display: grid;
332 | grid-template-columns: repeat( 5, 1fr);
333 | grid-template-rows: repeat( 5, 1fr);
334 | }
335 |
336 | .card {
337 | border: 1px solid rgb(189, 189, 189);
338 | border-radius: 3px;
339 | display: flex;
340 | flex-direction: column;
341 |
342 | }
343 |
344 | .card-header {
345 | border-radius: 3px 3px 0px 0px;
346 | height: 2em;
347 | background-color:rgb(214, 214, 214);
348 | color: black;
349 | font-size: 1.2em;
350 | padding: 5px;
351 | flex-grow: 0;
352 | }
353 |
354 | .card-body {
355 | width: 100%;
356 | height: 100%;
357 | color:rgba(0, 0, 0, 0.4);
358 | font-size: 1.2em;
359 | padding: 5px;
360 | flex-grow: 1;
361 | }
362 |
363 | .card-footer {
364 | border-radius: 0px 0px 3px 3px;
365 | height: 2em;
366 | background-color:rgb(214, 214, 214);
367 | color: gray;
368 | font-size: 1em;
369 | padding: 5px;
370 | flex-grow: 0;
371 | }
372 |
373 | .totalsize-card{
374 | grid-column: 1 / span 5;
375 | grid-row: 1 / span 1;
376 | }
377 |
378 | .percent-bar{
379 | width: 100%;
380 | height: 30px;
381 | border: 1px solid gray;
382 | display: flex;
383 | flex-direction: row;
384 | justify-content: space-between;
385 | }
386 |
387 | .bar {
388 | display: flex;
389 | height: 30px;
390 | font-size: 1.2em;
391 | font-family: 'Roboto', sans-serif;
392 | flex-direction: row;
393 | justify-content: center;
394 | overflow: hidden;
395 | }
396 |
397 | .bundle-version-div {
398 | width: 100%;
399 | padding: 10px;
400 | display: flex;
401 | flex-direction: column;
402 | align-items: center;
403 | }
404 | select {
405 | font-size: 1.5em;
406 | width: 200px;
407 | height: 40px;
408 | background-color: #e63946;
409 | color: white;
410 | }
411 |
412 | select option {
413 | font-size: 1em;
414 | background-color: #e63946;
415 | color: white;
416 | }
417 |
418 |
419 | .col-s1 {
420 | grid-column-start: 1;
421 | }
422 | .col-s2 {
423 | grid-column-start: 2;
424 | }
425 | .col-s3 {
426 | grid-column-start: 3;
427 | }
428 | .col-s4 {
429 | grid-column-start: 4;
430 | }
431 |
432 | .col-e2 {
433 | grid-column-end: 2;
434 | }
435 | .col-e3 {
436 | grid-column-end: 3;
437 | }
438 | .col-e4 {
439 | grid-column-end: 4;
440 | }
441 | .col-e5 {
442 | grid-column-end: 5;
443 | }
444 | .col-e6 {
445 | grid-column-end: 6;
446 | }
447 | .col-ee {
448 | grid-column-end: -1;
449 | }
450 |
451 | .row-s1 {
452 | grid-row-start: 1;
453 | }
454 | .row-s2 {
455 | grid-row-start: 2;
456 | }
457 | .row-s3 {
458 | grid-row-start: 3;
459 | }
460 | .row-s4 {
461 | grid-row-start: 4;
462 | }
463 |
464 | .row-e2 {
465 | grid-row-end: 2;
466 | }
467 | .row-e3 {
468 | grid-row-end: 3;
469 | }
470 | .row-e4 {
471 | grid-row-end: 4;
472 | }
473 | .row-e5 {
474 | grid-row-end: 5;
475 | }
476 |
477 | .row-ee {
478 | grid-row-end: -1;
479 | }
--------------------------------------------------------------------------------
/app/resources/paper.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/bev/4fed75141012eb7bbe4c6bbe200133a594b6f97c/app/resources/paper.png
--------------------------------------------------------------------------------
/assets/images/DisplayPanel.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/bev/4fed75141012eb7bbe4c6bbe200133a594b6f97c/assets/images/DisplayPanel.png
--------------------------------------------------------------------------------
/assets/images/table-of-contents.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/bev/4fed75141012eb7bbe4c6bbe200133a594b6f97c/assets/images/table-of-contents.png
--------------------------------------------------------------------------------
/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | presets: ['@babel/preset-env', '@babel/preset-react'],
3 | };
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "bev",
3 | "version": "1.0.0",
4 | "description": "Bird's Eye View - a microfrontend dependency management system.",
5 | "main": "app/index.js",
6 | "scripts": {
7 | "start": "electron .",
8 | "build": "cross-env NODE_ENV=development webpack",
9 | "postinstall": "electron-builder install-app-deps",
10 | "pack": "electron-builder -m",
11 | "test": "jest"
12 | },
13 | "repository": {
14 | "type": "git",
15 | "url": "git+https://github.com/oslabs-beta/bev.git"
16 | },
17 | "keywords": [],
18 | "author": "Tu Pham, Ryan Lee, Ian Tran, Michael Pay",
19 | "license": "MIT",
20 | "bugs": {
21 | "url": "https://github.com/oslabs-beta/bev"
22 | },
23 | "jest": {
24 | "testEnvironment": "jest-environment-jsdom"
25 | },
26 | "homepage": "https://github.com/oslabs-beta/bev",
27 | "devDependencies": {
28 | "@babel/core": "^7.16.0",
29 | "@babel/preset-env": "^7.16.0",
30 | "@babel/preset-react": "^7.16.0",
31 | "@testing-library/jest-dom": "^5.15.0",
32 | "@testing-library/react": "^12.1.2",
33 | "@testing-library/user-event": "^13.5.0",
34 | "babel-jest": "^27.3.1",
35 | "babel-loader": "^8.2.3",
36 | "cross-env": "^7.0.3",
37 | "css-loader": "^6.5.1",
38 | "electron": "^15.3.1",
39 | "electron-builder": "^22.13.1",
40 | "file-loader": "^6.2.0",
41 | "fs": "0.0.1-security",
42 | "html-webpack-plugin": "^5.5.0",
43 | "jest": "^27.3.1",
44 | "jest-environment-jsdom": "^27.3.1",
45 | "json-loader": "^0.5.7",
46 | "path": "^0.12.7",
47 | "react-test-renderer": "^17.0.2",
48 | "sass": "^1.43.4",
49 | "sass-loader": "^12.3.0",
50 | "style-loader": "^3.3.1"
51 | },
52 | "dependencies": {
53 | "bootstrap": "^5.1.3",
54 | "dagre": "^0.8.5",
55 | "dependency-cruiser": "^10.6.0",
56 | "drag-drop": "^6.1.0",
57 | "elkjs": "^0.7.1",
58 | "fs-extra": "^9.0.1",
59 | "open": "^7.3.0",
60 | "react": "^17.0.2",
61 | "react-bootstrap": "^2.0.2",
62 | "react-dom": "^17.0.2",
63 | "react-dropdown": "^1.9.2",
64 | "react-flow-renderer": "^9.6.11",
65 | "react-router": "^6.0.0",
66 | "react-router-dom": "^6.0.0",
67 | "react-use": "^17.3.1",
68 | "recharts": "^2.1.6",
69 | "treeify": "^1.1.0",
70 | "url-loader": "^4.1.1",
71 | "vue": "^2.6.14",
72 | "webpack": "^5.64.3",
73 | "webpack-cli": "^4.9.1"
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const webpack = require('webpack');
3 | const HtmlWebpackPlugin = require('html-webpack-plugin');
4 |
5 | module.exports = {
6 | mode: process.env.NODE_ENV,
7 | entry: './app/render/src/index.js',
8 | output: {
9 | path: path.resolve(__dirname, 'app/render/html'),
10 | filename: 'bundle.js',
11 | },
12 |
13 | plugins: [
14 | new HtmlWebpackPlugin({
15 | template: './app/render/src/index.html',
16 | }),
17 | ],
18 | module: {
19 | rules: [
20 | {
21 | test: /\.jsx?/,
22 | exclude: /node_modules/,
23 | use: {
24 | loader: 'babel-loader',
25 | options: {
26 | presets: ['@babel/preset-env', '@babel/preset-react']
27 | }
28 | },
29 | },
30 | {
31 | test: /\.(css)$/,
32 | exclude: /node_modules/,
33 | use: ['style-loader', 'css-loader'],
34 | },
35 | {
36 | test: /\.(jpe?g|png|gif|svg)$/i,
37 | loader: 'file-loader',
38 | // options: {
39 | // name: '/app/render/assets/[name].[ext]'
40 | // }
41 | },
42 | { test: /\.json$/, loader: 'json-loader' },
43 | ]
44 |
45 | },
46 | devServer: {
47 | publicPath: '/public',
48 | port: 8080,
49 | proxy: {
50 | '/api/**' : 'https://localhost:3000',
51 | },
52 | },
53 | resolve: {
54 | //Enable importing js or jsx without specifying type
55 | extensions: ['.js', '.jsx'],
56 | },
57 | };
--------------------------------------------------------------------------------