├── .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 | ![Click on the "Open Folders" button and select the folder(s) containing your project.](https://i.imgur.com/BP6liem.gif) 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 | ![Dependency Graph](https://i.imgur.com/QDyFgSs.gif) 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 | ![enter image description here](https://i.imgur.com/UDXgfVC.png) 27 | 28 | Select the version history of which bundle file to view to see how the bundle has changed since updating dependencies. 29 | 30 | ![enter image description here](https://i.imgur.com/jp8CSog.png) 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 | 5 | 6 | 7 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 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] ? (
{e.name}
{e.size} bytes
) : <>))} 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 | loading... 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 | }; --------------------------------------------------------------------------------