├── .eslintrc ├── .github └── workflows │ └── codeql-analysis.yml ├── .gitignore ├── LICENSE ├── README.md ├── __tests__ ├── 1-fileController.ts ├── 2-dockerfile.ts ├── 3-main.ts ├── data │ ├── people.csv │ ├── species.csv │ └── starwars_postgres_create.sql ├── electron-mock.ts └── sequencer.js ├── _config.yml ├── babel.config.json ├── docs ├── Jugglr Documentation.md ├── container.png ├── database.png ├── image_config.png ├── load_data.png └── project.png ├── index.html ├── jest.config.ts ├── jest └── setEnvVars.js ├── package.json ├── src ├── client │ ├── App.tsx │ ├── assets │ │ ├── Kollektif-Bold.woff │ │ ├── Kollektif-BoldItalic.woff │ │ ├── Kollektif-Italic.woff │ │ ├── Kollektif.woff │ │ ├── fonts.css │ │ └── jugglr-logo.png │ ├── components │ │ ├── AppLayout.tsx │ │ ├── DarkModeButton.tsx │ │ ├── DatabaseConfig.tsx │ │ ├── FooterButtons.tsx │ │ ├── LoadDataConfig.tsx │ │ ├── NavbarButtons.tsx │ │ ├── ProjectConfig.tsx │ │ ├── RunConfig.tsx │ │ └── StartupConfig.tsx │ ├── containers │ │ ├── BurgerIcon.tsx │ │ └── FileSearchButton.tsx │ ├── index.tsx │ ├── reducers │ │ └── envConfigSlice.ts │ ├── store.ts │ ├── themes │ │ ├── index.css │ │ └── themeFunctions.ts │ └── utility │ │ ├── dockerFunctions.ts │ │ ├── fileExplorer.ts │ │ ├── hooks.types.ts │ │ ├── postrgresFunctions.ts │ │ └── validations.ts ├── preload.ts ├── server │ ├── controllers │ │ ├── dockerController.ts │ │ ├── fileController.ts │ │ └── postgres.ts │ └── main.ts ├── types.d.ts └── types.ts ├── tsconfig.json └── webpack.config.js /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es2021": true, 5 | "node": true 6 | }, 7 | "extends": [ 8 | "eslint:recommended", 9 | "plugin:react/recommended", 10 | "plugin:@typescript-eslint/recommended" 11 | ], 12 | "parser": "@typescript-eslint/parser", 13 | "parserOptions": { 14 | "ecmaFeatures": { 15 | "jsx": true 16 | }, 17 | "ecmaVersion": "latest", 18 | "sourceType": "module" 19 | }, 20 | "plugins": [ 21 | "react", 22 | "@typescript-eslint" 23 | ], 24 | "rules": { 25 | "react/react-in-jsx-scope": "off", 26 | "@typescript-eslint/no-var-requires": "off", 27 | "@typescript-eslint/no-explicit-any": "off", 28 | "@typescript-eslint/no-unused-vars": "off" 29 | } 30 | } -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ main, dev ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ main ] 20 | schedule: 21 | - cron: '19 13 * * 5' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | permissions: 28 | actions: read 29 | contents: read 30 | security-events: write 31 | 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | language: [ 'javascript' ] 36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] 37 | # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support 38 | 39 | steps: 40 | - name: Checkout repository 41 | uses: actions/checkout@v3 42 | 43 | # Initializes the CodeQL tools for scanning. 44 | - name: Initialize CodeQL 45 | uses: github/codeql-action/init@v2 46 | with: 47 | languages: ${{ matrix.language }} 48 | # If you wish to specify custom queries, you can do so here or in a config file. 49 | # By default, queries listed here will override any specified in a config file. 50 | # Prefix the list here with "+" to use these queries and those in the config file. 51 | 52 | # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs 53 | # queries: security-extended,security-and-quality 54 | 55 | 56 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 57 | # If this step fails, then you should remove it and run the build manually (see below) 58 | - name: Autobuild 59 | uses: github/codeql-action/autobuild@v2 60 | 61 | # ℹ️ Command-line programs to run using the OS shell. 62 | # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun 63 | 64 | # If the Autobuild fails above, remove it and uncomment the following three lines. 65 | # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. 66 | 67 | # - run: | 68 | # echo "Run, Build Application using script" 69 | # ./location_of_script_within_repo/buildscript.sh 70 | 71 | - name: Perform CodeQL Analysis 72 | uses: github/codeql-action/analyze@v2 73 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | build 3 | jugglr 4 | .env 5 | .DS_Store -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 OSLabs Beta 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # Jugglr 3 | 4 | Jugglr is a tool for managing test data and running tests with a lightweight, dedicated database. Jugglr enables developers, testers, and CI/CD processes to run tests against containerized databases with data loaded at runtime. 5 | 6 | - [Authors](#authors) 7 | - [Installation](#installation) 8 | * [Clone the project](#clone-the-project) 9 | * [Go to the project directory](#go-to-the-project-directory) 10 | * [Install dependencies](#install-dependencies) 11 | * [Start the servers](#start-the-servers) 12 | * [Run tests](#run-tests) 13 | - [Documentation](#documentation) 14 | - [Running in CI/CD](#running-in-cicd) 15 | ## Authors 16 | 17 | - [@Anthony Stanislaus](https://github.com/STANISLAUSA) 18 | - [@Alvin Ma](http://github.com/ALVMA1945) 19 | - [@Miriam Feder](https://www.github.com/mirfeder) 20 | - [@S M Iftekhar Uddin](http://github.com/iuddin) 21 | 22 | 23 | ## Installation 24 | 25 | A. Download the Jugglr executable app from the project [website](https://jugglr-test.com/) to start using it right away. 26 | 27 | B. Alternatively, follow these steps: 28 | 29 | ### Clone the project 30 | 31 | ```bash 32 | git clone https://github.com/oslabs-beta/Jugglr 33 | ``` 34 | ### Go to the project directory 35 | 36 | ```bash 37 | cd Jugglr 38 | ``` 39 | 40 | ### Install dependencies 41 | 42 | ```bash 43 | npm install 44 | ``` 45 | 46 | ### Start the servers 47 | 48 | ```bash 49 | npm run build 50 | npm start 51 | ``` 52 | 53 | ### Run tests 54 | ```bash 55 | npm test 56 | ``` 57 | 58 | **Note: you must be running Docker to use Jugglr. Download Docker Desktop from [here](https://www.docker.com/get-started/).** 59 | 60 | The first time you build an image, it may take some time as Docker needs to download the Postgres image from DockerHub. After that, the Postgres image will be stored locally and image creation should take no more than a second or two. 61 | ## Documentation 62 | 63 | Detailed documentation on how to use Jugglr can be found [here](/docs/Jugglr%20Documentation.md). 64 | 65 | 66 | ## Running in CI/CD 67 | 68 | To run tests in a CI/CD process, the Docker image must be built and run in a container on the CI/CD server. 69 | You may choose to create a second Dockerfile (e.g.) Dockerfile.cli as follows: 70 | Using the baseline Dockerfile created by Jugglr (in the ``/jugglr/ directory), you 71 | can either load a sql file with all data included (i.e., a PostgreSQL dump file) 72 | or you can load data as a separate step, as described below 73 | 74 | #### First, in the Dockerfile, add a step at the end to copy the csv file(s) into the container: 75 | ```bash 76 | COPY 77 | ``` 78 | #### Build the image 79 | 80 | - note: the dot after the image name is important, it means build the image from the current directory. If you want to build image from elsewhere, specify that location relative to where command is being run from 81 | - if you name your Dockerfile a different name or put it in a different path, specify that after the -f flag. 82 | ```bash 83 | docker build -t . -f jugglr/Dockerfile //or other name you have given the Dockerfile 84 | ``` 85 | #### Run the image in a container: 86 | ```bash 87 | docker run -d \ 88 | --name \ 89 | -p :5432 \ //port number can be anything on the left of the colon. Leave the 5432 after the colon 90 | -e POSTGRES_PASSWORD= 91 | ``` 92 | #### Finally, load data from a file (keep the single and double quotes in the copy command below): 93 | 94 | ```bash 95 | docker exec -it psql -U -d -c "\copy FROM '' DELIMITER ',' CSV HEADER;" 96 | ``` 97 | ## How to Contribute: 98 | 99 | Jugglr is an open source product and we encourage other developers to make improvements to the application. If you would like to contribute to Jugglr, head over to the dev branch, fork it, pull down the code, and create your own feature branch in the format name/feature. Run 100 | ```'npm install', 'npm run build', and 'npm start' ``` 101 | to see the app at it's current state. Once you've made your changes, push it back up to your feature branch and create a pull request into the original dev branch and the team will review your changes. 102 | 103 | ## Enhancements: 104 | 105 | Ideas for enhancements: 106 | - Add support for MongoDB and other popular databases 107 | - Maintain more than one Dockerfile so users can load data to different databases for different projects without having to switch out the Dockerfile each time (Maybe consider using a .yml file) 108 | - Render Docker commands directly in the app to pass on to the CI/CD team 109 | -------------------------------------------------------------------------------- /__tests__/1-fileController.ts: -------------------------------------------------------------------------------- 1 | const fileCtrlr = require('../src/server/controllers/fileController.ts'); 2 | 3 | describe ("Gets Dockerfile Information", () => { 4 | describe('Returns Dockerfile information if Dockerfile Exists', () => { 5 | // promise resolves. See https://jestjs.io/docs/en/asynchronous 6 | it('finds and parses the Dockerfile', async () => { 7 | const response = await fileCtrlr.setProjectRoot(process.env.ROOTDIR); 8 | expect(response).toHaveProperty('user'); 9 | expect(response).toHaveProperty('database'); 10 | expect(response).toHaveProperty('password'); 11 | expect(response).toHaveProperty('schema'); 12 | expect(response.schema).toMatch (/starwars_postgres_create.sql/) 13 | }) 14 | }), 15 | describe ('No Dockerfile Exists', () => { 16 | it ('returns an empty object if no Dockerfile exists', async () => { 17 | const response = await fileCtrlr.setProjectRoot(process.env.DOCKDIR); 18 | expect(response).toBeInstanceOf(Object); 19 | expect(response).toMatchObject({}) 20 | }) 21 | }) 22 | }) -------------------------------------------------------------------------------- /__tests__/2-dockerfile.ts: -------------------------------------------------------------------------------- 1 | const { ipcMain, ipcRenderer } = require('./electron-mock') 2 | const Docker = require('dockerode'); 3 | const path = require('path'); 4 | const { createDockerfile, buildImage, run, getContainersList , getImagesList, stopContainer, startContainer, removeContainer} = require('../src/server/controllers/dockerController'); 5 | const fs = require('fs') 6 | const pg = require('../src/server/controllers/postgres'); 7 | jest.setTimeout(20000); 8 | process.env.SCHEMA = path.resolve(`${process.env.ROOTDIR}`, 'data/starwars_postgres_create.sql') 9 | let container1; 10 | 11 | describe('Docker and Postgres tests', () => { 12 | describe ("Dockerfile Create", () => { 13 | it ('overwrites a Dockerfile if one exists', async () => { 14 | const Dockerfile = { 15 | from: 'postgres:latest' , 16 | user: 'newperson' , 17 | host: 'localhost' , 18 | database: 'postgres' , 19 | password: 'postgres' , 20 | port: '5432', 21 | rootDir: process.env.ROOTDIR, 22 | schema: process.env.SCHEMA 23 | }; 24 | const dFile = path.resolve(process.env.DOCKDIR, 'Dockerfile'); 25 | const result = await createDockerfile(Dockerfile); 26 | const file = fs.readFileSync(dFile, { encoding: 'utf8'} ); 27 | expect(file).toContain('newperson') 28 | }), 29 | it ('creates a Dockerfile if none exists', async () => { 30 | const dFile = path.resolve(process.env.DOCKDIR, 'Dockerfile'); 31 | await fs.access(dFile, (err) => { if (!err) fs.unlinkSync(dFile, (err) => { 32 | if (err) console.log('error deleting Dockerfile') 33 | }) 34 | }) 35 | await new Promise(resolve => setTimeout(resolve, 1000)); 36 | const Dockerfile = { 37 | from: 'postgres:latest' , 38 | user: 'postgres' , 39 | host: 'localhost' , 40 | database: 'postgres' , 41 | password: 'postgres' , 42 | port: '5432', 43 | rootDir: process.env.ROOTDIR, 44 | schema: process.env.SCHEMA 45 | }; 46 | const result = await createDockerfile(Dockerfile); 47 | await new Promise(resolve => setTimeout(resolve, 1000)); 48 | expect(fs.existsSync(dFile)).not.toThrowError; 49 | const file = fs.readFileSync(dFile, { encoding: 'utf8'} ); 50 | expect(file).not.toContain('newperson') 51 | }) 52 | }), 53 | describe("Create Image", () => { 54 | it('returns false if build fails', async () => { 55 | const event = ipcRenderer._event; 56 | buildImage(event, 'Mistake'); 57 | let result; 58 | await ipcMain.on('buildImageResult', (_event: Event, arg: boolean|string) => { 59 | result = arg; 60 | }) 61 | await new Promise(resolve => setTimeout(resolve, 2000)); 62 | expect(result).toThrowError 63 | }), 64 | it('builds an image from the Dockerfile', async () => { 65 | const event = ipcRenderer._event; 66 | buildImage(event, 'test'); 67 | let result; 68 | await ipcMain.on('buildImageResult', (_event: Event, arg: boolean|string) => { 69 | result = arg; 70 | }) 71 | while (result === undefined) { 72 | console.log('...waiting') 73 | await new Promise(resolve => setTimeout(resolve, 1000)); 74 | } 75 | expect(result).toBe(true); 76 | }) 77 | }), 78 | describe ('run image', () => { 79 | it ('creates a container and runs it', async () => { 80 | const event = ipcRenderer._event; 81 | await run(event, "test", 'testc'); 82 | let result; 83 | await ipcMain.on('runResult', (_event: Event, arg: boolean|string) => { 84 | result = arg; 85 | expect(result).toBe(true) 86 | }) 87 | while (result === undefined) { 88 | console.log('...waiting') 89 | await new Promise(resolve => setTimeout(resolve, 1000)); 90 | } 91 | expect(result).toBe(true) 92 | }) 93 | }), 94 | describe ("Upload data", () => { 95 | describe('upload data to Postgres Table', () => { 96 | it('loads data to table successfully', async () => { 97 | const event = ipcRenderer._event; 98 | const dataDir = path.join(__dirname, 'data') 99 | await pg.uploadData(event, `species(name, classification, average_height, average_lifespan, hair_colors, skin_colors, eye_colors, language, homeworld_id)`, `${dataDir}/species.csv`, "5432") 100 | let result; 101 | await ipcMain.on('databaseResult', (_event: Event, arg: boolean|string) => { 102 | result = arg; 103 | }) 104 | await new Promise(resolve => setTimeout(resolve, 1000)); 105 | expect(result).toBe(true) 106 | }) 107 | }), 108 | describe ('returns error if information needed to connect to the DB is missing', () => { 109 | it('returns error if environment variables are not set', async() => { 110 | delete process.env.POSTGRES_DB; 111 | delete process.env.POSTGRES_USER; 112 | delete process.env.POSTGRES_PASSWORD; 113 | delete process.env.POSTGRES_PORT; 114 | const dataDir = path.join(__dirname, 'data'); 115 | const event = ipcRenderer._event; 116 | await pg.uploadData(event,'species', `${dataDir}/species.csv`) 117 | let result; 118 | await ipcMain.on('databaseResult', (_event: Event, arg: boolean|string) => { 119 | result = arg; 120 | }) 121 | await new Promise(resolve => setTimeout(resolve, 1000)); 122 | expect(result).toBe('Error: missing required database information') 123 | }) 124 | }) 125 | }), 126 | describe ('work with containers', () => { 127 | it ('gets a list of containers', async () => { 128 | //await new Promise(resolve => setTimeout(resolve, 7000)); 129 | const list = await getContainersList(true); 130 | const formatted = list.map(object => { 131 | const id = object.Id; 132 | const names = object.Names; 133 | const image = object.Image; 134 | const imageId = object.ImageID; 135 | const port = object.Ports[0].PublicPort; 136 | return { id, names, image, imageId, port } 137 | }) 138 | container1 = formatted[0].id; 139 | expect(list).toBeInstanceOf(Array) 140 | }), 141 | it ('stops a container', async () => { 142 | const event = ipcRenderer._event; 143 | await stopContainer(event, container1) 144 | let result; 145 | await ipcMain.on('stopContainerResult', (_event: Event, arg: boolean|string) => { 146 | result = arg; 147 | }) 148 | await new Promise(resolve => setTimeout(resolve, 1000)); 149 | expect(result).toBe(true) 150 | }), 151 | it ('returns a stopped container when "all" is specified', async () => { 152 | //await new Promise(resolve => setTimeout(resolve, 7000)); 153 | const list = await getContainersList( {all: true} ); 154 | const formatted = list.map(object => { 155 | const id = object.Id; 156 | const names = object.Names; 157 | const image = object.Image; 158 | const imageId = object.ImageID; 159 | const port = object.Ports[0]?.PublicPort; 160 | const state = object.State; 161 | return { id, names, image, imageId, port, state } 162 | }) 163 | expect(formatted[0].id).toEqual(container1); 164 | expect(formatted[0].state).toEqual('exited'); 165 | }), 166 | it ('does not return a stopped container when "all" is false', async () => { 167 | const list = await getContainersList(false); 168 | expect(list).toHaveLength(0); 169 | }), 170 | it ('starts a container', async () => { 171 | const event = ipcRenderer._event; 172 | await startContainer(event, container1) 173 | let result; 174 | await ipcMain.on('startContainerResult', (_event: Event, arg: boolean|string) => { 175 | result = arg; 176 | }) 177 | await new Promise(resolve => setTimeout(resolve, 1000)); 178 | expect(result).toBe(true) 179 | }), 180 | it ('does not remove a running container', async () => { 181 | const event = ipcRenderer._event; 182 | await removeContainer(event, container1) 183 | let result; 184 | await ipcMain.on('removeContainerResult', (_event: Event, arg: boolean|string) => { 185 | result = arg; 186 | }) 187 | await new Promise(resolve => setTimeout(resolve, 1000)); 188 | expect(result).toThrowError 189 | }), 190 | it ('removes a stopped container', async () => { 191 | const event = ipcRenderer._event; 192 | await stopContainer(event, container1); 193 | await new Promise(resolve => setTimeout(resolve, 1000)); 194 | await removeContainer(event, container1); 195 | let result; 196 | await ipcMain.on('removeContainerResult', (_event: Event, arg: boolean|string) => { 197 | result = arg; 198 | }) 199 | await new Promise(resolve => setTimeout(resolve, 1000)); 200 | expect(result).toBe(true); 201 | }) 202 | }) 203 | describe ('getImagesList', () => { 204 | it ('gets a list of images', async () => { 205 | const list = await getImagesList() 206 | expect(list).toBeInstanceOf(Array); 207 | }) 208 | }) 209 | }) -------------------------------------------------------------------------------- /__tests__/3-main.ts: -------------------------------------------------------------------------------- 1 | 2 | const fileController = require('../src/server/controllers/fileController.ts'); 3 | const dockerController = require('../src/server/controllers/dockerController') 4 | jest.mock('electron', () => ({ 5 | dialog: { 6 | showOpenDialog: jest.fn(()=>({filePaths:['This is a file']})), 7 | }, 8 | })); 9 | 10 | describe ("Server Routes", () => { 11 | describe('open', () => { 12 | // promise resolves. See https://jestjs.io/docs/en/asynchronous 13 | it('calls the open dialog function', async () => { 14 | const response = await fileController.openFile(); 15 | expect(response).toEqual('This is a file') 16 | }) 17 | }), 18 | describe ('getImages', () => { 19 | it ('gets a list of images', async () => { 20 | const list = await dockerController.getImagesList(); 21 | expect(list).toBeInstanceOf(Array) 22 | }) 23 | }), 24 | describe ('getContainers', () => { 25 | it ('gets a list of containers', async () => { 26 | const list = await dockerController.getContainersList(); 27 | expect(list).toBeInstanceOf(Array) 28 | }) 29 | }) 30 | }) -------------------------------------------------------------------------------- /__tests__/data/people.csv: -------------------------------------------------------------------------------- 1 | 1,Luke Skywalker,77,blond,fair,blue,19BBY,male,1,1,172 2 | 2,C-3PO,75,n/a,gold,yellow,112BBY,n/a,2,1,167 3 | 3,R2-D2,32,n/a,"white, blue",red,33BBY,n/a,2,8,96 4 | 4,Darth Vader,136,none,white,yellow,41.9BBY,male,1,1,202 5 | 5,Leia Organa,49,brown,light,brown,19BBY,female,1,2,150 6 | 6,Owen Lars,120,"brown, grey",light,blue,52BBY,male,1,1,178 7 | 7,Beru Whitesun lars,75,brown,light,blue,47BBY,female,1,1,165 8 | 8,R5-D4,32,n/a,"white, red",red,,n/a,2,1,97 9 | 9,Biggs Darklighter,84,black,light,brown,24BBY,male,1,1,183 10 | 10,Obi-Wan Kenobi,77,"auburn, white",fair,blue-gray,57BBY,male,1,20,182 11 | 11,Anakin Skywalker,84,blond,fair,blue,41.9BBY,male,1,1,188 12 | 12,Wilhuff Tarkin,,"auburn, grey",fair,blue,64BBY,male,1,21,180 13 | 13,Chewbacca,112,brown,,blue,200BBY,male,3,14,228 14 | 14,Han Solo,80,brown,fair,brown,29BBY,male,1,22,180 15 | 15,Greedo,74,n/a,green,black,44BBY,male,4,23,173 16 | 16,Jabba Desilijic Tiure,"1,358",n/a,"green-tan, brown",orange,600BBY,hermaphrodite,5,24,175 17 | 18,Wedge Antilles,77,brown,fair,hazel,21BBY,male,1,22,170 18 | 19,Jek Tono Porkins,110,brown,fair,blue,,male,1,26,180 19 | 20,Yoda,17,white,green,brown,896BBY,male,6,28,66 20 | 21,Palpatine,75,grey,pale,yellow,82BBY,male,1,8,170 21 | 22,Boba Fett,78.2,black,fair,brown,31.5BBY,male,1,10,183 22 | 23,IG-88,140,none,metal,red,15BBY,none,2,28,200 23 | 24,Bossk,113,none,green,red,53BBY,male,7,29,190 24 | 25,Lando Calrissian,79,black,dark,brown,31BBY,male,1,30,177 25 | 26,Lobot,79,none,light,blue,37BBY,male,1,6,175 26 | 27,Ackbar,83,none,brown mottle,orange,41BBY,male,8,31,180 27 | 28,Mon Mothma,,auburn,fair,blue,48BBY,female,1,32,150 28 | 29,Arvel Crynyd,,brown,fair,brown,,male,1,28, 29 | 30,Wicket Systri Warrick,20,brown,brown,brown,8BBY,male,9,7,88 30 | 31,Nien Nunb,68,none,grey,black,,male,10,33,160 31 | 32,Qui-Gon Jinn,89,brown,fair,blue,92BBY,male,1,28,193 32 | 33,Nute Gunray,90,none,mottled green,red,,male,11,18,191 33 | 34,Finis Valorum,,blond,fair,blue,91BBY,male,1,9,170 34 | 36,Jar Jar Binks,66,none,orange,orange,52BBY,male,12,8,196 35 | 37,Roos Tarpals,82,none,grey,orange,,male,12,8,224 36 | 38,Rugor Nass,,none,green,orange,,male,12,8,206 37 | 39,Ric Olié,,brown,fair,blue,,male,,8,183 38 | 40,Watto,,black,"blue, grey",yellow,,male,13,34,137 39 | 41,Sebulba,40,none,"grey, red",orange,,male,14,35,112 40 | 42,Quarsh Panaka,,black,dark,brown,62BBY,male,,8,183 41 | 43,Shmi Skywalker,,black,fair,brown,72BBY,female,1,1,163 42 | 44,Darth Maul,80,none,red,yellow,54BBY,male,22,36,175 43 | 45,Bib Fortuna,,none,pale,pink,,male,15,37,180 44 | 46,Ayla Secura,55,none,blue,hazel,48BBY,female,15,37,178 45 | 48,Dud Bolt,45,none,"blue, grey",yellow,,male,17,39,94 46 | 49,Gasgano,,none,"white, blue",black,,male,18,40,122 47 | 50,Ben Quadinaros,65,none,"grey, green, yellow",orange,,male,19,41,163 48 | 51,Mace Windu,84,none,dark,brown,72BBY,male,1,42,188 49 | 52,Ki-Adi-Mundi,82,white,pale,yellow,92BBY,male,20,43,198 50 | 53,Kit Fisto,87,none,green,black,,male,21,44,196 51 | 54,Eeth Koth,,black,brown,brown,,male,22,45,171 52 | 55,Adi Gallia,50,none,dark,blue,,female,23,9,184 53 | 56,Saesee Tiin,,none,pale,orange,,male,24,47,188 54 | 57,Yarael Poof,,none,white,yellow,,male,25,48,264 55 | 58,Plo Koon,80,none,orange,black,22BBY,male,26,49,188 56 | 59,Mas Amedda,,none,blue,blue,,male,27,50,196 57 | 60,Gregar Typho,85,black,dark,brown,,male,1,8,185 58 | 61,Cordé,,brown,light,brown,,female,1,8,157 59 | 62,Cliegg Lars,,brown,fair,blue,82BBY,male,1,1,183 60 | 63,Poggle the Lesser,80,none,green,yellow,,male,28,11,183 61 | 64,Luminara Unduli,56.2,black,yellow,blue,58BBY,female,29,51,170 62 | 65,Barriss Offee,50,black,yellow,blue,40BBY,female,29,51,166 63 | 66,Dormé,,brown,light,brown,,female,1,8,165 64 | 67,Dooku,80,white,fair,brown,102BBY,male,1,52,193 65 | 68,Bail Prestor Organa,,black,tan,brown,67BBY,male,1,2,191 66 | 69,Jango Fett,79,black,tan,brown,66BBY,male,1,53,183 67 | 70,Zam Wesell,55,blonde,"fair, green, yellow",yellow,,female,30,54,168 68 | 71,Dexter Jettster,102,none,brown,yellow,,male,31,55,198 69 | 72,Lama Su,88,none,grey,black,,male,32,10,229 70 | 73,Taun We,,none,grey,black,,female,32,10,213 71 | 74,Jocasta Nu,,white,fair,blue,,female,1,9,167 72 | 47,Ratts Tyerell,15,none,"grey, blue",,,male,16,38,79 73 | 75,R4-P17,,none,"silver, red","red, blue",,female,,28,96 74 | 76,Wat Tambor,48,none,"green, grey",,,male,33,56,193 75 | 77,San Hill,,none,grey,gold,,male,34,57,191 76 | 78,Shaak Ti,57,none,"red, blue, white",black,,female,35,58,178 77 | 79,Grievous,159,none,"brown, white","green, yellow",,male,36,59,216 78 | 80,Tarfful,136,brown,brown,blue,,male,3,14,234 79 | 81,Raymus Antilles,79,brown,light,brown,,male,1,2,188 80 | 82,Sly Moore,48,none,pale,white,,female,,60,178 81 | 83,Tion Medon,80,none,grey,black,,male,37,12,206 82 | 84,Finn,,black,dark,dark,,male,1,28, 83 | 85,Rey,,brown,light,hazel,,female,1,28, 84 | 86,Poe Dameron,,brown,light,brown,,male,1,28, 85 | 87,BB8,,none,none,black,,none,2,28, 86 | 88,Captain Phasma,,,,,,female,,28, 87 | 35,Padmé Amidala,45,brown,light,brown,46BBY,female,1,8,165 88 | -------------------------------------------------------------------------------- /__tests__/data/species.csv: -------------------------------------------------------------------------------- 1 | name,classification,average_height,average_lifespan,hair_colors,skin_colors,eye_colors,language,homeworld_id 2 | Hutt,gastropod,300,1000,n/a,"green, brown, tan","yellow, red",Huttese,24 3 | Yoda's species,mammal,66,900,"brown, white","green, yellow","brown, green, yellow",Galactic basic,28 4 | Trandoshan,reptile,200,unknown,none,"brown, green","yellow, orange",Dosh,29 5 | Mon Calamari,amphibian,160,unknown,none,"red, blue, brown, magenta",yellow,Mon Calamarian,31 6 | Ewok,mammal,100,unknown,"white, brown, black",brown,"orange, brown",Ewokese,7 7 | Sullustan,mammal,180,unknown,none,pale,black,Sullutese,33 8 | Neimodian,unknown,180,unknown,none,"grey, green","red, pink",Neimoidia,18 9 | Gungan,amphibian,190,unknown,none,"brown, green",orange,Gungan basic,8 10 | Toydarian,mammal,120,91,none,"blue, green, grey",yellow,Toydarian,34 11 | Dug,mammal,100,unknown,none,"brown, purple, grey, red","yellow, blue",Dugese,35 12 | Twi'lek,mammals,200,unknown,none,"orange, yellow, blue, green, pink, purple, tan","blue, brown, orange, pink",Twi'leki,37 13 | Aleena,reptile,80,79,none,"blue, gray",unknown,Aleena,38 14 | Vulptereen,unknown,100,unknown,none,grey,yellow,vulpterish,39 15 | Xexto,unknown,125,unknown,none,"grey, yellow, purple",black,Xextese,40 16 | Toong,unknown,200,unknown,none,"grey, green, yellow",orange,Tundan,41 17 | Cerean,mammal,200,unknown,"red, blond, black, white",pale pink,hazel,Cerean,43 18 | Nautolan,amphibian,180,70,none,"green, blue, brown, red",black,Nautila,44 19 | Zabrak,mammal,180,unknown,black,"pale, brown, red, orange, yellow","brown, orange",Zabraki,45 20 | Tholothian,mammal,unknown,unknown,unknown,dark,"blue, indigo",unknown,46 21 | Iktotchi,unknown,180,unknown,none,pink,orange,Iktotchese,47 22 | Quermian,mammal,240,86,none,white,yellow,Quermian,48 23 | Kel Dor,unknown,180,70,none,"peach, orange, red","black, silver",Kel Dor,49 24 | Chagrian,amphibian,190,unknown,none,blue,blue,Chagria,50 25 | Geonosian,insectoid,178,unknown,none,"green, brown","green, hazel",Geonosian,11 26 | Mirialan,mammal,180,unknown,"black, brown","yellow, green","blue, green, red, yellow, brown, orange",Mirialan,51 27 | Clawdite,reptilian,180,70,none,"green, yellow",yellow,Clawdite,54 28 | Besalisk,amphibian,178,75,none,brown,yellow,besalisk,55 29 | Kaminoan,amphibian,220,80,none,"grey, blue",black,Kaminoan,10 30 | Skakoan,mammal,unknown,unknown,none,"grey, green",unknown,Skakoan,56 31 | Muun,mammal,190,100,none,"grey, white",black,Muun,57 32 | Togruta,mammal,180,94,none,"red, white, orange, yellow, green, blue","red, orange, yellow, green, blue, black",Togruti,58 33 | Kaleesh,reptile,170,80,none,"brown, orange, tan",yellow,Kaleesh,59 34 | Pau'an,mammal,190,700,none,grey,black,Utapese,12 35 | Wookiee,mammal,210,400,"black, brown",gray,"blue, green, yellow, brown, golden, red",Shyriiwook,14 36 | Droid,artificial,n/a,indefinite,n/a,n/a,n/a,n/a, 37 | Human,mammal,180,120,"blonde, brown, black, red","caucasian, black, asian, hispanic","brown, blue, green, hazel, grey, amber",Galactic Basic,9 38 | Rodian,sentient,170,unknown,n/a,"green, blue",black,Galactic Basic,23 39 | -------------------------------------------------------------------------------- /__tests__/data/starwars_postgres_create.sql: -------------------------------------------------------------------------------- 1 | -- 2 | -- PostgreSQL database dump 3 | -- 4 | 5 | -- Dumped from database version 11.3 (Ubuntu 11.3-1.pgdg18.04+1) 6 | -- Dumped by pg_dump version 11.5 7 | 8 | -- Started on 2019-09-11 16:56:10 PDT 9 | --psql -U postgres -d postgres 10 | 11 | 12 | SET statement_timeout = 0; 13 | SET lock_timeout = 0; 14 | SET idle_in_transaction_session_timeout = 0; 15 | SET client_encoding = 'UTF8'; 16 | SET standard_conforming_strings = on; 17 | SELECT pg_catalog.set_config('search_path', '', false); 18 | SET check_function_bodies = false; 19 | SET xmloption = content; 20 | SET client_min_messages = warning; 21 | SET row_security = off; 22 | 23 | CREATE TABLE public.people ( 24 | "_id" serial NOT NULL, 25 | "name" varchar NOT NULL, 26 | "mass" varchar, 27 | "hair_color" varchar, 28 | "skin_color" varchar, 29 | "eye_color" varchar, 30 | "birth_year" varchar, 31 | "gender" varchar, 32 | "species_id" bigint, 33 | "homeworld_id" bigint, 34 | "height" integer, 35 | CONSTRAINT "people_pk" PRIMARY KEY ("_id") 36 | ) WITH ( 37 | OIDS=FALSE 38 | ); 39 | 40 | 41 | 42 | CREATE TABLE public.films ( 43 | "_id" serial NOT NULL, 44 | "title" varchar NOT NULL, 45 | "episode_id" integer NOT NULL, 46 | "opening_crawl" varchar NOT NULL, 47 | "director" varchar NOT NULL, 48 | "producer" varchar NOT NULL, 49 | "release_date" DATE NOT NULL, 50 | CONSTRAINT "films_pk" PRIMARY KEY ("_id") 51 | ) WITH ( 52 | OIDS=FALSE 53 | ); 54 | 55 | 56 | 57 | CREATE TABLE public.people_in_films ( 58 | "_id" serial NOT NULL, 59 | "person_id" bigint NOT NULL, 60 | "film_id" bigint NOT NULL, 61 | CONSTRAINT "people_in_films_pk" PRIMARY KEY ("_id") 62 | ) WITH ( 63 | OIDS=FALSE 64 | ); 65 | 66 | 67 | 68 | CREATE TABLE public.planets ( 69 | "_id" serial NOT NULL, 70 | "name" varchar, 71 | "rotation_period" integer, 72 | "orbital_period" integer, 73 | "diameter" integer, 74 | "climate" varchar, 75 | "gravity" varchar, 76 | "terrain" varchar, 77 | "surface_water" varchar, 78 | "population" bigint, 79 | CONSTRAINT "planets_pk" PRIMARY KEY ("_id") 80 | ) WITH ( 81 | OIDS=FALSE 82 | ); 83 | 84 | 85 | 86 | CREATE TABLE public.species ( 87 | "_id" serial NOT NULL, 88 | "name" varchar NOT NULL, 89 | "classification" varchar, 90 | "average_height" varchar, 91 | "average_lifespan" varchar, 92 | "hair_colors" varchar, 93 | "skin_colors" varchar, 94 | "eye_colors" varchar, 95 | "language" varchar, 96 | "homeworld_id" bigint, 97 | CONSTRAINT "species_pk" PRIMARY KEY ("_id") 98 | ) WITH ( 99 | OIDS=FALSE 100 | ); 101 | 102 | 103 | 104 | CREATE TABLE public.vessels ( 105 | "_id" serial NOT NULL, 106 | "name" varchar NOT NULL, 107 | "manufacturer" varchar, 108 | "model" varchar, 109 | "vessel_type" varchar NOT NULL, 110 | "vessel_class" varchar NOT NULL, 111 | "cost_in_credits" bigint, 112 | "length" varchar, 113 | "max_atmosphering_speed" varchar, 114 | "crew" integer, 115 | "passengers" integer, 116 | "cargo_capacity" varchar, 117 | "consumables" varchar, 118 | CONSTRAINT "vessels_pk" PRIMARY KEY ("_id") 119 | ) WITH ( 120 | OIDS=FALSE 121 | ); 122 | 123 | 124 | 125 | CREATE TABLE public.species_in_films ( 126 | "_id" serial NOT NULL, 127 | "film_id" bigint NOT NULL, 128 | "species_id" bigint NOT NULL, 129 | CONSTRAINT "species_in_films_pk" PRIMARY KEY ("_id") 130 | ) WITH ( 131 | OIDS=FALSE 132 | ); 133 | 134 | 135 | 136 | CREATE TABLE public.planets_in_films ( 137 | "_id" serial NOT NULL, 138 | "film_id" bigint NOT NULL, 139 | "planet_id" bigint NOT NULL, 140 | CONSTRAINT "planets_in_films_pk" PRIMARY KEY ("_id") 141 | ) WITH ( 142 | OIDS=FALSE 143 | ); 144 | 145 | 146 | 147 | CREATE TABLE public.pilots ( 148 | "_id" serial NOT NULL, 149 | "person_id" bigint NOT NULL, 150 | "vessel_id" bigint NOT NULL, 151 | CONSTRAINT "pilots_pk" PRIMARY KEY ("_id") 152 | ) WITH ( 153 | OIDS=FALSE 154 | ); 155 | 156 | 157 | 158 | CREATE TABLE public.vessels_in_films ( 159 | "_id" serial NOT NULL, 160 | "vessel_id" bigint NOT NULL, 161 | "film_id" bigint NOT NULL, 162 | CONSTRAINT "vessels_in_films_pk" PRIMARY KEY ("_id") 163 | ) WITH ( 164 | OIDS=FALSE 165 | ); 166 | 167 | 168 | 169 | CREATE TABLE public.starship_specs ( 170 | "_id" serial NOT NULL, 171 | "hyperdrive_rating" varchar, 172 | "MGLT" varchar, 173 | "vessel_id" bigint NOT NULL, 174 | CONSTRAINT "starship_specs_pk" PRIMARY KEY ("_id") 175 | ) WITH ( 176 | OIDS=FALSE 177 | ); 178 | 179 | 180 | 181 | 182 | -- 183 | -- TOC entry 4120 (class 0 OID 4163856) 184 | -- Dependencies: 225 185 | -- Data for Name: films; Type: TABLE DATA; Schema: Owner: - 186 | -- 187 | 188 | 189 | -- Completed on 2019-09-11 17:02:50 PDT 190 | 191 | -- 192 | -- PostgreSQL database dump complete 193 | -- 194 | 195 | -------------------------------------------------------------------------------- /__tests__/electron-mock.ts: -------------------------------------------------------------------------------- 1 | import createIPCMock from 'electron-mock-ipc' 2 | 3 | const mocked = createIPCMock() 4 | const ipcMain = mocked.ipcMain 5 | const ipcRenderer = mocked.ipcRenderer; 6 | export { ipcMain, ipcRenderer } 7 | -------------------------------------------------------------------------------- /__tests__/sequencer.js: -------------------------------------------------------------------------------- 1 | const Sequencer = require('@jest/test-sequencer').default; 2 | 3 | class CustomSequencer extends Sequencer { 4 | sort(tests) { 5 | const copyTests = Array.from(tests); 6 | return copyTests.sort((testA, testB) => (testA.path > testB.path ? 1 : -1)); 7 | } 8 | } 9 | 10 | module.exports = CustomSequencer; -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-cayman -------------------------------------------------------------------------------- /babel.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/preset-env", "@babel/preset-react", "@babel/preset-typescript"], 3 | "plugins": ["@babel/plugin-transform-runtime"] 4 | } 5 | -------------------------------------------------------------------------------- /docs/Jugglr Documentation.md: -------------------------------------------------------------------------------- 1 | # Jugglr 2 | Jugglr is a tool for managing test data and running tests with a lightweight, dedicated database. The database runs in a Docker container so as not to interfere with any other processes or databases. When testing is done, the data can be retained for investigating any issues that were found, then erased and replaced for the next run. 3 | 4 | 5 | Jugglr takes in information about your project root directory along with database details, then uses that information to create a Dockerfile in your project. From there, you can use Jugglr to create a Docker image, run the image in a container, and bulk load table data from a CSV file to the deployed database. When creating an image, Jugglr will use whatever sql file you supply to set up the database. Meaning, you can use a dump file to load the database schema along with data, load just the schema, or anything in between. 6 | 7 | 8 | In order to make use of Jugglr, you must also be running Docker. Docker can be installed locally by downloading Docker Desktop from https://www.docker.com/get-started/. Jugglr currently supports running a PostgreSQL database. It is recommended that you have a separate tool for any database interaction you may want to do outside of your tests. 9 | 10 | 11 | The first step in using Jugglr is to provide the root directory of the repository containing your tests. This enables Jugglr to create your Dockerfile within the project itself, so that it can be saved with your project code. 12 | 13 | ![project directory](project.png "Project Directory") 14 | 15 | The next step is to provide information for creating the database. Here you will enter the location of a sql schema file to initialize the database with, as well as the database name, superuser account name, and password for the database. Then just click the button to create a Dockerfile. If you previously created a Dockerfile with Jugglr, no need to re-enter the information. Jugglr will pre-populate it for you. You can proceed immediately to the next steps, or change any details you wish and create a new Dockerfile. 16 | ![Database Configuration](database.png "Database Configuration") 17 | 18 | Once you have a Dockerfile, you can create a Docker image. All that is required here is for you to give your image a name, Jugglr takes care of the rest. This also need only be done once, unless you want to change the database schema. For any database changes, it is recommended that you create a new image to capture the changes. 19 | 20 | NOTE: image names must be lowercase and have no spaces. Anything else will cause the image build to fail. 21 | 22 | The first time you build an image, it may take some time as Docker needs to download the Postgres image from DockerHub. After that, the Postgres image will be stored locally and image creation should take no more than a second or two. 23 | 24 | ![Create Image and Run Container](image_config.png "Create Image and Run Container") 25 | 26 | 27 | Having created the image (either for the first time or on any subsequent use), all that is left is to run the image in a container. The first time, you must enter information about the container. Any subsequent time, you will just start or stop the container (this can be done on the next tab of the app, ‘Docker Run’). You begin by selecting the image to run from. Then, give the container a name and if desired, specify a port. The port is defaulted to 5432, which is standard for the database, but you may change it if you wish (if, for example, you want to run multiple databases at once). If you do change the port, make sure your application is properly configured to use that port when connecting to the database. Note that if you want to start with fresh data, you would need to create a new container. If you want to resume testing with any prior changes, you would start an existing container. 28 | ![Start, stop, or delete container](container.png "Start, stop, or delete container") 29 | 30 | Finally, with your database now running in a container, you can load some data. You will bulk-load data from a csv file directly to your table, by supplying the path to your csv file and the table name. Jugglr takes care of the rest. 31 | ![Load data](load_data.png "Load data") 32 | 33 | When configuring database details in your application, use ‘localhost’ as the hostname/server, 5432 as the port (or whatever port you chose), and the database name, username, and password that you chose. 34 | 35 | 36 | Happy testing! -------------------------------------------------------------------------------- /docs/container.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Jugglr/19e632d5c8a95d68def9ae2c343baa1a6c25d745/docs/container.png -------------------------------------------------------------------------------- /docs/database.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Jugglr/19e632d5c8a95d68def9ae2c343baa1a6c25d745/docs/database.png -------------------------------------------------------------------------------- /docs/image_config.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Jugglr/19e632d5c8a95d68def9ae2c343baa1a6c25d745/docs/image_config.png -------------------------------------------------------------------------------- /docs/load_data.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Jugglr/19e632d5c8a95d68def9ae2c343baa1a6c25d745/docs/load_data.png -------------------------------------------------------------------------------- /docs/project.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Jugglr/19e632d5c8a95d68def9ae2c343baa1a6c25d745/docs/project.png -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | Jugglr 10 | 11 | 12 | 13 |
14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /jest.config.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * For a detailed explanation regarding each configuration property and type check, visit: 3 | * https://jestjs.io/docs/configuration 4 | */ 5 | const path = require("path") 6 | 7 | export default { 8 | // All imported modules in your tests should be mocked automatically 9 | // automock: false, 10 | 11 | // Stop running tests after `n` failures 12 | // bail: 0, 13 | 14 | // The directory where Jest should store its cached dependency information 15 | // cacheDirectory: "/private/var/folders/yh/6jdg70w963sgd0m5n8d42lf80000gn/T/jest_dx", 16 | 17 | // Automatically clear mock calls, instances and results before every test 18 | clearMocks: true, 19 | 20 | // Indicates whether the coverage information should be collected while executing the test 21 | // collectCoverage: false, 22 | 23 | // An array of glob patterns indicating a set of files for which coverage information should be collected 24 | // collectCoverageFrom: undefined, 25 | 26 | // The directory where Jest should output its coverage files 27 | // coverageDirectory: undefined, 28 | 29 | // An array of regexp pattern strings used to skip coverage collection 30 | // coveragePathIgnorePatterns: [ 31 | // "/node_modules/" 32 | // ], 33 | 34 | // Indicates which provider should be used to instrument code for coverage 35 | coverageProvider: "v8", 36 | 37 | // A list of reporter names that Jest uses when writing coverage reports 38 | // coverageReporters: [ 39 | // "json", 40 | // "text", 41 | // "lcov", 42 | // "clover" 43 | // ], 44 | 45 | // An object that configures minimum threshold enforcement for coverage results 46 | // coverageThreshold: undefined, 47 | 48 | // A path to a custom dependency extractor 49 | // dependencyExtractor: undefined, 50 | 51 | // Make calling deprecated APIs throw helpful error messages 52 | // errorOnDeprecated: false, 53 | 54 | // Force coverage collection from ignored files using an array of glob patterns 55 | // forceCoverageMatch: [], 56 | 57 | // A path to a module which exports an async function that is triggered once before all test suites 58 | // globalSetup: undefined, 59 | 60 | // A path to a module which exports an async function that is triggered once after all test suites 61 | // globalTeardown: undefined, 62 | 63 | // A set of global variables that need to be available in all test environments 64 | // globals: {}, 65 | 66 | // The maximum amount of workers used to run your tests. Can be specified as % or a number. E.g. maxWorkers: 10% will use 10% of your CPU amount + 1 as the maximum worker number. maxWorkers: 2 will use a maximum of 2 workers. 67 | // maxWorkers: "50%", 68 | 69 | // An array of directory names to be searched recursively up from the requiring module's location 70 | moduleDirectories: [ 71 | "node_modules", "", "/src" 72 | ], 73 | 74 | // An array of file extensions your modules use 75 | moduleFileExtensions: [ 76 | "js", 77 | "jsx", 78 | "ts", 79 | "tsx", 80 | "json", 81 | "node" 82 | ], 83 | verbose: true, 84 | extensionsToTreatAsEsm: ['.ts'], 85 | globals: { 86 | 'ts-jest': { 87 | //... // your other configurations here 88 | useESM: true, 89 | }, 90 | }, 91 | moduleNameMapper: { 92 | '^(\\.{1,2}/.*)\\.js$': '$1', 93 | }, 94 | testSequencer: "/__tests__/sequencer.js", 95 | // A map from regular expressions to module names or to arrays of module names that allow to stub out resources with a single module 96 | // moduleNameMapper: {}, 97 | 98 | // An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader 99 | // modulePathIgnorePatterns: [], 100 | 101 | // Activates notifications for test results 102 | // notify: false, 103 | 104 | // An enum that specifies notification mode. Requires { notify: true } 105 | // notifyMode: "failure-change", 106 | 107 | // A preset that is used as a base for Jest's configuration 108 | // preset: undefined, 109 | 110 | // Run tests from one or more projects 111 | // projects: undefined, 112 | 113 | // Use this configuration option to add custom reporters to Jest 114 | // reporters: undefined, 115 | 116 | // Automatically reset mock state before every test 117 | // resetMocks: false, 118 | 119 | // Reset the module registry before running each individual test 120 | // resetModules: false, 121 | 122 | // A path to a custom resolver 123 | // resolver: undefined, 124 | 125 | // Automatically restore mock state and implementation before every test 126 | // restoreMocks: false, 127 | 128 | // The root directory that Jest should scan for tests and modules within 129 | rootDir: path.join(__dirname), 130 | // "/Users/miriam/codesmith/Jugglr", 131 | 132 | // A list of paths to directories that Jest should use to search for files in 133 | // roots: [ 134 | // "" 135 | // ], 136 | 137 | // Allows you to use a custom runner instead of Jest's default test runner 138 | // runner: "jest-runner", 139 | 140 | // The paths to modules that run some code to configure or set up the testing environment before each test 141 | setupFiles: ["/jest/setEnvVars.js"], // 'dotenv/config' 142 | 143 | // A list of paths to modules that run some code to configure or set up the testing framework before each test 144 | // setupFilesAfterEnv: [], 145 | 146 | // The number of seconds after which a test is considered as slow and reported as such in the results. 147 | // slowTestThreshold: 5, 148 | 149 | // A list of paths to snapshot serializer modules Jest should use for snapshot testing 150 | // snapshotSerializers: [], 151 | 152 | // The test environment that will be used for testing 153 | testEnvironment: "jest-environment-jsdom", 154 | 155 | // Options that will be passed to the testEnvironment 156 | // testEnvironmentOptions: {}, 157 | 158 | // Adds a location field to test results 159 | // testLocationInResults: false, 160 | 161 | // The glob patterns Jest uses to detect test files 162 | testMatch: [ 163 | "**/__tests__/*.[jt]s?(x)", 164 | "**/?(*.)+(spec|tests).[tj]s?(x)" 165 | ], 166 | 167 | // An array of regexp pattern strings that are matched against all test paths, matched tests are skipped 168 | testPathIgnorePatterns: [ 169 | "/node_modules/", "__tests__/sequencer.js", "__tests__/electron-mock.ts" 170 | ], 171 | 172 | // The regexp pattern or array of patterns that Jest uses to detect test files 173 | //testRegex: "*.*", 174 | 175 | // This option allows the use of a custom results processor 176 | // testResultsProcessor: undefined, 177 | 178 | // This option allows use of a custom test runner 179 | // testRunner: "jest-circus/runner", 180 | 181 | // This option sets the URL for the jsdom environment. It is reflected in properties such as location.href 182 | // testURL: "http://localhost", 183 | 184 | // Setting this value to "fake" allows the use of fake timers for functions such as "setTimeout" 185 | // timers: "real", 186 | 187 | // A map from regular expressions to paths to transformers 188 | // transform: { 189 | // '^.+\\.ts?$': 'ts-jest' 190 | // } 191 | 192 | // An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation 193 | // transformIgnorePatterns: [ 194 | // "/node_modules/", 195 | // "\\.pnp\\.[^\\/]+$" 196 | // ], 197 | 198 | // An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them 199 | // unmockedModulePathPatterns: undefined, 200 | 201 | // Indicates whether each individual test should be reported during the run 202 | // verbose: undefined, 203 | 204 | // An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode 205 | // watchPathIgnorePatterns: [], 206 | 207 | // Whether to use watchman for file crawling 208 | // watchman: true, 209 | }; 210 | -------------------------------------------------------------------------------- /jest/setEnvVars.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | process.env.POSTGRES_DB = "postgres" 3 | process.env.POSTGRES_USER = "postgres" 4 | process.env.POSTGRES_PASSWORD = "postgres" 5 | process.env.POSTGRES_PORT = "5432" 6 | process.env.ROOTDIR = path.resolve(__dirname, '../__tests__/') 7 | process.env.DOCKDIR = path.resolve(__dirname, "../__tests__/jugglr") 8 | process.env.TESTIMAGE = 'test:test'; 9 | process.env.TESTSCHEMA = 'data/starwars_postgres_create.sql' 10 | process.env.TESTCONTAINER = "testcontainer" 11 | process.env.PGCOMMAND = 'postgres' -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jugglr-app", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "./build/server/main.js", 6 | "type": "commonjs", 7 | "scripts": { 8 | "tsc": "tsc", 9 | "build": "tsc && webpack -w", 10 | "clean": "rm -rf ./build", 11 | "start": "electron ./build/server/main.js", 12 | "test": "jest --config ./jest.config.ts --runInBand", 13 | "remove-all": "docker rm $(docker ps -q -a) --force; docker image rm $(docker image ls -q) --force;", 14 | "remove-cont": "docker rm $(docker ps -q -a) --force", 15 | "remove-images": "docker image rm $(docker image ls -q) --force" 16 | }, 17 | "keywords": [], 18 | "author": "", 19 | "license": "ISC", 20 | "dependencies": { 21 | "@mantine/core": "^4.2.1", 22 | "@mantine/form": "^4.2.1", 23 | "@mantine/hooks": "^4.2.1", 24 | "@mantine/next": "^4.2.1", 25 | "@mantine/notifications": "^4.2.4", 26 | "dockerode": "^3.3.1", 27 | "electron": "^18.1.0", 28 | "electron-devtools-installer": "^3.2.0", 29 | "pg": "^8.7.3", 30 | "pg-copy-streams": "^6.0.2", 31 | "react": "^18.0.0", 32 | "react-dom": "^18.1.0", 33 | "react-redux": "^8.0.1", 34 | "react-router-dom": "^6.3.0", 35 | "tabler-icons-react": "^1.47.0" 36 | }, 37 | "devDependencies": { 38 | "@babel/core": "^7.17.9", 39 | "@babel/plugin-transform-runtime": "^7.17.0", 40 | "@babel/preset-env": "^7.16.11", 41 | "@babel/preset-react": "^7.16.7", 42 | "@babel/preset-typescript": "^7.16.7", 43 | "@reduxjs/toolkit": "^1.8.1", 44 | "@testing-library/jest-dom": "^5.16.4", 45 | "@testing-library/react": "^13.2.0", 46 | "@testing-library/user-event": "^14.1.1", 47 | "@types/dockerode": "^3.3.8", 48 | "@types/jest": "^27.5.0", 49 | "@types/node": "^17.0.29", 50 | "@types/pg": "^8.6.5", 51 | "@types/pg-copy-streams": "^1.2.1", 52 | "@types/react": "^18.0.8", 53 | "@types/react-dom": "^18.0.0", 54 | "@types/react-redux": "^7.1.24", 55 | "@typescript-eslint/eslint-plugin": "^5.21.0", 56 | "@typescript-eslint/parser": "^5.21.0", 57 | "babel-loader": "^8.2.5", 58 | "css-loader": "^6.7.1", 59 | "electron-mock-ipc": "^0.3.12", 60 | "electron-reloader": "^1.2.3", 61 | "eslint": "^8.14.0", 62 | "eslint-plugin-react": "^7.29.4", 63 | "html-webpack-plugin": "^5.5.0", 64 | "jest": "^28.0.3", 65 | "jest-environment-jsdom": "^28.1.0", 66 | "jest-webpack-resolver": "^0.3.0", 67 | "prop-types": "^15.8.1", 68 | "sass": "^1.51.0", 69 | "sass-loader": "^12.6.0", 70 | "source-map-support": "^0.5.21", 71 | "style-loader": "^3.3.1", 72 | "ts-jest": "^28.0.1", 73 | "ts-loader": "^9.2.9", 74 | "ts-node": "^10.7.0", 75 | "typescript": "^4.6.4", 76 | "webpack": "^5.72.0", 77 | "webpack-cli": "^4.9.2" 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/client/App.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | ColorScheme, 3 | ColorSchemeProvider, 4 | MantineProvider 5 | } from "@mantine/core"; 6 | import { useLocalStorage } from "@mantine/hooks"; 7 | import { NotificationsProvider } from "@mantine/notifications"; 8 | 9 | import AppLayout from "./components/AppLayout"; 10 | 11 | 12 | const App = () => { 13 | const [colorScheme, setColorScheme] = useLocalStorage({ 14 | key: "mantine-color-scheme", 15 | defaultValue: "dark", 16 | getInitialValueInEffect: true 17 | }); 18 | 19 | const toggleColorScheme = (value?: ColorScheme) => 20 | setColorScheme(value || (colorScheme === "dark" ? "light" : "dark")); 21 | 22 | return ( 23 |
24 | 28 | 35 | 36 | 37 | 38 | 39 | 40 |
41 | ); 42 | }; 43 | 44 | export default App; 45 | -------------------------------------------------------------------------------- /src/client/assets/Kollektif-Bold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Jugglr/19e632d5c8a95d68def9ae2c343baa1a6c25d745/src/client/assets/Kollektif-Bold.woff -------------------------------------------------------------------------------- /src/client/assets/Kollektif-BoldItalic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Jugglr/19e632d5c8a95d68def9ae2c343baa1a6c25d745/src/client/assets/Kollektif-BoldItalic.woff -------------------------------------------------------------------------------- /src/client/assets/Kollektif-Italic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Jugglr/19e632d5c8a95d68def9ae2c343baa1a6c25d745/src/client/assets/Kollektif-Italic.woff -------------------------------------------------------------------------------- /src/client/assets/Kollektif.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Jugglr/19e632d5c8a95d68def9ae2c343baa1a6c25d745/src/client/assets/Kollektif.woff -------------------------------------------------------------------------------- /src/client/assets/fonts.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'Kollektif Regular'; 3 | font-style: normal; 4 | font-weight: normal; 5 | src: local('Kollektif Regular'), url('Kollektif.woff') format('woff'); 6 | } 7 | 8 | 9 | @font-face { 10 | font-family: 'Kollektif Italic'; 11 | font-style: normal; 12 | font-weight: normal; 13 | src: local('Kollektif Italic'), url('Kollektif-Italic.woff') format('woff'); 14 | } 15 | 16 | 17 | @font-face { 18 | font-family: 'Kollektif Bold'; 19 | font-style: normal; 20 | font-weight: normal; 21 | src: local('Kollektif Bold'), url('Kollektif-Bold.woff') format('woff'); 22 | } 23 | 24 | 25 | @font-face { 26 | font-family: 'Kollektif Bold Italic'; 27 | font-style: normal; 28 | font-weight: normal; 29 | src: local('Kollektif Bold Italic'), url('Kollektif-BoldItalic.woff') format('woff'); 30 | } -------------------------------------------------------------------------------- /src/client/assets/jugglr-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Jugglr/19e632d5c8a95d68def9ae2c343baa1a6c25d745/src/client/assets/jugglr-logo.png -------------------------------------------------------------------------------- /src/client/components/AppLayout.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Stateful component uses Mantine AppShell 3 | * AppShell takes props header, navbar, footer, aside for ease of layout 4 | * ref: https://mantine.dev/core/app-shell/ 5 | */ 6 | import { useEffect, useState } from "react"; 7 | import { Route, Routes, useNavigate } from "react-router-dom"; 8 | import { useMediaQuery, usePagination } from "@mantine/hooks"; 9 | import { 10 | AppShell, 11 | Navbar, 12 | Header, 13 | Container, 14 | useMantineTheme, 15 | Title, 16 | Image, 17 | Paper, 18 | Footer, 19 | Button 20 | } from "@mantine/core"; 21 | import { Help } from 'tabler-icons-react' 22 | import DarkModeButton from "./DarkModeButton"; 23 | import StartupConfig from "./StartupConfig"; 24 | import NavbarButtons from "./NavbarButtons"; 25 | import BurgerIcon from "../containers/BurgerIcon"; 26 | import ProjectConfig from "./ProjectConfig"; 27 | import DatabaseConfig from "./DatabaseConfig"; 28 | import ContainerConfig from "./RunConfig"; 29 | import LoadDataConfig from "./LoadDataConfig"; 30 | import FooterButtons from "./FooterButtons"; 31 | 32 | 33 | const AppLayout = () => { 34 | const [opened, setOpened] = useState(false); 35 | const theme = useMantineTheme(); 36 | const isSmallView = useMediaQuery("(min-width: 993px"); 37 | const page = usePagination({ total: 5, initialPage: 1 }); 38 | const navigate = useNavigate(); 39 | const endpoints = { 40 | 1: "/", 41 | 2: "/database", 42 | 3: "/startup", 43 | 4: "/run", 44 | 5: "/loadData" 45 | }; 46 | 47 | useEffect(() => { 48 | navigate(endpoints[page.active]); 49 | }, [page.active]); 50 | 51 | return ( 52 | 156 | ); 157 | }; 158 | 159 | export default AppLayout; 160 | -------------------------------------------------------------------------------- /src/client/components/DarkModeButton.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * ref: https://mantine.dev/theming/dark-theme/ 3 | */ 4 | import { ActionIcon, useMantineColorScheme } from "@mantine/core"; 5 | import { useHotkeys } from "@mantine/hooks"; 6 | import { MoonOff, MoonStars } from "tabler-icons-react"; 7 | 8 | const DarkModeButton = () => { 9 | const { colorScheme, toggleColorScheme } = useMantineColorScheme(); 10 | useHotkeys([["mod+J", () => toggleColorScheme()]]); 11 | const dark = colorScheme === "dark"; 12 | 13 | return ( 14 | toggleColorScheme()} 18 | title="Toggle color scheme" 19 | > 20 | {dark ? : } 21 | 22 | ); 23 | }; 24 | 25 | export default DarkModeButton; 26 | -------------------------------------------------------------------------------- /src/client/components/DatabaseConfig.tsx: -------------------------------------------------------------------------------- 1 | import PropTypes from "prop-types"; 2 | import { 3 | Box, 4 | Space, 5 | Paper, 6 | Title, 7 | TextInput, 8 | Grid, 9 | PasswordInput, 10 | Group, 11 | Button, 12 | Tooltip 13 | } from "@mantine/core"; 14 | import { useForm } from "@mantine/hooks"; 15 | import { showNotification } from "@mantine/notifications"; 16 | import { EyeOff, EyeCheck } from "tabler-icons-react"; 17 | 18 | import FileSearchButton from "../containers/FileSearchButton"; 19 | import { setEnvConfig } from "../reducers/envConfigSlice"; 20 | import { selectFile, setDockerFile } from "../utility/fileExplorer"; 21 | import { useAppDispatch, useAppSelector } from "../utility/hooks.types"; 22 | import { dockerReadyValidation } from "../utility/validations"; 23 | 24 | 25 | const DatabaseConfig = ({ navigate }) => { 26 | const reduxState = useAppSelector(state => state.envConfig); 27 | const dispatch = useAppDispatch(); 28 | /** 29 | * shape does match DockerFile type 30 | * remove rootDir, modify DockerFile type 31 | * ProjectConfig is setting rootDir 32 | */ 33 | const form = useForm({ 34 | initialValues: { 35 | user: reduxState.user, 36 | database: reduxState.database, 37 | password: reduxState.password, 38 | schema: reduxState.schema 39 | } 40 | }); 41 | 42 | /** 43 | * Closure for passing setFieldValue hook 44 | * Child components are designated specific fields 45 | * where they can modify useForm state 46 | * @param {string} field 47 | * @returns {Function -> void} 48 | */ 49 | const setFieldType = (field: any) => { 50 | return (value: string) => { 51 | form.setFieldValue(field, value); 52 | }; 53 | }; 54 | 55 | /** 56 | * set Redux state, then validate required fields set, 57 | * then call electron to create DockerFile 58 | * at given location with provided details 59 | * @fixed onSubmit app is rerendered 60 | * @todo properly handle setDockerFile returning false 61 | * @param {object} values 62 | * @returns {boolean} 63 | */ 64 | const setStateAndCall = async (values: object) => { 65 | dispatch(setEnvConfig(values)); 66 | const test = {...reduxState, ...values}; 67 | 68 | if (dockerReadyValidation(test)) { 69 | await setDockerFile(test); 70 | showNotification({ 71 | message: "DockerFile created successfully!" 72 | }); 73 | navigate(); 74 | return true; 75 | } else { 76 | showNotification({ 77 | message: "DockerFile creation failed!" 78 | }); 79 | return false; 80 | } 81 | }; 82 | 83 | return ( 84 | 85 | 86 | 87 | Database Configuration 88 | 89 | 90 | 91 | 92 |
setStateAndCall(values))}> 93 | 98 | 109 | 110 | } 111 | /> 112 | 113 | 114 | 115 | 121 | 122 | 123 | 124 | 125 | 131 | 132 | 133 | 134 | 139 | reveal ? : 140 | } 141 | {...form.getInputProps("password")} 142 | /> 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 |
152 | ); 153 | }; 154 | 155 | DatabaseConfig.propTypes = { 156 | navigate: PropTypes.func 157 | }; 158 | 159 | export default DatabaseConfig; 160 | -------------------------------------------------------------------------------- /src/client/components/FooterButtons.tsx: -------------------------------------------------------------------------------- 1 | import PropTypes from "prop-types"; 2 | import { Button } from "@mantine/core"; 3 | 4 | 5 | const FooterButtons = ({ page, prev, next }) => { 6 | return ( 7 |
8 |
9 | 10 |
19 | 26 | 27 | 34 |
35 |
36 | ); 37 | }; 38 | 39 | FooterButtons.propTypes = { 40 | page: PropTypes.number, 41 | prev: PropTypes.func, 42 | next: PropTypes.func 43 | }; 44 | 45 | export default FooterButtons; 46 | -------------------------------------------------------------------------------- /src/client/components/LoadDataConfig.tsx: -------------------------------------------------------------------------------- 1 | import { Space, Title, Paper, Button, TextInput, NativeSelect, Tooltip } from "@mantine/core"; 2 | import FileSearchButton from "../containers/FileSearchButton"; 3 | import { selectFile } from "../utility/fileExplorer"; 4 | import { loadData} from "../utility/postrgresFunctions"; 5 | import { destructureContainerPort} from "../utility/dockerFunctions"; 6 | 7 | import { useForm } from "@mantine/hooks"; 8 | import { useAppSelector,useAppDispatch} from "../utility/hooks.types"; 9 | import { LoadTable,container} from "../../types"; 10 | import {setEnvConfig,setDropDownPort } from "../reducers/envConfigSlice"; 11 | import { cleanNotifications, showNotification, updateNotification } from "@mantine/notifications"; 12 | import { ChangeEvent, useEffect } from "react"; 13 | import { CircleCheck, FileX, InfoCircle } from "tabler-icons-react"; 14 | 15 | /** 16 | * 17 | * @returns JSX definition of 'Load Data' tab 18 | * Load Data tab allows users to upload data to their containeraized database via a csv file. 19 | * Users must specify the name of the table they are uploading data to and it must match the table name in schema 20 | */ 21 | const LoadData = () => { 22 | 23 | //pull in redux global state and the redux dispatch function to update global state 24 | const { tablePath, tableName, activePorts } = useAppSelector(state => state.envConfig) 25 | const dispatch = useAppDispatch(); 26 | 27 | 28 | 29 | /** 30 | * local state of the form declared via Mantine's useForm hook 31 | */ 32 | const form = useForm({ 33 | initialValues: { 34 | tablePath: tablePath, 35 | tableName: tableName, 36 | portSelected:"", 37 | portRefresh:false 38 | 39 | } 40 | }); 41 | /** 42 | * 43 | * @param field 44 | * @returns 45 | * Sets the value of the tablePath field 46 | */ 47 | const setFieldType = (field:"tablePath") => { 48 | return (value: string) => { 49 | form.setFieldValue(field, value); 50 | }; 51 | } 52 | /** 53 | * 54 | * @param values 55 | * type LoadTable = { 56 | tablePath: string - absolute path to .csv file with table data 57 | tableName: string - table name specified via the form. Must match schema. 58 | } 59 | Function updates global state via dispatch and invokes uploadTableData to initiate backend process responsible for uploading data to containeraized database. 60 | Will also notify users if the data was uploaded successfully via psUploadData which listens for a true/false value from the backend. 61 | */ 62 | 63 | useEffect(() => { 64 | 65 | grabPorts().catch(console.error) 66 | 67 | },[form.values.portRefresh] 68 | ) 69 | 70 | /** 71 | * function called in useEffect to grab all active ports 72 | * will update global state with array of active ports obtained from Docker 73 | */ 74 | const grabPorts = async (): Promise => { 75 | const containers:container[] = await dockController.getContainersList(false) 76 | const pArray = destructureContainerPort(containers) 77 | dispatch(setDropDownPort({activePorts:pArray})) 78 | } 79 | 80 | 81 | /** 82 | * 83 | * @param bool 84 | * Function will display a success or failure message to the user based on what is received from postgres. 85 | * True indicates data was loaded successfully, whereas false indicates upload failed 86 | */ 87 | const notifyUser = (arg: boolean |string |Error, values:LoadTable) => { 88 | if(typeof arg==='object' || typeof arg==='string'){ 89 | const error = ""+arg 90 | updateNotification({ 91 | id:'upload-data', 92 | message: error, 93 | autoClose: 4000, 94 | icon: 95 | }) 96 | 97 | } else { 98 | updateNotification({ 99 | id:'upload-data', 100 | message: 'Data uploaded successfuly', 101 | autoClose: 4000, 102 | icon: 103 | }) 104 | 105 | } 106 | dispatch(setEnvConfig(values)); 107 | portRefresh(); 108 | } 109 | 110 | /** 111 | * 112 | * @param event 113 | * set local state 'portSelected' to the drop down value 114 | */ 115 | const setSelectedPort = async (event: ChangeEvent ):Promise => { 116 | form.setFieldValue('portSelected', event.currentTarget.value) 117 | } 118 | 119 | /** 120 | * function to update local state 'portRefresh', which will trigger useEffect hook' 121 | */ 122 | const portRefresh = ()=>{ 123 | if(form.values.portRefresh===true){ 124 | form.setFieldValue('portRefresh',false) 125 | } else { 126 | form.setFieldValue('portRefresh',true) 127 | } 128 | } 129 | return ( 130 | <> 131 | 132 | 133 | Load Data 134 | 135 | 136 | 137 | 138 | 139 | 140 | {/* tablePath and tableName fields are wrapped in a form to leverage Mantine useForm hook */} 141 |
{loadData(values,notifyUser); })}> 142 | {/* onclick calls useEffect to refresh container list in case user removes or starts ports outside of Jugglr app while app is opened to this page */} 143 | portRefresh()} data={activePorts} onChange={(event)=> setSelectedPort(event)} /> 144 | 145 | 146 | 147 | 158 | 162 | 163 | } 164 | /> 165 | 166 | 167 | 179 | 184 | 185 | } 186 | {...form.getInputProps("tableName")} 187 | /> 188 | 189 |
190 |
191 | 192 |
193 |
194 | 195 | 196 | 197 | 198 | ); 199 | }; 200 | 201 | export default LoadData; 202 | -------------------------------------------------------------------------------- /src/client/components/NavbarButtons.tsx: -------------------------------------------------------------------------------- 1 | import PropTypes from "prop-types"; 2 | import { UnstyledButton, Text, Group, ThemeIcon } from "@mantine/core"; 3 | import { 4 | BrandDocker, 5 | CloudStorm, 6 | DatabaseImport, 7 | Folder 8 | } from "tabler-icons-react"; 9 | 10 | import { navButtonTheme } from "../themes/themeFunctions"; 11 | 12 | 13 | const NavbarButtons = ({ navigate }) => { 14 | 15 | return ( 16 | <> 17 | navigate(1)} 19 | sx={theme => navButtonTheme(theme)} 20 | > 21 | 22 | 23 | 24 | 25 | Project 26 | 27 | 28 | 29 | navigate(2)} 31 | sx={theme => navButtonTheme(theme)} 32 | > 33 | 34 | 35 | 36 | 37 | Database 38 | 39 | 40 | 41 | navigate(3)} 43 | sx={theme => navButtonTheme(theme)} 44 | > 45 | 46 | 47 | 48 | 49 | Docker Setup 50 | 51 | 52 | 53 | 54 | navigate(4)} 56 | sx={theme => navButtonTheme(theme)} 57 | > 58 | 59 | 60 | 61 | 62 | Docker Run 63 | 64 | 65 | 66 | navigate(5)} 68 | sx={theme => navButtonTheme(theme)} 69 | > 70 | 71 | 72 | 73 | 74 | Load Data 75 | 76 | 77 | 78 | ); 79 | }; 80 | 81 | NavbarButtons.propTypes = { 82 | navigate: PropTypes.func.isRequired 83 | }; 84 | 85 | export default NavbarButtons; 86 | -------------------------------------------------------------------------------- /src/client/components/ProjectConfig.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Application uses Mantine form hook as part of application state flow. 3 | * useState is used under the hood, and values can be retrieved. 4 | * ref: https://mantine.dev/form/use-form/ 5 | */ 6 | import PropTypes from "prop-types"; 7 | import { 8 | Box, 9 | Button, 10 | Group, 11 | Paper, 12 | Space, 13 | TextInput, 14 | Title 15 | } from "@mantine/core"; 16 | import { useForm } from "@mantine/hooks"; 17 | import { showNotification } from "@mantine/notifications"; 18 | 19 | import { EnvConfig } from "../../types"; 20 | import FileSearchButton from "../containers/FileSearchButton"; 21 | import { setEnvConfig } from "../reducers/envConfigSlice"; 22 | import { 23 | selectProjectRootDirectory, 24 | setProjectDirectory 25 | } from "../utility/fileExplorer"; 26 | import { useAppDispatch, useAppSelector } from "../utility/hooks.types"; 27 | import { isEmptyObject } from "../utility/validations"; 28 | import React from "React" 29 | 30 | 31 | const ProjectConfig = ({ navigate }) => { 32 | const { rootDir } = useAppSelector(state => state.envConfig); 33 | const dispatch = useAppDispatch(); 34 | const form = useForm({ 35 | initialValues: { 36 | rootDir: rootDir 37 | } 38 | }); 39 | 40 | /** 41 | * Closure for passing setFieldValue hook 42 | * Child components are designated specific fields 43 | * where they can modify useForm state 44 | * @param {string} field 45 | * @returns {Function -> void} 46 | */ 47 | const setFieldType = (field: any) => { 48 | return (value: string) => { 49 | form.setFieldValue(field, value); 50 | }; 51 | }; 52 | 53 | /** 54 | * update Redux state and call backend with given directory 55 | * then go to database configuration page 56 | * @todo decouple page change away from component 57 | * @param {EnvConfig} values 58 | * @returns {void} 59 | */ 60 | const setStateAndCall = async (values: EnvConfig): Promise => { 61 | const response = await setProjectDirectory(values); 62 | const newState = { ...values, ...response }; 63 | dispatch(setEnvConfig(newState)); 64 | if (!isEmptyObject(response)) { 65 | showNotification({ 66 | title: "DockerFile found!", 67 | message: "Database details were preset!" 68 | }); 69 | } 70 | navigate(); 71 | }; 72 | 73 | return ( 74 | 75 | 76 | 77 | Project Configuration 78 | 79 | 80 | 81 | 82 |
setStateAndCall(values))}> 83 | 93 | } 94 | /> 95 | 96 | 97 | 98 | 99 | 100 | 101 |
102 | ); 103 | }; 104 | 105 | ProjectConfig.propTypes = { 106 | navigate: PropTypes.func 107 | }; 108 | 109 | export default ProjectConfig; 110 | -------------------------------------------------------------------------------- /src/client/components/RunConfig.tsx: -------------------------------------------------------------------------------- 1 | import { Space, Box, Title, Paper, Button, NativeSelect } from "@mantine/core"; 2 | import { destructureContainerList, destructureContainerId, modifyErrorRemove, startStopOrRemove} from "../utility/dockerFunctions"; 3 | import { ChangeEvent, useEffect } from "react"; 4 | import { useForm } from "@mantine/hooks"; 5 | import { useAppSelector, useAppDispatch } from "../utility/hooks.types"; 6 | import { setDropDownContainer } from "../reducers/envConfigSlice"; 7 | import { container } from "../../types"; 8 | import { showNotification} from "@mantine/notifications"; 9 | 10 | 11 | /** 12 | * 13 | * @returns JSX Definition for Docker Run tab 14 | * Docker Run tab enables users to start and stop existing containers in Docker Desktop 15 | */ 16 | const Run = ():JSX.Element => { 17 | 18 | //pull in redux global state and the redux dispatch function to update global state 19 | const { containerIdObject, containerNames } = useAppSelector(state => state.envConfig) 20 | const dispatch = useAppDispatch(); 21 | 22 | //local state of the form declared via Mantine's useForm hook 23 | 24 | const form2 = useForm({ 25 | initialValues: { 26 | containerSelected:"", 27 | removeSelected: false, 28 | } 29 | }) 30 | 31 | /** 32 | * useEffect will grab all containers from Docker on first render and every time dropdown is selected 33 | 34 | */ 35 | useEffect( () => { 36 | grabContainer().catch(console.error); 37 | 38 | },[form2.values.removeSelected]) 39 | 40 | /** 41 | * function called in useEffect to grab all containers 42 | * will update global state with list of container names and object with all container ids obtained from Docker 43 | */ 44 | const grabContainer = async (): Promise => { 45 | const containers:container[] = await dockController.getContainersList(true) 46 | const cList:string[] = destructureContainerList(containers) 47 | const cObject = destructureContainerId(containers) 48 | dispatch(setDropDownContainer({containerIdObject: cObject, containerNames:cList})) 49 | } 50 | 51 | 52 | /** 53 | * 54 | * @param event 55 | * set local state 'containerSelected' to the drop down value 56 | */ 57 | const updateSelectedContainer = async (event: ChangeEvent ):Promise => { 58 | form2.setFieldValue('containerSelected', event.currentTarget.value) 59 | } 60 | 61 | 62 | 63 | /** 64 | * 65 | * @param args 'args' is the listener response received from the back end 66 | * Notify users if container successfully started 67 | * callback function passed into listener - displays a notification that is either an error message (if args is a string) or a success message (if args is a boolean) 68 | * 69 | */ 70 | const notifyUserStart = async (arg:Boolean |string) => { 71 | if(typeof arg==='string'){ 72 | showNotification({ 73 | message: arg, 74 | autoClose: 3500 75 | }) 76 | } else { 77 | showNotification({ 78 | message: `Container '${form2.values.containerSelected}' started successfully`, 79 | autoClose: 3500 80 | }) 81 | } 82 | containerRefresh() 83 | 84 | } 85 | 86 | /** 87 | * 88 | * @param args 'args' is the listener response received from the back end 89 | * Notify users if container successfully stopped 90 | * callback function passed into listener - displays a notification that is either an error message (if args is a string) or a success message (if args is a boolean) 91 | * 92 | */ 93 | 94 | const notifyUserStop = async (arg:Boolean |string ) => { 95 | if(typeof arg==='string'){ 96 | showNotification({ 97 | message: arg, 98 | autoClose: 3500 99 | }) 100 | } else { 101 | showNotification({ 102 | message: `Container '${form2.values.containerSelected}' stopped successfully`, 103 | autoClose: 3500 104 | }) 105 | } 106 | containerRefresh() 107 | 108 | } 109 | 110 | /** 111 | * 112 | * @param args 'args' is the listener response received from the back end 113 | * Notify users if container successfully removed 114 | * callback function passed into listener - displays a notification that is either an error message (if args is a string) or a success message (if args is a boolean) 115 | * 116 | */ 117 | const notifyUserRemove = async (arg: Boolean|string) => { 118 | if(typeof arg==='string'){ 119 | arg = modifyErrorRemove(arg) 120 | showNotification({ 121 | message: arg, 122 | autoClose: 3500 123 | }) 124 | 125 | } else { 126 | showNotification({ 127 | message: `Container '${form2.values.containerSelected}' removed successfully`, 128 | autoClose: 3500 129 | }) 130 | } 131 | containerRefresh() 132 | } 133 | 134 | 135 | /** 136 | * function to update local state 'removeSelected', which will trigger useEffect hook' 137 | */ 138 | const containerRefresh = ()=>{ 139 | if(form2.values.removeSelected===true){ 140 | form2.setFieldValue('removeSelected',false) 141 | } else { 142 | form2.setFieldValue('removeSelected',true) 143 | } 144 | } 145 | 146 | return ( 147 | <> 148 | 149 | 150 | 151 | Docker Run Configuration 152 | 153 | 154 | 155 |
156 |
157 | {/* set containerNames global state as drop down options. Selected container will be stored in local form state */} 158 | {/* onclick calls useEffect to refresh container list in case user removes containers outside of Jugglr app while app is opened to this page */} 159 | 160 | containerRefresh()} label="Select A Container" data={containerNames} onChange={(event)=> updateSelectedContainer(event)} /> 161 |
162 |
171 |
172 | {/* When button is selected, call setStateAndCall passing in the id of the selected container and the action */} 173 | 174 |
175 | 176 |
177 | 178 |
179 | 180 |
181 | 182 |
183 |
184 |
185 | 186 |
187 |
188 | 189 | ); 190 | }; 191 | 192 | export default Run; 193 | -------------------------------------------------------------------------------- /src/client/components/StartupConfig.tsx: -------------------------------------------------------------------------------- 1 | import { Space, Title, Paper, Button, TextInput, NativeSelect, Grid,Center, Tooltip } from "@mantine/core"; 2 | import { destructureImageList, getImages, buildOrRun } from "../utility/dockerFunctions"; 3 | import { useEffect } from "react"; 4 | import { useForm, } from "@mantine/hooks"; 5 | import { useAppSelector, useAppDispatch } from "../utility/hooks.types"; 6 | import { setDropDownImage } from "../reducers/envConfigSlice"; 7 | import { image } from "../../types"; 8 | import { showNotification, updateNotification } from "@mantine/notifications"; 9 | import { InfoCircle, CircleCheck, FileX} from "tabler-icons-react"; 10 | import React from "react" 11 | 12 | 13 | /** 14 | * 15 | * @returns JSX Definition of Docker Setup tab 16 | * Startup Config allows users to create a new image from a Dockerfile and start a new container 17 | */ 18 | 19 | const Startup = ():JSX.Element => { 20 | //destructure redux global state 21 | //declare redux dispatch function. Used to update global state 22 | const { dropDownImage, port} = useAppSelector(state => state.envConfig) 23 | const dispatch = useAppDispatch(); 24 | 25 | //local state via Mantine hook 26 | const form1 = useForm({ 27 | initialValues: { 28 | image:"", 29 | imageSubmitted: false, 30 | container:"", 31 | selectedImage:"", 32 | port:port, 33 | } 34 | }) 35 | 36 | /** 37 | * useEffect hook will grab existing images from Docker, store the image names in an array and update redux state. 38 | * useEffect Hook will fire on first mount and everytime dropdown is selected 39 | */ 40 | useEffect( () => { 41 | const grabImages = async (): Promise => { 42 | const images:image[] = await getImages() 43 | const iList:string[] = destructureImageList(images) 44 | dispatch(setDropDownImage({dropDownImage:iList})) 45 | } 46 | 47 | grabImages().catch(console.error); 48 | 49 | },[form1.values.imageSubmitted]) 50 | 51 | 52 | 53 | /** 54 | * function to update local state 'imageSubmitted', which will trigger useEffect hook' 55 | */ 56 | const imageRefresh = ():void => { 57 | if(form1.values.imageSubmitted===false){ 58 | form1.setFieldValue('imageSubmitted',true) 59 | } else { 60 | form1.setFieldValue('imageSubmitted',false) 61 | } 62 | } 63 | 64 | /** 65 | * 66 | * @param args 'args' is the listener response received from the back end 67 | * Notify users if container successfully started up 68 | * callback function passed into listener - displays a notification that is either an error message (if args is a string) or a success message (if args is a boolean) 69 | * 70 | */ 71 | const notifyUserContainer = (args:boolean|string) => { 72 | if(typeof args === 'string'){ 73 | updateNotification({ 74 | id: 'run-new-container', 75 | message:args, 76 | autoClose: 3500, 77 | icon: , 78 | }) 79 | } else { 80 | updateNotification({ 81 | id: 'run-new-container', 82 | message:'Container started successfully', 83 | autoClose: 3500, 84 | icon: , 85 | }) 86 | } 87 | imageRefresh() 88 | } 89 | /** 90 | * 91 | * @param args 'args' is the listener response received from the back end 92 | * Notify users if image was created successfully 93 | * callback function passed into listener - displays a notification that is either an error message (if args is a string) or a success message (if args is a boolean) 94 | * 95 | */ 96 | const notifyUserImage = (arg:boolean|string|Error) => { 97 | if(typeof arg==='boolean'){ 98 | updateNotification({ 99 | id: 'create-image', 100 | message:'Image created successfully', 101 | autoClose: 3500, 102 | icon: , 103 | }) 104 | } else if (arg === 'string'){ 105 | updateNotification({ 106 | id: 'create-image', 107 | message:arg, 108 | autoClose: 3500, 109 | icon: , 110 | }) 111 | } else { 112 | const message = ""+arg 113 | updateNotification({ 114 | id: 'create-image', 115 | message: message, 116 | autoClose: 3500, 117 | icon: 118 | }) 119 | } 120 | imageRefresh() 121 | } 122 | 123 | 124 | 125 | 126 | return ( 127 | <> 128 | 129 | 130 | Image Configuration 131 | 132 | 133 | 134 |
buildOrRun(values,'buildImage',notifyUserImage))}> 135 |
136 | 137 | 150 | 155 | 156 | 157 | } 158 | {...form1.getInputProps("image")} 159 | /> 160 | 161 | 162 |
163 | 164 |
165 | 166 |
167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | Container Configuration 175 | 176 | 177 | 178 | 179 | 180 |
buildOrRun(values,'newContainer', notifyUserContainer))}> 181 | 182 | 183 | 184 | 185 |
186 | {/* onclick calls useEffect in case user creates docker images outside of application while app is opened to this page */} 187 | imageRefresh()} onChange={(event)=> form1.setFieldValue('selectedImage', event.currentTarget.value)} /> 188 |
189 |
190 | 191 | 192 |
193 |
194 | 201 | 202 | 208 | 209 |
210 |
211 |
212 | 213 | 214 |
215 | 216 |
217 |
218 | 219 |
220 | 221 |
222 | 223 | ); 224 | }; 225 | 226 | export default Startup; 227 | 228 | 229 | 230 | -------------------------------------------------------------------------------- /src/client/containers/BurgerIcon.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Switch to hamburger shelf on small view size < 993px 3 | */ 4 | import PropTypes from "prop-types"; 5 | import { Burger } from "@mantine/core"; 6 | 7 | const BurgerIcon = ({ opened, setOpened, color, isSmallView }) => { 8 | return ( 9 |