├── .eslintrc.js
├── .gitignore
├── LICENSE
├── README.md
├── assets
├── CreateStopStartDelete.gif
├── EmitMessages.gif
├── PlannedResponse.gif
└── socketcast_logo.png
├── babel.config.js
├── jest.config.js
├── package.json
├── src
├── ServerManager
│ ├── ServerAbstract.ts
│ ├── ServerManager.ts
│ ├── SocketEvent.js
│ └── type.ts
├── components
│ ├── EventConfig.tsx
│ ├── PlannedResponseCreator
│ │ ├── PRUnit.tsx
│ │ ├── PlannedResponseCreator.tsx
│ │ ├── type.ts
│ │ └── utils.ts
│ ├── ScrollAnchor.tsx
│ ├── ServerForm.tsx
│ ├── Sidebar.tsx
│ ├── StreamDisplay.tsx
│ ├── StreamInput.tsx
│ ├── StreamTab.tsx
│ ├── app.tsx
│ └── input
│ │ └── InputText.tsx
├── index.html
├── index.tsx
├── main.ts
├── store
│ ├── actions
│ │ ├── actionTypes.ts
│ │ ├── messagesActions.ts
│ │ ├── navigationActions.ts
│ │ └── serversActions.ts
│ ├── reducers
│ │ ├── index.ts
│ │ ├── messagesReducer.ts
│ │ ├── navigationReducer.ts
│ │ └── serversReducer.ts
│ └── store.ts
└── styles
│ ├── app.scss
│ └── hack-font
│ ├── fonts
│ ├── hack-bold-subset.woff
│ ├── hack-bold-subset.woff2
│ ├── hack-bold.woff
│ ├── hack-bold.woff2
│ ├── hack-bolditalic-subset.woff
│ ├── hack-bolditalic-subset.woff2
│ ├── hack-bolditalic.woff
│ ├── hack-bolditalic.woff2
│ ├── hack-italic-subset.woff
│ ├── hack-italic-subset.woff2
│ ├── hack-italic.woff
│ ├── hack-italic.woff2
│ ├── hack-regular-subset.woff
│ ├── hack-regular-subset.woff2
│ ├── hack-regular.woff
│ └── hack-regular.woff2
│ ├── hack-subset.css
│ ├── hack-subset.css.in
│ ├── hack.css
│ └── hack.css.in
├── tests
└── components
│ ├── PlannedResponseCreator
│ └── utils.test.ts
│ └── ServerManager
│ └── SocketEvent.test.js
├── tsconfig.json
└── webpack.config.js
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | extends: ['airbnb-typescript'],
3 | // parser: '@typescript-eslint/parser',
4 | parserOptions: {
5 | project: './tsconfig.json',
6 | },
7 | };
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | package-lock.json
2 | node_modules
3 | dist
4 | coverage
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 OSLabs Beta
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 | ## Motivation
17 | Developers working on real-time data applications and services may need to rapidly test their client applications with WebSocket or SSE servers. Socketcast provides developers with a tool to configure and run server instances that serve stub websocket and SSE data to connected clients.
18 |
19 | ## Features
20 | * Configure, launch, and manage multiple WebSocket or SSE servers
21 | * Send data to connected clients
22 | * Plan out a data stream to send connected clients
23 | * Persistent local storage of server configurations
24 |
25 | ## Getting Started
26 |
27 | 1. Fork and clone the Socketcast Repo
28 | 2. Install node dependencies
29 | ```bash
30 | npm i
31 | ```
32 | 3. Bundle the application
33 | ```js
34 | npm run build
35 | ```
36 | 4. Start the application
37 |
38 | ```bash
39 | npm start
40 | ```
41 |
42 | ## Create Servers and Configure Server Instances
43 | - Create one or many stub servers to run in parallel to test your application.
44 | - Choose the WebSocket or Server Side Events (SSE) protocol to connect over.
45 | - Click start server and voila! You have a functioning server to test your application.
46 |
47 |
51 |
52 | ## Send Data
53 | - Insert data into message input
54 | - Emit data!
55 |
56 |
60 |
61 |
62 | ## Create and send Data Streams to Connected Clients
63 | - Connect a client application by opening a websocket connection to the specified port.
64 | - Open stream planner and construct stream with messages and delays.
65 | - Send the stream!
66 |
67 |
71 |
72 | ## Development
73 | ### Built With
74 | * Electron
75 | * TypeScript
76 | * React
77 | * Redux
78 | * Redux-thunk
79 | * Websockets
80 | * Jest
81 | * Webpack
82 |
83 |
84 | ### Client
85 | Socketcast is an Electron cross platform desktop app. It is built with React.
86 |
87 | ### ServerManager
88 | Socketcast has the ability to launch multiple WebSocket servers and it does so through an abstraction called ServerManager. The client application should never manage any servers directly, but instead interact with ServerManager's exposed API through Redux actions.
89 |
90 | ## What's next?
91 | * Add support for other protocols such as HTTP2
92 | * Create the ability to build URL paths for the client application to subscribe selectively to events
93 | * Add ability to parse authentication headers
94 | * Log debugging information from client
95 |
96 | ## Want to contribute?
97 | Socketcast encourages contributions to this product, pull requests are welcome.
98 |
99 | Fork the repo and create a working branch from master.
100 | If you've added any code that requires testing, add tests.
101 | Check to ensure that all tests pass.
102 | Make sure code is formatted with prettier and follows the Airbnb React/JSX Style Guide.
103 | Create a pull request to master.
104 |
105 | Issues
106 | Please do not hesitate to file issues. Socketcast is based off of community feedback and is always looking for ways to get better. We are interested to hear about your experience and how we can improve Socketcast.
107 |
108 |
109 | ## Authors
110 | * Will Bladon [whbladon](https://github.com/whbladon)
111 | * Chris Docuyanan [cjo2](https://github.com/cjo2)
112 | * Chance Hernandez [ItsChance-BTW](https://github.com/ItsChance-BTW)
113 | * Colin Vandergraaf [colinvandergraaf](https://github.com/colinvandergraaf)
114 |
115 | ## License
116 | [License](https://github.com/oslabs-beta/socketcast/LICENSE)
117 |
--------------------------------------------------------------------------------
/assets/CreateStopStartDelete.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/socketcast/f0f1c7d9f7f414ace50f1dd8045ab91b4d68f858/assets/CreateStopStartDelete.gif
--------------------------------------------------------------------------------
/assets/EmitMessages.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/socketcast/f0f1c7d9f7f414ace50f1dd8045ab91b4d68f858/assets/EmitMessages.gif
--------------------------------------------------------------------------------
/assets/PlannedResponse.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/socketcast/f0f1c7d9f7f414ace50f1dd8045ab91b4d68f858/assets/PlannedResponse.gif
--------------------------------------------------------------------------------
/assets/socketcast_logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/socketcast/f0f1c7d9f7f414ace50f1dd8045ab91b4d68f858/assets/socketcast_logo.png
--------------------------------------------------------------------------------
/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | presets: [
3 | ["@babel/preset-env", { targets: { node: "current" } }],
4 | "@babel/preset-typescript",
5 | ],
6 | };
7 |
--------------------------------------------------------------------------------
/jest.config.js:
--------------------------------------------------------------------------------
1 | /*
2 | * For a detailed explanation regarding each configuration property, visit:
3 | * https://jestjs.io/docs/en/configuration.html
4 | */
5 |
6 | module.exports = {
7 | // All imported modules in your tests should be mocked automatically
8 | // automock: false,
9 |
10 | // Stop running tests after `n` failures
11 | // bail: 0,
12 |
13 | // The directory where Jest should store its cached dependency information
14 | // cacheDirectory: "/private/var/folders/y4/y327ndqj61jgklgthkpm9tyr0000gn/T/jest_dx",
15 |
16 | // Automatically clear mock calls and instances between every test
17 | // clearMocks: false,
18 |
19 | // Indicates whether the coverage information should be collected while executing the test
20 | collectCoverage: true,
21 |
22 | // An array of glob patterns indicating a set of files for which coverage information should be collected
23 | // collectCoverageFrom: undefined,
24 |
25 | // The directory where Jest should output its coverage files
26 | coverageDirectory: "coverage",
27 |
28 | // An array of regexp pattern strings used to skip coverage collection
29 | // coveragePathIgnorePatterns: [
30 | // "/node_modules/"
31 | // ],
32 |
33 | // Indicates which provider should be used to instrument code for coverage
34 | coverageProvider: "v8",
35 |
36 | // A list of reporter names that Jest uses when writing coverage reports
37 | // coverageReporters: [
38 | // "json",
39 | // "text",
40 | // "lcov",
41 | // "clover"
42 | // ],
43 |
44 | // An object that configures minimum threshold enforcement for coverage results
45 | // coverageThreshold: undefined,
46 |
47 | // A path to a custom dependency extractor
48 | // dependencyExtractor: undefined,
49 |
50 | // Make calling deprecated APIs throw helpful error messages
51 | // errorOnDeprecated: false,
52 |
53 | // Force coverage collection from ignored files using an array of glob patterns
54 | // forceCoverageMatch: [],
55 |
56 | // A path to a module which exports an async function that is triggered once before all test suites
57 | // globalSetup: undefined,
58 |
59 | // A path to a module which exports an async function that is triggered once after all test suites
60 | // globalTeardown: undefined,
61 |
62 | // A set of global variables that need to be available in all test environments
63 | // globals: {},
64 |
65 | // The maximum amount of workers used to run your tests. Can be specified as % or a number. E.g. maxWorkers: 10% will use 10% of your CPU amount + 1 as the maximum worker number. maxWorkers: 2 will use a maximum of 2 workers.
66 | // maxWorkers: "50%",
67 |
68 | // An array of directory names to be searched recursively up from the requiring module's location
69 | // moduleDirectories: [
70 | // "node_modules"
71 | // ],
72 |
73 | // An array of file extensions your modules use
74 | // moduleFileExtensions: [
75 | // "js",
76 | // "json",
77 | // "jsx",
78 | // "ts",
79 | // "tsx",
80 | // "node"
81 | // ],
82 |
83 | // A map from regular expressions to module names or to arrays of module names that allow to stub out resources with a single module
84 | // moduleNameMapper: {},
85 |
86 | // An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader
87 | // modulePathIgnorePatterns: [],
88 |
89 | // Activates notifications for test results
90 | // notify: false,
91 |
92 | // An enum that specifies notification mode. Requires { notify: true }
93 | // notifyMode: "failure-change",
94 |
95 | // A preset that is used as a base for Jest's configuration
96 | // preset: undefined,
97 |
98 | // Run tests from one or more projects
99 | // projects: undefined,
100 |
101 | // Use this configuration option to add custom reporters to Jest
102 | // reporters: undefined,
103 |
104 | // Automatically reset mock state between every test
105 | // resetMocks: false,
106 |
107 | // Reset the module registry before running each individual test
108 | // resetModules: false,
109 |
110 | // A path to a custom resolver
111 | // resolver: undefined,
112 |
113 | // Automatically restore mock state between every test
114 | // restoreMocks: false,
115 |
116 | // The root directory that Jest should scan for tests and modules within
117 | // rootDir: undefined,
118 |
119 | // A list of paths to directories that Jest should use to search for files in
120 | // roots: [
121 | // ""
122 | // ],
123 |
124 | // Allows you to use a custom runner instead of Jest's default test runner
125 | // runner: "jest-runner",
126 |
127 | // The paths to modules that run some code to configure or set up the testing environment before each test
128 | // setupFiles: [],
129 |
130 | // A list of paths to modules that run some code to configure or set up the testing framework before each test
131 | // setupFilesAfterEnv: [],
132 |
133 | // The number of seconds after which a test is considered as slow and reported as such in the results.
134 | // slowTestThreshold: 5,
135 |
136 | // A list of paths to snapshot serializer modules Jest should use for snapshot testing
137 | // snapshotSerializers: [],
138 |
139 | // The test environment that will be used for testing
140 | testEnvironment: "node",
141 |
142 | // Options that will be passed to the testEnvironment
143 | // testEnvironmentOptions: {},
144 |
145 | // Adds a location field to test results
146 | // testLocationInResults: false,
147 |
148 | // The glob patterns Jest uses to detect test files
149 | // testMatch: [
150 | // "**/__tests__/**/*.[jt]s?(x)",
151 | // "**/?(*.)+(spec|test).[tj]s?(x)"
152 | // ],
153 |
154 | // An array of regexp pattern strings that are matched against all test paths, matched tests are skipped
155 | // testPathIgnorePatterns: [
156 | // "/node_modules/"
157 | // ],
158 |
159 | // The regexp pattern or array of patterns that Jest uses to detect test files
160 | // testRegex: [],
161 |
162 | // This option allows the use of a custom results processor
163 | // testResultsProcessor: undefined,
164 |
165 | // This option allows use of a custom test runner
166 | // testRunner: "jasmine2",
167 |
168 | // This option sets the URL for the jsdom environment. It is reflected in properties such as location.href
169 | // testURL: "http://localhost",
170 |
171 | // Setting this value to "fake" allows the use of fake timers for functions such as "setTimeout"
172 | // timers: "real",
173 |
174 | // A map from regular expressions to paths to transformers
175 | // transform: undefined,
176 |
177 | // An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation
178 | // transformIgnorePatterns: [
179 | // "/node_modules/",
180 | // "\\.pnp\\.[^\\/]+$"
181 | // ],
182 |
183 | // An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them
184 | // unmockedModulePathPatterns: undefined,
185 |
186 | // Indicates whether each individual test should be reported during the run
187 | // verbose: undefined,
188 |
189 | // An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode
190 | // watchPathIgnorePatterns: [],
191 |
192 | // Whether to use watchman for file crawling
193 | // watchman: true,
194 | };
195 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "socketcast",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "index.js",
6 | "scripts": {
7 | "build": "webpack",
8 | "start": "npm run build && electron dist/main.js",
9 | "quickstart": "electron dist/main.js",
10 | "test": "jest",
11 | "eslint": "npx eslint ./src --ext .js,.jsx,.ts,.tsx"
12 | },
13 | "keywords": [
14 | "websocket",
15 | "websockets"
16 | ],
17 | "author": "",
18 | "license": "ISC",
19 | "dependencies": {
20 | "@material-ui/core": "^4.11.3",
21 | "@material-ui/icons": "^4.11.2",
22 | "cors": "^2.8.5",
23 | "electron": "^11.1.1",
24 | "enzyme": "^3.11.0",
25 | "express": "^4.17.1",
26 | "jest": "^26.6.3",
27 | "normalize.css": "^8.0.1",
28 | "react": "^17.0.1",
29 | "react-dom": "^17.0.1",
30 | "react-highlight": "^0.13.0",
31 | "react-redux": "^7.2.2",
32 | "redux": "^4.0.5",
33 | "redux-thunk": "^2.3.0",
34 | "uuid": "^8.3.2",
35 | "ws": "^7.4.2"
36 | },
37 | "devDependencies": {
38 | "@babel/core": "^7.12.10",
39 | "@babel/preset-env": "^7.12.11",
40 | "@babel/preset-typescript": "^7.12.7",
41 | "@types/cors": "^2.8.9",
42 | "@types/electron-devtools-installer": "^2.2.0",
43 | "@types/express": "^4.17.11",
44 | "@types/node": "^14.14.21",
45 | "@types/react": "^17.0.0",
46 | "@types/react-dom": "^17.0.0",
47 | "@types/react-highlight": "^0.12.2",
48 | "@types/react-redux": "^7.1.15",
49 | "@types/uuid": "^8.3.0",
50 | "@types/ws": "^7.4.0",
51 | "@typescript-eslint/eslint-plugin": "^4.14.0",
52 | "@typescript-eslint/parser": "^4.14.0",
53 | "babel-jest": "^26.6.3",
54 | "css-loader": "^5.0.1",
55 | "electron-devtools-installer": "^3.1.1",
56 | "eslint": "^7.18.0",
57 | "eslint-config-airbnb-typescript": "^12.0.0",
58 | "eslint-plugin-import": "^2.22.1",
59 | "eslint-plugin-jsx-a11y": "^6.4.1",
60 | "eslint-plugin-react": "^7.22.0",
61 | "eslint-plugin-react-hooks": "^4.2.0",
62 | "file-loader": "^6.2.0",
63 | "html-webpack-plugin": "^4.5.1",
64 | "redux-devtools-extension": "^2.13.8",
65 | "sass": "^1.32.4",
66 | "sass-loader": "^10.1.1",
67 | "style-loader": "^2.0.0",
68 | "ts-loader": "^8.0.14",
69 | "typescript": "^4.1.3",
70 | "webpack": "^4.43.0",
71 | "webpack-cli": "^4.3.1"
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/src/ServerManager/ServerAbstract.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable @typescript-eslint/semi */
2 | class ServerAbstract {
3 | public server: any;
4 |
5 | private broadcast: any
6 |
7 | private stopServer: any
8 |
9 | public name: string
10 |
11 | public id: string
12 |
13 | public port: number
14 |
15 | public protocol: any
16 |
17 | constructor(server: any,
18 | { broadcast, stopServer }: { broadcast: any, stopServer?: any },
19 | name: string, id: string, port: number, protocol?: string) {
20 | this.id = id;
21 | this.server = server;
22 | this.broadcast = broadcast;
23 | this.name = name
24 | this.port = port
25 | this.stopServer = stopServer;
26 | this.protocol = protocol
27 | }
28 |
29 | broadcastToAll = (message: any) => this.broadcast(message);
30 |
31 | isListening = () => this.server.listening;
32 |
33 | stop = () => {
34 | this.stopServer();
35 | };
36 | }
37 |
38 | export default ServerAbstract;
39 |
--------------------------------------------------------------------------------
/src/ServerManager/ServerManager.ts:
--------------------------------------------------------------------------------
1 | import http from 'http';
2 | import express, { Request, Response } from 'express';
3 | import { v4 as uuidv4 } from 'uuid';
4 | import cors from 'cors';
5 | import * as WebSocket from 'ws';
6 |
7 | import { ServerConfig, ServerState } from './type';
8 | import ServerAbstract from './ServerAbstract';
9 |
10 | enum ServerStatus {
11 | RUNNING = 'RUNNING',
12 | STOPPED = 'STOPPED',
13 | }
14 |
15 | class ServerManager {
16 | private servers: { [index: string]: ServerAbstract };
17 |
18 | constructor() {
19 | this.servers = {};
20 | }
21 |
22 | /**
23 | * Returns an object containing our servers. The keys are the
24 | * ID of the server within ServerManager.
25 | * @returns {Array.}
26 | */
27 | getServers = () => Object.keys(this.servers).map((currentKey: string) => ({
28 | id: this.servers[currentKey].id,
29 | name: this.servers[currentKey].name,
30 | status: this.servers[currentKey].isListening() ? ServerStatus.RUNNING : ServerStatus.STOPPED,
31 | }));
32 |
33 | /**
34 | * Get an individual ServerAbstract by the id of the server.
35 | * This returns an object that contains a reference the WebSocket server itself.
36 | * @param {string} id
37 | * @
38 | */
39 | getServerAbstract = (id: string) => this.servers[id];
40 |
41 | /**
42 | * Get an individual server's ServerRecord
43 | * @param {string} id
44 | */
45 | getServer = (id: string) => {
46 | const { name } = this.servers[id];
47 | return {
48 | id,
49 | name,
50 | status: this.servers[id].isListening() ? ServerStatus.RUNNING : ServerStatus.STOPPED,
51 | };
52 | };
53 |
54 | /**
55 | * Create a new WebSocket server based on configuration.
56 | * @param {ServerConfig} config - Configuration object for socket.io server
57 | * @param {string} config.name - Configuration object for socket.io server
58 | * @param {Integer} config.port - Port to run the socket.io server on
59 | * @param {Function} config.onConnection - Callback for when a client successfully
60 | * connects to the server
61 | * @param {Function} config.onMessage - Callback for when a message is sent to our
62 | * server by a client
63 | * @param {Function} config.onError - Callback for when an error happens on the
64 | * server. The error will be the only parameter of the callback.
65 | * @returns {Promise}
66 | */
67 | createServer = (config: ServerConfig): Promise => {
68 | const {
69 | name, port, onConnection, onMessage, id, onServerClose, protocol, endpoint
70 | } = config;
71 | const app = express();
72 | const server = new http.Server(app);
73 | const serverId = id || uuidv4();
74 | app.use(cors());
75 |
76 |
77 | if (protocol === "websocket") {
78 | const wss = new WebSocket.Server({ server });
79 |
80 | wss.on('connection', (ws: any, req: object) => {
81 | if (onConnection) onConnection();
82 | ws.on('message', (msg: string) => {
83 | if (onMessage) onMessage(msg, serverId);
84 | });
85 | });
86 |
87 | /**
88 | * TODO Promisify this somehow. The complication is promisifying it is that we
89 | * need to wait messages to be sent to ALL clients. If we don't do this, we assume
90 | * that the message is always successfully emitted at the time it displays on the UI
91 | * (which may not be true)
92 | */
93 |
94 | const broadcast = (message: string) => {
95 | wss.clients.forEach((client: any) => {
96 | client.send(message);
97 | });
98 | };
99 |
100 | const stopServer = () => {
101 | wss.clients.forEach((ws) => {
102 | ws.terminate();
103 | });
104 | server.close().once('close', () => {
105 | if (onServerClose) {
106 | onServerClose({
107 | id: serverId,
108 | name,
109 | port,
110 | protocol: protocol,
111 | status: ServerStatus.STOPPED,
112 | });
113 | }
114 | });
115 | };
116 |
117 | // eslint-disable-next-line max-len
118 | this.servers[serverId] = new ServerAbstract(wss, { broadcast, stopServer }, name, serverId, port, protocol);
119 |
120 | } else {
121 | let clients: any[] = [];
122 | const sseEndpointHandler = (req: Request, res: Response) => {
123 | res.writeHead(200, {
124 | 'Content-Type': 'text/event-stream',
125 | Connection: 'keep-alive',
126 | 'Cache-Control': 'no-cache',
127 | });
128 |
129 | const clientId = uuidv4();
130 | clients.push({ id: clientId, res });
131 |
132 | req.on('close', () => {
133 | clients = clients.filter((client) => client.id !== clientId);
134 | });
135 | };
136 |
137 | const broadcast = (message: string) => {
138 | clients.forEach((client) => client.res.write(message));
139 | };
140 |
141 | // To stop a server, all connections to clients must be closed. Then, you can close the actual server.
142 | const stopServer = () => {
143 | clients.forEach((client) => client.res.end());
144 | server.close().once('close', () => {
145 | if (onServerClose) {
146 | onServerClose({
147 | id: serverId,
148 | name,
149 | port,
150 | status: ServerStatus.STOPPED,
151 | protocol: protocol
152 | });
153 | }
154 | });
155 | };
156 | app.use((endpoint || '/'), sseEndpointHandler);
157 | // eslint-disable-next-line max-len
158 | this.servers[serverId] = new ServerAbstract(server, { broadcast, stopServer }, name, serverId, port);
159 | }
160 |
161 |
162 |
163 |
164 | /**
165 | * We return a promise, but we _cannot_ ever reject this promise because any errors
166 | * that would happen when calling server.listen() will be a Node Event. We choose
167 | * to use a promise, however, because we want to tell our consumers when our server
168 | * is able to listen to an event.
169 | * https://github.com/expressjs/express/issues/2856
170 | */
171 | return new Promise((resolve: Function, reject) => {
172 | server.listen(port || 3000, () => resolve({
173 | id,
174 | name,
175 | port,
176 | protocol,
177 | status: ServerStatus.RUNNING,
178 | }));
179 | });
180 | };
181 |
182 | // createSSEServer = (config: ServerConfig): Promise => {
183 | // const {
184 | // name, port, onConnection, onError, id, endpoint, onServerClose,
185 | // } = config;
186 | // const app = express();
187 | // const server = new http.Server(app);
188 | // const serverId = id || uuidv4();
189 | // let clients: any[] = [];
190 |
191 | // app.use(cors());
192 |
193 | // const sseEndpointHandler = (req: Request, res: Response) => {
194 | // res.writeHead(200, {
195 | // 'Content-Type': 'text/event-stream',
196 | // Connection: 'keep-alive',
197 | // 'Cache-Control': 'no-cache',
198 | // });
199 |
200 | // const clientId = uuidv4();
201 | // clients.push({
202 | // id: clientId,
203 | // res,
204 | // });
205 |
206 | // req.on('close', () => {
207 | // clients = clients.filter((client) => client.id !== clientId);
208 | // });
209 | // };
210 |
211 | // const broadcast = (message: string) => {
212 | // clients.forEach((client) => client.res.write(message));
213 | // };
214 |
215 | // // To stop a server, all connections to clients must be closed. Then, you can close the actual server.
216 | // const stopServer = () => {
217 | // clients.forEach((client) => client.res.end());
218 | // server.close().once('close', () => {
219 | // if (onServerClose) {
220 | // onServerClose({
221 | // id: serverId,
222 | // name,
223 | // port,
224 | // status: ServerStatus.STOPPED,
225 | // });
226 | // }
227 | // });
228 | // };
229 |
230 | // app.use((endpoint || '/'), sseEndpointHandler);
231 |
232 | // // eslint-disable-next-line max-len
233 | // this.servers[serverId] = new ServerAbstract(server, { broadcast, stopServer }, name, serverId, port);
234 |
235 | // return new Promise((resolve: Function, reject) => {
236 | // server.listen(port || 3000, () => resolve({
237 | // id: serverId,
238 | // name,
239 | // port,
240 | // status: ServerStatus.RUNNING,
241 | // }));
242 | // });
243 | // };
244 |
245 | // TODO Promisify this.
246 | /**
247 | * Broadcast a message to all the clients that are connected to the socket
248 | * @param {string} id - The id of the server who's clients you want to send messages to
249 | * @param {string} message - stringified message
250 | * @returns {Promise}
251 | */
252 | broadcastToAll = (id: string, message: string) => {
253 | this.servers[id].broadcastToAll(message);
254 | };
255 |
256 | // TODO Complete this.
257 | /**
258 | * Disconnect all the connections to the socket and close the server from listening to
259 | * new connections.
260 | * @param id
261 | * @returns {Promise}
262 | */
263 | stopServer = (id: number) => {
264 | // Close all connections to the sockets
265 | // call server.stop()
266 | this.servers[id].stop();
267 | };
268 | }
269 |
270 | // Singleton, there should only be one ServerManager running in our app
271 | export default new ServerManager();
272 |
--------------------------------------------------------------------------------
/src/ServerManager/SocketEvent.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Class representing a SocketEvent
3 | */
4 | class SocketEvent {
5 | /**
6 | * @callback onEventCallback
7 | * @param {string} message - This is the message emitted through the socket.io protocol
8 | */
9 |
10 | /**
11 | *
12 | * @param {string} eventName - This is the name of the event the server will be opening a
13 | * socket for. This is also the event name you will subscribe and emit.
14 | * @param {onEventCallback} - The callback function that will be invoked
15 | * when a message is received matching the eventName
16 | */
17 | constructor(eventName, config = {}) {
18 | const { onEvent } = config;
19 |
20 | if (!eventName || !(eventName.constructor === String)) {
21 | throw new Error(`expected parameter eventName to be of type string but got ${eventName.constructor.name} instead.`);
22 | }
23 |
24 | if (onEvent && !(onEvent instanceof Function)) {
25 | throw new Error(`expected parameter config.onEvent to be of type function but got ${onEvent.constructor.name} instead`);
26 | }
27 |
28 | this.eventName = eventName;
29 | this.onEvent = onEvent;
30 | }
31 | }
32 |
33 | module.exports = SocketEvent;
34 |
--------------------------------------------------------------------------------
/src/ServerManager/type.ts:
--------------------------------------------------------------------------------
1 | export interface ServerConfig {
2 | name: string,
3 | port: number,
4 | onConnection?: Function,
5 | onMessage?(message: string, id?: string): void,
6 | onServerClose?(record: ServerState): void,
7 | onError?: Function,
8 | id?: string
9 | endpoint?: string
10 | protocol?: string
11 | }
12 |
13 | export interface ServerState {
14 | id: string,
15 | name: string,
16 | port: number,
17 | status: string,
18 | protocol?: string,
19 | }
20 |
21 | export interface MessagesReducerState {
22 | streams?: any
23 | }
24 |
25 | export enum ProtocolType {
26 | WEBSOCKET,
27 | SERVERSIDEEVENTS,
28 | }
29 |
--------------------------------------------------------------------------------
/src/components/EventConfig.tsx:
--------------------------------------------------------------------------------
1 | /**
2 | * @parentComponent ServerConfig
3 | * @description Unused component - added to configure events
4 | */
5 |
6 | import React from 'react';
7 | import { useDispatch, useSelector } from 'react-redux';
8 | import { setCurrentEventId } from '@/store/actions/navigationActions';
9 | function EventConfig() {
10 | const dispatch = useDispatch();
11 | // @ts-ignore
12 | const currentServerID = useSelector((store) => store.navigation.currentServerId);
13 | // @ts-ignore
14 | const currentServer = useSelector((store) => store.servers.servers[currentServerID]);
15 | const eventsArray: any = [];
16 | if (currentServer) {
17 | currentServer.events.map((event: any) => {
18 | eventsArray.push(
19 | { dispatch(setCurrentEventId(event)) }}>
20 |
21 | {event.eventName}
22 |
23 |
24 |
Event = (name: string, serial: int, message: string)
25 |
26 |
27 | )
28 | })
29 | }
30 | return (
31 |
32 |
Event Stream Manager
33 | {eventsArray}
34 |
35 | );
36 | }
37 |
38 | export default EventConfig;
39 |
--------------------------------------------------------------------------------
/src/components/PlannedResponseCreator/PRUnit.tsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable padded-blocks */
2 | /* eslint-disable arrow-body-style */
3 | /* eslint-disable react/no-unknown-property */
4 | /* eslint-disable @typescript-eslint/semi */
5 | import React from 'react';
6 | import { PlannedResponseUnit, PlannedResponseUnitType } from './type';
7 | import Highlight from 'react-highlight'
8 |
9 | const PRUnit = ({
10 | pru, index, onMoveUp, onMoveDown, onRemove,
11 | }: { pru: PlannedResponseUnit, index: number, onMoveUp: any, onMoveDown: any, onRemove: any }) => {
12 |
13 | /**
14 | * TODO
15 | * Using ternaries probably is not a good idea here because there may be
16 | * a potential for more than two PlannedResponseUnitTypes
17 | */
18 | return (
19 |
20 |
21 |
22 | {pru.type === PlannedResponseUnitType.MESSAGE ? 'Message' : 'Delay'}
23 |
24 |
25 |
26 | Remove
27 | Up
28 | Down
29 |
30 |
31 |
32 |
33 | {pru.type === PlannedResponseUnitType.MESSAGE ?
34 | // @ts-ignore
35 | ({pru.message.toString()} )
36 | : pru.time}
37 |
38 |
39 | );
40 | }
41 |
42 | export default PRUnit;
43 |
--------------------------------------------------------------------------------
/src/components/PlannedResponseCreator/PlannedResponseCreator.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import { playbackResponseUnits } from './utils';
3 | import { PlannedResponseUnit, PlannedResponseUnitType } from './type';
4 | import PRUnit from './PRUnit';
5 | import ScrollAnchor from '../ScrollAnchor'
6 |
7 | const PlannedResponseCreator = (props:any) => {
8 | const [plannedResponse, setPlannedResponse] = useState([]);
9 | const [delay, setDelay] = useState(1)
10 |
11 | const addDelayHandler = (ms: number): void => {
12 | setPlannedResponse([...plannedResponse, { type: PlannedResponseUnitType.DELAY, time: ms }]);
13 | };
14 |
15 | const addMessageHandler = (message: string): void => {
16 | setPlannedResponse([...plannedResponse, { type: PlannedResponseUnitType.MESSAGE, message }]);
17 | };
18 |
19 | const resetPlannedResponse = () => setPlannedResponse([]);
20 |
21 | const onMoveUp = (arr: any, ele: object) => {
22 | let index = arr.indexOf(ele)
23 | if (index<1) return
24 | [arr[index-1], arr[index]] = [arr[index], arr[index-1]]
25 | setPlannedResponse(arr.slice())
26 | }
27 | const onMoveDown = (arr: any, ele: object) => {
28 | let index = arr.indexOf(ele)
29 | if (index === arr.length - 1) return
30 | [arr[index+1], arr[index]] = [arr[index], arr[index+1]]
31 | setPlannedResponse(arr.slice())
32 | }
33 | const onRemove = (arr:any, ele: object) => {
34 | let index = arr.indexOf(ele)
35 | arr.splice(index, 1)
36 | setPlannedResponse(arr.slice())
37 | }
38 |
39 | return (
40 | <>
41 |
42 | {plannedResponse.map((curr, count=0) => (
43 |
{onMoveDown(plannedResponse, curr)}}
45 | onMoveUp={() => {onMoveUp(plannedResponse, curr)}}
46 | onRemove={() => {onRemove(plannedResponse, curr)}}
47 | />))
48 | }
49 |
50 |
51 |
52 |
53 | {props.emitPlannedResponse(plannedResponse)}} >Emit Message Stream
54 | addMessageHandler(props.message)}>Add Message
55 | addDelayHandler(delay*1000)}>Add Delay
56 | {setDelay(e.target.value)}} type="number">
57 | Clear
58 | playbackResponseUnits(plannedResponse, { onMessage: (msg: string) => { console.log(`${msg} is being emitted`); } })}>Play Back
59 |
60 | >
61 | );
62 | };
63 |
64 | export default PlannedResponseCreator;
--------------------------------------------------------------------------------
/src/components/PlannedResponseCreator/type.ts:
--------------------------------------------------------------------------------
1 | export interface PlannedResponseUnit {
2 | type: PlannedResponseUnitType,
3 | time?: number,
4 | message?: string,
5 | id?: string
6 | }
7 |
8 | export enum PlannedResponseUnitType {
9 | MESSAGE,
10 | DELAY,
11 | }
12 |
--------------------------------------------------------------------------------
/src/components/PlannedResponseCreator/utils.ts:
--------------------------------------------------------------------------------
1 | import { PlannedResponseUnit, PlannedResponseUnitType } from './type';
2 |
3 | // eslint-disable-next-line import/prefer-default-export
4 | export const playbackResponseUnits = async (
5 | responseUnits: PlannedResponseUnit[],
6 | {
7 | onMessage, beforeDelay, afterDelay, onComplete,
8 | }: {
9 | onMessage?: Function,
10 | beforeDelay?: Function,
11 | afterDelay?: Function,
12 | onComplete?: Function,
13 | },
14 | ) => {
15 | const addDelay = (ms: number): Promise => new Promise((resolve, reject) => {
16 | setTimeout(() => {
17 | resolve();
18 | }, ms);
19 | });
20 |
21 | // eslint-disable-next-line no-restricted-syntax
22 | for (const pru of responseUnits) {
23 | if (pru.type === PlannedResponseUnitType.DELAY && pru.time) {
24 | if (beforeDelay) beforeDelay(pru.time);
25 | // eslint-disable-next-line no-await-in-loop
26 | await addDelay(pru.time);
27 | if (afterDelay) afterDelay(pru.time);
28 | } else if (onMessage) onMessage(pru.message);
29 | }
30 |
31 | if (onComplete) onComplete();
32 | };
33 |
34 | const example: PlannedResponseUnit[] = [
35 | { type: PlannedResponseUnitType.MESSAGE, message: 'Message 1' },
36 | { type: PlannedResponseUnitType.DELAY, time: 2000 },
37 | { type: PlannedResponseUnitType.MESSAGE, message: 'Message 2 (2 seconds later)' },
38 | { type: PlannedResponseUnitType.DELAY, time: 1000 },
39 | { type: PlannedResponseUnitType.MESSAGE, message: 'Message 3 (1 second later)' },
40 | { type: PlannedResponseUnitType.MESSAGE, message: 'Message 4' },
41 | { type: PlannedResponseUnitType.DELAY, time: 2000 },
42 | { type: PlannedResponseUnitType.MESSAGE, message: 'Message 5 (2 seconds later)' },
43 | { type: PlannedResponseUnitType.DELAY, time: 1000 },
44 | { type: PlannedResponseUnitType.MESSAGE, message: 'Message 6 (1 second later)' },
45 | ];
46 |
47 | export const examplePlayback = (plannedResponse: any) => {
48 | playbackResponseUnits(plannedResponse, {
49 | beforeDelay: (ms: number) => {
50 | // console.log(`delaying by ${ms}`);
51 | },
52 | onMessage: (message: string) => {
53 | console.log(`emitting ${message}`);
54 | },
55 | afterDelay: (ms: number) => {
56 | // console.log(`finished delaying by ${ms}`);
57 | },
58 | onComplete: () => {
59 | console.log('completed playback');
60 | },
61 | });
62 | };
63 |
--------------------------------------------------------------------------------
/src/components/ScrollAnchor.tsx:
--------------------------------------------------------------------------------
1 | /**
2 | * @parentComponent StreamDisplay, PlannedResponseCreator
3 | * @description Respositions scrollbar to bottom whenever elements are added
4 | **/
5 |
6 | import React, { useRef, useEffect } from "react";
7 |
8 | const AlwaysScrollToBottom = () => {
9 | const elementRef: any = useRef();
10 | useEffect(() => elementRef.current.scrollIntoView());
11 | return
;
12 | };
13 |
14 | export default AlwaysScrollToBottom
--------------------------------------------------------------------------------
/src/components/ServerForm.tsx:
--------------------------------------------------------------------------------
1 | /**
2 | * @parentComponent app
3 | * @description Holds functionality to create a new server and update server config
4 | **/
5 |
6 | import React, { useState } from 'react';
7 | import { useDispatch, useSelector } from 'react-redux';
8 | import { serverManagerCreateServer, serverManagerStopServer, serverManagerStartServer, removeServer } from '@/store/actions/serversActions';
9 | import InputText from './input/InputText';
10 | import { RootState } from '@/store/reducers';
11 |
12 | function ServerForm() {
13 | const [name, updateName] = useState('New Server');
14 | const [port, updatePort] = useState(3000);
15 | const [protocol, updateProtocol] = useState('websocket');
16 | const currentServerId = useSelector((store: RootState) => store.navigation.currentServerId);
17 | // @ts-ignore
18 | const servers = useSelector((store: RootState) => store.servers.servers)
19 | const dispatch = useDispatch();
20 |
21 | const createServerHandler = () => {
22 | dispatch(serverManagerCreateServer({ port: +port, name, protocol: protocol }));
23 | updateName('New Server');
24 | updatePort(3000);
25 | };
26 |
27 | const startServerHandler = () => {
28 | if (!currentServerId) return
29 | dispatch(serverManagerStartServer({ ...servers[currentServerId] }, currentServerId))
30 | }
31 |
32 | const buttonDisplay: any = [];
33 | if (!currentServerId) {
34 | buttonDisplay.push(
35 |
40 | Create Server
41 |
42 | )
43 | } else {
44 | if (servers[currentServerId].status === "RUNNING") {
45 | buttonDisplay.push(
46 | { dispatch(serverManagerStopServer(currentServerId)) }}
50 | >
51 | Stop Server
52 |
53 | )
54 | } else {
55 | buttonDisplay.push(
56 |
57 | Start Server
58 |
59 | )
60 | buttonDisplay.push(
61 | { dispatch(removeServer(currentServerId)) }}>
62 | Remove Server
63 | )
64 | }
65 | }
66 |
67 | return (
68 |
69 |
100 |
101 |
102 | {buttonDisplay}
103 |
104 |
105 | );
106 | }
107 |
108 | export default ServerForm;
109 |
--------------------------------------------------------------------------------
/src/components/Sidebar.tsx:
--------------------------------------------------------------------------------
1 | /**
2 | * @parentComponent app
3 | * @description Left pane of application. Displays list of active/inactive servers and holds functionality to set active server in state
4 | */
5 |
6 | import React from 'react';
7 | import { useDispatch, useSelector } from 'react-redux';
8 | import { setCurrentServerId } from '../store/actions/navigationActions';
9 | import FiberManualRecordIcon from '@material-ui/icons/FiberManualRecord';
10 | import { green, red } from '@material-ui/core/colors';
11 |
12 | function Sidebar() {
13 | const dispatch = useDispatch();
14 | // @ts-ignore
15 | const servers = useSelector((store) => store.servers.servers);
16 | // @ts-ignore
17 | const currentServerId = useSelector((store: RootState) => store.navigation.currentServerId);
18 |
19 | // Don't delete
20 | // @ts-ignore
21 | const store = useSelector(store => store)
22 |
23 | const displayServers = Object.values(servers).map((server: any) => {
24 | const status = server.status === "RUNNING" ? green[500] : red[500];
25 | return (
26 | dispatch(setCurrentServerId(server.id))}>
27 |
28 | {server.name}
29 | :{server.port}
30 |
31 |
32 |
33 | );
34 | });
35 |
36 | return (
37 |
38 |
39 | {
42 | dispatch(setCurrentServerId(''));
43 | }}
44 | >
45 | + New Server
46 |
47 |
48 |
49 |
Servers
50 | {
51 | displayServers.length
52 | ? displayServers
53 | :
No configured servers
54 | }
55 |
56 |
57 |
58 |
59 |
60 | );
61 | }
62 |
63 | export default Sidebar;
64 |
--------------------------------------------------------------------------------
/src/components/StreamDisplay.tsx:
--------------------------------------------------------------------------------
1 | /**
2 | * @parentComponent StreamTab
3 | * @description Display stream output field
4 | **/
5 |
6 | import React, {useEffect, useRef, useState} from 'react';
7 | import { useSelector } from 'react-redux';
8 | import { RootState } from '@/store/reducers';
9 | import Highlight from 'react-highlight'
10 | import ScrollAnchor from './ScrollAnchor'
11 |
12 |
13 | function StreamDisplay() {
14 | const plannedResponseBoolean = useSelector( (store: RootState) => store.navigation.plannedResponseBoolean)
15 |
16 | const currentServerId = useSelector(
17 | (store: RootState) => store.navigation.currentServerId,
18 | );
19 | const outputStream = useSelector(
20 | (store: RootState) => store.messages.streams,
21 | );
22 |
23 | let counter = 0;
24 |
25 | return (
26 |
27 |
28 | {currentServerId && outputStream[currentServerId]
29 | && outputStream[currentServerId].map((code: string) => (
30 |
31 | {code.toString()}
32 |
33 |
34 | ))}
35 |
36 |
37 | );
38 | }
39 |
40 | export default StreamDisplay;
41 |
--------------------------------------------------------------------------------
/src/components/StreamInput.tsx:
--------------------------------------------------------------------------------
1 | /**
2 | * @parentComponent StreamTab
3 | * @description Holds input boxes for emitting messages or message streams
4 | */
5 |
6 | import { RootState } from '@/store/reducers';
7 | import React, { useState } from 'react';
8 | import { useDispatch, useSelector } from 'react-redux';
9 | import { serverManagerBroadcastAll } from '../store/actions/serversActions';
10 | import PlannedResponseCreator from './PlannedResponseCreator/PlannedResponseCreator';
11 | import { playbackResponseUnits } from './PlannedResponseCreator/utils';
12 | import { togglePlannedResponse } from '../store/actions/navigationActions'
13 |
14 | function StreamInput() {
15 | const dispatch = useDispatch();
16 | const currentServerId = useSelector(
17 | (store: RootState) => store.navigation.currentServerId,
18 | );
19 | const [message, updateMessage] = useState('');
20 |
21 | const plannedResponseBoolean = useSelector( (store: RootState) => store.navigation.plannedResponseBoolean)
22 |
23 | const emitPlannedResponse = (prus:any) => {
24 | playbackResponseUnits(prus, {
25 | onMessage: (msg: string) => {
26 | dispatch(serverManagerBroadcastAll(currentServerId, msg));
27 | },
28 | });
29 | };
30 |
31 | return (
32 |
33 |
34 |
35 | {plannedResponseBoolean &&
}
36 |
37 |
38 |
45 |
46 |
47 | dispatch(serverManagerBroadcastAll(currentServerId, message))}
51 | >
52 | Emit Message
53 |
54 | dispatch(togglePlannedResponse()) }>
55 | Toggle Planner
56 |
57 |
58 |
59 |
60 |
61 | );
62 | }
63 |
64 | export default StreamInput;
65 |
--------------------------------------------------------------------------------
/src/components/StreamTab.tsx:
--------------------------------------------------------------------------------
1 | /**
2 | * @parentComponent StreamTab
3 | * @description Right pane of application - holds stream input boxes and output display
4 | */
5 |
6 | import React from "react";
7 | import StreamInput from "./StreamInput";
8 | import StreamDisplay from "./StreamDisplay"
9 |
10 |
11 | function StreamTab() {
12 | return (
13 |
14 |
15 |
16 |
17 | );
18 | }
19 |
20 | export default StreamTab;
21 |
--------------------------------------------------------------------------------
/src/components/app.tsx:
--------------------------------------------------------------------------------
1 | /**
2 | * @description Main container for all components
3 | */
4 |
5 | import React from 'react';
6 | import '../styles/app.scss';
7 |
8 | import ServerForm from './ServerForm';
9 | import Sidebar from './Sidebar';
10 | import StreamTab from './StreamTab';
11 |
12 | const App = () => (
13 |
14 |
15 |
16 |
17 |
18 | );
19 |
20 | export default App;
21 |
--------------------------------------------------------------------------------
/src/components/input/InputText.tsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable react/destructuring-assignment */
2 | import React from 'react';
3 |
4 | /**
5 | * TODO
6 | * The way this component is written means that if you have a
7 | * props.label value, it will also be assigned to . This
8 | * is not the best way to write this component.
9 | * @param props
10 | */
11 | const InputText = (props: any) => (
12 |
13 |
{props.label}
14 |
19 |
20 | );
21 |
22 | export default InputText;
23 |
--------------------------------------------------------------------------------
/src/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | socketcast
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/src/index.tsx:
--------------------------------------------------------------------------------
1 | /**
2 | * @description Entry point for application
3 | */
4 |
5 | import React from 'react';
6 | import { render } from 'react-dom';
7 | import { Provider } from 'react-redux';
8 | import App from './components/app';
9 | import store from './store/store';
10 |
11 | const root = document.getElementById('root');
12 |
13 | render(
14 |
15 |
16 | ,
17 | root,
18 | );
19 |
--------------------------------------------------------------------------------
/src/main.ts:
--------------------------------------------------------------------------------
1 | import { app, BrowserWindow } from 'electron';
2 | // import installExtension, {
3 | // REDUX_DEVTOOLS,
4 | // REACT_DEVELOPER_TOOLS,
5 | // } from 'electron-devtools-installer';
6 |
7 | let mainWindow: Electron.BrowserWindow | null;
8 |
9 | const createWindow = (): void => {
10 | mainWindow = new BrowserWindow({
11 | width: 1400,
12 | height: 809,
13 | webPreferences: {
14 |
15 | nodeIntegration: true,
16 | enableRemoteModule: true,
17 | },
18 | });
19 |
20 | mainWindow.loadFile('index.html');
21 | mainWindow.on('closed', () => {
22 | mainWindow = null;
23 | });
24 | };
25 |
26 | const onReady = (): void => {
27 | createWindow();
28 | };
29 |
30 | /**
31 | * There is a potential issue with Electron version 9+ when trying to use Redux Dev Tools
32 | * https://github.com/electron/electron/issues/24011
33 | * https://github.com/electron/electron/issues/24638
34 | */
35 | // app.whenReady().then(() => {
36 | // installExtension(REDUX_DEVTOOLS)
37 | // .then((name) => console.log(`Added Extension: ${name}`))
38 | // .catch((err) => console.log('An error occurred: ', err));
39 | // installExtension(REACT_DEVELOPER_TOOLS)
40 | // .then((name) => console.log(`Added Extension: ${name}`))
41 | // .catch((err) => console.log('An error occurred: ', err));
42 | // });
43 |
44 | app.on('ready', onReady);
--------------------------------------------------------------------------------
/src/store/actions/actionTypes.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * @description Action Type Constants
3 | */
4 |
5 | export const CREATE_SERVER = 'CREATE_SERVER';
6 | export const CREATE_STREAM = 'CREATE_STREAM';
7 | export const LOG_MESSAGE = 'LOG_MESSAGE';
8 | export const REMOVE_SERVER = 'REMOVE_SERVER';
9 | export const SET_CURRENT_EVENT = 'SET_CURRENT_EVENT';
10 | export const SET_CURRENT_SERVER_ID = 'SET_CURRENT_SERVER_ID';
11 | export const STOP_AND_REMOVE_SERVER = 'STOP_AND_REMOVE_SERVER';
12 | export const UPDATE_SERVER_STATE = 'UPDATE_SERVER_STATE';
13 | export const TOGGLE_PLANNED_RESPONSE = 'TOGGLE_PLANNED_RESPONSE';
14 |
15 |
16 |
--------------------------------------------------------------------------------
/src/store/actions/messagesActions.ts:
--------------------------------------------------------------------------------
1 | import * as types from './actionTypes';
2 |
3 | export const createStream = (id: string) => ({
4 | type: types.CREATE_STREAM,
5 | payload: id,
6 | });
7 |
8 | export const logMessage = (id: any, message: string) => ({
9 | type: types.LOG_MESSAGE,
10 | payload: { id, message },
11 | });
12 |
--------------------------------------------------------------------------------
/src/store/actions/navigationActions.ts:
--------------------------------------------------------------------------------
1 | import * as types from './actionTypes';
2 |
3 | export const setCurrentEventId = (event: object) => ({
4 | type: types.SET_CURRENT_EVENT,
5 | payload: event,
6 | });
7 |
8 | export const setCurrentServerId = (id: string) => ({
9 | type: types.SET_CURRENT_SERVER_ID,
10 | payload: id,
11 | });
12 |
13 | export const togglePlannedResponse = () => ({
14 | type: types.TOGGLE_PLANNED_RESPONSE,
15 | payload: ''
16 | })
17 |
--------------------------------------------------------------------------------
/src/store/actions/serversActions.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * @description Action Creators
3 | */
4 |
5 | import { ThunkAction } from 'redux-thunk';
6 | import { Action } from 'redux';
7 | import { v4 as uuidv4 } from 'uuid';
8 | import ServerManager from '../../ServerManager/ServerManager';
9 | import { RootState } from '../reducers';
10 | import * as types from './actionTypes';
11 | import { ServerConfig, ServerState } from '../../ServerManager/type';
12 | import { createStream, logMessage } from './messagesActions';
13 | import { setCurrentServerId } from './navigationActions';
14 |
15 | export const createServer = (data: ServerState) => ({
16 | type: types.CREATE_SERVER,
17 | payload: data,
18 | });
19 |
20 | export const updateServerState = (serverState: ServerState) => ({
21 | type: types.UPDATE_SERVER_STATE,
22 | payload: serverState,
23 | });
24 |
25 | export const removeServer = (id: number) => ({
26 | type: types.REMOVE_SERVER,
27 | payload: id,
28 | });
29 |
30 | export const stopAndRemoveServer = (id: number) => ({
31 | type: types.STOP_AND_REMOVE_SERVER,
32 | payload: id,
33 | });
34 |
35 | // eslint-disable-next-line max-len
36 | export const serverManagerStopServer = (id: number): ThunkAction> => (dispatch) => {
37 | ServerManager.stopServer(id);
38 | };
39 | // eslint-disable-next-line max-len
40 | export const serverManagerStartServer = (config: ServerConfig, id: any): ThunkAction> => dispatch => {
41 |
42 | ServerManager.createServer({
43 | ...config,
44 | id: id,
45 | onMessage: (message, id) => {
46 | console.log(`from client to ${id} server: ${message}`);
47 | dispatch(logMessage(id, message));
48 | },
49 | onServerClose: (serverState: ServerState) => {
50 | dispatch(updateServerState(serverState));
51 | },
52 | })
53 | .then((data: any) => {
54 | dispatch(createServer(data));
55 | dispatch(createStream(data.id));
56 | }).catch((err: any) => {
57 | console.log(err);
58 | });
59 | }
60 |
61 | // eslint-disable-next-line max-len
62 | export const serverManagerCreateServer = (config: ServerConfig): ThunkAction> => (dispatch) => {
63 | ServerManager.createServer({
64 | ...config,
65 | id: uuidv4(),
66 | onMessage: (message, id) => {
67 | console.log(`from client to ${id} server: ${message}`);
68 | dispatch(logMessage(id, message));
69 | },
70 | onServerClose: (serverState: ServerState) => {
71 | dispatch(updateServerState(serverState));
72 | },
73 | })
74 | .then((data: any) => {
75 | dispatch(createServer(data));
76 | dispatch(setCurrentServerId(data.id));
77 | dispatch(createStream(data.id));
78 | }).catch((err: any) => {
79 | console.log(err);
80 | });
81 | };
82 |
83 | // eslint-disable-next-line max-len
84 | // export const serverManagerCreateSSEServer = (config: ServerConfig): ThunkAction> => (dispatch) => {
85 | // ServerManager.createSSEServer({
86 | // ...config,
87 | // id: uuidv4(),
88 | // onServerClose: (serverState: ServerState) => {
89 | // dispatch(updateServerState(serverState));
90 | // },
91 | // }).then((data: any) => {
92 | // dispatch(createServer(data));
93 | // dispatch(createStream(data.id));
94 | // });
95 | // };
96 |
97 | // eslint-disable-next-line max-len
98 | export const serverManagerBroadcastAll = (id: string, message: string): ThunkAction> => (dispatch) => {
99 | /**
100 | * The following method, broadcastToAll() is not "promisified" yet.
101 | * However, it is an asynchronous action and Redux will "complain" if we do not
102 | * write this in thunk format.
103 | */
104 | ServerManager.broadcastToAll(id, message);
105 | /**
106 | * After calling the above broadcastToAll() method, dispatch some action
107 | * that adds to our store's state that keeps track of messages.
108 | */
109 |
110 | // once broadcasttoall gets promisified and we can guarantee message emission
111 | // call this dispatch to add the message to correct data storage (based on server_id)
112 | dispatch(logMessage(id, message));
113 | };
--------------------------------------------------------------------------------
/src/store/reducers/index.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * @description Combines reducers
3 | */
4 |
5 | import { combineReducers } from 'redux';
6 | import serversReducer from './serversReducer';
7 | import messagesReducer from './messagesReducer';
8 | import navigationReducer from './navigationReducer';
9 |
10 | const reducers = combineReducers({
11 | servers: serversReducer,
12 | messages: messagesReducer,
13 | navigation: navigationReducer,
14 | });
15 |
16 | export default reducers;
17 | export type RootState = ReturnType;
18 |
--------------------------------------------------------------------------------
/src/store/reducers/messagesReducer.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * @description Reducer for server data
3 | * @TODO Currently every server stream has on array of messages like this
4 | *** streams: {[server_id]: stream[]}
5 | *** in the future we may want to restructure state for event layer
6 | *** streams: {[server_id]: {[event_id]: stream[]}}
7 | */
8 |
9 | import { MessagesReducerState } from '@/ServerManager/type';
10 | import * as types from '../actions/actionTypes';
11 |
12 | const initialState: MessagesReducerState = {
13 | streams: {},
14 | };
15 |
16 | const messagesReducer = (state = initialState, action: any) => {
17 | switch (action.type) {
18 | case types.CREATE_STREAM: {
19 | return {
20 | ...state,
21 | streams: {
22 | ...state.streams,
23 | [action.payload]: [],
24 | },
25 | };
26 | }
27 | case types.LOG_MESSAGE: {
28 | const { id, message } = action.payload;
29 | const stream = state.streams[id];
30 | stream.push(message);
31 |
32 | return {
33 | ...state,
34 | streams: {
35 | ...state.streams,
36 | [id]: stream,
37 | },
38 | };
39 | }
40 | default: {
41 | return state;
42 | }
43 | }
44 | };
45 |
46 | export default messagesReducer;
47 |
--------------------------------------------------------------------------------
/src/store/reducers/navigationReducer.ts:
--------------------------------------------------------------------------------
1 | import * as types from '../actions/actionTypes';
2 |
3 | const initialState = {
4 | currentServerId: null,
5 | currentEvent: null,
6 | plannedResponseBoolean: false,
7 | };
8 |
9 | const navigationReducer = (state = initialState, action: any) => {
10 | switch (action.type) {
11 | case types.SET_CURRENT_SERVER_ID: {
12 | const currentServerId = action.payload;
13 | return { ...state, currentServerId };
14 | }
15 | case types.TOGGLE_PLANNED_RESPONSE: {
16 | let plannedResponseBoolean = !state.plannedResponseBoolean
17 | return { ...state, plannedResponseBoolean}
18 | }
19 | default: {
20 | return state;
21 | }
22 | }
23 | };
24 |
25 | export default navigationReducer;
26 |
--------------------------------------------------------------------------------
/src/store/reducers/serversReducer.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * @description Reducer for server data
3 | */
4 |
5 | import * as types from '../actions/actionTypes';
6 |
7 | const initialState = {
8 | servers: (window.localStorage.store ? JSON.parse(window.localStorage.store).servers : {}),
9 | };
10 |
11 | const serversReducer = (state = initialState, action: any) => {
12 | switch (action.type) {
13 | case types.CREATE_SERVER: {
14 |
15 | //--Persistent Data Storage--
16 | //let data = { ...state, servers: { ...state.servers, [action.payload.id]: { ...action.payload } } };
17 | let data = JSON.parse(JSON.stringify({ ...state, servers: { ...state.servers, [action.payload.id]: { ...action.payload }}}))
18 | Object.values(data.servers).map((server: any) => server.status = "STOPPED")
19 | window.localStorage.setItem("store", JSON.stringify(data))
20 | return {
21 | ...state,
22 | servers: {
23 | ...state.servers,
24 | [action.payload.id]: {
25 | ...action.payload,
26 | },
27 | },
28 | };
29 | }
30 | case types.REMOVE_SERVER: {
31 | let newState = { ...state }
32 | delete newState.servers[action.payload];
33 |
34 | //--Persistent Data Storage--
35 | const storage = JSON.parse(JSON.stringify({ ...newState }));
36 | Object.values(storage.servers).map((server: any) => server.status = "STOPPED");
37 | window.localStorage.setItem("store", JSON.stringify(storage));
38 |
39 | return newState;
40 | }
41 | case types.SET_CURRENT_EVENT: {
42 | const currentEvent = action.payload;
43 | return { ...state, currentEvent };
44 | }
45 | case types.UPDATE_SERVER_STATE: {
46 | return {
47 | ...state,
48 | servers: {
49 | ...state.servers,
50 | [action.payload.id]: {
51 | ...action.payload,
52 | },
53 | },
54 | };
55 | }
56 |
57 | default: {
58 | return state;
59 | }
60 | }
61 | };
62 |
63 | export default serversReducer;
64 |
--------------------------------------------------------------------------------
/src/store/store.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * @description App's single source of truth
3 | */
4 |
5 | import { applyMiddleware, createStore, compose } from 'redux';
6 | import { composeWithDevTools } from 'redux-devtools-extension';
7 | import thunk from 'redux-thunk';
8 | import reducers from './reducers/index';
9 |
10 | /**
11 | * There is a potential issue with Electron version 9+ when trying to use Redux Dev Tools
12 | * https://github.com/electron/electron/issues/24011
13 | * https://github.com/electron/electron/issues/24638
14 | */
15 | const store = createStore(
16 | reducers,
17 | composeWithDevTools(applyMiddleware(thunk)),
18 | );
19 |
20 | export default store;
21 |
--------------------------------------------------------------------------------
/src/styles/app.scss:
--------------------------------------------------------------------------------
1 | @import '../../node_modules/normalize.css/normalize.css';
2 | @import './hack-font/hack.css';
3 |
4 | $primary-background:#1b1b2d;
5 | $primary-background_hover:#3a3a4e;
6 | $base_teal2: rgb(204, 204, 204);
7 | $base_grey: rgb(234, 236, 238);
8 | $terminal_black: rgb(226, 226, 226);
9 | $primary-font-color: rgb(221, 133, 18);
10 | $terminal_black2: rgb(11, 14, 17);
11 |
12 | html,
13 | body {
14 | height: 100%;
15 | margin: 0;
16 | font-family: Arial, Helvetica, sans-serif;
17 | }
18 |
19 | .app {
20 | width: 100%;
21 | height: 100%;
22 | display: flex;
23 | background-color: $base_grey;
24 | }
25 |
26 | #root {
27 | height: 100%;
28 | }
29 |
30 | /* ---------------------------------------- */
31 | /* --------------- GENERAL -------------- */
32 | /* ---------------------------------------- */
33 |
34 | .button {
35 | border: 1px solid rgb(221, 133, 18);
36 | background-color: rgb(221, 133, 18);
37 | color: #1F2933;
38 | font-size: 12px;
39 | font-weight: lighter;
40 | padding: 10px;
41 | border-radius: 5px;
42 | }
43 |
44 | .button:hover {
45 | background-color: #1F2933;
46 | border: 1px solid rgb(221, 133, 18);
47 | color: rgb(221, 133, 18);
48 | cursor: pointer;
49 | }
50 |
51 | .primary {
52 | background-color: #0b5269;
53 | border: 1px solid #0b5269;
54 | color: #d1d4c9;
55 | margin-left: 10px;
56 | padding: 10px;
57 | }
58 |
59 | .primary:hover {
60 | background-color: #1F2933;
61 | color: rgb(221, 133, 18);
62 | border: 1px solid rgb(221, 133, 18);
63 | }
64 |
65 | .secondary {
66 | background-color: $primary-background;
67 | color: $primary-font-color;
68 | margin: 5px;
69 | padding: 5px
70 | }
71 |
72 |
73 | .button_special {
74 | background-color: $primary-font-color;
75 | border: 1px solid $primary-font-color;
76 | color: #1F2933;
77 | }
78 |
79 | .button_special:hover {
80 | background-color: #1F2933;
81 | border: 1px solid rgb(221, 133, 18);
82 | color: rgb(221, 133, 18);
83 | cursor: pointer;
84 | }
85 |
86 | .button_newServer {
87 | border: 1px solid rgb(221, 133, 18);
88 | background-color: rgb(221, 133, 18);
89 | color: #1F2933;
90 | font-size: 12px;
91 | font-weight: lighter;
92 | padding: 10px;
93 | border-radius: 5px;
94 | }
95 |
96 | .button_newServer:hover {
97 | background-color: #1F2933;
98 | border: 1px solid rgb(221, 133, 18);
99 | color: rgb(221, 133, 18);
100 | cursor: pointer;
101 | }
102 |
103 | .button_code {
104 | background-color: #1F2933;
105 | color: rgb(221, 133, 18);
106 | border: 1px solid rgb(221, 133, 18);
107 | margin: 5px;
108 | padding: 5px;
109 | }
110 |
111 | .button_code:hover {
112 | background-color: rgb(221, 133, 18);
113 | color: black;
114 | cursor: pointer;
115 | }
116 |
117 | /* ---------------------------------------- */
118 | /* -------------- LEFT PANE ------------- */
119 | /* ---------------------------------------- */
120 |
121 | .sidebar {
122 | height: 100%;
123 | width: 15%;
124 | display: flex;
125 | flex-direction: column;
126 | background-color: $primary-background;
127 |
128 | .buttons-display {
129 | align-self: center;
130 | padding: 20px;
131 | margin-top: 20px;
132 |
133 | .button {
134 | background-color: $primary-font-color
135 | }
136 | }
137 |
138 | .servers-display {
139 | flex-grow: 1;
140 | padding: 20px 0;
141 |
142 | .title {
143 | font-size: 26px;
144 | color: $primary-font-color;
145 | padding-left: 10px;
146 | margin-bottom: 10px;
147 | }
148 |
149 | .message {
150 | text-align: center;
151 | color: #9e9e9e;
152 | margin-top: 40px;
153 | }
154 |
155 | .selected_server {
156 | background-color: #1F2933;
157 | }
158 |
159 | .server {
160 | display: flex;
161 | align-items: center;
162 | justify-content: space-between;
163 | padding: 5px 15px;
164 |
165 | &:hover {
166 | background-color: #1F2933;
167 | cursor: pointer;
168 | }
169 |
170 | .name {
171 | margin-right: 3px;
172 | max-width: 175px;
173 | color: white;
174 | font-size: 16px;
175 | white-space: nowrap;
176 | text-overflow: ellipsis;
177 | overflow: hidden;
178 | }
179 |
180 | .port {
181 | color: grey;
182 | font-size: 14px;
183 | }
184 | }
185 | }
186 |
187 | .brand {
188 | font-variant: small-caps;
189 | align-self: center;
190 | color: $primary-font-color;
191 | padding: 20px 10px;
192 | }
193 |
194 | .logo {
195 | width: 135px;
196 | margin-bottom: 10px;
197 | }
198 | }
199 |
200 | /* ---------------------------------------- */
201 | /* ------------- MIDDLE PANE ------------ */
202 | /* ---------------------------------------- */
203 |
204 | .server-configuration {
205 | display: flex;
206 | flex-direction: column;
207 | padding: 15px 10px 20px 10px;
208 | border-bottom-color: #323f4b;
209 | border-bottom-style: solid;
210 | border-bottom-width: 1px;
211 | background-color: #1F2933;
212 | color: white;
213 |
214 | .server-form {
215 | margin-top: 5px;
216 | padding: 10px;
217 |
218 | .input-container {
219 |
220 | .label {
221 | margin-bottom: 5px;
222 | }
223 |
224 | input {
225 | width: 100%;
226 | box-sizing: border-box;
227 | background-color: #323f4b;
228 | border: 1px solid #636267;
229 | padding: 9px;
230 | border-radius: 5px;
231 | border-width: 1px;
232 | font-size: 14px;
233 | margin-bottom: 10px;
234 | color: white;
235 | }
236 | }
237 |
238 | .radio-container {
239 | display: flex;
240 | flex-direction: column;
241 | margin-top: 5px;
242 |
243 | .title {
244 | font-variant: all-petite-caps;
245 | margin-bottom: 10px;
246 | }
247 |
248 | .radio {
249 | margin-bottom: 7px;
250 |
251 | label {
252 | margin-left: 10px;
253 | }
254 | }
255 | }
256 | }
257 |
258 | .button-container {
259 | display: flex;
260 | flex-direction: column;
261 | margin-top: 10px;
262 |
263 | button {
264 | margin-bottom: 5px;
265 | }
266 |
267 | .remove {
268 | background-color: transparent;
269 | color: rgb(221, 133, 18);;
270 | font-weight: normal;
271 | }
272 |
273 | .remove:hover {
274 | background-color: transparent;
275 | color: red;
276 | font-weight: normal;
277 | }
278 | }
279 |
280 | }
281 |
282 | /* ---------------------------------------- */
283 | /* ------------- RIGHT PANE ------------- */
284 | /* ---------------------------------------- */
285 |
286 | .code {
287 | color: $primary-font-color;
288 | margin-bottom: 5px;
289 | margin-left: 10px;
290 | }
291 |
292 | .code-textarea {
293 | margin-top: 11px;
294 | width: 100%;
295 | border-radius: 5px;
296 | background-color: $primary-background;
297 | color: white;
298 | font-family: 'Hack';
299 | }
300 |
301 | .stream-column {
302 | background-color: #1F2933;
303 | flex: 2;
304 | padding: 5px 10px 5px 0;
305 | display: flex;
306 | flex-direction: column;
307 | }
308 |
309 | .streamDisplay_button {
310 | background-color: $primary-background;
311 | color: white;
312 | border-radius: 0px 0px 3px 3px;
313 | border: 2px $primary-background solid;
314 | font-weight: bold;
315 | }
316 |
317 | .streamDisplay_header {
318 | background-color: white;
319 | border-radius: 3px;
320 | padding: 10px;
321 | }
322 |
323 | .streamDisplay_container {
324 | border-radius: 3px;
325 | padding: 10px;
326 | }
327 |
328 | .streamDisplay_button {
329 | background-color: $primary-background;
330 | color: white;
331 | border-radius: 0px 0px 3px 3px;
332 | border: 2px $primary-background solid;
333 | font-weight: bold;
334 | }
335 |
336 | .streamDisplay_outputContainer {
337 | margin-top: 5px;
338 | flex: 1;
339 | max-height: 100%;
340 | }
341 |
342 | .streamDisplay_inputContainer {
343 | flex: .1
344 | }
345 |
346 | .streamDisplay_outputboxLarge {
347 | margin-top: 10px;
348 | height: 77vh;
349 | width: 100%;
350 | max-height:100%;
351 | border-radius: 5px;
352 | border: 2px $primary-background solid;
353 | overflow-y: scroll;
354 | font-family: 'Hack';
355 | color: white;
356 | background-color: $primary-background;
357 | object-fit: contain;
358 | }
359 |
360 | .streamDisplay_outputboxSmall {
361 | margin-top: 10px;
362 | height: 43vh;
363 | width: 100%;
364 | max-height:100%;
365 | border-radius: 5px;
366 | border: 2px $primary-background solid;
367 | overflow-y: scroll;
368 | font-family: 'Hack';
369 | color: white;
370 | background-color: $primary-background;
371 | object-fit: contain;
372 | }
373 |
374 | .streamInput_buttons {
375 | margin-bottom: 10px;
376 | }
377 |
378 | /* Planned Response */
379 |
380 | .planned-response-container {
381 | flex: .6;
382 | padding: 10px;
383 | border-radius: 3px;
384 | display: flex;
385 | flex-direction: column;
386 | }
387 |
388 | .planned-response-playground {
389 | height: 26vh;
390 | // max-height: 400px;
391 | overflow-y: scroll;
392 | color: white;
393 | width: 100%;
394 | background-color: $primary-background;
395 | border: 2px $primary-background solid;
396 | margin-top: 10px;
397 | }
398 |
399 | .PRC-input {
400 | width: 30px;
401 | background-color: #1b1b2d;
402 | color: white;
403 | border: 1px solid #0b5269;
404 | padding: 10px;
405 | border-radius: 5px;
406 | margin-left: 10px;
407 | font-size: 12px;
408 | font-weight: lighter;
409 | }
410 |
411 | .PRUnit {
412 | width: 100%;
413 | margin-bottom: 5px;
414 | border: 1px $primary-background_hover
415 | }
416 |
417 | .top-section {
418 | display: flex;
419 | align-items: center;
420 | justify-content: space-between;
421 | }
422 |
423 | .title-type {
424 | color: rgb(204, 204, 204)
425 | }
426 |
427 |
428 | /* Styled Scrollbar */
429 | /* width */
430 | ::-webkit-scrollbar {
431 | width: 10px;
432 | }
433 |
434 | /* Track */
435 | ::-webkit-scrollbar-track {
436 | background: $primary-background;
437 | }
438 |
439 | /* Handle */
440 | ::-webkit-scrollbar-thumb {
441 | background: #323f4b
442 | }
443 |
444 | /* Handle on hover */
445 | ::-webkit-scrollbar-thumb:hover {
446 | background: $primary-font-color;
447 | }
448 |
449 |
450 |
451 |
452 | //Modified version of Sarah Drasner's night-owl VS code theme
453 | //https://github.com/sdras/night-owl-vscode-theme
454 |
455 | .hljs {
456 | display: block;
457 | overflow-x: auto;
458 | // padding: 0.5em;
459 | background: $primary-background;
460 | color: #d6deeb;
461 | }
462 |
463 | pre {
464 | margin: 0px;
465 | }
466 |
467 | /* General Purpose */
468 | .hljs-keyword {
469 | color: #c792ea;
470 | font-style: italic;
471 | }
472 | .hljs-built_in {
473 | color: #addb67;
474 | font-style: italic;
475 | }
476 | .hljs-type {
477 | color: #82aaff;
478 | }
479 | .hljs-literal {
480 | color: #ff5874;
481 | }
482 | .hljs-number {
483 | color: #F78C6C;
484 | }
485 | .hljs-regexp {
486 | color: #5ca7e4;
487 | }
488 | .hljs-string {
489 | color: #ecc48d;
490 | }
491 | .hljs-subst {
492 | color: #d3423e;
493 | }
494 | .hljs-symbol {
495 | color: #82aaff;
496 | }
497 | .hljs-class {
498 | color: #ffcb8b;
499 | }
500 | .hljs-function {
501 | color: #82AAFF;
502 | }
503 | .hljs-title {
504 | color: #DCDCAA;
505 | font-style: italic;
506 | }
507 | .hljs-params {
508 | color: #7fdbca;
509 | }
510 |
511 | /* Meta */
512 | .hljs-comment {
513 | color: #637777;
514 | font-style: italic;
515 | }
516 | .hljs-doctag {
517 | color: #7fdbca;
518 | }
519 | .hljs-meta {
520 | color: #82aaff;
521 | }
522 | .hljs-meta-keyword {
523 | color: #82aaff;
524 | }
525 | .hljs-meta-string {
526 | color: #ecc48d;
527 | }
528 |
529 | /* Tags, attributes, config */
530 | .hljs-section {
531 | color: #82b1ff;
532 | }
533 | .hljs-tag,
534 | .hljs-name,
535 | .hljs-builtin-name {
536 | color: #7fdbca;
537 | }
538 | .hljs-attr {
539 | color: #7fdbca;
540 | }
541 | .hljs-attribute {
542 | color: #80cbc4;
543 | }
544 | .hljs-variable {
545 | color: #addb67;
546 | }
547 |
548 | /* Markup */
549 | .hljs-bullet {
550 | color: #d9f5dd;
551 | }
552 | .hljs-code {
553 | color: #80CBC4;
554 | }
555 | .hljs-emphasis {
556 | color: #c792ea;
557 | font-style: italic;
558 | }
559 | .hljs-strong {
560 | color: #addb67;
561 | font-weight: bold;
562 | }
563 | .hljs-formula {
564 | color: #c792ea;
565 | }
566 | .hljs-link {
567 | color: #ff869a;
568 | }
569 | .hljs-quote {
570 | color: #697098;
571 | font-style: italic;
572 | }
573 |
574 | /* CSS */
575 | .hljs-selector-tag {
576 | color: #ff6363;
577 | }
578 |
579 | .hljs-selector-id {
580 | color: #fad430;
581 | }
582 |
583 | .hljs-selector-class {
584 | color: #addb67;
585 | font-style: italic;
586 | }
587 |
588 | .hljs-selector-attr,
589 | .hljs-selector-pseudo {
590 | color: #c792ea;
591 | font-style: italic;
592 | }
593 |
594 | /* Templates */
595 | .hljs-template-tag {
596 | color: #c792ea;
597 | }
598 | .hljs-template-variable {
599 | color: #addb67;
600 | }
601 |
602 | /* diff */
603 | .hljs-addition {
604 | color: #addb67ff;
605 | font-style: italic;
606 | }
607 |
608 | .hljs-deletion {
609 | color: #EF535090;
610 | font-style: italic;
611 | }
612 |
--------------------------------------------------------------------------------
/src/styles/hack-font/fonts/hack-bold-subset.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/socketcast/f0f1c7d9f7f414ace50f1dd8045ab91b4d68f858/src/styles/hack-font/fonts/hack-bold-subset.woff
--------------------------------------------------------------------------------
/src/styles/hack-font/fonts/hack-bold-subset.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/socketcast/f0f1c7d9f7f414ace50f1dd8045ab91b4d68f858/src/styles/hack-font/fonts/hack-bold-subset.woff2
--------------------------------------------------------------------------------
/src/styles/hack-font/fonts/hack-bold.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/socketcast/f0f1c7d9f7f414ace50f1dd8045ab91b4d68f858/src/styles/hack-font/fonts/hack-bold.woff
--------------------------------------------------------------------------------
/src/styles/hack-font/fonts/hack-bold.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/socketcast/f0f1c7d9f7f414ace50f1dd8045ab91b4d68f858/src/styles/hack-font/fonts/hack-bold.woff2
--------------------------------------------------------------------------------
/src/styles/hack-font/fonts/hack-bolditalic-subset.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/socketcast/f0f1c7d9f7f414ace50f1dd8045ab91b4d68f858/src/styles/hack-font/fonts/hack-bolditalic-subset.woff
--------------------------------------------------------------------------------
/src/styles/hack-font/fonts/hack-bolditalic-subset.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/socketcast/f0f1c7d9f7f414ace50f1dd8045ab91b4d68f858/src/styles/hack-font/fonts/hack-bolditalic-subset.woff2
--------------------------------------------------------------------------------
/src/styles/hack-font/fonts/hack-bolditalic.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/socketcast/f0f1c7d9f7f414ace50f1dd8045ab91b4d68f858/src/styles/hack-font/fonts/hack-bolditalic.woff
--------------------------------------------------------------------------------
/src/styles/hack-font/fonts/hack-bolditalic.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/socketcast/f0f1c7d9f7f414ace50f1dd8045ab91b4d68f858/src/styles/hack-font/fonts/hack-bolditalic.woff2
--------------------------------------------------------------------------------
/src/styles/hack-font/fonts/hack-italic-subset.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/socketcast/f0f1c7d9f7f414ace50f1dd8045ab91b4d68f858/src/styles/hack-font/fonts/hack-italic-subset.woff
--------------------------------------------------------------------------------
/src/styles/hack-font/fonts/hack-italic-subset.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/socketcast/f0f1c7d9f7f414ace50f1dd8045ab91b4d68f858/src/styles/hack-font/fonts/hack-italic-subset.woff2
--------------------------------------------------------------------------------
/src/styles/hack-font/fonts/hack-italic.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/socketcast/f0f1c7d9f7f414ace50f1dd8045ab91b4d68f858/src/styles/hack-font/fonts/hack-italic.woff
--------------------------------------------------------------------------------
/src/styles/hack-font/fonts/hack-italic.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/socketcast/f0f1c7d9f7f414ace50f1dd8045ab91b4d68f858/src/styles/hack-font/fonts/hack-italic.woff2
--------------------------------------------------------------------------------
/src/styles/hack-font/fonts/hack-regular-subset.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/socketcast/f0f1c7d9f7f414ace50f1dd8045ab91b4d68f858/src/styles/hack-font/fonts/hack-regular-subset.woff
--------------------------------------------------------------------------------
/src/styles/hack-font/fonts/hack-regular-subset.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/socketcast/f0f1c7d9f7f414ace50f1dd8045ab91b4d68f858/src/styles/hack-font/fonts/hack-regular-subset.woff2
--------------------------------------------------------------------------------
/src/styles/hack-font/fonts/hack-regular.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/socketcast/f0f1c7d9f7f414ace50f1dd8045ab91b4d68f858/src/styles/hack-font/fonts/hack-regular.woff
--------------------------------------------------------------------------------
/src/styles/hack-font/fonts/hack-regular.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/socketcast/f0f1c7d9f7f414ace50f1dd8045ab91b4d68f858/src/styles/hack-font/fonts/hack-regular.woff2
--------------------------------------------------------------------------------
/src/styles/hack-font/hack-subset.css:
--------------------------------------------------------------------------------
1 | /*!
2 | * Hack typeface https://github.com/source-foundry/Hack
3 | * License: https://github.com/source-foundry/Hack/blob/master/LICENSE.md
4 | */
5 | /* FONT PATHS
6 | * -------------------------- */
7 | @font-face {
8 | font-family: 'Hack';
9 | src: url('fonts/hack-regular-subset.woff2?sha=3114f1256') format('woff2'), url('fonts/hack-regular-subset.woff?sha=3114f1256') format('woff');
10 | font-weight: 400;
11 | font-style: normal;
12 | }
13 |
14 | @font-face {
15 | font-family: 'Hack';
16 | src: url('fonts/hack-bold-subset.woff2?sha=3114f1256') format('woff2'), url('fonts/hack-bold-subset.woff?sha=3114f1256') format('woff');
17 | font-weight: 700;
18 | font-style: normal;
19 | }
20 |
21 | @font-face {
22 | font-family: 'Hack';
23 | src: url('fonts/hack-italic-subset.woff2?sha=3114f1256') format('woff2'), url('fonts/hack-italic-webfont.woff?sha=3114f1256') format('woff');
24 | font-weight: 400;
25 | font-style: italic;
26 | }
27 |
28 | @font-face {
29 | font-family: 'Hack';
30 | src: url('fonts/hack-bolditalic-subset.woff2?sha=3114f1256') format('woff2'), url('fonts/hack-bolditalic-subset.woff?sha=3114f1256') format('woff');
31 | font-weight: 700;
32 | font-style: italic;
33 | }
34 |
35 |
--------------------------------------------------------------------------------
/src/styles/hack-font/hack-subset.css.in:
--------------------------------------------------------------------------------
1 | /*!
2 | * Hack typeface https://github.com/source-foundry/Hack
3 | * License: https://github.com/source-foundry/Hack/blob/master/LICENSE.md
4 | */
5 | /* FONT PATHS
6 | * -------------------------- */
7 | @font-face {
8 | font-family: 'Hack';
9 | src: url('fonts/hack-regular-subset.woff2?sha={{ ink }}') format('woff2'), url('fonts/hack-regular-subset.woff?sha={{ ink }}') format('woff');
10 | font-weight: 400;
11 | font-style: normal;
12 | }
13 |
14 | @font-face {
15 | font-family: 'Hack';
16 | src: url('fonts/hack-bold-subset.woff2?sha={{ ink }}') format('woff2'), url('fonts/hack-bold-subset.woff?sha={{ ink }}') format('woff');
17 | font-weight: 700;
18 | font-style: normal;
19 | }
20 |
21 | @font-face {
22 | font-family: 'Hack';
23 | src: url('fonts/hack-italic-subset.woff2?sha={{ ink }}') format('woff2'), url('fonts/hack-italic-webfont.woff?sha={{ ink }}') format('woff');
24 | font-weight: 400;
25 | font-style: italic;
26 | }
27 |
28 | @font-face {
29 | font-family: 'Hack';
30 | src: url('fonts/hack-bolditalic-subset.woff2?sha={{ ink }}') format('woff2'), url('fonts/hack-bolditalic-subset.woff?sha={{ ink }}') format('woff');
31 | font-weight: 700;
32 | font-style: italic;
33 | }
34 |
35 |
--------------------------------------------------------------------------------
/src/styles/hack-font/hack.css:
--------------------------------------------------------------------------------
1 | /*!
2 | * Hack typeface https://github.com/source-foundry/Hack
3 | * License: https://github.com/source-foundry/Hack/blob/master/LICENSE.md
4 | */
5 | /* FONT PATHS
6 | * -------------------------- */
7 | @font-face {
8 | font-family: 'Hack';
9 | src: url('fonts/hack-regular.woff2?sha=3114f1256') format('woff2'), url('fonts/hack-regular.woff?sha=3114f1256') format('woff');
10 | font-weight: 400;
11 | font-style: normal;
12 | }
13 |
14 | @font-face {
15 | font-family: 'Hack';
16 | src: url('fonts/hack-bold.woff2?sha=3114f1256') format('woff2'), url('fonts/hack-bold.woff?sha=3114f1256') format('woff');
17 | font-weight: 700;
18 | font-style: normal;
19 | }
20 |
21 | @font-face {
22 | font-family: 'Hack';
23 | src: url('fonts/hack-italic.woff2?sha=3114f1256') format('woff2'), url('fonts/hack-italic.woff?sha=3114f1256') format('woff');
24 | font-weight: 400;
25 | font-style: italic;
26 | }
27 |
28 | @font-face {
29 | font-family: 'Hack';
30 | src: url('fonts/hack-bolditalic.woff2?sha=3114f1256') format('woff2'), url('fonts/hack-bolditalic.woff?sha=3114f1256') format('woff');
31 | font-weight: 700;
32 | font-style: italic;
33 | }
34 |
35 |
--------------------------------------------------------------------------------
/src/styles/hack-font/hack.css.in:
--------------------------------------------------------------------------------
1 | /*!
2 | * Hack typeface https://github.com/source-foundry/Hack
3 | * License: https://github.com/source-foundry/Hack/blob/master/LICENSE.md
4 | */
5 | /* FONT PATHS
6 | * -------------------------- */
7 | @font-face {
8 | font-family: 'Hack';
9 | src: url('fonts/hack-regular.woff2?sha={{ ink }}') format('woff2'), url('fonts/hack-regular.woff?sha={{ ink }}') format('woff');
10 | font-weight: 400;
11 | font-style: normal;
12 | }
13 |
14 | @font-face {
15 | font-family: 'Hack';
16 | src: url('fonts/hack-bold.woff2?sha={{ ink }}') format('woff2'), url('fonts/hack-bold.woff?sha={{ ink }}') format('woff');
17 | font-weight: 700;
18 | font-style: normal;
19 | }
20 |
21 | @font-face {
22 | font-family: 'Hack';
23 | src: url('fonts/hack-italic.woff2?sha={{ ink }}') format('woff2'), url('fonts/hack-italic.woff?sha={{ ink }}') format('woff');
24 | font-weight: 400;
25 | font-style: italic;
26 | }
27 |
28 | @font-face {
29 | font-family: 'Hack';
30 | src: url('fonts/hack-bolditalic.woff2?sha={{ ink }}') format('woff2'), url('fonts/hack-bolditalic.woff?sha={{ ink }}') format('woff');
31 | font-weight: 700;
32 | font-style: italic;
33 | }
34 |
35 |
--------------------------------------------------------------------------------
/tests/components/PlannedResponseCreator/utils.test.ts:
--------------------------------------------------------------------------------
1 | // @ts-nocheck
2 |
3 | import { PlannedResponseUnitType } from '../../../src/components/PlannedResponseCreator/type';
4 |
5 | const { playbackResponseUnits } = require('../../../src/components/PlannedResponseCreator/utils');
6 |
7 | let plannedResponses;
8 |
9 | beforeEach(() => {
10 | plannedResponses = [
11 | { type: PlannedResponseUnitType.MESSAGE, message: 'Message 1 (immediate)' },
12 | { type: PlannedResponseUnitType.DELAY, time: 1000 },
13 | { type: PlannedResponseUnitType.MESSAGE, message: 'Message 2 (2 seconds later)' },
14 | { type: PlannedResponseUnitType.DELAY, time: 1000 },
15 | { type: PlannedResponseUnitType.MESSAGE, message: 'Message 3 (1 second later)' },
16 | { type: PlannedResponseUnitType.MESSAGE, message: 'Message 4 (immediate)' },
17 | ];
18 | });
19 |
20 | test('calls onMessage() callback n number of times when provided with n messages', async () => {
21 | const onMessage = jest.fn();
22 | const messagesCount = plannedResponses.reduce((acc, curr) => (
23 | curr.type === PlannedResponseUnitType.MESSAGE ? acc + 1 : acc
24 | ), 0);
25 | // Run playbackResponseUnits
26 | await playbackResponseUnits(plannedResponses, { onMessage });
27 | // Then check if onMessage has been invoked n number of times
28 | expect(onMessage).toHaveBeenCalledTimes(messagesCount);
29 | }, 3000);
30 |
31 | test('calls complete() callback 1 time when provided with a complete function', async () => {
32 | const onComplete = jest.fn();
33 | // Run playbackResponseUnits
34 | await playbackResponseUnits(plannedResponses, { onComplete });
35 | expect(onComplete).toHaveBeenCalledTimes(1);
36 | });
37 |
38 | test('calls beforeDelay() and afterDelay() n number of times when provided with n delays', async () => {
39 | const beforeDelay = jest.fn();
40 | const afterDelay = jest.fn();
41 | const delayCount = plannedResponses.reduce((acc, curr) => (
42 | curr.type === PlannedResponseUnitType.DELAY? acc + 1 : acc
43 | ), 0);
44 | await playbackResponseUnits(plannedResponses, { beforeDelay, afterDelay });
45 | expect(beforeDelay).toHaveBeenCalledTimes(delayCount);
46 | expect(afterDelay).toHaveBeenCalledTimes(delayCount);
47 | });
48 |
--------------------------------------------------------------------------------
/tests/components/ServerManager/SocketEvent.test.js:
--------------------------------------------------------------------------------
1 | const SocketEvent = require('../../../src/ServerManager/SocketEvent');
2 |
3 | test('SocketEvent() takes a parameter String and parameter Function', () => {
4 | const myEvent = new SocketEvent('event name', () => { });
5 | expect(typeof myEvent.eventName === 'string' || myEvent.eventName instanceof String).toBeTruthy();
6 | // TODO Check for function as second parameter
7 | });
8 |
9 | test('SocketEvent() will throw an error if the first parameter is not a string', () => {
10 | expect(() => { new SocketEvent(12); }).toThrow(); // Number as eventName parameter
11 | expect(() => { new SocketEvent(); }).toThrow(); // null as eventName parameter
12 | });
13 |
14 | test('SocketEvent() will throw an error if the second parameter is provided and is not a Function', () => {
15 | expect(() => { new SocketEvent('string', { onEvent: 'string' }); }).toThrow(); // Number as onEvent parameter
16 | });
17 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | //"allowJs": true,
5 | "module": "commonjs",
6 | "strict": true,
7 | "esModuleInterop": true,
8 | "skipLibCheck": true,
9 | "forceConsistentCasingInFileNames": true,
10 | "jsx": "react",
11 | "baseUrl": "./",
12 | "paths": {
13 | "@/*": [
14 | "src/*"
15 | ]
16 | },
17 | }
18 | }
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | const HtmlWebpackPlugin = require('html-webpack-plugin');
2 | const path = require('path');
3 |
4 | const electronConfiguration = {
5 | mode: 'development',
6 | entry: './src/main.ts',
7 | target: 'electron-main',
8 | resolve: {
9 | alias: {
10 | '@': path.resolve(__dirname, 'src'),
11 | },
12 | extensions: ['.tsx', '.ts', '.js'],
13 | },
14 |
15 | module: {
16 | rules: [{
17 | test: /\.ts$/,
18 | include: /src/,
19 | use: [{ loader: 'ts-loader' }],
20 | }],
21 | },
22 | output: {
23 | path: `${__dirname}/dist`,
24 | filename: 'main.js',
25 | },
26 | };
27 |
28 | const reactConfiguration = {
29 | mode: 'development',
30 | entry: './src/index.tsx',
31 | target: 'electron-renderer',
32 | devtool: 'source-map',
33 | resolve: {
34 | alias: {
35 | '@': path.resolve(__dirname, 'src'),
36 | },
37 | mainFields: ['main', 'browser'],
38 | extensions: ['.tsx', '.ts', '.js'],
39 | },
40 | output: {
41 | path: `${__dirname}/dist`,
42 | filename: 'index.js',
43 | },
44 | module: {
45 | rules: [
46 | /**
47 | * TypeScript loaders
48 | */
49 | {
50 | test: /\.ts(x?)$/,
51 | include: /src/,
52 | use: [{ loader: 'ts-loader' }],
53 | },
54 | /**
55 | * TODO
56 | * This following rule was added because we encountered a webpack error on
57 | * startup of the electron application. This was a fix identified by
58 | * Chance, but it is worth re-visiting what exactly this does and breakdown the regex
59 | * to git our specific use case.
60 | */
61 | {
62 | test: /node_modules[\/\\](iconv-lite)[\/\\].+/,
63 | resolve: {
64 | aliasFields: ['main'],
65 | },
66 | },
67 | {
68 | test: /\.(woff|woff2|eot|ttf|otf)$/,
69 | use: [
70 | 'file-loader',
71 | ],
72 | },
73 | /**
74 | * CSS / Sass Loaders
75 | */
76 | {
77 | test: /\.s[ac]ss$/i,
78 | use: [
79 | 'style-loader',
80 | 'css-loader',
81 | 'sass-loader',
82 | ],
83 | },
84 | ],
85 | },
86 | plugins: [
87 | new HtmlWebpackPlugin({
88 | template: './src/index.html',
89 | }),
90 | ],
91 | };
92 |
93 | module.exports = [
94 | electronConfiguration,
95 | reactConfiguration,
96 | ];
97 |
--------------------------------------------------------------------------------