and a
', () => {
30 | expect(wrapper.find('div')).toHaveLength(1);
31 | expect(wrapper.find('input')).toHaveLength(1);
32 | })
33 |
34 | it('should render a repo name', () => {
35 | expect(wrapper.find('div').text()).toEqual(propsRepo.repoName);
36 |
37 | })
38 |
39 | it('should matches the snapshot', () => {
40 | expect(wrapper).toMatchSnapshot();
41 | })
42 |
43 | // TODO:
44 | xit('should possible to activate input checkbox', () => {
45 | // wrapper.find('div').simulate('click');
46 | // expect(propsRepo.isIncluded).toBe(true);
47 | })
48 | })
--------------------------------------------------------------------------------
/__tests__/SignIn.test.tsx:
--------------------------------------------------------------------------------
1 | /* Removing broken test for continuous integration*/
2 | // import * as React from 'react';
3 | // import { shallow } from 'enzyme';
4 |
5 | // import SignIn from '../src/client/components/signIn/SignIn';
6 | // import AppInfoModal from '../src/client/components/appInfo/AppInfoModal'
7 |
8 | describe('
unit testing', () => {
9 |
10 | it('this is a dummy test to allow for CI', () => {
11 | expect(2).toEqual(2);
12 | })
13 |
14 | // const wrapper = shallow(
);
15 |
16 | // it('should render a
', () => {
17 | // expect(wrapper.find('div')).toHaveLength(1)
18 | // })
19 |
20 | // it('should render two
', () => {
21 | // expect(wrapper.find('button')).toHaveLength(2)
22 | // });
23 |
24 | // // TODO:
25 | // xit('should render element if showModal evaluates to true', () => { });
26 | // // TODO:
27 | // xit('shouldn not render element if showModale evaluates to flase', () => { });
28 | })
29 |
30 |
--------------------------------------------------------------------------------
/__tests__/__snapshots__/ProjectRepoListItem.test.tsx.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[` unit test should matches the snapshot 1`] = `
4 |
12 |
17 | mockRepoName
18 |
19 | `;
20 |
--------------------------------------------------------------------------------
/__tests__/setupTest.ts:
--------------------------------------------------------------------------------
1 | import { configure } from 'enzyme';
2 | import Adapter from 'enzyme-adapter-react-16';
3 |
4 | // setup the adapter to be use by enzyme
5 | configure({ adapter: new Adapter() });
--------------------------------------------------------------------------------
/build/icon.icns:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/DockerLocal/96c9f25a981c85aed9e2f6e70a447a0f4e36cad8/build/icon.icns
--------------------------------------------------------------------------------
/build/icon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/DockerLocal/96c9f25a981c85aed9e2f6e70a447a0f4e36cad8/build/icon.ico
--------------------------------------------------------------------------------
/build/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/DockerLocal/96c9f25a981c85aed9e2f6e70a447a0f4e36cad8/build/icon.png
--------------------------------------------------------------------------------
/demoScreenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/DockerLocal/96c9f25a981c85aed9e2f6e70a447a0f4e36cad8/demoScreenshot.png
--------------------------------------------------------------------------------
/env.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable @typescript-eslint/no-inferrable-types */
2 | export const GITHUB_USERID: string = '';
3 | export const GITHUB_ACCESS_TOKEN: string = '';
--------------------------------------------------------------------------------
/images/Env_File_Sreenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/DockerLocal/96c9f25a981c85aed9e2f6e70a447a0f4e36cad8/images/Env_File_Sreenshot.png
--------------------------------------------------------------------------------
/images/add-project.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/DockerLocal/96c9f25a981c85aed9e2f6e70a447a0f4e36cad8/images/add-project.png
--------------------------------------------------------------------------------
/images/add-repos.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/DockerLocal/96c9f25a981c85aed9e2f6e70a447a0f4e36cad8/images/add-repos.png
--------------------------------------------------------------------------------
/images/clone-repos.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/DockerLocal/96c9f25a981c85aed9e2f6e70a447a0f4e36cad8/images/clone-repos.png
--------------------------------------------------------------------------------
/images/demoScreenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/DockerLocal/96c9f25a981c85aed9e2f6e70a447a0f4e36cad8/images/demoScreenshot.png
--------------------------------------------------------------------------------
/images/phlippy_icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/DockerLocal/96c9f25a981c85aed9e2f6e70a447a0f4e36cad8/images/phlippy_icon.png
--------------------------------------------------------------------------------
/images/success.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/DockerLocal/96c9f25a981c85aed9e2f6e70a447a0f4e36cad8/images/success.png
--------------------------------------------------------------------------------
/jest.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | preset: 'ts-jest',
3 | testEnvironment: 'node',
4 | // convert Enzyme wrappers for Jest snapshot matcher
5 | snapshotSerializers: ['enzyme-to-json/serializer'],
6 | // compile TSX to JavaScript using ts-jest preset
7 | transform: {
8 | '^.+\\.tsx?$': 'ts-jest'
9 | },
10 | // Matches patrent folder `__test__` and file name shold contain `test`
11 | testRegex: '/__tests__/.*\\.test.(ts|tsx)$',
12 | // Module file extension for importing
13 | moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'],
14 | // Path to setupTest.ts file for testing environment to be run immediately before running the test code.
15 | setupFiles: ['/__tests__/setupTest.ts']
16 | };
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "dockerlocal",
3 | "productName": "DockerLocal",
4 | "version": "1.0.0",
5 | "description": "a GUI application that will allow you to keep an up-to-date version of docker configurations for interconnected repositories locally while working on a single repository. ",
6 | "main": ".webpack/main",
7 | "scripts": {
8 | "start": "electron-forge start",
9 | "test": "jest --verbose ",
10 | "package": "electron-forge package",
11 | "make": "electron-forge make",
12 | "publish": "electron-forge publish",
13 | "lint": "eslint --ext .ts ."
14 | },
15 | "keywords": [],
16 | "author": "Vivian Cermeno, Kate Chanthakaew, Thomas Lutz, Katty Polyak, Louis Xavier Sheid III",
17 | "license": "MIT",
18 | "config": {
19 | "forge": {
20 | "packagerConfig": {
21 | "icon": "build/icon"
22 | },
23 | "makers": [
24 | {
25 | "name": "@electron-forge/maker-squirrel",
26 | "config": {
27 | "name": "dockerlocal"
28 | }
29 | },
30 | {
31 | "name": "@electron-forge/maker-zip",
32 | "platforms": [
33 | "darwin"
34 | ]
35 | },
36 | {
37 | "name": "@electron-forge/maker-deb",
38 | "config": {}
39 | },
40 | {
41 | "name": "@electron-forge/maker-rpm",
42 | "config": {}
43 | }
44 | ],
45 | "plugins": [
46 | [
47 | "@electron-forge/plugin-webpack",
48 | {
49 | "mainConfig": "./webpack.main.config.js",
50 | "renderer": {
51 | "config": "./webpack.renderer.config.js",
52 | "entryPoints": [
53 | {
54 | "html": "./src/index.html",
55 | "js": "./src/app.tsx",
56 | "name": "main_window"
57 | }
58 | ]
59 | }
60 | }
61 | ]
62 | ]
63 | }
64 | },
65 | "devDependencies": {
66 | "@electron-forge/cli": "^6.0.0-beta.51",
67 | "@electron-forge/maker-deb": "^6.0.0-beta.51",
68 | "@electron-forge/maker-rpm": "^6.0.0-beta.51",
69 | "@electron-forge/maker-squirrel": "^6.0.0-beta.51",
70 | "@electron-forge/maker-zip": "^6.0.0-beta.51",
71 | "@electron-forge/plugin-webpack": "^6.0.0-beta.51",
72 | "@marshallofsound/webpack-asset-relocator-loader": "^0.5.0",
73 | "@types/enzyme": "^3.10.5",
74 | "@types/enzyme-adapter-react-16": "^1.0.6",
75 | "@types/jest": "^26.0.4",
76 | "@types/react-test-renderer": "^16.9.2",
77 | "@typescript-eslint/eslint-plugin": "^2.34.0",
78 | "@typescript-eslint/parser": "^2.34.0",
79 | "css-loader": "^3.6.0",
80 | "electron": "9.0.5",
81 | "electron-devtools-installer": "^3.0.0",
82 | "enzyme": "^3.11.0",
83 | "enzyme-adapter-react-16": "^1.15.2",
84 | "enzyme-to-json": "^3.5.0",
85 | "eslint": "^6.8.0",
86 | "eslint-plugin-import": "^2.21.2",
87 | "fork-ts-checker-webpack-plugin": "^3.1.1",
88 | "html-webpack-plugin": "^4.3.0",
89 | "jest": "^26.1.0",
90 | "node-loader": "^0.6.0",
91 | "react-test-renderer": "^16.13.1",
92 | "sass": "^1.26.9",
93 | "sass-loader": "^8.0.2",
94 | "style-loader": "^0.23.1",
95 | "ts-jest": "^26.1.2",
96 | "ts-loader": "^6.2.2",
97 | "tslint": "^6.1.2",
98 | "typescript": "^3.9.5",
99 | "url-loader": "^4.1.0"
100 | },
101 | "dependencies": {
102 | "@types/crypto-js": "^3.1.47",
103 | "@types/electron-devtools-installer": "^2.2.0",
104 | "@types/express": "^4.17.6",
105 | "@types/node": "^14.0.13",
106 | "@types/node-fetch": "^2.5.7",
107 | "@types/oauth": "^0.9.1",
108 | "@types/passport": "^1.0.3",
109 | "@types/passport-oauth2": "^1.4.9",
110 | "@types/react": "^16.9.38",
111 | "@types/react-dom": "^16.9.8",
112 | "@types/uuid": "^8.0.0",
113 | "bulma": "^0.9.0",
114 | "cookie-parser": "^1.4.5",
115 | "cors": "^2.8.5",
116 | "crypto-js": "^4.0.0",
117 | "dotenv": "^8.2.0",
118 | "electron-squirrel-startup": "^1.0.0",
119 | "express": "^4.17.1",
120 | "fs": "0.0.1-security",
121 | "node-fetch": "^2.6.0",
122 | "node-sass": "^7.0.0",
123 | "passport": "^0.4.1",
124 | "passport-github2": "^0.1.12",
125 | "path": "^0.12.7",
126 | "react": "^16.13.1",
127 | "react-dom": "^16.13.1",
128 | "request": "^2.88.2",
129 | "universal-cookie": "^4.0.3",
130 | "universal-cookie-express": "^4.0.3",
131 | "uuid": "^8.2.0",
132 | "whatwg-fetch": "^3.0.0"
133 | }
134 | }
135 |
--------------------------------------------------------------------------------
/phlippy_icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/DockerLocal/96c9f25a981c85aed9e2f6e70a447a0f4e36cad8/phlippy_icon.png
--------------------------------------------------------------------------------
/src/App.sass:
--------------------------------------------------------------------------------
1 | @charset "utf-8";
2 |
3 | // Set your brand colors
4 | $purple: #8a4d76;
5 | $pink: #fa7c91;
6 | $brown: #757763;
7 | $beige-light: #d0d1cd;
8 | $beige-lighter: #eff0eb;
9 |
10 | // Update Bulma's global variables
11 | $family-sans-serif: 'Nunito', sans-serif;
12 | $grey-dark: $brown;
13 | $grey-light: $beige-light;
14 | $primary: #33B5E5;
15 | $link: #2847D2;
16 | $widescreen-enabled: false;
17 | $fullhd-enabled: false;
18 | $background: #F5F5F5;
19 | $text: #303030;
20 | $dark: #092B66;
21 | $success: #1E94DF;
22 |
23 | // Update some of Bulma's component variables
24 | $body-background-color: #EBECED;
25 | $control-border-width: 2px;
26 | $input-border-color: transparent;
27 | $input-shadow: none;
28 |
29 | // Import only what you need from Bulma
30 | @import '../node_modules/bulma/sass/utilities/_all.sass';
31 | @import '../node_modules/bulma/sass/elements/button.sass';
32 | @import '../node_modules/bulma/sass/components/modal.sass';
33 | @import '../node_modules/bulma/sass/components/menu.sass';
34 | @import '../node_modules/bulma/sass/components/panel.sass';
35 | @import '../node_modules/bulma/sass/grid/columns.sass';
36 | @import '../node_modules/bulma/sass/elements/content.sass';
37 | @import '../node_modules/bulma/sass/components/level.sass';
38 | @import '../node_modules/bulma/sass/form/_all.sass';
39 | @import '../node_modules/bulma/sass/helpers/color.sass';
40 | @import '../node_modules/bulma/sass/helpers/typography.sass';
41 | @import '../node_modules/bulma/sass/components/message.sass';
42 |
43 |
44 | @import '../node_modules/bulma/sass/base/_all.sass';
45 | @import '../node_modules/bulma/sass/elements/container.sass';
46 | @import '../node_modules/bulma/sass/elements/form.sass';
47 | @import '../node_modules/bulma/sass/elements/title.sass';
48 | @import '../node_modules/bulma/sass/components/navbar.sass';
49 | @import '../node_modules/bulma/sass/layout/section.sass';
--------------------------------------------------------------------------------
/src/app.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import * as ReactDOM from "react-dom";
3 | import './App.sass'
4 |
5 | import Routes from "./client/components/home/Routes";
6 |
7 | ReactDOM.render(
8 |
9 |
10 |
,
11 | document.getElementById("root")
12 | );
13 |
--------------------------------------------------------------------------------
/src/client/components/addRepos/AddRepos.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from 'react';
2 | import RepoSearchListItem from './RepoSearchListItem';
3 |
4 | import { findActiveProject } from '../../helpers/projectHelper';
5 | import { fetchRepos } from '../../helpers/fetchRepoHelper';
6 | import {
7 | Repo,
8 | RepoResponseType,
9 | AddReposProps,
10 | Project,
11 | } from '../../../types/types';
12 |
13 | /**
14 | * @function Add array of repositories to repos array in state
15 | * @desc Sends fetch requests to Github API to populate repository info for authenticated user
16 | */
17 | const AddRepos: React.FC = ({
18 | showAddRepos,
19 | setShowAddRepos,
20 | activeProject,
21 | projectList,
22 | dispatch,
23 | }) => {
24 | const [repos, setRepos] = useState({
25 | personal: [],
26 | organizations: [],
27 | collaborations: [],
28 | });
29 | const [selectedRepos, setSelectedRepos] = useState([]);
30 | const [searchValue, setSearchValue] = useState('');
31 | const [activeFilter, setActiveFilter] = useState('all');
32 |
33 | const handleChange = (e: React.ChangeEvent): void => {
34 | setSearchValue(e.target.value);
35 | };
36 |
37 | // switches active filter onclick
38 | const filterClick = (
39 | e: React.MouseEvent
40 | ): void => {
41 | setActiveFilter(e.currentTarget.id);
42 | };
43 |
44 | const handleSubmit = () => {
45 | // make copy of current project
46 | const currentProject: Project = findActiveProject(
47 | projectList,
48 | activeProject
49 | );
50 |
51 | // remove any repos that are already included in the project so user will not have duplicate repos
52 | const reposToAdd = selectedRepos.filter(
53 | (newRepo) =>
54 | !currentProject.projectRepos.some(
55 | (existingRepo) => newRepo.repoId === existingRepo.repoId
56 | )
57 | );
58 |
59 | // add selected repos to current project
60 | currentProject.projectRepos = [
61 | ...currentProject.projectRepos,
62 | ...reposToAdd,
63 | ];
64 |
65 | // insert new project into new project list
66 | const newProjectList: Project[] = projectList.map((project) =>
67 | project.projectId === currentProject.projectId ? currentProject : project
68 | );
69 |
70 | // update state
71 | dispatch({ type: 'addRepo', payload: newProjectList });
72 | setShowAddRepos(false);
73 | };
74 |
75 | useEffect(() => {
76 | // fetch repos on load, will not setRepos if modal is already closed when the fetch request returns
77 | fetchRepos().then((res) => showAddRepos && setRepos(res));
78 | }, []);
79 |
80 | return (
81 |
82 |
83 |
84 |
85 | Select Repositories to Add:
86 |
134 |
135 |
139 | {/* if activeFilter is all or personal, render personal repos */}
140 | {/* if something is typed in the search box, only show repos that include the exact string */}
141 | {/* could change filter to regex at a later date to include a more robust search */}
142 | {/* map filtered list to render comonent for each item */}
143 | {(activeFilter === 'all' || activeFilter === 'personal') &&
144 | repos.personal
145 | .filter(({ repoName, repoOwner }) => {
146 | return (
147 | searchValue === '' ||
148 | repoName.includes(searchValue) ||
149 | repoOwner.includes(searchValue)
150 | );
151 | })
152 | .map((repo) => (
153 |
163 | ))}
164 | {/* same as above for organizations */}
165 | {(activeFilter === 'all' || activeFilter === 'organizations') &&
166 | repos.organizations
167 | .filter(({ repoName, repoOwner }) => {
168 | return (
169 | searchValue === '' ||
170 | repoName.includes(searchValue) ||
171 | repoOwner.includes(searchValue)
172 | );
173 | })
174 | .map((repo) => (
175 |
185 | ))}
186 | {/* same as above for collabs */}
187 | {(activeFilter === 'all' || activeFilter === 'collaborations') &&
188 | repos.collaborations
189 | .filter(({ repoName, repoOwner }) => {
190 | return (
191 | searchValue === '' ||
192 | repoName.includes(searchValue) ||
193 | repoOwner.includes(searchValue)
194 | );
195 | })
196 | .map((repo) => (
197 |
207 | ))}
208 |
209 |
210 | {/* might want to add a style here to keep constant height */}
211 | {/* could add uncheck functionality to list at bottom, would have to rethink state if so*/}
212 |
236 |
237 |
238 |
239 | );
240 | };
241 |
242 | export default AddRepos;
243 |
--------------------------------------------------------------------------------
/src/client/components/addRepos/RepoSearchListItem.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect, Dispatch } from 'react';
2 | import { RepoSearchListItemProps, Project } from '../../../types/types';
3 |
4 | import { findActiveProject } from '../../helpers/projectHelper'
5 |
6 |
7 | const RepoSearchListItem: React.FC = ({ repo, selectedRepos, setSelectedRepos, projectList, activeProject }) => {
8 | const [isChecked, setIsChecked] = useState(false)
9 | const [isDisabled, setIsDisabled] = useState(false)
10 |
11 | // needed to account for switching back and forth between filters/tabs
12 | useEffect(() => {
13 | if (selectedRepos.includes(repo) && !isChecked) setIsChecked(true);
14 | const currentProject: Project = findActiveProject(projectList, activeProject);
15 |
16 | if (currentProject.projectRepos.some(({ repoId }) => repoId === repo.repoId)) setIsDisabled(true);
17 | }, [isChecked])
18 |
19 | // adds/removes from selected repos list and toggles checkbox
20 | const toggleSelect = (): void => {
21 | if (isDisabled) return;
22 | if (!isChecked) setSelectedRepos([...selectedRepos, repo]);
23 | else setSelectedRepos(selectedRepos.filter(({ repoId }) => repoId !== repo.repoId ))
24 | setIsChecked(!isChecked)
25 | }
26 |
27 | return (
28 |
29 | {/* checkbox is readonly because it rerenders on state change */}
30 |
31 | {`${repo.repoName} - ${repo.repoOwner}`}
32 |
33 | )
34 | }
35 |
36 | export default RepoSearchListItem;
--------------------------------------------------------------------------------
/src/client/components/appInfo/AppInfoList.tsx:
--------------------------------------------------------------------------------
1 | const AppInfoList =
2 | " Welcome to DockerLocal! A GUI application that automatically\
3 | creates easily configurable compose files for each of your saved projects.\
4 | Each project will contain public, private, and even organization repositiories of your choosing\
5 | directly from your GitHub account and will update those repositories with every pull request so that you\
6 | can run up-to-date containers to quickly test your working code locally.\n\n Just Sign In With GitHub To Get Started\
7 | or Visit Our Website To Learn Even More!!";
8 |
9 | export default AppInfoList;
10 |
--------------------------------------------------------------------------------
/src/client/components/appInfo/AppInfoModal.tsx:
--------------------------------------------------------------------------------
1 | import React, { Dispatch, SetStateAction } from "react";
2 |
3 | import AppInfoList from "./AppInfoList";
4 |
5 | // display AppInfoList component
6 | // constains 2 buttons listen to onClick to close modal
7 | const AppInfoModal: React.FC<{
8 | setShowModal: Dispatch>;
9 | }> = ({ setShowModal }) => {
10 | return (
11 |
12 |
13 |
14 |
15 | About DockerLocal
16 | setShowModal(false)}
20 | >
21 | x
22 |
23 |
24 |
25 |
26 | To get started, please create a personal access token from on your github account. Instructions are on our github readme.
27 |
28 |
29 | setShowModal(false)}>
30 | Close
31 |
32 |
33 |
34 |
35 | );
36 | };
37 |
38 | export default AppInfoModal;
39 |
--------------------------------------------------------------------------------
/src/client/components/getStarted/GetStarted.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | // import Home from "../home/home";
4 | // import Sidebar from "../sidebar/Sidebar";
5 | // import AddRepos from "../addRepos/AddRepos";
6 |
7 | // place holder for GetStarted components
8 | //TODO: add logic to render child components
9 | const GetStarted = () => {
10 | return Get Started Page
;
11 | };
12 |
13 | export default GetStarted;
14 |
--------------------------------------------------------------------------------
/src/client/components/home/Routes.tsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable @typescript-eslint/no-unused-vars */
2 | /* eslint-disable import/no-unresolved */
3 | import React, { useState, useEffect } from "react";
4 | import Home from './home';
5 | import SignIn from '../signIn/SignIn';
6 | import { User } from '../../../types/types';
7 | import { getUsernameAndToken } from "../../helpers/cookieClientHelper";
8 | import { GITHUB_USERID } from "../../../../env"
9 |
10 | const Routes: React.FC = (props) => {
11 | // hooks to define state
12 | const [isLoggedIn, setIsLoggedIn] = useState(false);
13 | const [userInfo, setUserInfo] = useState({userName: GITHUB_USERID, userId: 'abc'});
14 | const [rendered, setRendered] = useState( );
15 |
16 |
17 | // need to add type for userInfo if not null
18 | // const [userInfo, setUserInfo] = useState(null);
19 |
20 | // runs on render and checks 'isloggedin' display correct page depending on whether user is logged in
21 | // could add logic for 'hasprojects'
22 | // need to make prettier
23 | useEffect(()=> {
24 | // const checkIfLoggedIn = async (): Promise => {
25 | // const { username , accessToken } = await getUsernameAndToken();
26 | // if (accessToken){
27 | // setUserInfo({userName: username, userId: 'abc'})
28 | // setIsLoggedIn(true);
29 | // }
30 | // }
31 | // checkIfLoggedIn();
32 | if (isLoggedIn) {
33 | setRendered(
34 |
38 | )
39 | } else setRendered( )
40 | }, [isLoggedIn])
41 | return (
42 |
43 | {/* setIsLoggedIn(!isLoggedIn)} >isLoggedIn */}
44 | {rendered}
45 |
46 | );
47 | };
48 |
49 | export default Routes;
50 |
--------------------------------------------------------------------------------
/src/client/components/home/home.tsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable import/no-unresolved */
2 | import React, {
3 | useState,
4 | Dispatch,
5 | SetStateAction,
6 | useEffect,
7 | useReducer,
8 | } from 'react';
9 |
10 | import Sidebar from '../sidebar/Sidebar';
11 | import AddRepos from '../addRepos/AddRepos';
12 | import ProjectPage from '../projects/ProjectPage';
13 |
14 | import {
15 | Project,
16 | Repo,
17 | User,
18 | HomeProps,
19 | projectListActions,
20 | } from '../../../types/types';
21 | import { saveProjectList } from '../../helpers/projectHelper';
22 | import { EXPRESS_URL } from '../../helpers/constants';
23 | import logo from '../../../../phlippy_icon.png'
24 |
25 | const initialProjectList: Project[] = [];
26 |
27 | function projectListReducer(state: readonly Project[], action: projectListActions): readonly Project[] {
28 | switch (action.type) {
29 | case 'addProject':
30 | return action.payload;
31 | case 'deleteProject':
32 | return action.payload;
33 | case 'addRepo':
34 | return action.payload;
35 | case 'deleteRepo':
36 | return action.payload;
37 | case 'toggleRepo':
38 | return action.payload;
39 | default:
40 | return state;
41 | }
42 | }
43 |
44 | const Home: React.FC = ({ userInfo, setUserInfo }) => {
45 | const [projectList, dispatch] = useReducer(
46 | projectListReducer,
47 | initialProjectList
48 | );
49 |
50 | const [activeProject, setActiveProject] = useState('');
51 |
52 | // run once on render
53 | // reads project list from local file and sets state
54 | useEffect(() => {
55 | fetch(`${EXPRESS_URL}/config`)
56 | .then((res) => res.json())
57 | .then((res) => {
58 | if (res.projectList && res.activeProject) {
59 | dispatch({ type: 'addProject', payload: res.projectList });
60 | setActiveProject(res.activeProject);
61 | }
62 | // else alert(`It looks like you haven't added any projects yet. Click Add Project to get started. `) ** this has bugs on windows
63 | })
64 | .catch((err) => console.log('fail', err));
65 | }, []);
66 |
67 | // saves project list to disk whenever either are modified
68 | useEffect(() => {
69 | if (projectList[0]) saveProjectList(projectList, activeProject);
70 | }, [projectList, activeProject]);
71 |
72 | return (
73 |
74 |
{`${userInfo.userName}`}
75 |
76 |
77 |
81 |
89 |
90 |
108 |
109 |
110 | );
111 | };
112 |
113 | export default Home;
114 |
--------------------------------------------------------------------------------
/src/client/components/projects/CloningReposModal.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { CloningReposModalProps } from "../../../types/types";
3 |
4 | const CloningReposModal: React.FC = ({ showCloningReposModal, setShowCloningReposModal }) => {
5 |
6 |
7 | return (
8 |
9 |
10 |
11 |
12 | Cloning Repos
13 |
14 |
15 |
16 | Cloning your selected repos. This may take a while. This modal will close when finished. Please be patient.
17 |
18 |
19 | {/* setShowCloningReposModal(false)}
22 | >
23 | Close
24 | */}
25 |
26 |
27 |
28 | );
29 | };
30 |
31 | export default CloningReposModal;
32 |
--------------------------------------------------------------------------------
/src/client/components/projects/ComposeFileModal.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { ComposeFileModalProps} from '../../../types/types'
3 |
4 | /**
5 | * @descriotion ComposeFileModal component
6 | * @param props drilling from ProjectPage component
7 | */
8 | const ComposeFileModal: React.FC = ({
9 | setShowComposeModal,
10 | activeProject,
11 | projectList,
12 | composeFileData
13 | }) => {
14 |
15 | const ymlText = composeFileData.text;
16 | const ymlFilePath = composeFileData.path;
17 |
18 | /**
19 | * RENDER: 1. ymlText or error messgae
20 | * 2. file path or error message
21 | * CONTAINS: 2 buttons 'open folder' and 'close' buttons
22 | */
23 | return (
24 |
25 |
26 |
27 |
28 | docker-compose.yml
29 |
30 |
31 |
32 | {/* Display ymlText or error message */}
33 | {!ymlText && (
34 |
35 | ERROR: cannot compose file
36 |
37 | )}
38 | {ymlText && (
39 |
40 |
41 | {ymlText}
42 |
43 |
44 | )}
45 |
46 |
47 |
48 |
File Location:
49 | {/* Display file path or error message */}
50 | {!ymlFilePath && (
51 |
52 | ERROR: file path not found
53 |
54 | )}
55 | {ymlFilePath && (
56 |
57 | {ymlFilePath}
58 |
59 | )}
60 | {/* ============== TODO ================== */}
61 | {/* TODO: Open File or File Folder onClick */}
62 | {/* ====================================== */}
63 |
Open Folder
64 |
setShowComposeModal(false)}
67 | >
68 | Close
69 |
70 |
71 |
72 |
73 |
74 | );
75 | };
76 |
77 | export default ComposeFileModal;
78 |
--------------------------------------------------------------------------------
/src/client/components/projects/ProjectPage.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from 'react';
2 |
3 | import AddRepos from '../addRepos/AddRepos';
4 | import {
5 | Project,
6 | ProjectPageProps,
7 | ComposeFile,
8 | ComposeFileModalProps,
9 | } from '../../../types/types';
10 | import ProjectRepoListItem from './ProjectRepoListItem';
11 | import ComposeFileModal from './ComposeFileModal';
12 | import CloningReposModal from './CloningReposModal';
13 | import { findActiveProject } from '../../helpers/projectHelper';
14 | import { getUsernameAndToken } from '../../helpers/cookieClientHelper';
15 | import { EXPRESS_URL } from '../../helpers/constants';
16 |
17 | const ProjectPage: React.FC = ({
18 | activeProject,
19 | userInfo,
20 | projectList,
21 | dispatch,
22 | }) => {
23 | const [showAddRepos, setShowAddRepos] = useState(false);
24 | const [projectRepoListItems, setprojectRepoListItems] = useState([]);
25 |
26 | const [showCloningReposModal, setShowCloningReposModal] = useState(false);
27 | const [showComposeModal, setShowComposeModal] = useState(false);
28 | const [composeFileData, setComposeFileData] = useState(
29 | null
30 | );
31 |
32 | // populate repo list items when active project changes and when request from home.tsx comes back to update project list
33 | useEffect(() => {
34 | const currentProject: Project = findActiveProject(
35 | projectList,
36 | activeProject
37 | );
38 |
39 | if (currentProject && currentProject.projectRepos) {
40 | const newList = currentProject.projectRepos.map((repo) => {
41 | return (
42 |
46 | );
47 | });
48 | setprojectRepoListItems(newList);
49 | }
50 | }, [activeProject, projectList]);
51 |
52 | /**
53 | * @function onClick button Compose File button
54 | * @description send 'POST' request
55 | * send: projectName
56 | * receive: yml file and pathfile
57 | */
58 | const composeFile = (): void => {
59 | const currentProject: Project = findActiveProject(
60 | projectList,
61 | activeProject
62 | );
63 |
64 | const reposToClone = currentProject.projectRepos.filter(
65 | ({ isIncluded }) => isIncluded
66 | );
67 |
68 | const body = {
69 | projectName: currentProject.projectName,
70 | repos: reposToClone,
71 | };
72 |
73 | fetch(`${EXPRESS_URL}/docker`, {
74 | headers: {
75 | 'content-type': 'application/json; charset=UTF-8',
76 | },
77 | body: JSON.stringify(body),
78 | method: 'POST',
79 | })
80 | .then((data) => {
81 | return data.json();
82 | })
83 | .then((res) => {
84 | const newYmlData: ComposeFile = {
85 | text: res.file,
86 | path: res.path,
87 | };
88 | setComposeFileData(newYmlData);
89 | setShowComposeModal(true);
90 | })
91 | .catch((error) => console.log(error));
92 | };
93 |
94 | const cloneRepos = async (): Promise => {
95 | setShowCloningReposModal(true);
96 |
97 | const currentProject: Project = findActiveProject(
98 | projectList,
99 | activeProject
100 | );
101 |
102 | // get selected repos that are checked to clone
103 | const reposToClone = currentProject.projectRepos.filter(
104 | ({ isIncluded }) => isIncluded
105 | );
106 |
107 | // get username and token
108 | const { username, accessToken } = await getUsernameAndToken();
109 |
110 | const body = JSON.stringify({
111 | username,
112 | accessToken,
113 | repos: reposToClone,
114 | projectName: currentProject.projectName,
115 | });
116 |
117 | fetch(`${EXPRESS_URL}/api/clonerepos`, {
118 | method: 'POST',
119 | body,
120 | headers: {
121 | 'Content-Type': 'application/json',
122 | },
123 | })
124 | .then((res) => {
125 | setShowCloningReposModal(false);
126 | return res.json();
127 | })
128 | .then((res) => console.log('success', res))
129 | .catch((err) => console.log('fail', err));
130 | };
131 |
132 | return (
133 |
134 |
Select your repositories:
135 |
setShowAddRepos(true)}
138 | style={{ margin: '10px' }}
139 | >
140 | Add Repositories
141 |
142 |
=> cloneRepos()}
145 | style={{ margin: '10px' }}
146 | >
147 | Clone Repos
148 |
149 |
150 |
composeFile()}
153 | style={{ margin: '10px' }}
154 | >
155 | Compose File
156 |
157 |
158 | {projectRepoListItems}
159 |
160 | {/* shows this element if showAddRepos is true */}
161 | {showAddRepos && (
162 |
171 | )}
172 | {showCloningReposModal && (
173 |
176 | )}
177 |
178 | {showComposeModal && (
179 |
188 | )}
189 |
190 | );
191 | };
192 |
193 | export default ProjectPage;
194 |
--------------------------------------------------------------------------------
/src/client/components/projects/ProjectRepoListItem.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from 'react';
2 |
3 | import { ProjectRepoListItemProps, Project, Repo } from '../../../types/types';
4 | import { findActiveProject } from '../../helpers/projectHelper';
5 |
6 | const ProjectRepoListItem: React.FC = ({
7 | repo,
8 | activeProject,
9 | projectList,
10 | dispatch,
11 | }) => {
12 | const [isChecked, setIsChecked] = useState(repo.isIncluded || false);
13 |
14 | useEffect(() => {
15 | setIsChecked(repo.isIncluded || false);
16 | });
17 |
18 | const toggleIsIncluded = (): void => {
19 | // define current repo
20 | const currentProject: Project = findActiveProject(
21 | projectList,
22 | activeProject
23 | );
24 |
25 | // need to make copy of states to include in new to not mutate state directly
26 | // make copy of repo with toggled isIncluded value
27 | const newRepo: Repo = { ...repo, isIncluded: !repo.isIncluded };
28 |
29 | // make copy of active project repo list with new repo included
30 | const newProjectRepos = currentProject.projectRepos.map((repo) =>
31 | repo.repoId === newRepo.repoId ? newRepo : repo
32 | );
33 |
34 | // copy active project
35 | const newActiveProject: Project = { ...currentProject };
36 | newActiveProject.projectRepos = newProjectRepos;
37 |
38 | // insert new project into new project list
39 | const newProjectList: Project[] = projectList.map((project) =>
40 | project.projectId === newActiveProject.projectId
41 | ? newActiveProject
42 | : project
43 | );
44 |
45 | // set new project list
46 | dispatch({ type: 'addRepo', payload: newProjectList });
47 | };
48 |
49 | return (
50 |
51 | {/* checkbox is readonly because it rerenders on state change */}
52 |
53 | {repo.repoName}
54 |
55 | );
56 | };
57 |
58 | export default ProjectRepoListItem;
59 |
--------------------------------------------------------------------------------
/src/client/components/projects/ProjectSidebar.tsx:
--------------------------------------------------------------------------------
1 | import React, { Dispatch, SetStateAction, useState } from 'react';
2 | import { v4 as uuidv4 } from 'uuid';
3 | import { Project, ProjectSideBarProps } from '../../../types/types';
4 |
5 | import { checkValidName } from '../../helpers/projectHelper';
6 |
7 | const ProjectSideBar: React.FC = ({
8 | setShowProjectSidebarModal,
9 | projectList,
10 | setActiveProject,
11 | dispatch,
12 | }) => {
13 | // State hook for new project name and boolean whether the name is valid
14 | const [newProject, setNewProject] = useState({
15 | name: '',
16 | isValid: false,
17 | });
18 |
19 | // State hook for message body and style
20 | const validBody =
21 | 'Project names can contain alphanumeric characters, hypens, or underscores. Project names must be unique, contain less than 26 characters, and cannot be blank.';
22 | const invalidBody = `You have entered an invalid project name. ${validBody}`;
23 | const [newProjectStyle, setNewProjectStyle] = useState({
24 | messageBody: validBody,
25 | messageStyle: 'is-info',
26 | });
27 |
28 | /**
29 | * @function handleChange
30 | * @description updates newProject.name, newProject.isValid, newProjectStyle.messageBody, newProjectStyle.messageStyle
31 | * @param e each update to new project name field
32 | */
33 | const handleChange = (e: React.ChangeEvent): void => {
34 | const isValid = checkValidName(e.target.value, projectList);
35 | setNewProject({
36 | name: e.target.value,
37 | isValid,
38 | });
39 | setNewProjectStyle({
40 | messageBody: isValid ? validBody : invalidBody,
41 | messageStyle: isValid ? 'is-info' : 'is-danger',
42 | });
43 | };
44 |
45 | const handleSubmit = (): void => {
46 | // create a new project object
47 | if (checkValidName(newProject.name, projectList)) {
48 | const newProjectId: string = uuidv4();
49 |
50 | const newProjectObject: Project = {
51 | projectId: newProjectId,
52 | projectName: newProject.name,
53 | projectRepos: [],
54 | };
55 |
56 | // set copy of a new project
57 | dispatch({
58 | type: 'addProject',
59 | payload: [...projectList, newProjectObject],
60 | });
61 | setActiveProject(newProjectId);
62 | // then close the modal
63 | setShowProjectSidebarModal(false);
64 | }
65 | };
66 |
67 | const errorIcon = (
68 |
69 |
70 |
71 | );
72 |
73 | // input for projectname
74 | return (
75 |
76 |
77 |
78 |
79 | Create A New Project
80 |
81 |
82 |
116 |
117 | handleSubmit()}
120 | disabled={!newProject.isValid}
121 | >
122 | Create Project
123 |
124 | setShowProjectSidebarModal(false)}
127 | >
128 | Cancel
129 |
130 |
131 |
132 |
133 | );
134 | };
135 |
136 | export default ProjectSideBar;
137 |
--------------------------------------------------------------------------------
/src/client/components/sidebar/Sidebar.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState, Dispatch, SetStateAction } from 'react';
2 |
3 | import SidebarButton from './SidebarButton';
4 | import ProjectSideBar from '../projects/ProjectSidebar';
5 | import { Project, SidebarProps } from '../../../types/types';
6 |
7 | const Sidebar: React.FC = ({
8 | projectList,
9 | dispatch,
10 | activeProject,
11 | setActiveProject,
12 | }) => {
13 | const sidebarButtons = projectList.map((project) => (
14 |
21 | ));
22 |
23 | const [showProjectSideBarModal, setShowProjectSidebarModal] = useState(false);
24 | // render a sidebar button for every project in list
25 | return (
26 |
27 |
31 |
32 |
35 | setShowProjectSidebarModal(!showProjectSideBarModal)
36 | }
37 | style={{ marginTop: '10%', marginLeft: '35%' }}
38 | >
39 | Add Project
40 |
41 | {showProjectSideBarModal && (
42 |
51 | )}
52 |
53 | );
54 | };
55 |
56 | export default Sidebar;
57 |
--------------------------------------------------------------------------------
/src/client/components/sidebar/SidebarButton.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState, Dispatch, SetStateAction, useEffect, } from 'react';
2 |
3 | import { Project, SidebarButtonProps } from '../../../types/types'
4 |
5 |
6 | const SidebarButton: React.FC = ({ projectName, projectId, projectRepos, activeProject, setActiveProject}) => {
7 | const [isActive, setIsActive] = useState('');
8 |
9 | // listens for change in [activeProject], changes isActive to string to style element
10 | // come check if we should be listening for [activeProject or more efficient to listen for specific property]
11 | useEffect(() => {
12 | if (projectId === activeProject) {
13 | setIsActive('is-active')
14 | } else {
15 | setIsActive('')
16 | }}, [activeProject])
17 |
18 |
19 | return (
20 |
21 | setActiveProject(projectId)}>
22 | {projectName}
23 |
24 |
25 | )
26 |
27 | }
28 |
29 | export default SidebarButton;
--------------------------------------------------------------------------------
/src/client/components/signIn/SignIn.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 |
3 | import AppInfoModal from "../appInfo/AppInfoModal";
4 | import { EXPRESS_URL } from "../../helpers/constants";
5 | import { getUsernameAndToken } from "../../helpers/cookieClientHelper";
6 | import { SignInProps } from "../../../types/types"
7 |
8 | const SignIn: React.FC = ({ isLoggedIn, setIsLoggedIn, setUserInfo }) => {
9 | //initailize showModal to false
10 | const [showModal, setShowModal] = useState(false);
11 |
12 | //display signIn button and appInfo button
13 | return (
14 |
15 |
18 | // window.location.replace(`${EXPRESS_URL}/auth/github`)
19 | // }
20 | onClick={(): void=> {
21 | const checkIfLoggedIn = async (): Promise => {
22 | const { username , accessToken } = await getUsernameAndToken();
23 | if (username && accessToken){
24 | setUserInfo({userName: username, userId: 'abc'})
25 | setIsLoggedIn(true);
26 | } else {
27 | setShowModal(true)
28 | }
29 | }
30 | checkIfLoggedIn();
31 | }}
32 | >
33 | Sign In With Github
34 |
35 |
setShowModal(!showModal)}
38 | >
39 | More Info About DockerLocal
40 |
41 |
42 | {/* this will render AppInfoModal if showModal evaluates to true */}
43 | {showModal &&
}
44 |
45 | );
46 | };
47 |
48 | export default SignIn;
49 |
--------------------------------------------------------------------------------
/src/client/helpers/constants.ts:
--------------------------------------------------------------------------------
1 | export const EXPRESS_URL = 'http://localhost:3001';
--------------------------------------------------------------------------------
/src/client/helpers/cookieClientHelper.ts:
--------------------------------------------------------------------------------
1 | import CryptoJS from "crypto-js";
2 | import { GITHUB_USERID, GITHUB_ACCESS_TOKEN } from '../../../env'
3 |
4 | /**
5 | * @function getUsernameAndToken
6 | * @description a helper function that take in a document cookie and parses and decrypts it to seperate the username and token from the cookie
7 | * @description In the current version, this returns the username and token stored in .env variables
8 | * @param username user's github username
9 | * @param accessToken user's github access token
10 | * @returns boolean. True if no invalid characters
11 | */
12 |
13 | export const getUsernameAndToken = async (): Promise<{
14 | username: string;
15 | accessToken: string;
16 | }> => {
17 | /*
18 | // PARSE TOKEN AND USERNAME FROM COOKIES
19 | let username;
20 | let token;
21 | const cookie = document.cookie;
22 | // if there is no cookie yet since user has not signed in, do NOT run the function
23 | if (cookie === ''){
24 | return {username, accessToken: token};
25 | }
26 | const nameAndToken = cookie.split(";");
27 | // the cookie comes in with either token or username first since we dont know when or why that happens
28 | // this function is supposed to check which one comes first and assign it respectively before parsing
29 | if (nameAndToken[0].slice(0,9) === "username"){
30 | username = nameAndToken[0];
31 | token = nameAndToken[1];
32 | } else {
33 | username = nameAndToken[1];
34 | token = nameAndToken[0];
35 | }
36 | username = username.replace("username=", "").trim();
37 | token = token.replace("token=", "").trim();
38 | const parsedToken = decodeURIComponent(token);
39 |
40 | // DECRYPT TOKEN FROM COOKIES
41 | const decryptedToken = await CryptoJS.AES.decrypt(
42 | parsedToken,
43 | "super_secret"
44 | ).toString(CryptoJS.enc.Utf8);
45 | */
46 | return { username: GITHUB_USERID, accessToken: GITHUB_ACCESS_TOKEN};
47 | };
48 |
--------------------------------------------------------------------------------
/src/client/helpers/fetchRepoHelper.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable import/no-unresolved */
2 | import { RepoResponseType } from "../../types/types";
3 | import { getUsernameAndToken } from "../helpers/cookieClientHelper";
4 |
5 | // dummy request and response
6 | export const fetchRepos = async (): Promise => {
7 | // *** need to add conditional if user is not logged in or if fetch error etc ***
8 |
9 | const { username, accessToken } = await getUsernameAndToken();
10 |
11 | const response: RepoResponseType = {
12 | personal: [],
13 | organizations: [],
14 | collaborations: [],
15 | };
16 |
17 | // WILL USE USERNAME AND ACCESS TOKEN FOR THESE REQUESTS
18 | // ADD HEADERS FOR POST REQUEST
19 | const myHeaders = new Headers();
20 | myHeaders.append("Authorization", `Bearer ${accessToken}`);
21 | myHeaders.append("Content-Type", "application/json");
22 |
23 | // PUPLIC REPO FETCH //////////////////////////////////////////////////
24 | // Query to get the repo id, repo name, and repo owner for public repositories
25 | const publicRepoQuery = JSON.stringify({
26 | query: `{
27 | user(login: "${username}") {
28 | repositories(first: 100) {
29 | edges {
30 | node {
31 | id
32 | name
33 | owner {
34 | ... on User {
35 | login
36 | }
37 | }
38 | }
39 | }
40 | }
41 | }
42 | }`,
43 | variables: {},
44 | });
45 |
46 | // Request options for fetches with appended headers and correct query
47 | const requestOptions: RequestInit = {
48 | method: "POST",
49 | headers: myHeaders,
50 | body: publicRepoQuery,
51 | redirect: "follow",
52 | };
53 |
54 | // Send fetch request to Github GraphQL API with requestOptions object for Public Repos
55 | await fetch("https://api.github.com/graphql", requestOptions)
56 | .then((response) => response.text())
57 | .then((res) => {
58 | const result = JSON.parse(res);
59 | let currentNode;
60 | const pubLength = result.data.user.repositories.edges.length;
61 | const pubRepos = [];
62 | for (let i = 0; i < pubLength; i++) {
63 | currentNode = result.data.user.repositories.edges[i].node;
64 | pubRepos.push({
65 | repoId: currentNode.id,
66 | repoName: currentNode.name,
67 | repoOwner: currentNode.owner.login,
68 | });
69 | }
70 | response.personal = pubRepos;
71 | })
72 | .catch((error) => console.log("error", error));
73 |
74 | // COLLABORATOR REPO FETCH//////////////////////////////////////////////////
75 | // gets the id, name and owner username from repos collaborated on
76 | const collabFetch = JSON.stringify({
77 | query: `{
78 | user(login: "${username}") {
79 | repositories(first: 100, affiliations: COLLABORATOR) {
80 | nodes {
81 | id
82 | name
83 | owner {
84 | login
85 | }
86 | }
87 | }
88 | }
89 | }`,
90 | variables: {},
91 | });
92 |
93 | // changes the query to be for collaborated repos (above)
94 | requestOptions.body = collabFetch;
95 |
96 | await fetch("https://api.github.com/graphql", requestOptions)
97 | .then((response) => response.text())
98 | .then((res) => {
99 | const result = JSON.parse(res);
100 | let currentNode;
101 | const collabChain = result.data.user.repositories.nodes;
102 | const collabLength = collabChain.length;
103 | const collabRepos = [];
104 | for (let i = 0; i < collabLength; i++) {
105 | currentNode = result.data.user.repositories.nodes[i];
106 | collabRepos.push({
107 | repoId: currentNode.id,
108 | repoName: currentNode.name,
109 | repoOwner: currentNode.owner.login,
110 | });
111 | }
112 | response.collaborations = collabRepos;
113 | })
114 | .catch((error) => console.log("error", error));
115 |
116 | // ORGANIZATION REPO FETCH//////////////////////////////////////////////////
117 | // gets the id, name and owner username from repos inside of organizations the user is a member of
118 | const orgFetch = JSON.stringify({
119 | query: `{
120 | user(login: "${username}") {
121 | organizations(first: 100) {
122 | edges {
123 | node {
124 | repositories(affiliations: ORGANIZATION_MEMBER, first: 100) {
125 | edges {
126 | node {
127 | name
128 | id
129 | owner {
130 | login
131 | }
132 | }
133 | }
134 | }
135 | }
136 | }
137 | }
138 | }
139 | }`,
140 | variables: {},
141 | });
142 |
143 | // changes the query to be for organization repos (above)
144 | requestOptions.body = orgFetch;
145 |
146 | await fetch("https://api.github.com/graphql", requestOptions)
147 | .then((response) => response.text())
148 | .then((res) => {
149 | const result = JSON.parse(res);
150 | let currentNode;
151 | let reposLength;
152 | const objChain = result.data.user.organizations.edges;
153 | const orgLength = objChain.length;
154 | const orgArr = [];
155 | // Iterate through the returned object and save the name, id, and owner information for each repo node
156 | for (let x = 0; x < orgLength; x++) {
157 | reposLength = objChain[x].node.repositories.edges.length;
158 | for (let i = 0; i < reposLength; i++) {
159 | currentNode =
160 | result.data.user.organizations.edges[x].node.repositories.edges[i]
161 | .node;
162 | orgArr.push({
163 | repoName: currentNode.name,
164 | repoId: currentNode.id,
165 | repoOwner: currentNode.owner.login,
166 | });
167 | }
168 | }
169 | response.organizations = orgArr;
170 | })
171 | .catch((error) => console.log("error", error));
172 |
173 | // logs all gathered and parsed repository data;
174 | // console.log("RESPONSE: ", response);
175 | return response;
176 | };
177 |
--------------------------------------------------------------------------------
/src/client/helpers/projectHelper.ts:
--------------------------------------------------------------------------------
1 | import { Project } from '../../types/types';
2 | import { EXPRESS_URL } from './constants';
3 |
4 | /**
5 | * @function findActiveProject
6 | * @description finds the active project from the list of user's projects
7 | * @param projectList contains info about all projects
8 | * @param activeProject ID of active project
9 | * @returns project object whose ID matches the current active project's ID
10 | */
11 | export const findActiveProject = (
12 | projectList: readonly Project[],
13 | activeProject: string
14 | ): Project => {
15 | return {
16 | ...projectList.find((project) => project.projectId === activeProject),
17 | };
18 | };
19 |
20 | /**
21 | * @function saveProjectList
22 | * @description saves project list to disk
23 | * @param projectList contains info about all projects
24 | * @param activeProject ID of active project
25 | *
26 | */
27 |
28 | export const saveProjectList = (projectList: readonly Project[], activeProject: string): void => {
29 | fetch(`${EXPRESS_URL}/config`, {
30 | method: "POST",
31 | headers: {
32 | 'Content-Type': 'application/json',
33 | },
34 | body: JSON.stringify({
35 | projectList: [...projectList],
36 | activeProject
37 | }),
38 | })
39 | .catch((err) => console.log("fail", err));
40 | };
41 |
42 | /**
43 | * @function checkValidName
44 | * @description function that updates boolean in state that indicates whether project name entered is valid
45 | * @param projectNameValue string which will be checked for valid format
46 | * @returns boolean. True if no invalid characters
47 | */
48 | export const checkValidName = (
49 | projectNameValue: string,
50 | projectList: readonly Project[]
51 | ): boolean => {
52 | // RegExp to test if project name will be valid project/folder name.
53 | // Can have letters, numbers, hyphens, underscores only. Limited to 1 to 25 characters
54 | const regex = /^[\w-]{1,25}$/;
55 | const isValid: boolean = regex.test(projectNameValue);
56 | // Check to make sure we don't have duplicate project names.
57 | const nameArray = projectList.map((project) =>
58 | project.projectName.toLowerCase()
59 | );
60 | const isUnique = !nameArray.includes(projectNameValue.toLowerCase());
61 |
62 | return isValid && isUnique;
63 | };
64 |
--------------------------------------------------------------------------------
/src/index.css:
--------------------------------------------------------------------------------
1 | body {
2 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
3 | margin: auto;
4 | max-width: 38rem;
5 | padding: 2rem;
6 | }
7 |
--------------------------------------------------------------------------------
/src/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
13 |
14 |
15 |
16 | DockerLocal
17 |
18 |
19 |
20 |
21 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | import { app, BrowserWindow, shell, session, WebPreferences } from 'electron';
2 | import installExtension, {
3 | REACT_DEVELOPER_TOOLS,
4 | } from 'electron-devtools-installer';
5 | import { EXPRESS_URL } from "./client/helpers/constants";
6 |
7 |
8 |
9 | const express = require('./server/server.ts');
10 | const path = require('path');
11 |
12 | // global variable pointing to main_window under entrypoints in package.json
13 | // loads index.html and app.tsx
14 | declare const MAIN_WINDOW_WEBPACK_ENTRY: string;
15 |
16 | // // use this to disable dark mode
17 | // const { nativeTheme } = require('electron')
18 | // console.log(nativeTheme.themeSource = 'light')
19 | // console.log('menuItems!! ', Menu)
20 | // const viewSettingsMenu = new Menu()
21 |
22 |
23 | // let mainWindow: BrowserWindow;
24 |
25 | // Handle creating/removing shortcuts on Windows when installing/uninstalling.
26 | // eslint-disable-line global-require
27 | if (require('electron-squirrel-startup')) app.quit();
28 |
29 | const createWindow = (): void => {
30 | // // Create the browser window.
31 | const mainWindow = new BrowserWindow({
32 | width: 1280,
33 | height: 720,
34 | autoHideMenuBar: true,
35 | useContentSize: true,
36 | resizable: true,
37 | webPreferences: {
38 | contextIsolation: true,
39 | enableRemoteModule: false
40 |
41 | }
42 |
43 |
44 | });
45 |
46 | // set permission requests to false for all remote content - electron security checklist
47 |
48 |
49 | mainWindow
50 | .loadURL(MAIN_WINDOW_WEBPACK_ENTRY)
51 | // .then(() => mainWindow.webContents.openDevTools()); // uncomment to display dev tools
52 | mainWindow.focus();
53 | };
54 |
55 |
56 |
57 | app.whenReady().then(() => {
58 | // automatically deny all permission requests from remote content
59 | session.defaultSession.setPermissionRequestHandler((webcontents, permission, callback) => callback(false));
60 | // session.defaultSession.webRequest.onHeadersReceived((details, callback) => {
61 | // callback({
62 | // responseHeaders: {
63 | // ...details.responseHeaders,
64 | // 'Content-Security-Policy': ['default-src \'self\'']
65 | // }
66 | // })
67 | // })
68 |
69 |
70 | installExtension(REACT_DEVELOPER_TOOLS)
71 | .then((devtool: any) => console.log(`Added Extension: ${devtool.name}`))
72 | .catch((err: any) => console.log('An error occurred: ', err));
73 | });
74 |
75 | // This method will be called when Electron has finished
76 | // initialization and is ready to create browser windows.
77 | // Some APIs can only be used after this event occurs.
78 | app.on('ready', createWindow);
79 |
80 | // Quit when all windows are closed.
81 | app.on('window-all-closed', () => {
82 | // On OS X it is common for applications and their menu bar
83 | // to stay active until the user quits explicitly with Cmd + Q
84 | if (process.platform !== 'darwin') {
85 | app.quit();
86 | }
87 | });
88 |
89 | app.on('activate', () => {
90 | // On OS X it's common to re-create a window in the app when the
91 | // dock icon is clicked and there are no other windows open.
92 | if (BrowserWindow.getAllWindows().length === 0) {
93 | createWindow();
94 | }
95 | });
96 |
97 |
98 | // Verify WebView Options Before Creation - from electron security docs - https://www.electronjs.org/docs/tutorial/security#11-verify-webview-options-before-creation
99 | app.on('web-contents-created', (event, contents) => {
100 | contents.on('will-attach-webview', (event, webPreferences: WebPreferences & { preloadURL: string}, params) => {
101 | // Strip away preload scripts if unused or verify their location is legitimate
102 | delete webPreferences.preload;
103 | delete webPreferences.preloadURL;
104 |
105 | // Disable Node.js integration
106 | webPreferences.nodeIntegration = false;
107 |
108 | // No URLS should be loaded
109 | event.preventDefault();
110 |
111 | })
112 | })
113 |
114 | const URL = require('url').URL
115 |
116 | // only allows navigation to our express server URL
117 | app.on('web-contents-created', (event, contents) => {
118 | contents.on('will-navigate', (event, navigationUrl) => {
119 | const parsedUrl = new URL(navigationUrl)
120 | if (parsedUrl.origin !== EXPRESS_URL) {
121 | event.preventDefault()
122 | }
123 | })
124 | })
125 |
126 | // doesnt allow new windows to be created
127 | app.on('web-contents-created', (event, contents) => {
128 | contents.on('new-window', async (event, navigationUrl) => {
129 | event.preventDefault()
130 | await shell.openExternal(navigationUrl)
131 | })
132 | })
133 |
134 |
135 |
136 | // In this file you can include the rest of your app's specific main process
137 | // code. You can also put them in separate files and import them here.
138 |
--------------------------------------------------------------------------------
/src/renderer.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * This file will automatically be loaded by webpack and run in the "renderer" context.
3 | * To learn more about the differences between the "main" and the "renderer" context in
4 | * Electron, visit:
5 | *
6 | * https://electronjs.org/docs/tutorial/application-architecture#main-and-renderer-processes
7 | *
8 | * By default, Node.js integration in this file is disabled. When enabling Node.js integration
9 | * in a renderer process, please be aware of potential security implications. You can read
10 | * more about security risks here:
11 | *
12 | * https://electronjs.org/docs/tutorial/security
13 | *
14 | * To enable Node.js integration in this file, open up `main.js` and enable the `nodeIntegration`
15 | * flag:
16 | *
17 | * ```
18 | * // Create the browser window.
19 | * mainWindow = new BrowserWindow({
20 | * width: 800,
21 | * height: 600,
22 | * webPreferences: {
23 | * nodeIntegration: true
24 | * }
25 | * });
26 | * ```
27 | */
28 |
29 | // import './index.css';
30 | // import * as React from 'react';
31 | // import { render } from 'react-dom';
32 | // import App from './App';
33 |
34 |
35 | console.log('👋 This message is being logged by "renderer.js", included via webpack');
36 |
37 |
--------------------------------------------------------------------------------
/src/scripts/cloneRepo.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | # Script to clone a github repository
3 |
4 | # handle incoming arguments, username and repoName
5 | username=$1
6 | repoName=$2
7 | projectName=$3
8 |
9 | # get the sock address of our ssh agent so that we can connect to it
10 | temp_sock=$(cat ./tmpAgent/agentSock)
11 | # add the ssh agent sock to the environment variables
12 | export SSH_AUTH_SOCK=$temp_sock
13 |
14 | # create a myProjects folder to store repositories if it doesn't already exist
15 | if [ ! -d "./myProjects" ]
16 | then
17 | mkdir ./myProjects
18 | fi
19 | cd myProjects
20 | # create a folder named after the current project if one doesn't exist already
21 | if [ ! -d "./$projectName" ]
22 | then
23 | mkdir $projectName
24 | fi
25 |
26 | cd $projectName
27 |
28 | # clone a git repository from github using ssh connection
29 | git clone git@github.com:$username/$repoName.git
30 | echo "We finished cloning"
31 | exit 0
--------------------------------------------------------------------------------
/src/scripts/findDockerfiles.sh:
--------------------------------------------------------------------------------
1 | # Find Dockerfiles in chosen repository
2 | projectFolder=$1
3 | FILE=*[dD]ocker*
4 | # Folder path WILL change in production
5 | find ./myProjects/"$projectFolder" -name "$FILE" -type f
6 | # allows for error output if no
7 | if test $(find ./myProjects/"$projectFolder" -name "$FILE" -type f| wc -c) -eq 0
8 | then
9 | echo missing repository with Dockerfile
10 | exit 1
11 | fi
12 | exit 0
13 |
--------------------------------------------------------------------------------
/src/scripts/sshKeyDelete.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | # Script to generate an SSH key and add github.com to known hosts
3 |
4 | # get the sock address of our ssh agent so that we can connect to it
5 | temp_sock=$(cat ./tmpAgent/agentSock)
6 | # add the ssh agent sock to the environment variables
7 | export SSH_AUTH_SOCK=$temp_sock
8 |
9 | # get the process id of the ssh-agent we created in sshKeygen.sh
10 | agent_pid=$(cat ./tmpAgent/agentPID)
11 |
12 | # Kill the ssh-agent which we created in sshKeygen.sh
13 | ssh-add -D $agent_pid >/dev/null
14 |
15 | # remove the ./tmpKeys and .tmpAgent folders and all contents
16 | rm -rf ./tmpKeys
17 | rm -rf ./tmpAgent
18 |
19 | exit 0
--------------------------------------------------------------------------------
/src/scripts/sshKeygen.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | # Script to generate an SSH key and add github.com to known hosts
3 |
4 | # remove temporary folders that store agent info and ssh keys, if they exist
5 | if [ -d './tmpKeys' ]
6 | then
7 | rm -rf ./tmpKeys
8 | elif [ -d './tmpAgent' ]
9 | then
10 | rm -rf ./tmpAgent
11 | fi
12 |
13 | # create temporary folders to store agent info and ssh keys
14 | mkdir ./tmpKeys
15 | mkdir ./tmpAgent
16 |
17 | # generate public and private SSH keys with no password
18 | ssh-keygen -t ed25519 -f ./tmpKeys/dockerKey -g -N ""
19 | # add github.com to ssh known hosts so that we can ssh connect to github.com
20 | ssh-keyscan -H github.com -y >> ~/.ssh/known_hosts
21 |
22 | # start ssh agent
23 | eval $(ssh-agent -s)
24 |
25 | # save ssh agent sock info and agent PID so that we can connect later
26 | echo $SSH_AUTH_SOCK > ./tmpAgent/agentSock
27 | echo $SSH_AGENT_PID > ./tmpAgent/agentPID
28 |
29 | # add ssh key to ssh agent
30 | ssh-add ./tmpKeys/dockerKey
31 | exit 0
--------------------------------------------------------------------------------
/src/server/config/passport-setup.ts:
--------------------------------------------------------------------------------
1 | export { };
2 |
3 | require("dotenv").config();
4 | const passport = require("passport");
5 | const GithubStrategy = require("passport-github2");
6 | const { GITHUB_CLIENT_ID, GITHUB_CLIENT_SECRET } = process.env;
7 | // Configure the Github strategy for use by Passport.
8 |
9 | // OAuth 2.0-based strategies require a `verify` function which receives the
10 | // credential (`accessToken`) for accessing the Github API on the user's
11 | // behalf, along with the user's profile. The function must invoke callback
12 | // with a user object, which will be set at `req.user` in route handlers after
13 | // authentication.
14 |
15 | passport.use(
16 | new GithubStrategy(
17 | {
18 | // https://github.com/login/oauth/authorize
19 | // will be given through API. used to identify our app to github
20 | clientID: GITHUB_CLIENT_ID,
21 | // will be given through API. used to identify our app to github
22 | clientSecret: GITHUB_CLIENT_SECRET,
23 | // callback url that sends client to github login page
24 | callbackURL: "/auth/github/callback",
25 | },
26 | (
27 | accessToken: string,
28 | refreshToken: string,
29 | profile: any,
30 | done: Function
31 | ) => {
32 | // basic 4 params -> getting github profile information from auth-route
33 | /** passport callback fn
34 | * accessToken - is how we will make an API call on behalf of the user. It is sent to us by github in the response.
35 | * refreshToken - is a token that can refresh the access token if it 'times out'.
36 | * profile - is the user's record in Github. We associate this profile with a user record in our application database.
37 | * done - after getting successully authenticated - run this callback function
38 | * routes to 'authenticated page' w/ correct user information
39 | **/
40 | const { username } = profile;
41 |
42 | // in case we want to use profile picture
43 | const { avatar_url:profilePic } = profile;
44 |
45 | const payload: object = {
46 | username,
47 | accessToken,
48 | };
49 | done(null, payload);
50 | }
51 | )
52 | );
53 |
54 | /** Configure Passport authenticated session persistence.
55 | *
56 | * In order to restore authentication state across HTTP requests, Passport needs
57 | * to serialize users into and deserialize users out of the session. We
58 | * supply the user ID when serializing, and query the user record by ID
59 | * from the database when deserializing.
60 | **/
61 | passport.serializeUser((payload: object, done: Function) => done(null, payload));
62 |
63 | passport.deserializeUser((payload: string, done: Function) =>done(null, payload));
64 |
65 | module.exports = passport.serializeUser, passport.deserializeUser;
66 |
--------------------------------------------------------------------------------
/src/server/controllers/authController.ts:
--------------------------------------------------------------------------------
1 | export {};
2 | import { Request, Response, NextFunction } from "express";
3 | import CryptoJS = require("crypto-js");
4 |
5 | const authController: any = {};
6 |
7 | // Middleware to get username and access token from passport-setup.js's done callback (payload)
8 | authController.saveAccessToken = (
9 | req: Request,
10 | res: Response,
11 | next: NextFunction
12 | ): void => {
13 | // Destructure username/accessToken from req.user
14 | const { username, accessToken }: any = req.user;
15 |
16 | // CryptoJS -> encrypt accessToken with AES and a "super_secret" password
17 | const encrypted = CryptoJS.AES.encrypt(
18 | accessToken,
19 | "super_secret"
20 | ).toString();
21 | // Save encrypted token as cookie
22 | res.cookie("token", encrypted, { maxAge: 360000 });
23 |
24 | // Save username as cookie
25 | res.cookie("username", username, { maxAge: 360000 });
26 |
27 | // error handling
28 | if (!(username || encrypted)){
29 | return next({
30 | log: `Error caught in authContoller.saveAccessToken: Missing username: ${username} or encrypted: ${Boolean(encrypted)}`,
31 | msg: { err: 'authContoller.saveAccessToken: ERROR: Check server logs for details'}
32 | })
33 | }
34 |
35 | return next();
36 | };
37 |
38 | // Middleware to get username and access token from cookies and store each in locals
39 | authController.getNameAndTokenFromCookies = (
40 | req: Request,
41 | res: Response,
42 | next: NextFunction
43 | ): void => {
44 | // Destructure username and token from cookies
45 | const { username, token }: {username: string; token: string} = req.cookies;
46 |
47 | // CryptoJS -> decrypt accessToken and convert back to original
48 | const decrypted = CryptoJS.AES.decrypt(token, "super_secret").toString(
49 | CryptoJS.enc.Utf8
50 | );
51 |
52 | // Store decrypted access token in locals
53 | res.locals.accessToken = decrypted;
54 |
55 | // Store username in locals
56 | res.locals.username = username;
57 |
58 | // error handling
59 | if (!(decrypted || username)){
60 | return next({
61 | log: `Error caught in authContoller.getNameAndTokenFromCookies: Missing ${username}, decrypt: ${Boolean(decrypted)}`,
62 | msg: { err: 'authController.getNameAndTokenFromCookies: ERROR: Check server logs for details'}
63 | });
64 | }
65 |
66 | return next();
67 | };
68 |
69 | // USE THIS MIDDLEWARE TO GET CHECKED REPO DATA
70 | authController.saveUserInfoAndRepos = (
71 | req: Request,
72 | res: Response,
73 | next: NextFunction
74 | ): void => {
75 | // save username, access token and repos from request body
76 | const { username, accessToken, repos, projectName }: {username: string; accessToken: string; repos: string []; projectName: string} = req.body;
77 | res.locals.username = username;
78 | res.locals.accessToken = accessToken;
79 | res.locals.repos = repos;
80 | res.locals.projectName = projectName;
81 |
82 | // error handling
83 | if (!(username || accessToken || repos || projectName)){
84 | return next({
85 | log: `Error caught in authController.saveUserInfoAndRepos: Missing username: ${username}, accessToken:${Boolean(accessToken)} , repos:${repos}, or projectName ${projectName}`,
86 | msg: { err: `authController.saveUserInfoAndRepos: ERROR: Check server logs for details`}
87 | });
88 | }
89 |
90 | return next();
91 | };
92 |
93 | module.exports = authController;
--------------------------------------------------------------------------------
/src/server/controllers/configController.ts:
--------------------------------------------------------------------------------
1 | export { };
2 | import { Request, Response, NextFunction } from 'express';
3 | import fs = require('fs');
4 | import path = require('path');
5 |
6 | const configController: any = {};
7 |
8 | // for GET
9 | // check for folder
10 | configController.readJSONFromFile = async (req: Request, res: Response, next: NextFunction): Promise => {
11 | // Get JSON data from local file
12 | // save to res.locals.projects
13 |
14 | const filePath = path.join(__dirname, '../../user-projects/projects.json');
15 | try {
16 | const data = fs.readFileSync(filePath, 'utf8')
17 | res.locals.projects = data;
18 | return next();
19 | } catch (error) {
20 | return next({
21 | log: `Error caught in configController- readJSONFromFile ${error}`,
22 | status: 500,
23 | msg: {
24 | err: 'configController.readFromJSONFile: ERROR: Check server log for details'
25 | }
26 | })
27 | }
28 |
29 | };
30 |
31 | // for POST
32 | configController.writeJSONToFile = async (req: Request, res: Response, next: NextFunction): Promise => {
33 | // takes in json from req.body;
34 | // writes req.body JSON to local file
35 |
36 | const filePath = path.join(__dirname, '../../user-projects/projects.json');
37 |
38 | try{
39 | fs.writeFileSync(filePath, JSON.stringify(req.body));
40 | return next();
41 | } catch (error) {
42 | return next({
43 | log: `Error caught in configController- writeJSONToFile ${error}`,
44 | status: 500,
45 | msg: {
46 | err: 'configController.writeJSONToFile: ERROR: Check server log for details',
47 | }
48 | })
49 | }
50 | };
51 |
52 | module.exports = configController;
--------------------------------------------------------------------------------
/src/server/controllers/dockerController.ts:
--------------------------------------------------------------------------------
1 | export { };
2 | import { Request, Response, NextFunction } from 'express';
3 | import { exec } from 'child_process';
4 | import fs = require('fs');
5 | let portNo = 5001;
6 | let dockerPortNo = portNo;
7 |
8 | // WORKING ASSUMPTIONS:
9 | // All chosen directories will be stored in the same folder so that findDockerfiles.sh will access it
10 | // My Repos folder will exist before dockerController is run
11 | // Docker-Compose file will be stored in DockerLocal/myProjects
12 | // Dockerfiles will have docker in the name and no other files will have docker in the name
13 | // Dockerfiles will be located in the root folder of a project and so will be descriptive of the project(i.e. Container Name)
14 |
15 | const dockerController: any = {};
16 |
17 | /**
18 | * @middlware getFilePaths
19 | * @description Insert and run (findDockerfile.sh) inside repo root directory to find all dockerfile build paths
20 | */
21 | dockerController.getFilePaths = (req: Request, res: Response, next: NextFunction): void => {
22 | // the name of the project folder that you are storing all the repos inside DockerLocal/myProjects/
23 | const projectFolder: string = req.body.projectName;
24 | const buildPathArray: string[] = [];
25 | const myShellScript = exec(`sh src/scripts/findDockerfiles.sh ${projectFolder}`);
26 | myShellScript.stdout.on('data', (data: string) => {
27 | const output = data;
28 |
29 | // checking for shell script error message output caused by lack of dockerfile inside active Project
30 | if (output === "missing repository with Dockerfile\n"){
31 | return next({
32 | log: `ERROR caught in dockerController.getFilePaths SHELL SCRIPT: ${data.slice(0, data.length-1)} in ${projectFolder}`,
33 | msg: { err: 'dockerContoller.getFilePaths: ERROR: Check server logs for details' }
34 | });
35 | }
36 |
37 | // get filepaths from one long data string
38 | const filePathArray: string[] = output.split('\n').slice(0, -1);
39 | let buildPath: string;
40 |
41 | // make filepaths into buildpaths by removing the name of the file from the path
42 | // "src/server/happy/dockerfile" => "src/server/happy"
43 | for (const filePath of filePathArray) {
44 | for (let char = filePath.length - 1; char >= 0; char--) {
45 | if (filePath[char] === '/') {
46 | buildPath = filePath.substring(0, char);
47 | buildPathArray.push(buildPath);
48 | break;
49 | }
50 | }
51 | }
52 | res.locals.buildPathArray = buildPathArray;
53 |
54 | return next();
55 | });
56 |
57 | // shell script errror handling
58 | myShellScript.stderr.on('data', (data: string) => {
59 | return next({
60 | log: `ERROR caught in dockerController.getFilePaths SHELL SCRIPT: ${data}`,
61 | msg: { err: 'dockerContoller.getFilePaths: ERROR: Check server logs for details' }
62 | });
63 | })
64 |
65 | }
66 |
67 |
68 | /**
69 | * @middlware getContainerNames
70 | * @description Use build paths to get Container Names
71 | */
72 | dockerController.getContainerNames = (req: Request, res: Response, next: NextFunction): void => {
73 | const containerNameArray: string[] = [];
74 | const { buildPathArray } = res.locals;
75 | let containerName: string;
76 | // use folder names as the container name
77 | // "src/server/happy" => "happy"
78 | for (const buildPath of buildPathArray) {
79 | for (let char = buildPath.length - 1; char >= 0; char--) {
80 | if (buildPath[char] === '/') {
81 | containerName = buildPath.substring(char + 1);
82 | containerNameArray.push(containerName);
83 | break;
84 | }
85 | }
86 | }
87 | res.locals.containerNameArray = containerNameArray;
88 |
89 | // error handling
90 | if (!(buildPathArray || containerNameArray)){
91 | return next({
92 | log: `Error caught in dockerContoller.getContainerNames: Missing containerNameArray: ${Boolean(containerNameArray)} or buildPathArray: ${Boolean(buildPathArray)}`,
93 | msg: { err: `dockerController.getContainerNames: ERROR: Check server log for details. `}
94 | });
95 | }
96 |
97 | return next();
98 | }
99 |
100 |
101 | /**
102 | * @middlware getContainerNames
103 | * @description Use container names and build paths to create docker compose file
104 | */
105 | dockerController.createDockerCompose = (req: Request, res: Response, next: NextFunction): void => {
106 | const projectFolder: string = req.body.projectName;
107 | const { buildPathArray } = res.locals;
108 | const { containerNameArray } = res.locals;
109 | let directory: string;
110 | let containerName: string;
111 | const composeFilePath = `./myProjects/${projectFolder}/docker-compose.yaml`
112 |
113 | /* writeFile will create a new docker compose file each time the controller is run
114 | so user can have leave-one-out functionality. Indentation is important in yaml files so it looks weird on purpose */
115 | try {
116 | fs.writeFileSync(composeFilePath, `version: "3"\nservices:\n`);
117 | } catch(error){
118 | return next({
119 | log: `ERROR in writeFileSync in dockerController.createDockerCompose: ${error}`,
120 | msg: { err: 'dockerController.createDockerCompose: ERROR: Check server log for details' }
121 | })
122 | }
123 |
124 | // Taking the 'checked' repositories and storing each name into an array
125 | const { repos } = res.locals;
126 | const repoArray = [];
127 | for (const repo of repos) {
128 | repoArray.push(repo.repoName);
129 | }
130 |
131 | // adding service information to docker compose file
132 | for (let i = 0; i < buildPathArray.length; i++) {
133 | directory = buildPathArray[i];
134 | containerName = containerNameArray[i];
135 | // only gets repos stored in the active Project that have dockerfiles (using buildPath to grab repo folder)
136 | const repoFolder = directory.slice(14 + projectFolder.length, directory.length - containerName.length - 1);
137 |
138 | // if the repo folder is in the 'checked' repositories array then add it to the docker compose file
139 | // will also ignore docker-compose file we create that is stored in root project folder
140 | if (repoArray.includes(repoFolder)) {
141 | portNo++;
142 | dockerPortNo++;
143 |
144 | // appending the file with the configurations for each service and error handling
145 | try{
146 | fs.appendFileSync(composeFilePath,
147 | ` ${containerName}:\n build: "${directory}"\n ports:\n - ${portNo}:${dockerPortNo}\n`);
148 | } catch (error){
149 | return next({
150 | log: `ERROR in appendFileSync in dockerController.createDockerCompose: ${error}`,
151 | msg: { err: 'dockerController.createDockerCompose appendFile: ERROR: Check server log for details' }
152 | });
153 | }
154 |
155 | }
156 | }
157 |
158 | return next();
159 | }
160 |
161 | module.exports = dockerController;
--------------------------------------------------------------------------------
/src/server/controllers/gitController.ts:
--------------------------------------------------------------------------------
1 | export {};
2 |
3 | import { Request, Response, NextFunction } from "express";
4 | import { Repo } from '../../types/types'
5 |
6 | // import helper function to execute shell scripts
7 | const execShellCommand = require("./helpers/shellHelper");
8 |
9 | const gitController: any = {};
10 |
11 | /**
12 | * @middleware Clone Github repositor(y/ies) using an SSH connection
13 | * @desc Clones git repository from github. Expects repository info to be in res.locals.
14 | */
15 | gitController.cloneRepo = async (
16 | req: Request,
17 | res: Response,
18 | next: NextFunction
19 | ): Promise => {
20 |
21 | const { repos, projectName }: {repos: Repo[]; projectName: string} = res.locals;
22 | const shellCommand = "./src/scripts/cloneRepo.sh";
23 |
24 | // make an array of promises to clone all selected repos
25 | const promises = repos.map(async (currentRepo: {repoOwner: string; repoName: string}) => {
26 | const repoOwner = currentRepo.repoOwner;
27 | const repoName = currentRepo.repoName;
28 |
29 | if (!(repoOwner || repoName)){
30 | return next({
31 | log: `Error caught in gitContoller.cloneRepo: Missing repoOwner: ${repoOwner} or name ${repoName}`,
32 | msg: { err: 'gitContoller.cloneRepo: ERROR: Check server logs for more details'}
33 | });
34 | }
35 |
36 | // shell script clones github repo using SSH connection
37 | const shellResp = await execShellCommand(shellCommand, [
38 | repoOwner,
39 | repoName,
40 | projectName,
41 | ]);
42 |
43 | return shellResp;
44 | });
45 |
46 | // execute cloning ALL repos
47 | await Promise.all(promises).catch(
48 | err => next({
49 | log: `Error in shell scripts cloning repos in gitController.cloneRepo: ${err}`,
50 | msg: {
51 | err: 'gitContoller.cloneRepo: ERROR: Check server logs for more details'
52 | }
53 | })
54 | )
55 |
56 | return next();
57 | };
58 |
59 | module.exports = gitController;
60 |
--------------------------------------------------------------------------------
/src/server/controllers/helpers/shellHelper.ts:
--------------------------------------------------------------------------------
1 | export {};
2 |
3 | const exec = require("child_process").execFile;
4 |
5 | /**
6 | * @function Executes a shell command and return it as a Promise.
7 | * @param shellCommand {"./src/scripts/cloneRepo.sh"}
8 | * @param args {[repoOwner, repoName, projectName ]}
9 | * @return {Promise}
10 | */
11 |
12 | function execShellCommand(shellCommand: string, args: string []): Promise {
13 | return new Promise((resolve, reject) => {
14 | exec(
15 | shellCommand,
16 | args,
17 | (error: string, stdout: string, stderr: string) => {
18 | if (error) {
19 | console.warn(error);
20 | }
21 | resolve(stdout? stdout : stderr);
22 | }
23 | );
24 | });
25 | }
26 |
27 | module.exports = execShellCommand;
28 |
--------------------------------------------------------------------------------
/src/server/controllers/sshKeyController.ts:
--------------------------------------------------------------------------------
1 | export {};
2 |
3 | import { Request, Response, NextFunction } from "express";
4 | const fetch = require("node-fetch").default;
5 | const fs = require("fs");
6 |
7 | // import helper function to execute shell scripts
8 | const execShellCommand = require("./helpers/shellHelper");
9 |
10 | const sshKeyController: any = {};
11 |
12 | /**
13 | * @middleware Create SSH key to be used for connection to clone/update github repos
14 | * @desc Executes sshKeygen.sh
15 | */
16 | sshKeyController.createSSHkey = async(
17 | req: Request,
18 | res: Response,
19 | next: NextFunction
20 | ): Promise => {
21 |
22 | // shell script adds the github.com domain name to the known_hosts file using the ssh-keyscan command
23 | // script then clones github repo using SSH connection
24 | const shellCommand = "./src/scripts/sshKeygen.sh";
25 |
26 | const shellResult = await execShellCommand(shellCommand, [])
27 | if (shellResult instanceof Error){
28 | return next({
29 | log: `Error caught in sshKeyController.createSSHkey: execShellCommand produces error: ${shellResult}`,
30 | msg: {err:`sshKeyController.createSSHkey: ERROR: Check server logs for details`},
31 | })
32 | }
33 | return next();
34 | };
35 |
36 | /**
37 | * @middleware Push SSH key to user's github account
38 | * @desc Uses a post request to github API
39 | */
40 | sshKeyController.addSSHkeyToGithub = async (
41 | req: Request,
42 | res: Response,
43 | next: NextFunction
44 | ): Promise => {
45 | const { accessToken, username } = res.locals;
46 |
47 | const sshKey = fs.readFileSync("./tmpKeys/dockerKey.pub", "utf8");
48 |
49 | // create the request body which we will use to create the ssh key on Github
50 | const reqBody = JSON.stringify({
51 | title: `${username}@DockerLocal`,
52 | key: sshKey,
53 | });
54 |
55 | // send a post request to Github api to add the ssh key to user's keys
56 | const url = `https://api.github.com/user/keys`;
57 | const response = await fetch(url, {
58 | method: "POST",
59 | headers: {
60 | Authorization: `Bearer ${accessToken}`,
61 | "Content-type": "application/json",
62 | },
63 | body: reqBody,
64 | }).catch((err: Error) => next({
65 | log: `Error caught in sshKeyController.addSSHkeyToGithub: Issue sending post request to Github API: ${err}`,
66 | msg: {err:'sshKeyController.addSSHkeyToGithub: ERROR: Check server logs for details'}
67 | }));
68 |
69 | // converts the response body into JSON
70 | const jsonResponse = await response.json();
71 |
72 | // save the key id from the response. this will be used to delete the key from Github after we are done using it
73 | const { id } = jsonResponse;
74 | res.locals.keyId = id;
75 | return next();
76 | };
77 |
78 | /**
79 | * @middleware Deletes public SSH key from user's github account and private/public keys from ./tmpKeys folder
80 | * @desc Uses a post request to github API
81 | */
82 | sshKeyController.deleteSSHkey = async (
83 | req: Request,
84 | res: Response,
85 | next: NextFunction
86 | ): Promise => {
87 | const { accessToken, keyId } = res.locals;
88 |
89 | const shellCommand = "./src/scripts/sshKeyDelete.sh";
90 |
91 | await execShellCommand(shellCommand, []);
92 |
93 | const url = `https://api.github.com/user/keys/${keyId}`;
94 | await fetch(url, {
95 | method: "delete",
96 | headers: {
97 | Authorization: `Bearer ${accessToken}`,
98 | },
99 | }).catch((err: Error) => next({
100 | log: `Error caught in sshKeyController.deleteSSHkey: deleting key from github produces error: ${err}`,
101 | msg: {err: 'sshKeyController.deleteSSHkey: ERROR: Check server logs for details'}
102 | }));
103 |
104 | return next();
105 | };
106 |
107 | module.exports = sshKeyController;
108 |
--------------------------------------------------------------------------------
/src/server/routes/api-route.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable @typescript-eslint/no-var-requires */
2 | export {};
3 | import express, { Request, Response } from "express";
4 | const router = express.Router();
5 | const authController = require("../controllers/authController");
6 | const gitController = require("../controllers/gitController");
7 | const sshKeyController = require("../controllers/sshKeyController");
8 | require("dotenv/config");
9 |
10 | router.post(
11 | "/clonerepos",
12 | authController.saveUserInfoAndRepos,
13 | sshKeyController.createSSHkey,
14 | sshKeyController.addSSHkeyToGithub,
15 | gitController.cloneRepo,
16 | sshKeyController.deleteSSHkey,
17 | (req: Request, res: Response) => res.status(201).json(res.locals.repos)
18 | );
19 |
20 | module.exports = router;
21 |
--------------------------------------------------------------------------------
/src/server/routes/auth-route.ts:
--------------------------------------------------------------------------------
1 | export { };
2 | import express, { Request, Response } from 'express';
3 | const router = express.Router();
4 | const passport = require('passport');
5 | const authController = require('../controllers/authController');
6 | declare const MAIN_WINDOW_WEBPACK_ENTRY: any;
7 |
8 |
9 | // using passport to authenticate the github
10 | router.get(
11 | '/github',
12 | passport.authenticate('github', {
13 | scope: ['user','repo','admin:public_key'],
14 | 'X-OAuth-Scopes': ['repo', 'user'],
15 | 'X-Accepted-OAuth-Scopes': ['repo','user']
16 | }));
17 |
18 | // Github callback function (authentication)
19 | // if successful
20 | // save username/accessToken to cookies
21 | // redirect to /api/repos
22 | // if unsuccessful
23 | // redirect to /fail
24 | router.get(
25 | '/github/callback',
26 | passport.authenticate('github', {
27 | failureRedirect: '/fail'
28 | }),
29 | authController.saveAccessToken,
30 | (req: Request, res: Response) => {
31 | res.redirect(MAIN_WINDOW_WEBPACK_ENTRY)
32 | }
33 | );
34 |
35 | module.exports = router;
36 |
--------------------------------------------------------------------------------
/src/server/routes/config-route.ts:
--------------------------------------------------------------------------------
1 | export { };
2 | import express, { Request, Response } from 'express';
3 | const router = express.Router();
4 | const configController = require('../controllers/configController');
5 |
6 | // /config endpoint to send back saved project data from local file
7 | router.get('/',
8 | configController.readJSONFromFile,
9 | (req: Request, res: Response) => res.status(200).send(JSON.parse(res.locals.projects)));
10 |
11 | // /config endpoint to write incoming json object to local file
12 | router.post('/',
13 | configController.writeJSONToFile,
14 | (req: Request, res: Response) => res.sendStatus(201));
15 |
16 | module.exports = router;
--------------------------------------------------------------------------------
/src/server/routes/docker-route.ts:
--------------------------------------------------------------------------------
1 | export { };
2 | import express, { Request, Response } from 'express';
3 | import path from 'path';
4 | import fs = require('fs');
5 | const router = express.Router();
6 | const authController = require('../controllers/authController');
7 | const dockerController = require('../controllers/dockerController');
8 |
9 | // creates docker compose file using project folder names
10 | router.post(
11 | '/',
12 | authController.saveUserInfoAndRepos,
13 | dockerController.getFilePaths,
14 | dockerController.getContainerNames,
15 | dockerController.createDockerCompose,
16 | (req: Request, res: Response) => {
17 | // gets project folder name to provide docker compose correct file path to front end
18 | // so end user knows where to find their docker compose file in their system
19 | const projectFolder = req.body.projectName;
20 | // send docker compose filepath and yaml file to front end
21 | const composeFilePath = path.resolve(__dirname, `../../myProjects/${projectFolder}/docker-compose.yaml`);
22 | const fileAsJSON = fs.readFileSync(composeFilePath, "utf8");
23 | const payload = {
24 | path: composeFilePath,
25 | file: fileAsJSON
26 | }
27 | res.status(200).send(payload);
28 | }
29 | )
30 |
31 | module.exports = router;
--------------------------------------------------------------------------------
/src/server/server.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable @typescript-eslint/no-var-requires */
2 | export {};
3 | import express, { Request, Response, NextFunction, ErrorRequestHandler } from "express";
4 | import path = require("path");
5 | // const passportSetup = require("../../src/server/config/passport-setup");
6 | // import passport = require("passport");
7 | require("dotenv/config");
8 | const app = express();
9 | const cookieParser = require("cookie-parser");
10 | const cors = require("cors");
11 |
12 | // disables 'powered by express' header
13 | app.disable('x-powered-by')
14 |
15 | // only allow CORS from react front end
16 | declare const MAIN_WINDOW_WEBPACK_ENTRY: string;
17 | const reactOrigin = MAIN_WINDOW_WEBPACK_ENTRY.substring(0, MAIN_WINDOW_WEBPACK_ENTRY.lastIndexOf("/"))
18 | const corsOptions = { origin: reactOrigin}
19 | app.use(cors(corsOptions));
20 |
21 | // Bring in routes
22 | const authRoute = require('../../src/server/routes/auth-route');
23 | const apiRoute = require('../../src/server/routes/api-route');
24 | const dockerRoute = require('../../src/server/routes/docker-route');
25 | const configRoute = require("../../src/server/routes/config-route");
26 |
27 | // Body Parsing Middleware
28 | app.use(express.json());
29 | app.use(express.urlencoded({ extended: false }));
30 | // app.use(passport.initialize());
31 | app.use(cookieParser());
32 |
33 | // Use routes
34 | app.use('/auth', authRoute);
35 | app.use('/api', apiRoute);
36 | app.use('/docker', dockerRoute);
37 | app.use("/config", configRoute);
38 |
39 | // Serve static files
40 | app.use(express.static("assets"));
41 |
42 | // Home endpoint
43 | app.get("/", (req: Request, res: Response) =>
44 | res.sendFile(path.resolve(__dirname, "../../src/index.html"))
45 | );
46 |
47 | // Handle redirections
48 | app.get("*", (req: Request, res: Response) => res.sendStatus(200));
49 |
50 | // Failed auth redirect
51 | app.get("/fail", (req: Request, res: Response) =>
52 | res.status(200).send("❌ FAILURE TO AUTHENTICATE ❌")
53 | );
54 |
55 | // Global Error handler
56 | app.use(
57 | (
58 | err: ErrorRequestHandler,
59 | req: Request,
60 | res: Response,
61 | next: NextFunction
62 | ) => {
63 | // Set up default error
64 | const defaultError = {
65 | log: "Error caught in global error handler",
66 | status: 500,
67 | msg: {
68 | err,
69 | },
70 | };
71 |
72 | // Update default error message with provided error if there is one
73 | const output = Object.assign(defaultError, err);
74 | console.log(output.log);
75 | res.send(output.msg);
76 | }
77 | );
78 |
79 | const PORT = 3001;
80 |
81 | app.listen(PORT, () => console.log(`Server running on port ${PORT}`));
82 |
--------------------------------------------------------------------------------
/src/types/types.tsx:
--------------------------------------------------------------------------------
1 | import { SetStateAction, Dispatch } from 'react';
2 |
3 | export type Project = {
4 | projectName: string;
5 | projectId: string;
6 | projectRepos: readonly Repo[];
7 | };
8 |
9 | export type projectListActions = {
10 | type:
11 | | 'addProject'
12 | | 'deleteProject'
13 | | 'addRepo'
14 | | 'deleteRepo'
15 | | 'toggleRepo';
16 | payload: readonly Project[];
17 | };
18 |
19 | export type Repo = {
20 | repoName: string;
21 | repoId: string;
22 | repoOwner: string;
23 | isIncluded?: boolean;
24 | };
25 |
26 | export type User = {
27 | userName: string;
28 | userId: string;
29 | };
30 |
31 | export type ProjectReposType = {
32 | personal: readonly Repo[];
33 | organizations: readonly Repo[];
34 | collaborations: readonly Repo[];
35 | };
36 |
37 | export type RepoSearchListItemProps = {
38 | repo: Repo;
39 | selectedRepos: readonly Repo[];
40 | setSelectedRepos: Dispatch>;
41 | projectList: readonly Project[];
42 | activeProject: string;
43 | };
44 |
45 | export type AddReposProps = {
46 | showAddRepos: boolean;
47 | setShowAddRepos: Dispatch>;
48 | activeProject: string;
49 | projectList: readonly Project[];
50 | dispatch: React.Dispatch;
51 | };
52 |
53 | export type ProjectPageProps = {
54 | activeProject: string;
55 | userInfo: User;
56 | projectList: readonly Project[];
57 | dispatch: React.Dispatch;
58 | };
59 |
60 | export type ProjectRepoListItemProps = {
61 | activeProject: string;
62 | repo: Repo;
63 | projectList: readonly Project[];
64 | dispatch: React.Dispatch;
65 | };
66 |
67 | export type RepoResponseType = {
68 | personal: readonly Repo[];
69 | organizations: readonly Repo[];
70 | collaborations: readonly Repo[];
71 | };
72 |
73 | export type ProjectSideBarProps = {
74 | setShowProjectSidebarModal: Dispatch>;
75 | projectList: readonly Project[];
76 | dispatch: React.Dispatch;
77 | setActiveProject: Dispatch>;
78 | };
79 |
80 | export type SidebarButtonProps = Project & {
81 | activeProject: string;
82 | setActiveProject: Dispatch>;
83 | };
84 |
85 | export type SidebarProps = {
86 | projectList: readonly Project[];
87 | dispatch: React.Dispatch;
88 | activeProject: string;
89 | setActiveProject: Dispatch>;
90 | };
91 |
92 | export type HomeProps = {
93 | userInfo: User;
94 | setUserInfo: Dispatch>;
95 | };
96 |
97 | export type CloningReposModalProps = {
98 | showCloningReposModal: boolean;
99 | setShowCloningReposModal: Dispatch>;
100 | };
101 |
102 | export type ComposeFileModalProps = {
103 | setShowComposeModal: Dispatch>;
104 | activeProject: string;
105 | projectList: readonly Project[];
106 | composeFileData: { text: string; path: string };
107 | };
108 |
109 | export type ComposeFile = {
110 | text: string;
111 | path: string;
112 | };
113 |
114 | export type SignInProps = {
115 | isLoggedIn: boolean;
116 | setIsLoggedIn: Dispatch>;
117 | setUserInfo: Dispatch>;
118 | }
119 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "jsx": "react",
4 | "allowJs": true,
5 | "module": "commonjs",
6 | "skipLibCheck": true,
7 | "esModuleInterop": true,
8 | "noImplicitAny": true,
9 | "sourceMap": true,
10 | "baseUrl": ".",
11 | "outDir": "dist",
12 | "moduleResolution": "node",
13 | "resolveJsonModule": true,
14 | "paths": {
15 | "*": [
16 | "node_modules/*"
17 | ]
18 | }
19 | },
20 | "include": [
21 | "src/**/*"
22 | ]
23 | }
24 |
--------------------------------------------------------------------------------
/tslint.json:
--------------------------------------------------------------------------------
1 | {
2 | "defaultSeverity": "error",
3 | "extends": [
4 | "tslint:recommended"
5 | ],
6 | "jsRules": {},
7 | "rules": {
8 | "trailing-comma": [ false ]
9 | },
10 | "rulesDirectory": []
11 | }
--------------------------------------------------------------------------------
/user-projects/empty:
--------------------------------------------------------------------------------
1 | empty
--------------------------------------------------------------------------------
/webpack.main.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | /**
3 | * This is the main entry point for your application, it's the first file
4 | * that runs in the main process.
5 | */
6 | entry: './src/index.ts',
7 | // Put your normal webpack config below here
8 | module: {
9 | rules: require('./webpack.rules'),
10 | },
11 | resolve: {
12 | extensions: ['.js', '.ts', '.jsx', '.tsx', '.css', '.json']
13 | },
14 | };
--------------------------------------------------------------------------------
/webpack.plugins.js:
--------------------------------------------------------------------------------
1 | const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin');
2 |
3 | module.exports = [
4 | new ForkTsCheckerWebpackPlugin()
5 | ];
6 |
--------------------------------------------------------------------------------
/webpack.renderer.config.js:
--------------------------------------------------------------------------------
1 | const rules = require('./webpack.rules');
2 | const plugins = require('./webpack.plugins');
3 |
4 | rules.push({
5 | test: /\.s[ac]ss$/i,
6 | use: [{ loader: 'style-loader' }, { loader: 'css-loader' }, { loader: 'sass-loader' }],
7 | });
8 |
9 | module.exports = {
10 | module: {
11 | rules,
12 | },
13 | plugins: plugins,
14 | resolve: {
15 | extensions: ['.js', '.ts', '.jsx', '.tsx', '.css']
16 | },
17 | };
18 |
--------------------------------------------------------------------------------
/webpack.rules.js:
--------------------------------------------------------------------------------
1 | module.exports = [
2 | // Add support for native node modules
3 | {
4 | test: /\.node$/,
5 | use: 'node-loader',
6 | },
7 | {
8 | test: /\.(m?js|node)$/,
9 | parser: { amd: false },
10 | use: {
11 | loader: '@marshallofsound/webpack-asset-relocator-loader',
12 | options: {
13 | outputAssetBase: 'native_modules',
14 | },
15 | },
16 | },
17 | {
18 | test: /\.tsx?$/,
19 | exclude: /(node_modules|\.webpack)/,
20 | use: {
21 | loader: 'ts-loader',
22 | options: {
23 | transpileOnly: true
24 | }
25 | }
26 | },
27 | {
28 | test: /\.(jpg|png)$/,
29 | use: {
30 | loader: 'url-loader',
31 | }
32 | },
33 | ];
34 |
--------------------------------------------------------------------------------