├── .env ├── .env.example ├── src ├── logo.png ├── locale │ ├── translations │ │ └── en.js │ └── i18n.js ├── setupTests.js ├── App.test.js ├── App.jsx ├── index.js ├── App.css ├── Store.js ├── Routes.jsx ├── Helpers.js ├── components │ ├── LogItem.jsx │ ├── PayloadPopup.jsx │ └── WsClient.jsx ├── layouts │ └── AppLayout.jsx └── serviceWorker.js ├── public ├── robots.txt ├── favicon.ico ├── assets │ ├── favicon.ico │ ├── favicon-16x16.png │ ├── favicon-32x32.png │ ├── apple-touch-icon.png │ ├── android-chrome-192x192.png │ └── android-chrome-512x512.png ├── manifest.json └── index.html ├── screenshots └── screen_1.png ├── Dockerfile ├── docker-compose.yml ├── config-overrides.js ├── .gitignore ├── LICENSE.md ├── package.json └── README.md /.env: -------------------------------------------------------------------------------- 1 | REACT_APP_APP_NAME="Socktest" 2 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | REACT_APP_APP_NAME="WebSocket Tester React" 2 | -------------------------------------------------------------------------------- /src/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rahulhaque/websocket-tester-react/HEAD/src/logo.png -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rahulhaque/websocket-tester-react/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /screenshots/screen_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rahulhaque/websocket-tester-react/HEAD/screenshots/screen_1.png -------------------------------------------------------------------------------- /public/assets/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rahulhaque/websocket-tester-react/HEAD/public/assets/favicon.ico -------------------------------------------------------------------------------- /src/locale/translations/en.js: -------------------------------------------------------------------------------- 1 | export default { 2 | translation: { 3 | welcome: 'Welcome to React' 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /public/assets/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rahulhaque/websocket-tester-react/HEAD/public/assets/favicon-16x16.png -------------------------------------------------------------------------------- /public/assets/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rahulhaque/websocket-tester-react/HEAD/public/assets/favicon-32x32.png -------------------------------------------------------------------------------- /public/assets/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rahulhaque/websocket-tester-react/HEAD/public/assets/apple-touch-icon.png -------------------------------------------------------------------------------- /public/assets/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rahulhaque/websocket-tester-react/HEAD/public/assets/android-chrome-192x192.png -------------------------------------------------------------------------------- /public/assets/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rahulhaque/websocket-tester-react/HEAD/public/assets/android-chrome-512x512.png -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:17-alpine3.12 2 | 3 | EXPOSE 3000 4 | 5 | RUN mkdir -p /socktest 6 | 7 | WORKDIR /socktest 8 | 9 | RUN npm install -g serve 10 | 11 | CMD [ "serve", "-s", "-C" ] 12 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | services: 4 | socktest: 5 | build: . 6 | image: socktest 7 | volumes: 8 | - ./build:/socktest 9 | ports: 10 | - "3000:3000" 11 | -------------------------------------------------------------------------------- /src/setupTests.js: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom/extend-expect'; 6 | -------------------------------------------------------------------------------- /src/App.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render } from '@testing-library/react'; 3 | import App from './App'; 4 | 5 | test('renders learn react link', () => { 6 | const { getByText } = render(); 7 | const linkElement = getByText(/learn react/i); 8 | expect(linkElement).toBeInTheDocument(); 9 | }); 10 | -------------------------------------------------------------------------------- /src/App.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import AppLayout from './layouts/AppLayout'; 4 | 5 | import { Provider } from './Store'; 6 | 7 | import 'rsuite/dist/styles/rsuite-dark.css'; 8 | import './App.css'; 9 | 10 | const App = () => ( 11 | 12 | 13 | 14 | ); 15 | 16 | export default App; 17 | -------------------------------------------------------------------------------- /config-overrides.js: -------------------------------------------------------------------------------- 1 | const { override, addLessLoader, addBabelPlugin } = require("customize-cra"); 2 | 3 | module.exports = override( 4 | addLessLoader({ 5 | lessOptions: { 6 | javascriptEnabled: true, 7 | modifyVars: {} 8 | } 9 | }), 10 | addBabelPlugin(["prismjs", { 11 | "languages": ["json"], 12 | "plugins": ["line-numbers", "show-language"], 13 | "theme": "okaidia", 14 | "css": true 15 | }]) 16 | ); 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | # /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | 25 | .idea 26 | .vscode 27 | .package-lock.json 28 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './App'; 4 | import './locale/i18n'; 5 | import * as serviceWorker from './serviceWorker'; 6 | 7 | ReactDOM.render( 8 | 9 | 10 | , 11 | document.getElementById('root') 12 | ); 13 | 14 | // If you want your app to work offline and load faster, you can change 15 | // unregister() to register() below. Note this comes with some pitfalls. 16 | // Learn more about service workers: https://bit.ly/CRA-PWA 17 | serviceWorker.register(); 18 | -------------------------------------------------------------------------------- /src/App.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | padding: 0; 4 | } 5 | 6 | .navbar-brand { 7 | padding: 18px 20px; 8 | display: inline-block; 9 | } 10 | 11 | .navbar-brand:hover, .navbar-brand:focus { 12 | text-decoration: none; 13 | } 14 | 15 | /* For iPhone 5/SE */ 16 | textarea.rs-input { 17 | min-width: 200px; 18 | } 19 | 20 | .rs-notification { 21 | top: 12px !important; 22 | right: 12px; 23 | bottom: 12px; 24 | max-width: 294px; 25 | } 26 | 27 | .rs-notification-item-content { 28 | padding: 15px; 29 | } 30 | 31 | .show-grid [class*=rs-col-] { 32 | padding-top: 10px; 33 | margin-top: 10px; 34 | } 35 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "Socktest", 3 | "name": "Socktest", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "./assets/android-chrome-192x192.png", 12 | "sizes": "192x192", 13 | "type": "image/png" 14 | }, 15 | { 16 | "src": "./assets/android-chrome-512x512.png", 17 | "sizes": "512x512", 18 | "type": "image/png" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#1a1d24", 24 | "background_color": "#1a1d24" 25 | } 26 | -------------------------------------------------------------------------------- /src/locale/i18n.js: -------------------------------------------------------------------------------- 1 | import i18n from 'i18next'; 2 | import { initReactI18next } from 'react-i18next'; 3 | 4 | // Add translation file in 'translation' folder 5 | // Import them here 6 | import en from './translations/en'; 7 | 8 | i18n 9 | .use(initReactI18next) // passes i18n down to react-i18next 10 | .init({ 11 | resources: { 12 | en: en, 13 | // Add imported translation 14 | }, 15 | lng: 'en', // Initial locale 16 | keySeparator: false, // we do not use keys in form messages.welcome 17 | interpolation: { 18 | escapeValue: false // react already safes from xss 19 | } 20 | }); 21 | 22 | export default i18n; 23 | -------------------------------------------------------------------------------- /src/Store.js: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from 'react'; 2 | import { createContainer } from 'react-tracked'; 3 | import dayjs from 'dayjs'; 4 | 5 | import { loadState, saveState } from './Helpers'; 6 | 7 | const globalState = { 8 | // Declare your global variables here 9 | host: 'localhost:6001', 10 | protocols: '', 11 | payload: '', 12 | activePayload: '0', 13 | payloads: [ 14 | {id: '0', label: 'Default', payload: ''} 15 | ], 16 | secure: true, 17 | autoConnect: false, 18 | connectionLog: [{ 19 | datetime: dayjs().format('YYYY-MM-DD hh:mm:ss A'), 20 | message: `App started` 21 | }] 22 | }; 23 | 24 | // Returns state from localstorage if exists 25 | const useLocalState = () => { 26 | const [processedState, setProcessedState] = useState((loadState() || globalState)); 27 | useEffect(() => { 28 | saveState(processedState); 29 | }, [processedState]); 30 | return [processedState, setProcessedState]; 31 | }; 32 | 33 | export const { Provider, useTracked } = createContainer(useLocalState); 34 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 rahulhaque 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 | -------------------------------------------------------------------------------- /src/Routes.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | HashRouter, 4 | Route, 5 | Switch, 6 | Redirect 7 | } from 'react-router-dom'; 8 | 9 | import AppLayout from './layouts/AppLayout'; 10 | 11 | import { isLoggedIn } from './Helpers'; 12 | 13 | export const PrivateRoute = ({ component: Component, ...rest }) => ( 14 | 17 | isLoggedIn() ? ( 18 | 19 | ) : ( 20 | 26 | ) 27 | } 28 | /> 29 | ); 30 | 31 | export const GuestRoute = ({ component: Component, ...rest }) => ( 32 | 35 | !isLoggedIn() ? ( 36 | 37 | ) : ( 38 | 44 | ) 45 | } 46 | /> 47 | ); 48 | 49 | const Routes = () => ( 50 | 51 | 52 | 53 | 54 | 55 | ); 56 | 57 | export default React.memo(Routes); 58 | -------------------------------------------------------------------------------- /src/Helpers.js: -------------------------------------------------------------------------------- 1 | export const loadState = () => { 2 | try { 3 | const serialisedState = localStorage.getItem('socktest-state'); 4 | if (serialisedState === null) { 5 | return undefined; 6 | } 7 | return JSON.parse(serialisedState); 8 | } catch (error) { 9 | return undefined; 10 | } 11 | }; 12 | 13 | export const saveState = (state) => { 14 | try { 15 | const serialisedState = JSON.stringify(state); 16 | localStorage.setItem('socktest-state', serialisedState); 17 | } catch (error) { 18 | // Ignore write errors. 19 | } 20 | }; 21 | 22 | // Get item from localstorage 23 | export const getItem = (item, def = undefined) => { 24 | try { 25 | const localItem = localStorage.getItem(item); 26 | if (localItem === null) { 27 | return def; 28 | } 29 | return JSON.parse(localItem); 30 | } 31 | catch (e) { 32 | return undefined; 33 | } 34 | }; 35 | 36 | // Save item in localstorage 37 | export const setItem = (item, value) => { 38 | localStorage.setItem(item, JSON.stringify(value)); 39 | }; 40 | 41 | // Remove item from localstorage 42 | export const removeItem = (item) => { 43 | localStorage.removeItem(item); 44 | }; 45 | 46 | // Check if user logged in from localstorage 47 | export const isLoggedIn = () => { 48 | return getItem('access_token') && getItem('token_created') && getItem('expires_in'); 49 | }; 50 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "socktest", 3 | "version": "1.0.0", 4 | "homepage": "./", 5 | "private": true, 6 | "dependencies": { 7 | "@rsuite/responsive-nav": "0.0.10", 8 | "@testing-library/jest-dom": "^4.2.4", 9 | "@testing-library/react": "^9.5.0", 10 | "@testing-library/user-event": "^7.2.1", 11 | "dayjs": "^1.8.28", 12 | "i18next": "^19.4.5", 13 | "prismjs": "^1.20.0", 14 | "react": "^16.13.1", 15 | "react-dom": "^16.13.1", 16 | "react-i18next": "^11.5.1", 17 | "react-scripts": "^5.0.1", 18 | "react-tracked": "^1.3.0", 19 | "rsuite": "^4.7.4" 20 | }, 21 | "scripts": { 22 | "start": "react-app-rewired start", 23 | "build": "react-app-rewired build", 24 | "test": "react-app-rewired test", 25 | "eject": "react-app-rewired eject" 26 | }, 27 | "eslintConfig": { 28 | "extends": "react-app" 29 | }, 30 | "browserslist": { 31 | "production": [ 32 | ">0.2%", 33 | "not dead", 34 | "not op_mini all" 35 | ], 36 | "development": [ 37 | "last 1 chrome version", 38 | "last 1 firefox version", 39 | "last 1 safari version" 40 | ] 41 | }, 42 | "devDependencies": { 43 | "babel-plugin-prismjs": "^2.0.1", 44 | "customize-cra": "^1.0.0", 45 | "less": "^3.11.3", 46 | "less-loader": "^6.1.1", 47 | "react-app-rewired": "^2.1.6" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/components/LogItem.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import Prism from 'prismjs'; 4 | 5 | import { 6 | Icon, 7 | Timeline, 8 | } from 'rsuite'; 9 | 10 | const LogItem = (props) => { 11 | 12 | useEffect(() => { 13 | Prism.highlightAll(); 14 | }, [props.logs]); 15 | 16 | return ( 17 | 18 | { 19 | props.logs.map((item, index) => { 20 | return 24 |

{item.datetime}

25 |

{item.message}

26 | { 27 | item?.payload ? 28 |
29 | { 30 | item.dataflow === 'incoming' ? 31 | : 32 | 33 | } 34 |
35 |                     
36 |                       {item?.payload}
37 |                     
38 |                   
39 |
: "" 40 | } 41 |
42 | }) 43 | } 44 |
45 | ) 46 | } 47 | 48 | LogItem.propTypes = { 49 | logs: PropTypes.array 50 | }; 51 | 52 | export default React.memo(LogItem); 53 | -------------------------------------------------------------------------------- /src/layouts/AppLayout.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { 4 | Grid, 5 | Row, 6 | Nav, 7 | Navbar, 8 | Toggle, 9 | } from 'rsuite'; 10 | 11 | import WsClient from './../components/WsClient'; 12 | 13 | import { useTracked } from './../Store'; 14 | 15 | const app_name = process.env.REACT_APP_APP_NAME; 16 | 17 | const AppLayout = (props) => { 18 | 19 | const [state, setState] = useTracked(); 20 | 21 | return ( 22 | 23 | 24 | 25 | 26 | 31 | logo {app_name} 32 | 33 | 34 | 35 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | ) 53 | } 54 | 55 | export default React.memo(AppLayout) 56 | -------------------------------------------------------------------------------- /src/components/PayloadPopup.jsx: -------------------------------------------------------------------------------- 1 | import React, { useRef } from 'react'; 2 | 3 | import { Whisper, Popover, Tooltip, Input, InputGroup, Icon } from 'rsuite'; 4 | 5 | import { useTracked } from './../Store'; 6 | 7 | let payloadPopupTrigger = null; 8 | 9 | const PayloadPopup = (props) => { 10 | 11 | const [state, setState] = useTracked(); 12 | 13 | const protocols = useRef(); 14 | 15 | return ( 16 | (payloadPopupTrigger = ref)} 20 | speaker={ 21 | 23 | Websocket Protocol 24 | Enter protocols separated by comma. 27 | } 28 | trigger="hover" 29 | placement="bottomEnd" 30 | > 31 | 32 | 33 | 34 | 35 | 36 | }> 37 | payloadPopupTrigger.hide()} /> 38 | 39 | } 40 | onOpen={() => protocols.current.focus()} 41 | onExit={() => { 42 | setState(prev => ({ ...prev, protocols: protocols.current.value })) 43 | }} 44 | > 45 | 46 | 47 | 48 | ) 49 | }; 50 | 51 | export default React.memo(PayloadPopup); 52 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 14 | 15 | 24 | Socktest 25 | 26 | 27 | 28 |
29 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Socktest 🔌 WebSocket Tester React 2 | 3 | **Socktest** is a simple websocket client application built with React for testing and debugging websocket server. It can connect to a websocket server and send payloads and receive responses from the server along with tracking connection activity log. 4 | 5 | Demo - [Socktest](https://rahulhaque.github.io/websocket-tester-react/build) 6 | 7 | ## Features 8 | 9 | - Connect to any websocket. 10 | - Send and receive payloads and responses. 11 | - Payload is saved on successful send. 12 | - Multiple payloads storage support. 13 | - Websocket protocol support. 14 | - Realtime persistent connection activity log. 15 | - Auto reconnect option. 16 | - Clear log view. 17 | - Responsive UI. 18 | - JSON highlighter. 19 | - Progressive Web App (Works both online and offline). 20 | 21 | ## Shortcuts 22 | 23 | - Protocol set on `enter` press. 24 | 25 | ## Usage 26 | 27 | - Clone or download the repo. 28 | - `cd` into directory and run `npm i` to install the dependencies. 29 | - Run `npm start` to launch the app. 30 | - Go to `http://localhost:3000` 31 | 32 | ## Docker 33 | 34 | - Run `docker-compose up -d` to start. 35 | - Go to `http://localhost:3000` 36 | - Run `docker-compose down` to stop. 37 | 38 | ## Screenshots 39 | 40 | 41 | 42 | ## More Info 43 | 44 | This is a basic approach towards making a websocket tester app which can be used to test the logic of any websocket server. Often we need to implement a websocket server for ourselves. However, testing process of the inner logic of the websocket quickly becomes tiresome. This app aims to lessen that effort. 45 | 46 | Spare a ⭐ to keep me motivated. 😃 47 | 48 | ## License 49 | 50 | This software is licensed under the MIT License (MIT). You are free to use and modify the code. A simple mention or reference would be highly appreciated. 51 | -------------------------------------------------------------------------------- /src/serviceWorker.js: -------------------------------------------------------------------------------- 1 | // This optional code is used to register a service worker. 2 | // register() is not called by default. 3 | 4 | // This lets the app load faster on subsequent visits in production, and gives 5 | // it offline capabilities. However, it also means that developers (and users) 6 | // will only see deployed updates on subsequent visits to a page, after all the 7 | // existing tabs open on the page have been closed, since previously cached 8 | // resources are updated in the background. 9 | 10 | // To learn more about the benefits of this model and instructions on how to 11 | // opt-in, read https://bit.ly/CRA-PWA 12 | 13 | const isLocalhost = Boolean( 14 | window.location.hostname === 'localhost' || 15 | // [::1] is the IPv6 localhost address. 16 | window.location.hostname === '[::1]' || 17 | // 127.0.0.0/8 are considered localhost for IPv4. 18 | window.location.hostname.match( 19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 20 | ) 21 | ); 22 | 23 | export function register(config) { 24 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 25 | // The URL constructor is available in all browsers that support SW. 26 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href); 27 | if (publicUrl.origin !== window.location.origin) { 28 | // Our service worker won't work if PUBLIC_URL is on a different origin 29 | // from what our page is served on. This might happen if a CDN is used to 30 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374 31 | return; 32 | } 33 | 34 | window.addEventListener('load', () => { 35 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 36 | 37 | if (isLocalhost) { 38 | // This is running on localhost. Let's check if a service worker still exists or not. 39 | checkValidServiceWorker(swUrl, config); 40 | 41 | // Add some additional logging to localhost, pointing developers to the 42 | // service worker/PWA documentation. 43 | navigator.serviceWorker.ready.then(() => { 44 | console.log( 45 | 'This web app is being served cache-first by a service ' + 46 | 'worker. To learn more, visit https://bit.ly/CRA-PWA' 47 | ); 48 | }); 49 | } else { 50 | // Is not localhost. Just register service worker 51 | registerValidSW(swUrl, config); 52 | } 53 | }); 54 | } 55 | } 56 | 57 | function registerValidSW(swUrl, config) { 58 | navigator.serviceWorker 59 | .register(swUrl) 60 | .then(registration => { 61 | registration.onupdatefound = () => { 62 | const installingWorker = registration.installing; 63 | if (installingWorker == null) { 64 | return; 65 | } 66 | installingWorker.onstatechange = () => { 67 | if (installingWorker.state === 'installed') { 68 | if (navigator.serviceWorker.controller) { 69 | // At this point, the updated precached content has been fetched, 70 | // but the previous service worker will still serve the older 71 | // content until all client tabs are closed. 72 | console.log( 73 | 'New content is available and will be used when all ' + 74 | 'tabs for this page are closed. See https://bit.ly/CRA-PWA.' 75 | ); 76 | 77 | // Execute callback 78 | if (config && config.onUpdate) { 79 | config.onUpdate(registration); 80 | } 81 | } else { 82 | // At this point, everything has been precached. 83 | // It's the perfect time to display a 84 | // "Content is cached for offline use." message. 85 | console.log('Content is cached for offline use.'); 86 | 87 | // Execute callback 88 | if (config && config.onSuccess) { 89 | config.onSuccess(registration); 90 | } 91 | } 92 | } 93 | }; 94 | }; 95 | }) 96 | .catch(error => { 97 | console.error('Error during service worker registration:', error); 98 | }); 99 | } 100 | 101 | function checkValidServiceWorker(swUrl, config) { 102 | // Check if the service worker can be found. If it can't reload the page. 103 | fetch(swUrl, { 104 | headers: { 'Service-Worker': 'script' }, 105 | }) 106 | .then(response => { 107 | // Ensure service worker exists, and that we really are getting a JS file. 108 | const contentType = response.headers.get('content-type'); 109 | if ( 110 | response.status === 404 || 111 | (contentType != null && contentType.indexOf('javascript') === -1) 112 | ) { 113 | // No service worker found. Probably a different app. Reload the page. 114 | navigator.serviceWorker.ready.then(registration => { 115 | registration.unregister().then(() => { 116 | window.location.reload(); 117 | }); 118 | }); 119 | } else { 120 | // Service worker found. Proceed as normal. 121 | registerValidSW(swUrl, config); 122 | } 123 | }) 124 | .catch(() => { 125 | console.log( 126 | 'No internet connection found. App is running in offline mode.' 127 | ); 128 | }); 129 | } 130 | 131 | export function unregister() { 132 | if ('serviceWorker' in navigator) { 133 | navigator.serviceWorker.ready 134 | .then(registration => { 135 | registration.unregister(); 136 | }) 137 | .catch(error => { 138 | console.error(error.message); 139 | }); 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /src/components/WsClient.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useRef, useEffect } from 'react'; 2 | import dayjs from 'dayjs'; 3 | 4 | import { 5 | Icon, 6 | Input, 7 | InputGroup, 8 | Panel, 9 | PanelGroup, 10 | Button, 11 | Notification, 12 | IconButton, 13 | Row, 14 | Col 15 | } from 'rsuite'; 16 | import ResponsiveNav from '@rsuite/responsive-nav'; 17 | 18 | import LogItem from './LogItem'; 19 | import PayloadPopup from './PayloadPopup'; 20 | 21 | import { useTracked } from './../Store'; 22 | 23 | let websocket = null; 24 | let wsHost = ''; 25 | 26 | const WsClient = (props) => { 27 | 28 | const [state, setState] = useTracked(); 29 | const [connection, setConnection] = useState({ connected: false, connecting: false }); 30 | 31 | const host = useRef(); 32 | const payload = useRef(); 33 | const stateRef = useRef({ 34 | connectionLog: state.connectionLog, 35 | retryCount: 0, 36 | autoConnect: state.autoConnect 37 | }); 38 | 39 | useEffect(() => { 40 | stateRef.current.autoConnect = state.autoConnect; 41 | }, [state.autoConnect]); 42 | 43 | useEffect(() => { 44 | let currentPayload = state.payloads.find(payload => payload.id === state.activePayload); 45 | if (!currentPayload) { 46 | setState(prev => ({ ...prev, activePayload: '0' })); 47 | } 48 | payload.current.value = state.payloads.find(payload => payload.id === state.activePayload)?.payload; 49 | }, [state.activePayload, state.payloads, setState]); 50 | 51 | const updateLog = (log) => { 52 | stateRef.current.connectionLog.unshift(log); 53 | setState(prev => ({ ...prev, connectionLog: [...stateRef.current.connectionLog] })); 54 | }; 55 | 56 | const clearLog = () => { 57 | stateRef.current.connectionLog = [{ 58 | datetime: dayjs().format('YYYY-MM-DD hh:mm:ss A'), 59 | message: `App started` 60 | }]; 61 | setState(prev => ({ 62 | ...prev, connectionLog: stateRef.current.connectionLog 63 | })); 64 | }; 65 | 66 | const onOpen = (event) => { 67 | stateRef.current.retryCount = 0; 68 | updateLog({ 69 | datetime: dayjs().format('YYYY-MM-DD hh:mm:ss A'), 70 | message: `Connected to "${event.target.url}"` 71 | }); 72 | setConnection({ ...connection, connected: true, connecting: false }); 73 | payload.current.focus(); 74 | }; 75 | 76 | const onMessage = (event) => { 77 | // console.log(event); 78 | updateLog({ 79 | datetime: dayjs().format('YYYY-MM-DD hh:mm:ss A'), 80 | message: `Message received from "${event.origin}"`, 81 | payload: event.data, 82 | dataflow: 'incoming' 83 | }); 84 | }; 85 | 86 | const onError = (event) => { 87 | // Error handling 88 | // console.error(event); 89 | updateLog({ 90 | datetime: dayjs().format('YYYY-MM-DD hh:mm:ss A'), 91 | message: `Could not connect to "${event.target.url}". You may be able to find more information using inspector.` 92 | }); 93 | setConnection({ ...connection, connected: false, connecting: false }); 94 | }; 95 | 96 | const onClose = (event) => { 97 | // Close handling 98 | // console.log(event); 99 | updateLog({ 100 | datetime: dayjs().format('YYYY-MM-DD hh:mm:ss A'), 101 | message: `Connection closed "${event.target.url}"` 102 | }); 103 | setConnection({ ...connection, connected: false, connecting: false }); 104 | reconnect(); 105 | }; 106 | 107 | const reconnect = () => { 108 | if (stateRef.current.autoConnect) { 109 | if (stateRef.current.retryCount >= 3) { 110 | Notification.warning({ title: 'Warning', description: `Stopped trying to reconnect after ${stateRef.current.retryCount} attempts.` }); 111 | stateRef.current.retryCount = 0; 112 | } 113 | else { 114 | stateRef.current.retryCount = stateRef.current.retryCount + 1; 115 | connect(); 116 | Notification.info({ title: 'Info', description: `Tried to reconnect ${stateRef.current.retryCount} times.` }); 117 | } 118 | } 119 | }; 120 | 121 | const connect = () => { 122 | if (host.current.value === '') { 123 | Notification.error({ title: 'Error', description: 'Websocket host is not defined.' }); 124 | } 125 | else { 126 | wsHost = `${state.secure ? 'wss://' : 'ws://'}${host.current.value}`; 127 | 128 | setState(prev => ({ ...prev, host: host.current.value })); 129 | if (websocket?.readyState !== 1) { 130 | updateLog({ 131 | datetime: dayjs().format('YYYY-MM-DD hh:mm:ss A'), 132 | message: `Connecting to "${wsHost}/"` 133 | }); 134 | setConnection({ ...connection, connecting: true }); 135 | 136 | if (state.protocols === '') { 137 | websocket = new WebSocket(wsHost); 138 | } 139 | else { 140 | websocket = new WebSocket(wsHost, state.protocols.replace(/\s+/g, '').split(',')); 141 | } 142 | } 143 | 144 | websocket.onopen = onOpen; 145 | websocket.onmessage = onMessage; 146 | websocket.onerror = onError; 147 | websocket.onclose = onClose; 148 | } 149 | }; 150 | 151 | const disconnect = () => { 152 | if (websocket?.readyState === 1) { 153 | websocket.close(); 154 | } 155 | }; 156 | 157 | const sendMessage = (message) => { 158 | // console.log(websocket?.readyState); 159 | setState(prev => ({ 160 | ...prev, payloads: state.payloads.map(item => { 161 | if (item.id === state.activePayload) { 162 | return { ...item, payload: message }; 163 | } 164 | return item; 165 | }) 166 | })); 167 | switch (websocket?.readyState) { 168 | case 1: 169 | if (message) { 170 | websocket.send(message); 171 | updateLog({ 172 | datetime: dayjs().format('YYYY-MM-DD hh:mm:ss A'), 173 | message: `Payload send to "${wsHost}"`, 174 | payload: message, 175 | dataflow: 'outgoing' 176 | }); 177 | break; 178 | } 179 | 180 | Notification.error({ title: 'Error', description: 'Payload is empty.' }); 181 | break; 182 | 183 | default: 184 | Notification.error({ title: 'Error', description: 'Websocket disconnected.' }); 185 | break; 186 | } 187 | 188 | }; 189 | 190 | return ( 191 | 192 | 193 | 194 | 195 | 196 | 197 | setState(prev => ({ ...prev, secure: !state.secure }))} 200 | color={state.secure ? 'green' : 'orange'} 201 | > 202 | {state.secure ? 'wss://' : 'ws://'} 203 | 204 | 205 | 206 | { 207 | connection.connected ? ( 208 | disconnect()} 211 | loading={connection.connecting} 212 | > 213 | Disconnect 214 | 215 | ) : ( 216 | connect()} 219 | loading={connection.connecting} 220 | > 221 | Connect 222 | 223 | ) 224 | } 225 | 226 | 227 | 228 | { 229 | 230 | if (key === 'add_new') { 231 | let max = Math.max(...state.payloads.map(item => item.id)) + 1; 232 | 233 | setState(prev => ({ 234 | ...prev, 235 | activePayload: `${max}`, 236 | payloads: [...prev.payloads, { 237 | id: `${max}`, 238 | label: `Payload ${max}`, 239 | payload: `` 240 | }] 241 | })); 242 | } 243 | else if (key) { 244 | payload.current.value = state.payloads.find(payload => payload.id === key).payload 245 | setState(prev => ({ ...prev, activePayload: key })); 246 | } 247 | 248 | }}> 249 | { 250 | state.payloads.map(payload => { 251 | return 252 | {payload.label} 253 | { 254 | payload.id !== '0' ? 255 | { 260 | const slicedPayloads = [...state.payloads]; 261 | slicedPayloads.splice( 262 | slicedPayloads.map(payload => payload.id).indexOf(payload.id), 1 263 | ); 264 | setState(prev => ({ 265 | ...prev, 266 | payloads: slicedPayloads 267 | })) 268 | }} 269 | icon={} 270 | style={{ height: '16px', width: '16px', top: '-5px', marginLeft: '4px' }}> 271 | : '' 272 | } 273 | 274 | }) 275 | } 276 | } 280 | style={{ background: '#292d33', borderRadius: '6px 6px 0 0' }}> Add New 281 | 282 | 283 |
284 | 293 | 294 | 295 | 307 | 308 | 309 | 320 | 321 | 322 | 323 | 324 | 325 |
326 | 328 | Connection Log 329 | 330 | 331 | }> 332 | 333 | 334 |
335 | ) 336 | } 337 | 338 | export default React.memo(WsClient); 339 | --------------------------------------------------------------------------------