├── .github └── workflows │ └── ci.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── assets ├── dashboard.png └── login-modal.png ├── docker-compose.yml ├── examples ├── .gitignore ├── cluster-nodejs │ ├── index.js │ └── package.json ├── cluster-redis │ ├── docker-compose.yml │ ├── index.js │ └── package.json ├── single-server-msgpack-parser │ ├── index.js │ └── package.json ├── single-server-nestjs │ ├── .eslintrc.js │ ├── .gitignore │ ├── .prettierrc │ ├── README.md │ ├── nest-cli.json │ ├── package.json │ ├── src │ │ ├── app.controller.spec.ts │ │ ├── app.controller.ts │ │ ├── app.module.ts │ │ ├── app.service.ts │ │ ├── events.gateway.ts │ │ ├── events.module.ts │ │ └── main.ts │ ├── test │ │ ├── app.e2e-spec.ts │ │ └── jest-e2e.json │ ├── tsconfig.build.json │ └── tsconfig.json └── single-server │ ├── index.js │ └── package.json ├── lib ├── index.ts ├── stores.ts └── typed-events.ts ├── package-lock.json ├── package.json ├── test ├── events.ts ├── index.ts ├── stores.ts └── util.ts ├── tsconfig.json └── ui ├── .browserslistrc ├── .env ├── .eslintrc.js ├── .gitignore ├── Dockerfile ├── README.md ├── babel.config.js ├── dist ├── css │ ├── app.6e3b9661.css │ └── chunk-vendors.9f55d012.css ├── favicon.png ├── img │ ├── logo-dark.3727fec5.svg │ └── logo-light.73342c25.svg ├── index.html └── js │ ├── app.0d7d7845.js │ ├── app.0d7d7845.js.map │ ├── chunk-vendors.934c03ff.js │ └── chunk-vendors.934c03ff.js.map ├── package-lock.json ├── package.json ├── public ├── favicon.png └── index.html ├── src ├── App.vue ├── SocketHolder.js ├── assets │ ├── logo-dark.svg │ └── logo-light.svg ├── components │ ├── AppBar.vue │ ├── Client │ │ ├── ClientDetails.vue │ │ └── ClientSockets.vue │ ├── ConnectionModal.vue │ ├── ConnectionStatus.vue │ ├── Dashboard │ │ ├── BytesHistogram.vue │ │ ├── ClientsOverview.vue │ │ ├── ConnectionsHistogram.vue │ │ ├── NamespacesOverview.vue │ │ └── ServersOverview.vue │ ├── EventType.vue │ ├── KeyValueTable.vue │ ├── LangSelector.vue │ ├── NamespaceSelector.vue │ ├── NavigationDrawer.vue │ ├── ReadonlyToggle.vue │ ├── Room │ │ ├── RoomDetails.vue │ │ ├── RoomSockets.vue │ │ ├── RoomStatus.vue │ │ └── RoomType.vue │ ├── ServerStatus.vue │ ├── Socket │ │ ├── InitialRequest.vue │ │ ├── SocketDetails.vue │ │ └── SocketRooms.vue │ ├── Status.vue │ ├── ThemeSelector.vue │ └── Transport.vue ├── i18n.js ├── locales │ ├── bn.json │ ├── en.json │ ├── fr.json │ ├── ko.json │ ├── pt-BR.json │ ├── tr.json │ └── zh-CN.json ├── main.js ├── plugins │ ├── chartjs.js │ └── vuetify.js ├── router │ └── index.js ├── store │ ├── index.js │ └── modules │ │ ├── config.js │ │ ├── connection.js │ │ ├── main.js │ │ └── servers.js ├── util.js └── views │ ├── Client.vue │ ├── Clients.vue │ ├── Dashboard.vue │ ├── Events.vue │ ├── Room.vue │ ├── Rooms.vue │ ├── Servers.vue │ ├── Socket.vue │ └── Sockets.vue └── vue.config.js /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | pull_request: 6 | schedule: 7 | - cron: '0 0 * * 1' 8 | 9 | permissions: 10 | contents: read 11 | 12 | jobs: 13 | test-node: 14 | runs-on: ubuntu-latest 15 | timeout-minutes: 10 16 | 17 | strategy: 18 | matrix: 19 | node-version: 20 | - 14 21 | - 18 22 | 23 | services: 24 | redis: 25 | image: redis 26 | options: >- 27 | --health-cmd "redis-cli ping" 28 | --health-interval 10s 29 | --health-timeout 5s 30 | --health-retries 5 31 | ports: 32 | - 6379:6379 33 | 34 | steps: 35 | - name: Checkout repository 36 | uses: actions/checkout@v3 37 | 38 | - name: Use Node.js ${{ matrix.node-version }} 39 | uses: actions/setup-node@v3 40 | with: 41 | node-version: ${{ matrix.node-version }} 42 | 43 | - name: Install dependencies 44 | run: npm ci 45 | 46 | - name: Run tests 47 | run: npm test 48 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | .nyc_output 4 | dist 5 | 6 | 7 | # local env files 8 | .env.local 9 | .env.*.local 10 | 11 | # Log files 12 | npm-debug.log* 13 | yarn-debug.log* 14 | yarn-error.log* 15 | pnpm-debug.log* 16 | 17 | # Editor directories and files 18 | .idea 19 | .vscode 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | 26 | .vercel 27 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## [0.5.1](https://github.com/socketio/socket.io-admin-ui/compare/0.5.0...0.5.1) (2022-10-07) 2 | 3 | 4 | ### Bug Fixes 5 | 6 | * **server:** handle late HTTP server binding ([b21b649](https://github.com/socketio/socket.io-admin-ui/commit/b21b649ea246eec87af121ffe676c876b001de05)) 7 | * **server:** properly track events with acknowledgement ([6d58a75](https://github.com/socketio/socket.io-admin-ui/commit/6d58a755b4d692970d3f2066903a4d503f334f0a)) 8 | 9 | 10 | 11 | # [0.5.0](https://github.com/socketio/socket.io-admin-ui/compare/0.4.0...0.5.0) (2022-09-19) 12 | 13 | 14 | ### Features 15 | 16 | * **server:** switch to the bcryptjs module ([81af1d4](https://github.com/socketio/socket.io-admin-ui/commit/81af1d40ae91b6f83e45b505a609b7ec25a75115)) 17 | * **ui:** add tr locale ([#52](https://github.com/socketio/socket.io-admin-ui/issues/52)) ([7e36532](https://github.com/socketio/socket.io-admin-ui/commit/7e365322421bbfd28f01ad8a1065025ede054ecf)) 18 | * **ui:** update ko locale ([#51](https://github.com/socketio/socket.io-admin-ui/issues/51)) ([2226f7b](https://github.com/socketio/socket.io-admin-ui/commit/2226f7bcd97461902f9dac4f44a7ed62f804f188)) 19 | 20 | 21 | 22 | # [0.4.0](https://github.com/socketio/socket.io-admin-ui/compare/0.3.0...0.4.0) (2022-06-23) 23 | 24 | 25 | ### Bug Fixes 26 | 27 | * **ui:** properly set initial nav drawer state ([77ee068](https://github.com/socketio/socket.io-admin-ui/commit/77ee0683185aceababc4439a3a945e273d547944)) 28 | 29 | 30 | ### Features 31 | 32 | * add page displaying all events ([481ef22](https://github.com/socketio/socket.io-admin-ui/commit/481ef22b3aff37b40b142a29cb78e116d6d1e8e9)) 33 | * add production mode ([e0d91ca](https://github.com/socketio/socket.io-admin-ui/commit/e0d91cadb11205c5f2c686c239a50cb2eef9795d)) 34 | * display sent and received events ([8542601](https://github.com/socketio/socket.io-admin-ui/commit/8542601b55022f6ca00b677b7d7c7664a326526f)) 35 | * **ui:** add support for relative links ([fdec2ce](https://github.com/socketio/socket.io-admin-ui/commit/fdec2ce17bf7cad77a04e8eef42a26104b6a05b8)) 36 | * **ui:** separate the namespace from the server URL ([5a8a75e](https://github.com/socketio/socket.io-admin-ui/commit/3d4aed972f16dad3dd847d61f4db5e6f55978c4b)) 37 | 38 | 39 | 40 | # [0.3.0](https://github.com/socketio/socket.io-admin-ui/compare/0.2.0...0.3.0) (2022-05-03) 41 | 42 | 43 | ### Features 44 | 45 | * add navigation drawer for mobile devices ([#31](https://github.com/socketio/socket.io-admin-ui/issues/31)) ([62e1467](https://github.com/socketio/socket.io-admin-ui/commit/62e146709f1b4ceee86b6c9d414d0538b2991833)) 46 | * add socket data in the UI ([#37](https://github.com/socketio/socket.io-admin-ui/issues/37)) ([3773fe4](https://github.com/socketio/socket.io-admin-ui/commit/3773fe4b1cbf2206708e1f21ce65f430a522527f)) 47 | * add support for the msgpack parser ([4359536](https://github.com/socketio/socket.io-admin-ui/commit/4359536a4b9c09395c52ac7e983123f02043ac5c)) 48 | * **ui:** improve Bengali (বাংলা) translation ([#27](https://github.com/socketio/socket.io-admin-ui/issues/27)) ([925c617](https://github.com/socketio/socket.io-admin-ui/commit/925c617af10996b7e31709d74afb340701104fc0)) 49 | 50 | 51 | 52 | # [0.2.0](https://github.com/socketio/socket.io-admin-ui/compare/0.1.2...0.2.0) (2021-06-11) 53 | 54 | 55 | ### Features 56 | 57 | * **ui:** add Bengali (বাংলা) translation ([#18](https://github.com/socketio/socket.io-admin-ui/issues/18)) ([b9c09f4](https://github.com/socketio/socket.io-admin-ui/commit/b9c09f4c7d690c13c662e734ad6b142af3d9dfef)) 58 | * **ui:** add pt-BR locale ([#13](https://github.com/socketio/socket.io-admin-ui/issues/13)) ([e8d05bd](https://github.com/socketio/socket.io-admin-ui/commit/e8d05bd11833c21d65055a92c0ab21973c515052)) 59 | * display the real IP address of the user ([#16](https://github.com/socketio/socket.io-admin-ui/issues/16)) ([2e05f70](https://github.com/socketio/socket.io-admin-ui/commit/2e05f706c62792f9d497910bdabb44d12292c806)) 60 | 61 | 62 | 63 | ## [0.1.2](https://github.com/socketio/socket.io-admin-ui/compare/0.1.1...0.1.2) (2021-06-02) 64 | 65 | 66 | ### Bug Fixes 67 | 68 | * **server:** add support for dynamic namespaces ([74f1c20](https://github.com/socketio/socket.io-admin-ui/commit/74f1c20f6ad878c3d11c5fc80dd8d12ee02d7bfb)) 69 | * **server:** only serialize required handshake attributes ([1cf991e](https://github.com/socketio/socket.io-admin-ui/commit/1cf991e49a1e2b172acca40ca3d259dad9c22915)) 70 | * **ui:** allow to specify the connection path ([7ad384d](https://github.com/socketio/socket.io-admin-ui/commit/7ad384dd3485b8500217a489f8a376d2641d81e0)) 71 | 72 | 73 | 74 | ## [0.1.1](https://github.com/socketio/socket.io-admin-ui/compare/0.1.0...0.1.1) (2021-05-09) 75 | 76 | 77 | ### Bug Fixes 78 | 79 | * **ui:** allow to connect to a websocket-only server ([3ec64a6](https://github.com/socketio/socket.io-admin-ui/commit/3ec64a62d331c5100c026313700119a4a97df64a)) 80 | * **ui:** set withCredentials to true ([51fac0a](https://github.com/socketio/socket.io-admin-ui/commit/51fac0aeb8ae2cfb6fa319525de9b1208aada463)) 81 | 82 | 83 | 84 | # [0.1.0](https://github.com/socketio/socket.io-admin-ui/compare/0.0.1...0.1.0) (2021-05-04) 85 | 86 | 87 | ### Features 88 | 89 | * **locale:** add ko locale ([#1](https://github.com/socketio/socket.io-admin-ui/issues/1)) ([16a1da5](https://github.com/socketio/socket.io-admin-ui/commit/16a1da57362b87de64508b7595d94096d4f2b47d)) 90 | * **locale:** add zh-CN locale ([#2](https://github.com/socketio/socket.io-admin-ui/issues/2)) ([9612bc6](https://github.com/socketio/socket.io-admin-ui/commit/9612bc6f212c0364c2a6ce27fa20ec873a2041fe)) 91 | 92 | 93 | 94 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2021 The Socket.IO team 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Socket.IO Admin UI 2 | 3 | ![dashboard screenshot](assets/dashboard.png) 4 | 5 | ## Table of contents 6 | 7 | - [How to use](#how-to-use) 8 | - [Server-side](#server-side) 9 | - [Client-side](#client-side) 10 | - [Available options](#available-options) 11 | - [`auth`](#auth) 12 | - [`namespaceName`](#namespacename) 13 | - [`readonly`](#readonly) 14 | - [`serverId`](#serverid) 15 | - [`store`](#store) 16 | - [`mode`](#mode) 17 | - [How it works](#how-it-works) 18 | - [License](#license) 19 | 20 | ## How to use 21 | 22 | ### Server-side 23 | 24 | First, install the `@socket.io/admin-ui` package: 25 | 26 | ``` 27 | npm i @socket.io/admin-ui 28 | ``` 29 | 30 | And then invoke the `instrument` method on your Socket.IO server: 31 | 32 | ```js 33 | const { createServer } = require("http"); 34 | const { Server } = require("socket.io"); 35 | const { instrument } = require("@socket.io/admin-ui"); 36 | 37 | const httpServer = createServer(); 38 | 39 | const io = new Server(httpServer, { 40 | cors: { 41 | origin: ["https://admin.socket.io"], 42 | credentials: true 43 | } 44 | }); 45 | 46 | instrument(io, { 47 | auth: false 48 | }); 49 | 50 | httpServer.listen(3000); 51 | ``` 52 | 53 | The module is compatible with: 54 | 55 | - Socket.IO v4 server 56 | - Socket.IO v3 server (>= 3.1.0), but without the operations on rooms (join, leave, disconnection) 57 | 58 | ### Client-side 59 | 60 | You can then head up to https://admin.socket.io, or host the files found in the `ui/dist` folder. 61 | 62 | **Important note**: the website at https://admin.socket.io is totally static (hosted on [Vercel](https://vercel.com)), we do not (and will never) store any information about yourself or your browser (no tracking, no analytics, ...). That being said, hosting the files yourself is totally fine. 63 | 64 | You should see the following modal: 65 | 66 | ![login modal screenshot](assets/login-modal.png) 67 | 68 | Please enter the URL of your server (for example, `http://localhost:3000` or `https://example.com`) and the credentials, if applicable (see the `auth` option [below](#auth)). 69 | 70 | ### Available options 71 | 72 | #### `auth` 73 | 74 | Default value: `-` 75 | 76 | This option is mandatory. You can either disable authentication (please use with caution): 77 | 78 | ```js 79 | instrument(io, { 80 | auth: false 81 | }); 82 | ``` 83 | 84 | Or use basic authentication: 85 | 86 | ```js 87 | instrument(io, { 88 | auth: { 89 | type: "basic", 90 | username: "admin", 91 | password: "$2b$10$heqvAkYMez.Va6Et2uXInOnkCT6/uQj1brkrbyG3LpopDklcq7ZOS" // "changeit" encrypted with bcrypt 92 | }, 93 | }); 94 | ``` 95 | 96 | WARNING! Please note that the `bcrypt` package does not currently support hashes starting with the `$2y$` prefix, which is used by some BCrypt implementations (for example https://bcrypt-generator.com/ or https://www.bcrypt.fr/). You can check the validity of the hash with: 97 | 98 | ``` 99 | $ node 100 | > require("bcrypt").compareSync("", "") 101 | true 102 | ``` 103 | 104 | You can generate a valid hash with: 105 | 106 | ``` 107 | $ node 108 | > require("bcrypt").hashSync("changeit", 10) 109 | '$2b$10$LQUE...' 110 | ``` 111 | 112 | See also: 113 | 114 | - https://github.com/kelektiv/node.bcrypt.js/issues/849 115 | - https://stackoverflow.com/a/36225192/5138796 116 | 117 | #### `namespaceName` 118 | 119 | Default value: `/admin` 120 | 121 | The name of the namespace which will be created to handle the administrative tasks. 122 | 123 | ```js 124 | instrument(io, { 125 | namespaceName: "/custom" 126 | }); 127 | ``` 128 | 129 | This namespace is a classic Socket.IO namespace, you can access it with: 130 | 131 | ```js 132 | const adminNamespace = io.of("/admin"); 133 | ``` 134 | 135 | More information [here](https://socket.io/docs/v4/namespaces/). 136 | 137 | #### `readonly` 138 | 139 | Default value: `false` 140 | 141 | Whether to put the admin UI in read-only mode (no join, leave or disconnect allowed). 142 | 143 | ```js 144 | instrument(io, { 145 | readonly: true 146 | }); 147 | ``` 148 | 149 | #### `serverId` 150 | 151 | Default value: `require("os").hostname()` 152 | 153 | The ID of the given server. If you have several Socket.IO servers on the same machine, please give them a distinct ID: 154 | 155 | ```js 156 | instrument(io, { 157 | serverId: `${require("os").hostname()}#${process.pid}` 158 | }); 159 | ``` 160 | 161 | #### `store` 162 | 163 | Default value: `new InMemoryStore()` 164 | 165 | The store is used to store the session IDs so the user do not have to retype the credentials upon reconnection. 166 | 167 | If you use basic authentication in a multi-server setup, you should provide a custom store: 168 | 169 | ```js 170 | const { instrument, RedisStore } = require("@socket.io/admin-ui"); 171 | 172 | instrument(io, { 173 | store: new RedisStore(redisClient) 174 | }); 175 | ``` 176 | 177 | #### `mode` 178 | 179 | Default value: `development` 180 | 181 | In production mode, the server won't send all details about the socket instances and the rooms, thus reducing the memory footprint of the instrumentation. 182 | 183 | ```js 184 | instrument(io, { 185 | mode: "production" 186 | }); 187 | ``` 188 | 189 | The production mode can also be enabled with the NODE_ENV environment variable: 190 | 191 | ``` 192 | NODE_ENV=production node index.js 193 | ``` 194 | 195 | ## How it works 196 | 197 | You can check the details of the implementation in the [lib/index.ts](lib/index.ts) file. 198 | 199 | The `instrument` method simply: 200 | 201 | - creates a [namespace](https://socket.io/docs/v4/namespaces/) and adds an authentication [middleware](https://socket.io/docs/v4/middlewares/) if applicable 202 | - register listeners for the `connection` and `disconnect` event for each existing namespaces to track socket instances 203 | - register a timer which will periodically send stats from the server to the UI 204 | - register handlers for the `join`, `leave` and `_disconnect` commands sent from the UI 205 | 206 | ## License 207 | 208 | MIT 209 | -------------------------------------------------------------------------------- /assets/dashboard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/socketio/socket.io-admin-ui/158864989dddeba67df9975a4cad48ef310f8c80/assets/dashboard.png -------------------------------------------------------------------------------- /assets/login-modal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/socketio/socket.io-admin-ui/158864989dddeba67df9975a4cad48ef310f8c80/assets/login-modal.png -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '2.0' 2 | services: 3 | redis: 4 | image: redis:5 5 | ports: 6 | - "6379:6379" 7 | -------------------------------------------------------------------------------- /examples/.gitignore: -------------------------------------------------------------------------------- 1 | package-lock.json 2 | -------------------------------------------------------------------------------- /examples/cluster-nodejs/index.js: -------------------------------------------------------------------------------- 1 | import cluster from "cluster"; 2 | import { createServer } from "http"; 3 | import { setupMaster, setupWorker } from "@socket.io/sticky"; 4 | import { createAdapter, setupPrimary } from "@socket.io/cluster-adapter"; 5 | import { Server } from "socket.io"; 6 | import { instrument } from "../../dist/index.js"; 7 | import { cpus } from "os"; 8 | 9 | if (cluster.isMaster) { 10 | console.log(`Master ${process.pid} is running`); 11 | 12 | const httpServer = createServer(); 13 | 14 | setupMaster(httpServer, { 15 | loadBalancingMethod: "least-connection", 16 | }); 17 | 18 | setupPrimary(); 19 | 20 | httpServer.listen(3000); 21 | 22 | for (let i = 0; i < cpus().length; i++) { 23 | cluster.fork(); 24 | } 25 | 26 | cluster.on("exit", (worker) => { 27 | console.log(`Worker ${worker.process.pid} died`); 28 | cluster.fork(); 29 | }); 30 | } else { 31 | console.log(`Worker ${process.pid} started`); 32 | 33 | const httpServer = createServer(); 34 | 35 | const io = new Server(httpServer, { 36 | cors: { 37 | origin: ["https://admin.socket.io", "http://localhost:8080"], 38 | credentials: true, 39 | }, 40 | }); 41 | 42 | io.adapter(createAdapter()); 43 | setupWorker(io); 44 | 45 | instrument(io, { 46 | auth: false, 47 | }); 48 | } 49 | -------------------------------------------------------------------------------- /examples/cluster-nodejs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cluster-nodejs", 3 | "version": "0.0.1", 4 | "description": "Sample server to be used with the Socket.IO Admin UI", 5 | "private": true, 6 | "main": "index.js", 7 | "type": "module", 8 | "scripts": { 9 | "start": "node index.js" 10 | }, 11 | "author": "Damien Arrachequesne ", 12 | "license": "MIT", 13 | "dependencies": { 14 | "@socket.io/cluster-adapter": "^0.1.0", 15 | "@socket.io/sticky": "^1.0.1", 16 | "socket.io": "^4.4.1" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /examples/cluster-redis/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.0' 2 | services: 3 | redis: 4 | image: redis:5 5 | ports: 6 | - "6379:6379" 7 | -------------------------------------------------------------------------------- /examples/cluster-redis/index.js: -------------------------------------------------------------------------------- 1 | import cluster from "cluster"; 2 | import { Server } from "socket.io"; 3 | import { createClient } from "redis"; 4 | import { createAdapter } from "@socket.io/redis-adapter"; 5 | import { instrument } from "../../dist/index.js"; 6 | 7 | if (cluster.isMaster) { 8 | console.log(`Master ${process.pid} is running`); 9 | 10 | cluster.fork({ port: 3000 }); 11 | cluster.fork({ port: 3001 }); 12 | cluster.fork({ port: 3002 }); 13 | cluster.fork({ port: 3003 }); 14 | } else { 15 | console.log(`Worker ${process.pid} started`); 16 | 17 | const port = parseInt(process.env.port, 10); 18 | const io = new Server({ 19 | cors: { 20 | origin: ["https://admin.socket.io", "http://localhost:8080"], 21 | credentials: true, 22 | }, 23 | }); 24 | 25 | const pubClient = createClient({ host: "localhost", port: 6379 }); 26 | const subClient = pubClient.duplicate(); 27 | 28 | Promise.all([pubClient.connect(), subClient.connect()]).then(() => { 29 | io.adapter(createAdapter(pubClient, subClient)); 30 | 31 | io.listen(port); 32 | 33 | instrument(io, { 34 | auth: false, 35 | serverId: `app${port}`, 36 | }); 37 | }); 38 | } 39 | -------------------------------------------------------------------------------- /examples/cluster-redis/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cluster-redis", 3 | "version": "0.0.1", 4 | "description": "Sample server to be used with the Socket.IO Admin UI", 5 | "private": true, 6 | "main": "index.js", 7 | "type": "module", 8 | "scripts": { 9 | "start": "node index.js" 10 | }, 11 | "author": "Damien Arrachequesne ", 12 | "license": "MIT", 13 | "dependencies": { 14 | "@socket.io/redis-adapter": "^7.1.0", 15 | "redis": "^4.0.6", 16 | "socket.io": "^4.4.1" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /examples/single-server-msgpack-parser/index.js: -------------------------------------------------------------------------------- 1 | import { Server } from "socket.io"; 2 | import { instrument } from "../../dist/index.js"; 3 | import parser from "socket.io-msgpack-parser"; 4 | 5 | const io = new Server(3000, { 6 | cors: { 7 | origin: ["https://admin.socket.io", "http://localhost:8080"], 8 | credentials: true, 9 | }, 10 | parser, 11 | }); 12 | 13 | instrument(io, { 14 | auth: false, 15 | }); 16 | -------------------------------------------------------------------------------- /examples/single-server-msgpack-parser/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "single-server-msgpack-parser", 3 | "version": "0.0.1", 4 | "description": "Sample server to be used with the Socket.IO Admin UI", 5 | "private": true, 6 | "main": "index.js", 7 | "type": "module", 8 | "scripts": { 9 | "start": "node index.js" 10 | }, 11 | "author": "Damien Arrachequesne ", 12 | "license": "MIT", 13 | "dependencies": { 14 | "socket.io": "^4.4.1", 15 | "socket.io-msgpack-parser": "^3.0.1" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /examples/single-server-nestjs/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: '@typescript-eslint/parser', 3 | parserOptions: { 4 | project: 'tsconfig.json', 5 | tsconfigRootDir : __dirname, 6 | sourceType: 'module', 7 | }, 8 | plugins: ['@typescript-eslint/eslint-plugin'], 9 | extends: [ 10 | 'plugin:@typescript-eslint/recommended', 11 | 'plugin:prettier/recommended', 12 | ], 13 | root: true, 14 | env: { 15 | node: true, 16 | jest: true, 17 | }, 18 | ignorePatterns: ['.eslintrc.js'], 19 | rules: { 20 | '@typescript-eslint/interface-name-prefix': 'off', 21 | '@typescript-eslint/explicit-function-return-type': 'off', 22 | '@typescript-eslint/explicit-module-boundary-types': 'off', 23 | '@typescript-eslint/no-explicit-any': 'off', 24 | }, 25 | }; 26 | -------------------------------------------------------------------------------- /examples/single-server-nestjs/.gitignore: -------------------------------------------------------------------------------- 1 | # compiled output 2 | /dist 3 | /node_modules 4 | 5 | # Logs 6 | logs 7 | *.log 8 | npm-debug.log* 9 | pnpm-debug.log* 10 | yarn-debug.log* 11 | yarn-error.log* 12 | lerna-debug.log* 13 | 14 | # OS 15 | .DS_Store 16 | 17 | # Tests 18 | /coverage 19 | /.nyc_output 20 | 21 | # IDEs and editors 22 | /.idea 23 | .project 24 | .classpath 25 | .c9/ 26 | *.launch 27 | .settings/ 28 | *.sublime-workspace 29 | 30 | # IDE - VSCode 31 | .vscode/* 32 | !.vscode/settings.json 33 | !.vscode/tasks.json 34 | !.vscode/launch.json 35 | !.vscode/extensions.json -------------------------------------------------------------------------------- /examples/single-server-nestjs/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all" 4 | } -------------------------------------------------------------------------------- /examples/single-server-nestjs/README.md: -------------------------------------------------------------------------------- 1 |

2 | Nest Logo 3 |

4 | 5 | [circleci-image]: https://img.shields.io/circleci/build/github/nestjs/nest/master?token=abc123def456 6 | [circleci-url]: https://circleci.com/gh/nestjs/nest 7 | 8 |

A progressive Node.js framework for building efficient and scalable server-side applications.

9 |

10 | NPM Version 11 | Package License 12 | NPM Downloads 13 | CircleCI 14 | Coverage 15 | Discord 16 | Backers on Open Collective 17 | Sponsors on Open Collective 18 | 19 | Support us 20 | 21 |

22 | 24 | 25 | ## Description 26 | 27 | [Nest](https://github.com/nestjs/nest) framework TypeScript starter repository. 28 | 29 | ## Installation 30 | 31 | ```bash 32 | $ npm install 33 | ``` 34 | 35 | ## Running the app 36 | 37 | ```bash 38 | # development 39 | $ npm run start 40 | 41 | # watch mode 42 | $ npm run start:dev 43 | 44 | # production mode 45 | $ npm run start:prod 46 | ``` 47 | 48 | ## Test 49 | 50 | ```bash 51 | # unit tests 52 | $ npm run test 53 | 54 | # e2e tests 55 | $ npm run test:e2e 56 | 57 | # test coverage 58 | $ npm run test:cov 59 | ``` 60 | 61 | ## Support 62 | 63 | Nest is an MIT-licensed open source project. It can grow thanks to the sponsors and support by the amazing backers. If you'd like to join them, please [read more here](https://docs.nestjs.com/support). 64 | 65 | ## Stay in touch 66 | 67 | - Author - [Kamil Myśliwiec](https://kamilmysliwiec.com) 68 | - Website - [https://nestjs.com](https://nestjs.com/) 69 | - Twitter - [@nestframework](https://twitter.com/nestframework) 70 | 71 | ## License 72 | 73 | Nest is [MIT licensed](LICENSE). 74 | -------------------------------------------------------------------------------- /examples/single-server-nestjs/nest-cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/nest-cli", 3 | "collection": "@nestjs/schematics", 4 | "sourceRoot": "src" 5 | } 6 | -------------------------------------------------------------------------------- /examples/single-server-nestjs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "single-server-nestjs", 3 | "version": "0.0.1", 4 | "description": "", 5 | "author": "", 6 | "private": true, 7 | "license": "UNLICENSED", 8 | "scripts": { 9 | "prebuild": "rimraf dist", 10 | "build": "nest build", 11 | "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", 12 | "start": "nest start", 13 | "start:dev": "nest start --watch", 14 | "start:debug": "nest start --debug --watch", 15 | "start:prod": "node dist/main", 16 | "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", 17 | "test": "jest", 18 | "test:watch": "jest --watch", 19 | "test:cov": "jest --coverage", 20 | "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", 21 | "test:e2e": "jest --config ./test/jest-e2e.json" 22 | }, 23 | "dependencies": { 24 | "@nestjs/common": "^9.0.0", 25 | "@nestjs/core": "^9.0.0", 26 | "@nestjs/platform-express": "^9.0.0", 27 | "@nestjs/platform-socket.io": "^9.2.0", 28 | "@nestjs/websockets": "^9.2.0", 29 | "reflect-metadata": "^0.1.13", 30 | "rimraf": "^3.0.2", 31 | "rxjs": "^7.2.0" 32 | }, 33 | "devDependencies": { 34 | "@nestjs/cli": "^9.0.0", 35 | "@nestjs/schematics": "^9.0.0", 36 | "@nestjs/testing": "^9.0.0", 37 | "@types/express": "^4.17.13", 38 | "@types/jest": "28.1.8", 39 | "@types/node": "^16.0.0", 40 | "@types/supertest": "^2.0.11", 41 | "@typescript-eslint/eslint-plugin": "^5.0.0", 42 | "@typescript-eslint/parser": "^5.0.0", 43 | "eslint": "^8.0.1", 44 | "eslint-config-prettier": "^8.3.0", 45 | "eslint-plugin-prettier": "^4.0.0", 46 | "jest": "28.1.3", 47 | "prettier": "^2.3.2", 48 | "source-map-support": "^0.5.20", 49 | "supertest": "^6.1.3", 50 | "ts-jest": "28.0.8", 51 | "ts-loader": "^9.2.3", 52 | "ts-node": "^10.0.0", 53 | "tsconfig-paths": "4.1.0", 54 | "typescript": "^4.7.4" 55 | }, 56 | "jest": { 57 | "moduleFileExtensions": [ 58 | "js", 59 | "json", 60 | "ts" 61 | ], 62 | "rootDir": "src", 63 | "testRegex": ".*\\.spec\\.ts$", 64 | "transform": { 65 | "^.+\\.(t|j)s$": "ts-jest" 66 | }, 67 | "collectCoverageFrom": [ 68 | "**/*.(t|j)s" 69 | ], 70 | "coverageDirectory": "../coverage", 71 | "testEnvironment": "node" 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /examples/single-server-nestjs/src/app.controller.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { AppController } from './app.controller'; 3 | import { AppService } from './app.service'; 4 | 5 | describe('AppController', () => { 6 | let appController: AppController; 7 | 8 | beforeEach(async () => { 9 | const app: TestingModule = await Test.createTestingModule({ 10 | controllers: [AppController], 11 | providers: [AppService], 12 | }).compile(); 13 | 14 | appController = app.get(AppController); 15 | }); 16 | 17 | describe('root', () => { 18 | it('should return "Hello World!"', () => { 19 | expect(appController.getHello()).toBe('Hello World!'); 20 | }); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /examples/single-server-nestjs/src/app.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Get } from '@nestjs/common'; 2 | import { AppService } from './app.service'; 3 | 4 | @Controller() 5 | export class AppController { 6 | constructor(private readonly appService: AppService) {} 7 | 8 | @Get() 9 | getHello(): string { 10 | return this.appService.getHello(); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /examples/single-server-nestjs/src/app.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { AppController } from './app.controller'; 3 | import { AppService } from './app.service'; 4 | import { EventsModule } from './events.module'; 5 | 6 | @Module({ 7 | imports: [EventsModule], 8 | controllers: [AppController], 9 | providers: [AppService], 10 | }) 11 | export class AppModule {} 12 | -------------------------------------------------------------------------------- /examples/single-server-nestjs/src/app.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | 3 | @Injectable() 4 | export class AppService { 5 | getHello(): string { 6 | return 'Hello World!'; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /examples/single-server-nestjs/src/events.gateway.ts: -------------------------------------------------------------------------------- 1 | import { WebSocketGateway, WebSocketServer } from '@nestjs/websockets'; 2 | import { Server } from 'socket.io'; 3 | import { instrument } from '../../../dist'; 4 | 5 | @WebSocketGateway({ 6 | cors: { 7 | origin: '*', 8 | }, 9 | }) 10 | export class EventsGateway { 11 | @WebSocketServer() 12 | server: Server; 13 | 14 | afterInit() { 15 | // @ts-ignore 16 | instrument(this.server, { 17 | auth: false, 18 | }); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /examples/single-server-nestjs/src/events.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { EventsGateway } from './events.gateway'; 3 | 4 | @Module({ 5 | providers: [EventsGateway], 6 | }) 7 | export class EventsModule {} 8 | -------------------------------------------------------------------------------- /examples/single-server-nestjs/src/main.ts: -------------------------------------------------------------------------------- 1 | import { NestFactory } from '@nestjs/core'; 2 | import { AppModule } from './app.module'; 3 | 4 | async function bootstrap() { 5 | const app = await NestFactory.create(AppModule); 6 | await app.listen(3000); 7 | } 8 | bootstrap(); 9 | -------------------------------------------------------------------------------- /examples/single-server-nestjs/test/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { INestApplication } from '@nestjs/common'; 3 | import * as request from 'supertest'; 4 | import { AppModule } from './../src/app.module'; 5 | 6 | describe('AppController (e2e)', () => { 7 | let app: INestApplication; 8 | 9 | beforeEach(async () => { 10 | const moduleFixture: TestingModule = await Test.createTestingModule({ 11 | imports: [AppModule], 12 | }).compile(); 13 | 14 | app = moduleFixture.createNestApplication(); 15 | await app.init(); 16 | }); 17 | 18 | it('/ (GET)', () => { 19 | return request(app.getHttpServer()) 20 | .get('/') 21 | .expect(200) 22 | .expect('Hello World!'); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /examples/single-server-nestjs/test/jest-e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "moduleFileExtensions": ["js", "json", "ts"], 3 | "rootDir": ".", 4 | "testEnvironment": "node", 5 | "testRegex": ".e2e-spec.ts$", 6 | "transform": { 7 | "^.+\\.(t|j)s$": "ts-jest" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /examples/single-server-nestjs/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["node_modules", "test", "dist", "**/*spec.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /examples/single-server-nestjs/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "declaration": true, 5 | "removeComments": true, 6 | "emitDecoratorMetadata": true, 7 | "experimentalDecorators": true, 8 | "allowSyntheticDefaultImports": true, 9 | "target": "es2017", 10 | "sourceMap": true, 11 | "outDir": "./dist", 12 | "baseUrl": "./", 13 | "incremental": true, 14 | "skipLibCheck": true, 15 | "strictNullChecks": false, 16 | "noImplicitAny": false, 17 | "strictBindCallApply": false, 18 | "forceConsistentCasingInFileNames": false, 19 | "noFallthroughCasesInSwitch": false 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /examples/single-server/index.js: -------------------------------------------------------------------------------- 1 | import { Server } from "socket.io"; 2 | import { instrument } from "../../dist/index.js"; 3 | 4 | const io = new Server(3000, { 5 | cors: { 6 | origin: ["https://admin.socket.io", "http://localhost:8080"], 7 | credentials: true, 8 | }, 9 | }); 10 | 11 | instrument(io, { 12 | auth: false, 13 | }); 14 | -------------------------------------------------------------------------------- /examples/single-server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "single-server", 3 | "version": "0.0.1", 4 | "description": "Sample server to be used with the Socket.IO Admin UI", 5 | "private": true, 6 | "main": "index.js", 7 | "type": "module", 8 | "scripts": { 9 | "start": "node index.js" 10 | }, 11 | "author": "Damien Arrachequesne ", 12 | "license": "MIT", 13 | "dependencies": { 14 | "socket.io": "^4.4.1" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /lib/stores.ts: -------------------------------------------------------------------------------- 1 | export abstract class Store { 2 | abstract doesSessionExist(sessionId: string): Promise; 3 | abstract saveSession(sessionId: string): void; 4 | } 5 | 6 | export class InMemoryStore extends Store { 7 | private sessions: Set = new Set(); 8 | 9 | doesSessionExist(sessionId: string): Promise { 10 | return Promise.resolve(this.sessions.has(sessionId)); 11 | } 12 | 13 | saveSession(sessionId: string) { 14 | this.sessions.add(sessionId); 15 | } 16 | } 17 | 18 | interface RedisStoreOptions { 19 | /** 20 | * The prefix of the keys stored in Redis 21 | * @default "socket.io-admin" 22 | */ 23 | prefix: string; 24 | /** 25 | * The duration of the session in seconds 26 | * @default 86400 27 | */ 28 | sessionDuration: number; 29 | } 30 | 31 | export class RedisStore extends Store { 32 | private options: RedisStoreOptions; 33 | 34 | constructor(readonly redisClient: any, options?: Partial) { 35 | super(); 36 | this.options = Object.assign( 37 | { 38 | prefix: "socket.io-admin", 39 | sessionDuration: 86400, 40 | }, 41 | options 42 | ); 43 | } 44 | 45 | private computeKey(sessionId: string) { 46 | return `${this.options.prefix}#${sessionId}`; 47 | } 48 | 49 | doesSessionExist(sessionId: string): Promise { 50 | return new Promise((resolve, reject) => { 51 | this.redisClient.exists( 52 | this.computeKey(sessionId), 53 | (err: Error | null, result: any) => { 54 | if (err) { 55 | reject(err); 56 | } else { 57 | resolve(result === 1); 58 | } 59 | } 60 | ); 61 | }); 62 | } 63 | 64 | saveSession(sessionId: string) { 65 | this.redisClient.set( 66 | this.computeKey(sessionId), 67 | "1", 68 | "EX", 69 | "" + this.options.sessionDuration 70 | ); 71 | } 72 | } 73 | 74 | export class RedisV4Store extends Store { 75 | private options: RedisStoreOptions; 76 | 77 | constructor(readonly redisClient: any, options?: Partial) { 78 | super(); 79 | this.options = Object.assign( 80 | { 81 | prefix: "socket.io-admin", 82 | sessionDuration: 86400, 83 | }, 84 | options 85 | ); 86 | } 87 | 88 | private computeKey(sessionId: string) { 89 | return `${this.options.prefix}#${sessionId}`; 90 | } 91 | 92 | doesSessionExist(sessionId: string): Promise { 93 | return this.redisClient.exists(this.computeKey(sessionId)); 94 | } 95 | 96 | saveSession(sessionId: string) { 97 | return this.redisClient.set(this.computeKey(sessionId), "1", { 98 | EX: this.options.sessionDuration, 99 | }); 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /lib/typed-events.ts: -------------------------------------------------------------------------------- 1 | export enum Feature { 2 | EMIT = "EMIT", 3 | JOIN = "JOIN", 4 | LEAVE = "LEAVE", 5 | DISCONNECT = "DISCONNECT", 6 | // the following features are only available starting with Socket.IO v4.0.0 7 | MJOIN = "MJOIN", 8 | MLEAVE = "MLEAVE", 9 | MDISCONNECT = "MDISCONNECT", 10 | 11 | AGGREGATED_EVENTS = "AGGREGATED_EVENTS", 12 | ALL_EVENTS = "ALL_EVENTS", 13 | } 14 | 15 | interface Config { 16 | supportedFeatures: Feature[]; 17 | } 18 | 19 | export type NamespaceEvent = { 20 | timestamp: number; 21 | type: string; 22 | subType?: string; 23 | count: number; 24 | }; 25 | 26 | export type NamespaceDetails = { 27 | name: string; 28 | socketsCount: number; 29 | }; 30 | 31 | interface ServerStats { 32 | serverId: string; 33 | hostname: string; 34 | pid: number; 35 | uptime: number; 36 | clientsCount: number; 37 | pollingClientsCount: number; 38 | namespaces: NamespaceDetails[]; 39 | } 40 | 41 | export interface SerializedSocket { 42 | id: string; 43 | clientId: string; 44 | transport: string; 45 | nsp: string; 46 | data: any; 47 | handshake: any; 48 | rooms: string[]; 49 | } 50 | 51 | export interface ServerEvents { 52 | session: (sessionId: string) => void; 53 | config: (config: Config) => void; 54 | server_stats: (stats: ServerStats) => void; 55 | all_sockets: (sockets: SerializedSocket[]) => void; 56 | socket_connected: (socket: SerializedSocket, timestamp: Date) => void; 57 | socket_updated: (socket: Partial) => void; 58 | socket_disconnected: ( 59 | nsp: string, 60 | id: string, 61 | reason: string, 62 | timestamp: Date 63 | ) => void; 64 | room_joined: (nsp: string, room: string, id: string, timestamp: Date) => void; 65 | room_left: (nsp: string, room: string, id: string, timestamp: Date) => void; 66 | event_received: ( 67 | nsp: string, 68 | id: string, 69 | args: any[], 70 | timestamp: Date 71 | ) => void; 72 | event_sent: (nsp: string, id: string, args: any[], timestamp: Date) => void; 73 | } 74 | 75 | export interface ClientEvents { 76 | emit: ( 77 | nsp: string, 78 | filter: string | undefined, 79 | ev: string, 80 | ...args: any[] 81 | ) => void; 82 | join: (nsp: string, room: string, filter?: string) => void; 83 | leave: (nsp: string, room: string, filter?: string) => void; 84 | _disconnect: (nsp: string, close: boolean, filter?: string) => void; 85 | } 86 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@socket.io/admin-ui", 3 | "version": "0.5.1", 4 | "description": "Admin UI for Socket.IO", 5 | "files": [ 6 | "dist/", 7 | "ui/dist/" 8 | ], 9 | "main": "./dist/index.js", 10 | "types": "./dist/index.d.ts", 11 | "scripts": { 12 | "compile": "tsc", 13 | "test": "npm run format:check && npm run compile && npm run test:unit", 14 | "test:unit": "nyc mocha --require ts-node/register --timeout 5000 test/*.ts --quit", 15 | "format:check": "prettier --parser typescript --check 'lib/**/*.ts' 'test/**/*.ts'", 16 | "format:fix": "prettier --parser typescript --write 'lib/**/*.ts' 'test/**/*.ts'", 17 | "prepack": "npm run compile" 18 | }, 19 | "repository": { 20 | "type": "git", 21 | "url": "git+https://github.com/socketio/socket.io-admin-ui.git" 22 | }, 23 | "keywords": [ 24 | "socket.io", 25 | "admin", 26 | "websocket" 27 | ], 28 | "author": "Damien Arrachequesne ", 29 | "license": "MIT", 30 | "bugs": { 31 | "url": "https://github.com/socketio/socket.io-admin-ui/issues" 32 | }, 33 | "homepage": "https://github.com/socketio/socket.io-admin-ui#readme", 34 | "dependencies": { 35 | "@types/bcryptjs": "^2.4.2", 36 | "bcryptjs": "^2.4.3", 37 | "debug": "^4.3.4" 38 | }, 39 | "peerDependencies": { 40 | "socket.io": ">=3.1.0" 41 | }, 42 | "devDependencies": { 43 | "@types/debug": "^4.1.7", 44 | "@types/expect.js": "^0.3.29", 45 | "@types/mocha": "^10.0.0", 46 | "@types/node": "^14.14.37", 47 | "@types/redis": "^2.8.28", 48 | "expect.js": "^0.3.1", 49 | "ioredis": "^5.3.1", 50 | "mocha": "^10.0.0", 51 | "nyc": "^15.1.0", 52 | "prettier": "^2.2.1", 53 | "redis": "^3.1.2", 54 | "redis-v4": "npm:redis@^4.6.5", 55 | "socket.io": "^4.5.2", 56 | "socket.io-client": "^4.5.2", 57 | "socket.io-v3": "npm:socket.io@^3.1.2", 58 | "ts-node": "^10.9.1", 59 | "typescript": "^4.2.3" 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /test/events.ts: -------------------------------------------------------------------------------- 1 | import { Server } from "socket.io"; 2 | import { createServer } from "http"; 3 | import { AddressInfo } from "net"; 4 | import { instrument } from "../lib"; 5 | import { io as ioc } from "socket.io-client"; 6 | import expect = require("expect.js"); 7 | import { createPartialDone } from "./util"; 8 | 9 | describe("events", () => { 10 | let port: number, io: Server; 11 | 12 | beforeEach((done) => { 13 | const httpServer = createServer(); 14 | io = new Server(httpServer); 15 | 16 | httpServer.listen(() => { 17 | port = (httpServer.address() as AddressInfo).port; 18 | done(); 19 | }); 20 | }); 21 | 22 | afterEach(() => { 23 | io.close(); 24 | }); 25 | 26 | it("should track events sent to the client", (done) => { 27 | instrument(io, { 28 | auth: false, 29 | }); 30 | 31 | const adminSocket = ioc(`http://localhost:${port}/admin`); 32 | const clientSocket = ioc(`http://localhost:${port}`, { 33 | forceNew: true, 34 | }); 35 | 36 | const partialDone = createPartialDone(2, () => { 37 | adminSocket.disconnect(); 38 | clientSocket.disconnect(); 39 | done(); 40 | }); 41 | 42 | io.on("connection", (socket) => { 43 | socket.emit("hello", 1, "2", Buffer.from([3])); 44 | }); 45 | 46 | clientSocket.on("hello", partialDone); 47 | 48 | adminSocket.on("event_sent", (arg1, arg2, arg3, arg4) => { 49 | expect(arg1).to.eql("/"); 50 | expect(arg2).to.eql(clientSocket.id); 51 | expect(arg3).to.eql(["hello", 1, "2", Buffer.from([3])]); 52 | expect(new Date(arg4).toISOString()).to.eql(arg4); 53 | 54 | partialDone(); 55 | }); 56 | }); 57 | 58 | it("should track events sent to the client (with ack)", (done) => { 59 | instrument(io, { 60 | auth: false, 61 | }); 62 | 63 | const adminSocket = ioc(`http://localhost:${port}/admin`); 64 | const clientSocket = ioc(`http://localhost:${port}`, { 65 | forceNew: true, 66 | }); 67 | 68 | const partialDone = createPartialDone(2, () => { 69 | adminSocket.disconnect(); 70 | clientSocket.disconnect(); 71 | done(); 72 | }); 73 | 74 | io.on("connection", (socket) => { 75 | socket.emit("hello", (arg: string) => { 76 | expect(arg).to.eql("world"); 77 | 78 | partialDone(); 79 | }); 80 | }); 81 | 82 | clientSocket.on("hello", (cb) => { 83 | cb("world"); 84 | }); 85 | 86 | adminSocket.on("event_sent", (arg1, arg2, arg3, arg4) => { 87 | expect(arg1).to.eql("/"); 88 | expect(arg2).to.eql(clientSocket.id); 89 | expect(arg3).to.eql(["hello"]); 90 | expect(new Date(arg4).toISOString()).to.eql(arg4); 91 | 92 | partialDone(); 93 | }); 94 | }); 95 | 96 | it("should track events received from the client", (done) => { 97 | instrument(io, { 98 | auth: false, 99 | }); 100 | 101 | const adminSocket = ioc(`http://localhost:${port}/admin`); 102 | const clientSocket = ioc(`http://localhost:${port}`, { 103 | forceNew: true, 104 | }); 105 | 106 | const partialDone = createPartialDone(2, () => { 107 | adminSocket.disconnect(); 108 | clientSocket.disconnect(); 109 | done(); 110 | }); 111 | 112 | io.on("connection", (socket) => { 113 | socket.on("hello", partialDone); 114 | }); 115 | 116 | adminSocket.on("event_received", (arg1, arg2, arg3, arg4) => { 117 | expect(arg1).to.eql("/"); 118 | expect(arg2).to.eql(clientSocket.id); 119 | expect(arg3).to.eql(["hello", 1, "2", Buffer.from([3])]); 120 | expect(new Date(arg4).toISOString()).to.eql(arg4); 121 | 122 | partialDone(); 123 | }); 124 | 125 | clientSocket.emit("hello", 1, "2", Buffer.from([3])); 126 | }); 127 | 128 | it("should track events received from the client (with ack)", (done) => { 129 | instrument(io, { 130 | auth: false, 131 | }); 132 | 133 | const adminSocket = ioc(`http://localhost:${port}/admin`); 134 | const clientSocket = ioc(`http://localhost:${port}`, { 135 | forceNew: true, 136 | }); 137 | 138 | const partialDone = createPartialDone(2, () => { 139 | adminSocket.disconnect(); 140 | clientSocket.disconnect(); 141 | done(); 142 | }); 143 | 144 | io.on("connection", (socket) => { 145 | socket.on("hello", (arg, cb) => { 146 | expect(arg).to.eql("world"); 147 | 148 | cb("123"); 149 | }); 150 | }); 151 | 152 | adminSocket.on("event_received", (arg1, arg2, arg3, arg4) => { 153 | expect(arg1).to.eql("/"); 154 | expect(arg2).to.eql(clientSocket.id); 155 | expect(arg3).to.eql(["hello", "world"]); 156 | expect(new Date(arg4).toISOString()).to.eql(arg4); 157 | 158 | partialDone(); 159 | }); 160 | 161 | clientSocket.emit("hello", "world", (arg: string) => { 162 | expect(arg).to.eql("123"); 163 | 164 | partialDone(); 165 | }); 166 | }); 167 | }); 168 | -------------------------------------------------------------------------------- /test/index.ts: -------------------------------------------------------------------------------- 1 | import { createServer } from "http"; 2 | import { Server } from "socket.io"; 3 | import { Server as ServerV3 } from "socket.io-v3"; 4 | import { io as ioc } from "socket.io-client"; 5 | import { AddressInfo } from "net"; 6 | import { instrument } from "../lib"; 7 | import expect = require("expect.js"); 8 | 9 | const waitFor = (emitter: any, event: string): Promise => { 10 | return new Promise((resolve) => { 11 | emitter.once(event, (...args: any[]) => { 12 | resolve(args.length === 1 ? args[0] : args); 13 | }); 14 | }); 15 | }; 16 | 17 | const sleep = (duration: number) => 18 | new Promise((resolve) => setTimeout(resolve, duration)); 19 | 20 | describe("Socket.IO Admin (server instrumentation)", () => { 21 | ( 22 | [ 23 | ["v4", Server], 24 | ["v3", ServerV3], 25 | ] as [string, typeof Server][] 26 | ).forEach(([version, ServerConstructor]) => { 27 | describe(`Socket.IO ${version}`, () => { 28 | let port: number, io: Server; 29 | 30 | beforeEach((done) => { 31 | const httpServer = createServer(); 32 | io = new ServerConstructor(httpServer); 33 | 34 | httpServer.listen(() => { 35 | port = (httpServer.address() as AddressInfo).port; 36 | done(); 37 | }); 38 | }); 39 | 40 | afterEach(() => { 41 | io.close(); 42 | }); 43 | 44 | it("creates an admin namespace", (done) => { 45 | instrument(io, { 46 | auth: false, 47 | }); 48 | 49 | const adminSocket = ioc(`http://localhost:${port}/admin`); 50 | 51 | adminSocket.on("connect", () => { 52 | adminSocket.disconnect(); 53 | done(); 54 | }); 55 | }); 56 | 57 | it("creates an admin namespace with custom name", (done) => { 58 | instrument(io, { 59 | auth: false, 60 | namespaceName: "/custom", 61 | }); 62 | 63 | const adminSocket = ioc(`http://localhost:${port}/custom`); 64 | 65 | adminSocket.on("connect", () => { 66 | adminSocket.disconnect(); 67 | done(); 68 | }); 69 | }); 70 | 71 | it("should work with io.listen()", () => { 72 | const io = new Server(); 73 | 74 | instrument(io, { 75 | auth: false, 76 | }); 77 | 78 | io.listen(0); 79 | io.close(); 80 | }); 81 | 82 | describe("authentication", () => { 83 | it("prevents anonymous connection", (done) => { 84 | instrument(io, { 85 | auth: { 86 | type: "basic", 87 | username: "admin", 88 | password: 89 | "$2b$10$nu1FHRkuxkZkqID.31gz4uQsyERZAn.m4IIruysTsHDODBtrqS5Me", // "changeit" 90 | }, 91 | }); 92 | 93 | const adminSocket = ioc(`http://localhost:${port}/admin`); 94 | 95 | adminSocket.on("connect", () => { 96 | done(new Error("should not happen")); 97 | }); 98 | 99 | adminSocket.on("connect_error", (err: any) => { 100 | expect(err.message).to.eql("invalid credentials"); 101 | adminSocket.disconnect(); 102 | done(); 103 | }); 104 | }); 105 | 106 | it("allows connection with valid credentials", (done) => { 107 | instrument(io, { 108 | auth: { 109 | type: "basic", 110 | username: "admin", 111 | password: 112 | "$2b$10$nu1FHRkuxkZkqID.31gz4uQsyERZAn.m4IIruysTsHDODBtrqS5Me", // "changeit" 113 | }, 114 | }); 115 | 116 | const adminSocket = ioc(`http://localhost:${port}/admin`, { 117 | auth: { 118 | username: "admin", 119 | password: "changeit", 120 | }, 121 | }); 122 | 123 | adminSocket.on("connect", () => { 124 | adminSocket.disconnect(); 125 | done(); 126 | }); 127 | }); 128 | 129 | it("allows connection with a valid session ID", (done) => { 130 | instrument(io, { 131 | auth: { 132 | type: "basic", 133 | username: "admin", 134 | password: 135 | "$2b$10$nu1FHRkuxkZkqID.31gz4uQsyERZAn.m4IIruysTsHDODBtrqS5Me", // "changeit" 136 | }, 137 | }); 138 | 139 | const adminSocket = ioc(`http://localhost:${port}/admin`, { 140 | reconnectionDelay: 100, 141 | auth: { 142 | username: "admin", 143 | password: "changeit", 144 | }, 145 | }); 146 | 147 | adminSocket.on("session", (sessionId) => { 148 | adminSocket.auth = { 149 | sessionId, 150 | }; 151 | adminSocket.on("connect", () => { 152 | adminSocket.disconnect(); 153 | done(); 154 | }); 155 | adminSocket.disconnect().connect(); 156 | }); 157 | }); 158 | 159 | it("prevents connection with an invalid session ID", (done) => { 160 | instrument(io, { 161 | auth: { 162 | type: "basic", 163 | username: "admin", 164 | password: 165 | "$2b$10$nu1FHRkuxkZkqID.31gz4uQsyERZAn.m4IIruysTsHDODBtrqS5Me", // "changeit" 166 | }, 167 | }); 168 | 169 | const adminSocket = ioc(`http://localhost:${port}/admin`, { 170 | auth: { 171 | sessionId: "1234", 172 | }, 173 | }); 174 | 175 | adminSocket.on("connect", () => { 176 | done(new Error("should not happen")); 177 | }); 178 | 179 | adminSocket.on("connect_error", (err: any) => { 180 | expect(err.message).to.eql("invalid credentials"); 181 | adminSocket.disconnect(); 182 | done(); 183 | }); 184 | }); 185 | }); 186 | 187 | it("returns the list of supported features", (done) => { 188 | instrument(io, { 189 | auth: false, 190 | }); 191 | 192 | const adminSocket = ioc(`http://localhost:${port}/admin`); 193 | 194 | adminSocket.on("config", (config: any) => { 195 | if (version === "v4") { 196 | expect(config.supportedFeatures).to.eql([ 197 | "EMIT", 198 | "JOIN", 199 | "LEAVE", 200 | "DISCONNECT", 201 | "MJOIN", 202 | "MLEAVE", 203 | "MDISCONNECT", 204 | "AGGREGATED_EVENTS", 205 | "ALL_EVENTS", 206 | ]); 207 | } else { 208 | expect(config.supportedFeatures).to.eql([ 209 | "EMIT", 210 | "JOIN", 211 | "LEAVE", 212 | "DISCONNECT", 213 | "AGGREGATED_EVENTS", 214 | "ALL_EVENTS", 215 | ]); 216 | } 217 | adminSocket.disconnect(); 218 | done(); 219 | }); 220 | }); 221 | 222 | it("returns an empty list of supported features when in readonly mode", (done) => { 223 | instrument(io, { 224 | auth: false, 225 | readonly: true, 226 | }); 227 | 228 | const adminSocket = ioc(`http://localhost:${port}/admin`); 229 | 230 | adminSocket.on("config", (config: any) => { 231 | expect(config.supportedFeatures).to.eql([ 232 | "AGGREGATED_EVENTS", 233 | "ALL_EVENTS", 234 | ]); 235 | adminSocket.disconnect(); 236 | done(); 237 | }); 238 | }); 239 | 240 | it("returns an empty list of supported features when in production mode", (done) => { 241 | instrument(io, { 242 | auth: false, 243 | readonly: true, 244 | mode: "production", 245 | }); 246 | 247 | const adminSocket = ioc(`http://localhost:${port}/admin`); 248 | 249 | adminSocket.on("config", (config: any) => { 250 | expect(config.supportedFeatures).to.eql(["AGGREGATED_EVENTS"]); 251 | adminSocket.disconnect(); 252 | done(); 253 | }); 254 | }); 255 | 256 | it("returns the list of all sockets upon connection", async () => { 257 | instrument(io, { 258 | auth: false, 259 | readonly: true, 260 | }); 261 | 262 | const clientSocket = ioc(`http://localhost:${port}`); 263 | await waitFor(clientSocket, "connect"); 264 | 265 | const adminSocket = ioc(`http://localhost:${port}/admin`, { 266 | forceNew: true, 267 | }); 268 | 269 | const sockets = await waitFor(adminSocket, "all_sockets"); 270 | expect(sockets.length).to.eql(2); 271 | sockets.forEach((socket: any) => { 272 | if (socket.nsp === "/") { 273 | expect(socket.id).to.eql(clientSocket.id); 274 | } else { 275 | expect(socket.id).to.eql(adminSocket.id); 276 | expect(socket.nsp).to.eql("/admin"); 277 | } 278 | }); 279 | 280 | clientSocket.disconnect(); 281 | adminSocket.disconnect(); 282 | 283 | // FIXME without this, the process does not exit properly (?) 284 | await sleep(100); 285 | }); 286 | 287 | it("emits administrative events", async () => { 288 | instrument(io, { 289 | auth: false, 290 | }); 291 | 292 | const adminSocket = ioc(`http://localhost:${port}/admin`); 293 | 294 | await waitFor(adminSocket, "connect"); 295 | 296 | const clientSocket = ioc(`http://localhost:${port}`, { 297 | forceNew: true, 298 | }); 299 | 300 | // connect 301 | const serverSocket = await waitFor(io, "connection"); 302 | const [socket] = await waitFor(adminSocket, "socket_connected"); 303 | 304 | expect(socket.id).to.eql(serverSocket.id); 305 | expect(socket.nsp).to.eql("/"); 306 | expect(socket.handshake).to.eql({ 307 | address: serverSocket.handshake.address, 308 | headers: serverSocket.handshake.headers, 309 | query: serverSocket.handshake.query, 310 | issued: serverSocket.handshake.issued, 311 | secure: serverSocket.handshake.secure, 312 | time: serverSocket.handshake.time, 313 | url: serverSocket.handshake.url, 314 | xdomain: serverSocket.handshake.xdomain, 315 | }); 316 | expect(socket.rooms).to.eql([...serverSocket.rooms]); 317 | 318 | // join 319 | serverSocket.join("room1"); 320 | const [nsp, room, id] = await waitFor(adminSocket, "room_joined"); 321 | 322 | expect(nsp).to.eql("/"); 323 | expect(room).to.eql("room1"); 324 | expect(id).to.eql(socket.id); 325 | 326 | // leave 327 | serverSocket.leave("room1"); 328 | const [nsp2, room2, id2] = await waitFor(adminSocket, "room_left"); 329 | 330 | expect(nsp2).to.eql("/"); 331 | expect(room2).to.eql("room1"); 332 | expect(id2).to.eql(socket.id); 333 | 334 | // disconnect 335 | serverSocket.disconnect(); 336 | const [nsp3, id3] = await waitFor(adminSocket, "socket_disconnected"); 337 | 338 | expect(nsp3).to.eql("/"); 339 | expect(id3).to.eql(socket.id); 340 | 341 | adminSocket.disconnect(); 342 | }); 343 | 344 | it("emits event when socket.data is updated", async () => { 345 | instrument(io, { 346 | auth: false, 347 | }); 348 | 349 | const adminSocket = ioc(`http://localhost:${port}/admin`); 350 | 351 | await waitFor(adminSocket, "connect"); 352 | 353 | const clientSocket = ioc(`http://localhost:${port}`, { 354 | forceNew: true, 355 | transports: ["polling"], 356 | }); 357 | 358 | io.use((socket, next) => { 359 | socket.data = socket.data || {}; 360 | socket.data.count = 1; 361 | socket.data.array = [1]; 362 | next(); 363 | }); 364 | 365 | const serverSocket = await waitFor(io, "connection"); 366 | 367 | const [socket] = await waitFor(adminSocket, "socket_connected"); 368 | expect(socket.data).to.eql({ count: 1, array: [1] }); 369 | 370 | serverSocket.data.count++; 371 | 372 | const updatedSocket1 = await waitFor(adminSocket, "socket_updated"); 373 | expect(updatedSocket1.data).to.eql({ count: 2, array: [1] }); 374 | 375 | serverSocket.data.array.push(2); 376 | 377 | const updatedSocket2 = await waitFor(adminSocket, "socket_updated"); 378 | expect(updatedSocket2.data).to.eql({ count: 2, array: [1, 2] }); 379 | 380 | adminSocket.disconnect(); 381 | clientSocket.disconnect(); 382 | }); 383 | 384 | it("performs administrative tasks", async () => { 385 | instrument(io, { 386 | auth: false, 387 | }); 388 | 389 | const adminSocket = ioc(`http://localhost:${port}/admin`); 390 | await waitFor(adminSocket, "connect"); 391 | 392 | const clientSocket = ioc(`http://localhost:${port}`, { 393 | forceNew: true, 394 | }); 395 | 396 | const serverSocket = await waitFor(io, "connection"); 397 | await waitFor(adminSocket, "room_joined"); 398 | 399 | // emit 400 | adminSocket.emit("emit", "/", serverSocket.id, "hello", "world"); 401 | const value = await waitFor(clientSocket, "hello"); 402 | expect(value).to.eql("world"); 403 | 404 | // join 405 | adminSocket.emit("join", "/", "room1", serverSocket.id); 406 | await waitFor(adminSocket, "room_joined"); 407 | expect(serverSocket.rooms.has("room1")).to.eql(true); 408 | 409 | // leave 410 | adminSocket.emit("leave", "/", "room1", serverSocket.id); 411 | await waitFor(adminSocket, "room_left"); 412 | expect(serverSocket.rooms.has("room1")).to.eql(false); 413 | 414 | // disconnect 415 | adminSocket.emit("_disconnect", "/", false, serverSocket.id); 416 | await waitFor(clientSocket, "disconnect"); 417 | 418 | adminSocket.disconnect(); 419 | }); 420 | 421 | it("supports dynamic namespaces", async function () { 422 | // requires `socket.io>=4.1.0` with the "new_namespace" event 423 | if (version === "v3") { 424 | return this.skip(); 425 | } 426 | instrument(io, { 427 | auth: false, 428 | }); 429 | 430 | io.of(/\/dynamic-\d+/); 431 | 432 | const adminSocket = ioc(`http://localhost:${port}/admin`); 433 | await waitFor(adminSocket, "connect"); 434 | 435 | const clientSocket = ioc(`http://localhost:${port}/dynamic-101`, { 436 | forceNew: true, 437 | }); 438 | 439 | const [socket] = await waitFor(adminSocket, "socket_connected"); 440 | 441 | expect(socket.nsp).to.eql("/dynamic-101"); 442 | clientSocket.disconnect(); 443 | adminSocket.disconnect(); 444 | }); 445 | }); 446 | }); 447 | }); 448 | -------------------------------------------------------------------------------- /test/stores.ts: -------------------------------------------------------------------------------- 1 | import { InMemoryStore, RedisStore, RedisV4Store } from "../lib"; 2 | import { createClient as createLegacyClient } from "redis"; 3 | import { createClient } from "redis-v4"; 4 | import { Redis } from "ioredis"; 5 | import expect = require("expect.js"); 6 | 7 | describe("Stores", () => { 8 | describe("InMemoryStore", () => { 9 | it("works", async () => { 10 | const store = new InMemoryStore(); 11 | store.saveSession("123"); 12 | 13 | expect(await store.doesSessionExist("123")).to.eql(true); 14 | expect(await store.doesSessionExist("456")).to.eql(false); 15 | }); 16 | }); 17 | 18 | describe("RedisStore", () => { 19 | it("works with redis@4", async () => { 20 | const redisClient = createClient(); 21 | await redisClient.connect(); 22 | 23 | const store = new RedisV4Store(redisClient, { 24 | prefix: "redis@4", 25 | sessionDuration: 1, 26 | }); 27 | store.saveSession("123"); 28 | 29 | expect(await store.doesSessionExist("123")).to.eql(true); 30 | expect(await store.doesSessionExist("456")).to.eql(false); 31 | 32 | redisClient.quit(); 33 | }); 34 | 35 | it("works with redis@3", async () => { 36 | const redisClient = createLegacyClient(); 37 | 38 | const store = new RedisStore(redisClient, { 39 | prefix: "redis@3", 40 | sessionDuration: 1, 41 | }); 42 | store.saveSession("123"); 43 | 44 | expect(await store.doesSessionExist("123")).to.eql(true); 45 | expect(await store.doesSessionExist("456")).to.eql(false); 46 | 47 | redisClient.quit(); 48 | }); 49 | 50 | it("works with ioredis", async () => { 51 | const redisClient = new Redis(); 52 | 53 | const store = new RedisStore(redisClient, { 54 | prefix: "ioredis", 55 | sessionDuration: 1, 56 | }); 57 | store.saveSession("123"); 58 | 59 | expect(await store.doesSessionExist("123")).to.eql(true); 60 | expect(await store.doesSessionExist("456")).to.eql(false); 61 | 62 | redisClient.quit(); 63 | }); 64 | }); 65 | }); 66 | -------------------------------------------------------------------------------- /test/util.ts: -------------------------------------------------------------------------------- 1 | export function createPartialDone(count: number, done: (err?: Error) => void) { 2 | let i = 0; 3 | return () => { 4 | if (++i === count) { 5 | done(); 6 | } else if (i > count) { 7 | done(new Error(`partialDone() called too many times: ${i} > ${count}`)); 8 | } 9 | }; 10 | } 11 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "./dist", 4 | "allowJs": false, 5 | "target": "es2017", 6 | "module": "commonjs", 7 | "strict": true, 8 | "declaration": true 9 | }, 10 | "include": [ 11 | "./lib/**/*" 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /ui/.browserslistrc: -------------------------------------------------------------------------------- 1 | > 1% 2 | last 2 versions 3 | not dead 4 | -------------------------------------------------------------------------------- /ui/.env: -------------------------------------------------------------------------------- 1 | VUE_APP_I18N_LOCALE=en 2 | VUE_APP_I18N_FALLBACK_LOCALE=en 3 | -------------------------------------------------------------------------------- /ui/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | node: true, 5 | }, 6 | extends: ["plugin:vue/essential", "eslint:recommended", "@vue/prettier"], 7 | parserOptions: { 8 | parser: "babel-eslint", 9 | }, 10 | rules: { 11 | "no-console": process.env.NODE_ENV === "production" ? "warn" : "off", 12 | "no-debugger": process.env.NODE_ENV === "production" ? "warn" : "off", 13 | }, 14 | }; 15 | -------------------------------------------------------------------------------- /ui/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | !dist/ 4 | 5 | 6 | # local env files 7 | .env.local 8 | .env.*.local 9 | 10 | # Log files 11 | npm-debug.log* 12 | yarn-debug.log* 13 | yarn-error.log* 14 | pnpm-debug.log* 15 | 16 | # Editor directories and files 17 | .idea 18 | .vscode 19 | *.suo 20 | *.ntvs* 21 | *.njsproj 22 | *.sln 23 | *.sw? 24 | -------------------------------------------------------------------------------- /ui/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:14-alpine as node-builder 2 | 3 | WORKDIR /app 4 | 5 | # Install app dependencies 6 | COPY package.json . 7 | RUN npm install 8 | 9 | # Bundle app source 10 | COPY . . 11 | 12 | RUN npm run build 13 | 14 | FROM nginx:1.20.1 15 | 16 | WORKDIR /usr/share/nginx/html 17 | 18 | COPY --from=node-builder /app/dist . 19 | -------------------------------------------------------------------------------- /ui/README.md: -------------------------------------------------------------------------------- 1 | # ui 2 | 3 | ## Project setup 4 | ``` 5 | npm install 6 | ``` 7 | 8 | ### Compiles and hot-reloads for development 9 | ``` 10 | npm run serve 11 | ``` 12 | 13 | ### Compiles and minifies for production 14 | ``` 15 | npm run build 16 | ``` 17 | 18 | ### Lints and fixes files 19 | ``` 20 | npm run lint 21 | ``` 22 | 23 | ### Customize configuration 24 | See [Configuration Reference](https://cli.vuejs.org/config/). 25 | -------------------------------------------------------------------------------- /ui/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: ["@vue/cli-plugin-babel/preset"], 3 | }; 4 | -------------------------------------------------------------------------------- /ui/dist/css/app.6e3b9661.css: -------------------------------------------------------------------------------- 1 | .chart[data-v-0ad5cc14],.chart[data-v-68c0c5d5]{max-width:160px;margin:20px}.selector[data-v-2c330798]{max-width:200px}.row-pointer[data-v-1d29c60a] tbody>tr:hover{cursor:pointer}.select-room[data-v-5631eb89]{max-width:200px}.row-pointer[data-v-5631eb89] tbody>tr:hover{cursor:pointer}.key-column[data-v-3c0dcfcd]{width:30%}.link[data-v-3c0dcfcd]{color:inherit}.key-column[data-v-18284f59]{width:30%}.row-pointer[data-v-57b53591] tbody>tr:hover,.row-pointer[data-v-29992f63] tbody>tr:hover{cursor:pointer}.key-column[data-v-8d2424e4]{width:30%}.row-pointer[data-v-38772079] tbody>tr:hover,.row-pointer[data-v-c9425064] tbody>tr:hover{cursor:pointer}.link[data-v-2c2337d4]{color:inherit} -------------------------------------------------------------------------------- /ui/dist/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/socketio/socket.io-admin-ui/158864989dddeba67df9975a4cad48ef310f8c80/ui/dist/favicon.png -------------------------------------------------------------------------------- /ui/dist/img/logo-dark.3727fec5.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /ui/dist/img/logo-light.73342c25.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /ui/dist/index.html: -------------------------------------------------------------------------------- 1 | Socket.IO Admin UI
-------------------------------------------------------------------------------- /ui/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ui", 3 | "version": "0.5.1", 4 | "private": true, 5 | "scripts": { 6 | "serve": "vue-cli-service serve", 7 | "build": "vue-cli-service build", 8 | "lint": "vue-cli-service lint", 9 | "i18n:report": "vue-cli-service i18n:report --src \"./src/**/*.?(js|vue)\" --locales \"./src/locales/**/*.json\"" 10 | }, 11 | "dependencies": { 12 | "chart.js": "^3.9.1", 13 | "chartjs-adapter-date-fns": "^2.0.0", 14 | "core-js": "^3.6.5", 15 | "date-fns": "^2.28.0", 16 | "socket.io-msgpack-parser": "^3.0.1", 17 | "vue": "^2.6.11", 18 | "vue-chartjs": "^4.1.1", 19 | "vue-i18n": "^8.22.3", 20 | "vue-router": "^3.2.0", 21 | "vuetify": "^2.4.0", 22 | "vuex": "^3.4.0" 23 | }, 24 | "devDependencies": { 25 | "@vue/cli-plugin-babel": "~4.5.0", 26 | "@vue/cli-plugin-eslint": "~4.5.0", 27 | "@vue/cli-plugin-router": "~4.5.0", 28 | "@vue/cli-plugin-vuex": "^4.5.12", 29 | "@vue/cli-service": "~4.5.0", 30 | "@vue/eslint-config-prettier": "^6.0.0", 31 | "babel-eslint": "^10.1.0", 32 | "eslint": "^6.7.2", 33 | "eslint-plugin-prettier": "^3.3.1", 34 | "eslint-plugin-vue": "^6.2.2", 35 | "lodash-es": "^4.17.21", 36 | "node-sass": "^4.12.0", 37 | "prettier": "^2.2.1", 38 | "sass": "^1.32.0", 39 | "sass-loader": "^10.0.0", 40 | "socket.io-client": "^4.5.0", 41 | "vue-cli-plugin-i18n": "~2.0.3", 42 | "vue-cli-plugin-vuetify": "~2.3.1", 43 | "vue-template-compiler": "^2.6.11", 44 | "vuetify-loader": "^1.7.0", 45 | "webpack-remove-debug": "^0.1.0" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /ui/public/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/socketio/socket.io-admin-ui/158864989dddeba67df9975a4cad48ef310f8c80/ui/public/favicon.png -------------------------------------------------------------------------------- /ui/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | <%= htmlWebpackPlugin.options.title %> 9 | 10 | 11 | 12 | 13 | 16 |
17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /ui/src/App.vue: -------------------------------------------------------------------------------- 1 | 28 | 29 | 252 | -------------------------------------------------------------------------------- /ui/src/SocketHolder.js: -------------------------------------------------------------------------------- 1 | export default { 2 | set socket(socket) { 3 | this._socket = socket; 4 | }, 5 | 6 | get socket() { 7 | return this._socket; 8 | }, 9 | }; 10 | -------------------------------------------------------------------------------- /ui/src/assets/logo-dark.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /ui/src/assets/logo-light.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /ui/src/components/AppBar.vue: -------------------------------------------------------------------------------- 1 | 50 | 51 | 107 | -------------------------------------------------------------------------------- /ui/src/components/Client/ClientDetails.vue: -------------------------------------------------------------------------------- 1 | 55 | 56 | 90 | 91 | 96 | -------------------------------------------------------------------------------- /ui/src/components/Client/ClientSockets.vue: -------------------------------------------------------------------------------- 1 | 36 | 37 | 87 | 88 | 93 | -------------------------------------------------------------------------------- /ui/src/components/ConnectionModal.vue: -------------------------------------------------------------------------------- 1 | 78 | 79 | 143 | 144 | 145 | -------------------------------------------------------------------------------- /ui/src/components/ConnectionStatus.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 22 | -------------------------------------------------------------------------------- /ui/src/components/Dashboard/BytesHistogram.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 114 | -------------------------------------------------------------------------------- /ui/src/components/Dashboard/ClientsOverview.vue: -------------------------------------------------------------------------------- 1 | 49 | 50 | 138 | 139 | 145 | -------------------------------------------------------------------------------- /ui/src/components/Dashboard/ConnectionsHistogram.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 114 | -------------------------------------------------------------------------------- /ui/src/components/Dashboard/NamespacesOverview.vue: -------------------------------------------------------------------------------- 1 | 33 | 34 | 63 | 64 | 65 | -------------------------------------------------------------------------------- /ui/src/components/Dashboard/ServersOverview.vue: -------------------------------------------------------------------------------- 1 | 56 | 57 | 113 | 114 | 120 | -------------------------------------------------------------------------------- /ui/src/components/EventType.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 38 | -------------------------------------------------------------------------------- /ui/src/components/KeyValueTable.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 39 | 40 | 45 | -------------------------------------------------------------------------------- /ui/src/components/LangSelector.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 67 | -------------------------------------------------------------------------------- /ui/src/components/NamespaceSelector.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 32 | 33 | 38 | -------------------------------------------------------------------------------- /ui/src/components/NavigationDrawer.vue: -------------------------------------------------------------------------------- 1 | 36 | 37 | 103 | -------------------------------------------------------------------------------- /ui/src/components/ReadonlyToggle.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 30 | -------------------------------------------------------------------------------- /ui/src/components/Room/RoomDetails.vue: -------------------------------------------------------------------------------- 1 | 60 | 61 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /ui/src/components/Room/RoomSockets.vue: -------------------------------------------------------------------------------- 1 | 51 | 52 | 135 | 136 | 141 | -------------------------------------------------------------------------------- /ui/src/components/Room/RoomStatus.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 22 | -------------------------------------------------------------------------------- /ui/src/components/Room/RoomType.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 22 | -------------------------------------------------------------------------------- /ui/src/components/ServerStatus.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 22 | -------------------------------------------------------------------------------- /ui/src/components/Socket/InitialRequest.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 32 | -------------------------------------------------------------------------------- /ui/src/components/Socket/SocketDetails.vue: -------------------------------------------------------------------------------- 1 | 130 | 131 | 194 | 195 | 204 | -------------------------------------------------------------------------------- /ui/src/components/Socket/SocketRooms.vue: -------------------------------------------------------------------------------- 1 | 61 | 62 | 159 | 160 | 168 | -------------------------------------------------------------------------------- /ui/src/components/Status.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 29 | -------------------------------------------------------------------------------- /ui/src/components/ThemeSelector.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 32 | -------------------------------------------------------------------------------- /ui/src/components/Transport.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 39 | -------------------------------------------------------------------------------- /ui/src/i18n.js: -------------------------------------------------------------------------------- 1 | import Vue from "vue"; 2 | import VueI18n from "vue-i18n"; 3 | 4 | Vue.use(VueI18n); 5 | 6 | function loadLocaleMessages() { 7 | const locales = require.context( 8 | "./locales", 9 | true, 10 | /[A-Za-z0-9-_,\s]+\.json$/i 11 | ); 12 | const messages = {}; 13 | locales.keys().forEach((key) => { 14 | const matched = key.match(/([A-Za-z0-9-_]+)\./i); 15 | if (matched && matched.length > 1) { 16 | const locale = matched[1]; 17 | messages[locale] = locales(key); 18 | } 19 | }); 20 | return messages; 21 | } 22 | 23 | export default new VueI18n({ 24 | locale: process.env.VUE_APP_I18N_LOCALE || "en", 25 | fallbackLocale: process.env.VUE_APP_I18N_FALLBACK_LOCALE || "en", 26 | messages: loadLocaleMessages(), 27 | }); 28 | -------------------------------------------------------------------------------- /ui/src/locales/bn.json: -------------------------------------------------------------------------------- 1 | { 2 | "separator": ": ", 3 | "id": "আইডি", 4 | "update": "হালনাগাদ", 5 | "details": "বর্ণনা", 6 | "actions": "ক্রিয়াগুলো", 7 | "select-namespace": "নেমস্পেস নির্বাচন করুন", 8 | "namespace": "নেমস্পেস", 9 | "namespaces": "নেমস্পেসগুলো", 10 | "disconnect": "সংযোগ বিচ্ছিন্ন", 11 | "name": "নাম", 12 | "value": "মান", 13 | "type": "প্রকার", 14 | "status": "অবস্থা", 15 | "connected": "সংযুক্ত", 16 | "disconnected": "সংযোগহীন", 17 | "connection": { 18 | "title": "সংযোগ", 19 | "serverUrl": "সার্ভার ইউআরএল", 20 | "username": "ব্যবহারকারীর নাম", 21 | "password": "গুপ্তমন্ত্র", 22 | "connect": "সংযোগ করুন", 23 | "invalid-credentials": "অবৈধ প্রশংসাপত্র", 24 | "error": "ত্রুটি", 25 | "websocket-only": "কেবল ওয়েবসকেট?", 26 | "path": "পথ" 27 | }, 28 | "dashboard": { 29 | "title": "ড্যাশবোর্ড" 30 | }, 31 | "sockets": { 32 | "title": "সকেটগুলো", 33 | "details": "সকেটের বিশদ", 34 | "address": "আইপি ঠিকানা", 35 | "transport": "পরিবহন", 36 | "disconnect": "এই সকেট দৃষ্টান্তের সংযোগ বিচ্ছিন্ন করুন", 37 | "displayDetails": "এই সকেট দৃষ্টান্তের বিস্তারিত প্রদর্শন করুন", 38 | "client": "ক্লায়েন্ট", 39 | "socket": "সকেট", 40 | "creation-date": "তৈরির তারিখ", 41 | "leave": "এই ঘর ছেড়ে দিন", 42 | "join": "যোগদান", 43 | "join-a-room": "একটি ঘরে যোগদান করুন", 44 | "initial-request": "প্রাথমিক এইচটিটিপি অনুরোধ", 45 | "headers": "হেডারগুলো", 46 | "query-params": "অনুসন্ধানের প্যারামিটার" 47 | }, 48 | "rooms": { 49 | "title": "রুমগুলো", 50 | "details": "ঘরের বিস্তারিত", 51 | "active": "সক্রিয়", 52 | "deleted": "মোছা হয়েছে", 53 | "public": "পাবলিক", 54 | "private": "ব্যক্তিগত", 55 | "show-private": "ব্যক্তিগত কক্ষগুলি দেখান?", 56 | "sockets-count": "# সকেটের সংখ্যা", 57 | "clear": "এই ঘর থেকে সমস্ত সকেট দৃষ্টান্ত গুলো সরান", 58 | "leave": "এই ঘর থেকে এই সকেট দৃষ্টান্তটি সরান", 59 | "disconnect": "এই ঘরে থাকা সমস্ত সকেট দৃষ্টান্তের সংযোগ বিচ্ছিন্ন করুন", 60 | "displayDetails": "এই ঘরের বিস্তারিত প্রদর্শন করুন" 61 | }, 62 | "clients": { 63 | "title": "ক্লায়েন্ট", 64 | "details": "ক্লায়েন্টের খুঁটিনাটি", 65 | "sockets-count": "# সকেটের সংখ্যা", 66 | "disconnect": "এই ক্লায়েন্টের সংযোগ বিচ্ছিন্ন করুন (এবং সমস্ত সংযুক্তকৃত সকেট দৃষ্টান্ত গুলি)", 67 | "displayDetails": "এই ক্লায়েন্টের বিস্তারিত প্রদর্শন করুন" 68 | }, 69 | "servers": { 70 | "title": "সার্ভারগুলো", 71 | "hostname": "হোস্টনেম", 72 | "pid": "পিআইডি", 73 | "uptime": "আপটাইম", 74 | "clients-count": "# ক্লায়েন্টের সংখ্যা", 75 | "last-ping": "শেষ পিং", 76 | "healthy": "সুস্থ", 77 | "unhealthy": "অসুস্থ" 78 | }, 79 | "config": { 80 | "language": "ভাষা", 81 | "readonly": "শুধুমাত্র পাঠযোগ্য?", 82 | "dark-theme": "অন্ধকার থিম?" 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /ui/src/locales/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "separator": ": ", 3 | "id": "ID", 4 | "update": "Update", 5 | "details": "Details", 6 | "actions": "Actions", 7 | "select-namespace": "Select namespace", 8 | "namespace": "Namespace", 9 | "namespaces": "Namespaces", 10 | "disconnect": "Disconnect", 11 | "name": "Name", 12 | "value": "Value", 13 | "type": "Type", 14 | "status": "Status", 15 | "connected": "connected", 16 | "disconnected": "disconnected", 17 | "data": "Data", 18 | "timestamp": "Timestamp", 19 | "args": "Arguments", 20 | "connection": { 21 | "title": "Connection", 22 | "serverUrl": "Server URL", 23 | "username": "Username", 24 | "password": "Password", 25 | "connect": "Connect", 26 | "invalid-credentials": "Invalid credentials", 27 | "error": "Error", 28 | "websocket-only": "WebSocket only?", 29 | "path": "Path", 30 | "parser": "Parser", 31 | "default-parser": "Built-in parser", 32 | "msgpack-parser": "MessagePack parser", 33 | "namespace": "Admin namespace", 34 | "advanced-options": "Advanced options" 35 | }, 36 | "dashboard": { 37 | "title": "Dashboard", 38 | "connectionsHistogram": { 39 | "title": "Connection and disconnection events" 40 | }, 41 | "bytesHistogram": { 42 | "title": "Bytes received and sent", 43 | "bytesIn": "Bytes received", 44 | "bytesOut": "Bytes sent" 45 | } 46 | }, 47 | "sockets": { 48 | "title": "Sockets", 49 | "details": "Socket details", 50 | "address": "IP address", 51 | "transport": "Transport", 52 | "disconnect": "Disconnect this Socket instance", 53 | "displayDetails": "Display the details of this Socket instance", 54 | "client": "Client", 55 | "socket": "Socket", 56 | "creation-date": "Creation date", 57 | "leave": "Leave this room", 58 | "join": "Join", 59 | "join-a-room": "Join a room", 60 | "initial-request": "Initial HTTP request", 61 | "headers": "Headers", 62 | "query-params": "Query parameters" 63 | }, 64 | "rooms": { 65 | "title": "Rooms", 66 | "details": "Room details", 67 | "active": "Active", 68 | "deleted": "Deleted", 69 | "public": "Public", 70 | "private": "Private", 71 | "show-private": "Show private rooms?", 72 | "sockets-count": "# of sockets", 73 | "clear": "Remove all the Socket instances from this room", 74 | "leave": "Remove the Socket instance from this room", 75 | "disconnect": "Disconnect all the Socket instances that are in this room", 76 | "displayDetails": "Display the details of this room" 77 | }, 78 | "clients": { 79 | "title": "Clients", 80 | "details": "Client details", 81 | "sockets-count": "# of sockets", 82 | "disconnect": "Disconnect this client (and all attached Socket instances)", 83 | "displayDetails": "Display the details of this client" 84 | }, 85 | "servers": { 86 | "title": "Servers", 87 | "hostname": "Hostname", 88 | "pid": "PID", 89 | "uptime": "Uptime", 90 | "clients-count": "# of clients", 91 | "last-ping": "Last ping", 92 | "healthy": "Healthy", 93 | "unhealthy": "Unhealthy" 94 | }, 95 | "config": { 96 | "language": "Language", 97 | "readonly": "Read-only?", 98 | "dark-theme": "Dark theme?" 99 | }, 100 | "events": { 101 | "title": "Events", 102 | "type": { 103 | "connection": "Connection", 104 | "disconnection": "Disconnection", 105 | "room_joined": "Room joined", 106 | "room_left": "Room left", 107 | "event_received": "Event received", 108 | "event_sent": "Event sent" 109 | }, 110 | "eventName": "Event name", 111 | "eventArgs": "Event arguments", 112 | "reason": "Reason", 113 | "room": "Room" 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /ui/src/locales/fr.json: -------------------------------------------------------------------------------- 1 | { 2 | "separator": " : ", 3 | "id": "ID", 4 | "update": "Mettre à jour", 5 | "details": "Détails", 6 | "actions": "Actions", 7 | "select-namespace": "Sélection de l'espace de noms", 8 | "namespace": "Espace de noms", 9 | "namespaces": "Espaces de noms", 10 | "disconnect": "Déconnexion", 11 | "name": "Nom", 12 | "value": "Valeur", 13 | "type": "Type", 14 | "status": "Statut", 15 | "connected": "connecté", 16 | "disconnected": "déconnecté", 17 | "data": "Données", 18 | "timestamp": "Horodatage", 19 | "args": "Arguments", 20 | "connection": { 21 | "title": "Connexion", 22 | "serverUrl": "URL du serveur", 23 | "username": "Nom d'utilisateur", 24 | "password": "Mot de passe", 25 | "connect": "Se connecter", 26 | "invalid-credentials": "Identifiants invalides", 27 | "error": "Erreur", 28 | "websocket-only": "WebSocket uniquement ?", 29 | "path": "Chemin HTTP", 30 | "parser": "Encodeur", 31 | "default-parser": "Encodeur par défaut", 32 | "msgpack-parser": "Encodeur basé sur MessagePack", 33 | "namespace": "Espace de nom d'administration", 34 | "advanced-options": "Options avancées" 35 | }, 36 | "dashboard": { 37 | "title": "Accueil", 38 | "connectionsHistogram": { 39 | "title": "Évènements de connexion et de déconnexion" 40 | }, 41 | "bytesHistogram": { 42 | "title": "Octets reçus et envoyés", 43 | "bytesIn": "Octets reçus", 44 | "bytesOut": "Octets envoyés" 45 | } 46 | }, 47 | "sockets": { 48 | "title": "Connexions", 49 | "details": "Détails de la connexion", 50 | "address": "Adresse IP", 51 | "transport": "Transport", 52 | "disconnect": "Termine cette connexion", 53 | "displayDetails": "Voir les détails de cette connexion", 54 | "client": "Client", 55 | "socket": "Connexion", 56 | "creation-date": "Date de création", 57 | "leave": "Quitter cette salle", 58 | "join": "Rejoindre", 59 | "join-a-room": "Rejoindre une salle", 60 | "initial-request": "Requête HTTP initiale", 61 | "headers": "Entêtes HTTP", 62 | "query-params": "Paramètres de requête" 63 | }, 64 | "rooms": { 65 | "title": "Salles", 66 | "details": "Détails de la salle", 67 | "active": "Active", 68 | "deleted": "Supprimée", 69 | "public": "Publique", 70 | "private": "Privée", 71 | "show-private": "Afficher les salles privées ?", 72 | "sockets-count": "# de connexions", 73 | "clear": "Vider cette salle", 74 | "leave": "Sortir cette connexion de la salle", 75 | "disconnect": "Sortir toutes les connexions de cette salle", 76 | "displayDetails": "Voir les détails de cette salle" 77 | }, 78 | "clients": { 79 | "title": "Clients", 80 | "details": "Détails du client", 81 | "sockets-count": "# de connexions", 82 | "disconnect": "Déconnecte ce client (et toutes les connexions liées)", 83 | "displayDetails": "Voir les détails de ce client" 84 | }, 85 | "servers": { 86 | "title": "Serveurs", 87 | "hostname": "Nom d'hôte", 88 | "pid": "PID", 89 | "uptime": "Uptime", 90 | "clients-count": "# de clients", 91 | "last-ping": "Dernier ping", 92 | "healthy": "Actif", 93 | "unhealthy": "Inactif" 94 | }, 95 | "config": { 96 | "language": "Langue", 97 | "readonly": "Lecture seule ?", 98 | "dark-theme": "Mode sombre ?" 99 | }, 100 | "events": { 101 | "title": "Évènements", 102 | "type": { 103 | "connection": "Connexion", 104 | "disconnection": "Déconnexion", 105 | "room_joined": "Salle rejointe", 106 | "room_left": "Salle quittée", 107 | "event_received": "Évènement reçu", 108 | "event_sent": "Évènement envoyé" 109 | }, 110 | "eventName": "Nom de l'évènement", 111 | "eventArgs": "Argument de l'évènement", 112 | "reason": "Raison", 113 | "room": "Salle" 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /ui/src/locales/ko.json: -------------------------------------------------------------------------------- 1 | { 2 | "separator": ": ", 3 | "id": "ID", 4 | "update": "업데이트", 5 | "details": "상세정보", 6 | "actions": "액션", 7 | "select-namespace": "네임 스페이스 선택", 8 | "namespace": "네임 스페이스", 9 | "namespaces": "네임 스페이스", 10 | "disconnect": "연결 끊기", 11 | "name": "이름", 12 | "value": "값", 13 | "type": "타입", 14 | "status": "상태", 15 | "connected": "연결됨", 16 | "disconnected": "연결되지 않음", 17 | "data": "데이터", 18 | "timestamp": "타임스탬프", 19 | "args": "전달 인자 (Arguments)", 20 | "connection": { 21 | "title": "접속", 22 | "serverUrl": "서버 URL", 23 | "username": "유저 이름 (Username)", 24 | "password": "비밀번호 (Password)", 25 | "connect": "접속하기", 26 | "invalid-credentials": "올바르지 않은 인증", 27 | "error": "에러", 28 | "websocket-only": "웹소켓 전용?", 29 | "path": "경로", 30 | "parser": "파서 (Parser)", 31 | "default-parser": "Built-in parser", 32 | "msgpack-parser": "MessagePack parser", 33 | "namespace": "관리자 네임 스페이스", 34 | "advanced-options": "고급 옵션" 35 | }, 36 | "dashboard": { 37 | "title": "대시보드", 38 | "connectionsHistogram": { 39 | "title": "Connection 및 Disconnection 이벤트" 40 | }, 41 | "bytesHistogram": { 42 | "title": "수신 및 전송된 바이트", 43 | "bytesIn": "수신된 바이트", 44 | "bytesOut": "전송된 바이트" 45 | } 46 | }, 47 | "sockets": { 48 | "title": "소켓", 49 | "details": "소켓 상세정보", 50 | "address": "IP 주소", 51 | "transport": "통신 방식 (Transport)", 52 | "disconnect": "소켓 인스턴스 연결 끊기", 53 | "displayDetails": "소켓 인스턴스 상세정보", 54 | "client": "클라이언트", 55 | "socket": "소켓", 56 | "creation-date": "생성일", 57 | "leave": "룸 떠나기", 58 | "join": "참여", 59 | "join-a-room": "룸에 참여", 60 | "initial-request": "초기 HTTP 요청", 61 | "headers": "헤더", 62 | "query-params": "쿼리 파라미터" 63 | }, 64 | "rooms": { 65 | "title": "룸", 66 | "details": "룸 상세정보", 67 | "active": "활성화", 68 | "deleted": "삭제됨", 69 | "public": "Public", 70 | "private": "Private", 71 | "show-private": "프라이빗(Private) 룸 보기?", 72 | "sockets-count": "소켓 수", 73 | "clear": "룸에서 모든 소켓 인스턴스 제거", 74 | "leave": "룸에서 소켓 인스턴스 제거", 75 | "disconnect": "룸의 모든 소켓 인스턴스 연결 끊기", 76 | "displayDetails": "룸 상세정보" 77 | }, 78 | "clients": { 79 | "title": "클라이언트", 80 | "details": "클라언트 상세정보", 81 | "sockets-count": "소켓 수", 82 | "disconnect": "클라이언트 연결 끊기 (연결된 모든 소켓 인스턴스 끊기)", 83 | "displayDetails": "클라이언트 상세정보" 84 | }, 85 | "servers": { 86 | "title": "서버", 87 | "hostname": "호스트이름", 88 | "pid": "PID", 89 | "uptime": "가동 시간", 90 | "clients-count": "클라이언트 수", 91 | "last-ping": "마지막 ping", 92 | "healthy": "Healthy", 93 | "unhealthy": "Unhealthy" 94 | }, 95 | "config": { 96 | "language": "언어", 97 | "readonly": "읽기 전용?", 98 | "dark-theme": "다크 테마?" 99 | }, 100 | "events": { 101 | "title": "이벤트", 102 | "type": { 103 | "connection": "연결 (Connection)", 104 | "disconnection": "연결 해제 (Disconnection)", 105 | "room_joined": "참여중인 룸(Room joined)", 106 | "room_left": "나간 룸(Room left)", 107 | "event_received": "수신한 이벤트", 108 | "event_sent": "전송한 이벤트" 109 | }, 110 | "eventName": "이벤트 이름", 111 | "eventArgs": "이벤트 전달 인자 (Event Arguments)", 112 | "reason": "원인 (Reason)", 113 | "room": "룸" 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /ui/src/locales/pt-BR.json: -------------------------------------------------------------------------------- 1 | { 2 | "separator": ": ", 3 | "id": "ID", 4 | "update": "Atualizar", 5 | "details": "Detalhes", 6 | "actions": "Ações", 7 | "select-namespace": "Selecione o e", 8 | "namespace": "Espaço de nome", 9 | "namespaces": "Espaço de nomes", 10 | "disconnect": "Desconectar", 11 | "name": "Nome", 12 | "value": "Valor", 13 | "type": "Tipo", 14 | "status": "Status", 15 | "connected": "conectado", 16 | "disconnected": "desconectado", 17 | "connection": { 18 | "title": "Conexão", 19 | "serverUrl": "URL do Servidor", 20 | "username": "Usuário", 21 | "password": "Senha", 22 | "connect": "Conectar", 23 | "invalid-credentials": "Credenciais inválidas", 24 | "error": "Error", 25 | "websocket-only": "Apenas WebSocket?", 26 | "path": "Caminho" 27 | }, 28 | "dashboard": { 29 | "title": "Dashboard" 30 | }, 31 | "sockets": { 32 | "title": "Sockets", 33 | "details": "Detalhes do Socket", 34 | "address": "Endereço IP", 35 | "transport": "Transporte", 36 | "disconnect": "Desconectar esta instância", 37 | "displayDetails": "Exibir os detalhes desta instância", 38 | "client": "Cliente", 39 | "socket": "Socket", 40 | "creation-date": "Data de criação", 41 | "leave": "Saia desta sala", 42 | "join": "Entrar", 43 | "join-a-room": "Entrar em uma sala", 44 | "initial-request": "Solicitação HTTP inicial", 45 | "headers": "Cabeçalhos", 46 | "query-params": "Parâmetros de consulta" 47 | }, 48 | "rooms": { 49 | "title": "Salas", 50 | "details": "Detalhes da sala", 51 | "active": "Ativa", 52 | "deleted": "Deletada", 53 | "public": "Pública", 54 | "private": "Privada", 55 | "show-private": "Mostrar salas privadas?", 56 | "sockets-count": "# de sockets", 57 | "clear": "Remover todas as instâncias de Socket desta sala", 58 | "leave": "Remover a instância de Socket desta sala", 59 | "disconnect": "Desconecte todas as instâncias de Socket que estão nesta sala", 60 | "displayDetails": "Exibir os detalhes desta sala" 61 | }, 62 | "clients": { 63 | "title": "Clientes", 64 | "details": "Detalhes do cliente", 65 | "sockets-count": "# de sockets", 66 | "disconnect": "Desconecte este cliente (e todas as instâncias anexadas)", 67 | "displayDetails": "Mostrar os detalhes deste cliente" 68 | }, 69 | "servers": { 70 | "title": "Servidores", 71 | "hostname": "Nome do Host", 72 | "pid": "PID", 73 | "uptime": "Tempo de atividade", 74 | "clients-count": "# de clientes", 75 | "last-ping": "Último ping", 76 | "healthy": "Bom", 77 | "unhealthy": "Ruim" 78 | }, 79 | "config": { 80 | "language": "Idioma", 81 | "readonly": "Somente leitura?", 82 | "dark-theme": "Tema escuro?" 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /ui/src/locales/tr.json: -------------------------------------------------------------------------------- 1 | { 2 | "separator": ": ", 3 | "id": "ID", 4 | "update": "Güncelle", 5 | "details": "Detaylar", 6 | "actions": "Hareketler", 7 | "select-namespace": "Namespace seç", 8 | "namespace": "Namespace", 9 | "namespaces": "Namespaceler", 10 | "disconnect": "Bağlantıyı Kes", 11 | "name": "Ad", 12 | "value": "Değer", 13 | "type": "Tip", 14 | "status": "Durum", 15 | "connected": "bağlandı", 16 | "disconnected": "bağlantı kesildi", 17 | "data": "Ver,", 18 | "timestamp": "Zaman dilimi", 19 | "args": "Argümanlar", 20 | "connection": { 21 | "title": "Bağlantı", 22 | "serverUrl": "Server URL", 23 | "username": "Kullanıcı Adı", 24 | "password": "Şifre", 25 | "connect": "Bağlan", 26 | "invalid-credentials": "Geçersiz kimlik bilgileri", 27 | "error": "Hata", 28 | "websocket-only": "Yalnızca WebSocket?", 29 | "path": "Yol", 30 | "parser": "Derleyici", 31 | "default-parser": "Yerleşik Derleyici", 32 | "msgpack-parser": "MessagePack Derleyici", 33 | "namespace": "Admin namespace", 34 | "advanced-options": "Gelişmiş Seçenekler" 35 | }, 36 | "dashboard": { 37 | "title": "Gösterge Panneli", 38 | "connectionsHistogram": { 39 | "title": "Bağlantı ve bağlantı kesilmesi olayları" 40 | }, 41 | "bytesHistogram": { 42 | "title": "Alınan ve gönderilen baytlar", 43 | "bytesIn": "Alınan baytlar", 44 | "bytesOut": "gönderilen baytlar" 45 | } 46 | }, 47 | "sockets": { 48 | "title": "Socketler", 49 | "details": "Socket detayları", 50 | "address": "IP adresi", 51 | "transport": "Ulaşım", 52 | "disconnect": "Bu Socket'in bağlantısını kesin", 53 | "displayDetails": "Bu Socket'in ayrıntılarını görüntüle", 54 | "client": "Alıcı", 55 | "socket": "Socket", 56 | "creation-date": "Oluşturma tarihi", 57 | "leave": "Bu odadan ayrıl", 58 | "join": "Katıl", 59 | "join-a-room": "Odaya katıl", 60 | "initial-request": "İlk HTTP isteği", 61 | "headers": "Header'lar", 62 | "query-params": "Query parametreleri" 63 | }, 64 | "rooms": { 65 | "title": "Oda", 66 | "details": "Oda detayları", 67 | "active": "Aktif", 68 | "deleted": "Silinmiş", 69 | "public": "Herkese açık", 70 | "private": "Gizli", 71 | "show-private": "Gizli odaları göstermek ister misin", 72 | "sockets-count": "# socketlerin", 73 | "clear": "Bu odadaki tüm Socketleri kaldırın", 74 | "leave": "Socketi bu odadan kaldır", 75 | "disconnect": "Bu odadaki Socketlerin bağlantısını kesin", 76 | "displayDetails": "Bu odanın ayrıntılarını göster" 77 | }, 78 | "clients": { 79 | "title": "Alıcı", 80 | "details": "Alıcı detayları", 81 | "sockets-count": "# socketlerin", 82 | "disconnect": "Bu istemcinin (ve tüm bağlı Socketlerin) bağlantısını kesin", 83 | "displayDetails": "Bu istemcinin ayrıntılarını göster" 84 | }, 85 | "servers": { 86 | "title": "Servers", 87 | "hostname": "Host Adı", 88 | "pid": "PID", 89 | "uptime": "Çalışma Süresi", 90 | "clients-count": "# alıcılar", 91 | "last-ping": "Son ping", 92 | "healthy": "Sağlıklı", 93 | "unhealthy": "Sağlıksız" 94 | }, 95 | "config": { 96 | "language": "Dil", 97 | "readonly": "Sadece okuma modu", 98 | "dark-theme": "Koyu Tema" 99 | }, 100 | "events": { 101 | "title": "Etkinlikler", 102 | "type": { 103 | "connection": "Bağlandı", 104 | "disconnection": "Bağlantıyı Kesildi", 105 | "room_joined": "Oda katıldı", 106 | "room_left": "Oda Ayrıldı", 107 | "event_received": "Olay alındı", 108 | "event_sent": "Olay gönderildi" 109 | }, 110 | "eventName": "Etkinlik Adı", 111 | "eventArgs": "Etkinlik argümanları", 112 | "reason": "Neden", 113 | "room": "Oda" 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /ui/src/locales/zh-CN.json: -------------------------------------------------------------------------------- 1 | { 2 | "separator": ": ", 3 | "id": "ID", 4 | "update": "更新", 5 | "details": "详情", 6 | "actions": "Actions", 7 | "select-namespace": "选择 Namespace", 8 | "namespace": "Namespace", 9 | "namespaces": "Namespaces", 10 | "disconnect": "断开连接", 11 | "name": "名称", 12 | "value": "值", 13 | "type": "类型", 14 | "status": "状态", 15 | "connected": "已连接", 16 | "disconnected": "未连接", 17 | "connection": { 18 | "title": "连接", 19 | "serverUrl": "服务器 URL", 20 | "username": "用户名", 21 | "password": "密码", 22 | "connect": "提交", 23 | "invalid-credentials": "无效的密钥", 24 | "error": "错误" 25 | }, 26 | "dashboard": { 27 | "title": "状态面板" 28 | }, 29 | "sockets": { 30 | "title": "Sockets", 31 | "details": "Socket 详情", 32 | "address": "IP 地址", 33 | "transport": "协议", 34 | "disconnect": "与该 Socket 实例断开连接", 35 | "displayDetails": "显示该 Socket 实例详情", 36 | "client": "客户端", 37 | "socket": "Socket", 38 | "creation-date": "创建时间", 39 | "leave": "离开房间", 40 | "join": "加入", 41 | "join-a-room": "加入房间", 42 | "initial-request": "初始 HTTP 请求", 43 | "headers": "Headers", 44 | "query-params": "查询参数" 45 | }, 46 | "rooms": { 47 | "title": "房间", 48 | "details": "房间 详情", 49 | "active": "活跃", 50 | "deleted": "已删除", 51 | "public": "公开", 52 | "private": "私有", 53 | "show-private": "显示私人房间?", 54 | "sockets-count": "Sockets 数量", 55 | "clear": "从此房间移除所有 Socket 实例", 56 | "leave": "从该房间移除此 Socket 实例", 57 | "disconnect": "与此房间内所有 Socket 实例断开连接", 58 | "displayDetails": "显示此房间详情" 59 | }, 60 | "clients": { 61 | "title": "客户端", 62 | "details": "客户端详情", 63 | "sockets-count": "Sockets 数量", 64 | "disconnect": "与该客户端断开连接", 65 | "displayDetails": "显示该客户端详情" 66 | }, 67 | "servers": { 68 | "title": "服务器", 69 | "hostname": "Hostname", 70 | "pid": "PID", 71 | "uptime": "已经运行", 72 | "clients-count": "客户端数量", 73 | "last-ping": "上次 ping", 74 | "healthy": "健康", 75 | "unhealthy": "不健康" 76 | }, 77 | "config": { 78 | "language": "语言", 79 | "readonly": "只读", 80 | "dark-theme": "夜间模式" 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /ui/src/main.js: -------------------------------------------------------------------------------- 1 | import Vue from "vue"; 2 | import App from "./App.vue"; 3 | import router from "./router"; 4 | import i18n from "./i18n"; 5 | import store from "./store"; 6 | import vuetify from "./plugins/vuetify"; 7 | import "./plugins/chartjs"; 8 | 9 | Vue.config.productionTip = false; 10 | 11 | store.commit("config/init"); 12 | store.commit("connection/init"); 13 | 14 | i18n.locale = store.state.config.lang; 15 | 16 | setInterval(() => { 17 | store.commit("servers/updateState"); 18 | }, 1000); 19 | 20 | new Vue({ 21 | router, 22 | i18n, 23 | store, 24 | vuetify, 25 | render: (h) => h(App), 26 | }).$mount("#app"); 27 | -------------------------------------------------------------------------------- /ui/src/plugins/chartjs.js: -------------------------------------------------------------------------------- 1 | import { 2 | Chart as ChartJS, 3 | DoughnutController, 4 | Tooltip, 5 | Legend, 6 | ArcElement, 7 | BarElement, 8 | TimeScale, 9 | LinearScale, 10 | } from "chart.js"; 11 | 12 | ChartJS.register( 13 | DoughnutController, 14 | Tooltip, 15 | Legend, 16 | ArcElement, 17 | BarElement, 18 | TimeScale, 19 | LinearScale 20 | ); 21 | 22 | import "chartjs-adapter-date-fns"; 23 | -------------------------------------------------------------------------------- /ui/src/plugins/vuetify.js: -------------------------------------------------------------------------------- 1 | import Vue from "vue"; 2 | import Vuetify from "vuetify/lib/framework"; 3 | 4 | Vue.use(Vuetify); 5 | 6 | export default new Vuetify({}); 7 | -------------------------------------------------------------------------------- /ui/src/router/index.js: -------------------------------------------------------------------------------- 1 | import Vue from "vue"; 2 | import VueRouter from "vue-router"; 3 | import Dashboard from "../views/Dashboard"; 4 | import Sockets from "../views/Sockets"; 5 | import Socket from "../views/Socket"; 6 | import Rooms from "../views/Rooms"; 7 | import Clients from "../views/Clients"; 8 | import Client from "../views/Client"; 9 | import Servers from "../views/Servers"; 10 | import Room from "../views/Room"; 11 | import Events from "@/views/Events"; 12 | 13 | Vue.use(VueRouter); 14 | 15 | const routes = [ 16 | { 17 | path: "/", 18 | name: "dashboard", 19 | component: Dashboard, 20 | meta: { 21 | topLevel: true, 22 | index: 0, 23 | }, 24 | }, 25 | { 26 | path: "/sockets/", 27 | name: "sockets", 28 | component: Sockets, 29 | meta: { 30 | topLevel: true, 31 | index: 1, 32 | }, 33 | }, 34 | { 35 | path: "/n/:nsp/sockets/:id", 36 | name: "socket", 37 | component: Socket, 38 | meta: { 39 | topLevel: false, 40 | }, 41 | }, 42 | { 43 | path: "/rooms/", 44 | name: "rooms", 45 | component: Rooms, 46 | meta: { 47 | topLevel: true, 48 | index: 2, 49 | }, 50 | }, 51 | { 52 | path: "/n/:nsp/rooms/:name", 53 | name: "room", 54 | component: Room, 55 | meta: { 56 | topLevel: false, 57 | }, 58 | }, 59 | { 60 | path: "/clients/", 61 | name: "clients", 62 | component: Clients, 63 | meta: { 64 | topLevel: true, 65 | index: 3, 66 | }, 67 | }, 68 | { 69 | path: "/clients/:id", 70 | name: "client", 71 | component: Client, 72 | meta: { 73 | topLevel: false, 74 | }, 75 | }, 76 | { 77 | path: "/events/", 78 | name: "events", 79 | component: Events, 80 | meta: { 81 | topLevel: true, 82 | index: 4, 83 | }, 84 | }, 85 | { 86 | path: "/servers/", 87 | name: "servers", 88 | component: Servers, 89 | meta: { 90 | topLevel: true, 91 | index: 5, 92 | }, 93 | }, 94 | ]; 95 | 96 | const router = new VueRouter({ 97 | mode: "hash", 98 | base: process.env.BASE_URL, 99 | routes, 100 | }); 101 | 102 | export default router; 103 | -------------------------------------------------------------------------------- /ui/src/store/index.js: -------------------------------------------------------------------------------- 1 | import Vue from "vue"; 2 | import Vuex from "vuex"; 3 | import config from "./modules/config"; 4 | import connection from "./modules/connection"; 5 | import main from "./modules/main"; 6 | import servers from "./modules/servers"; 7 | 8 | Vue.use(Vuex); 9 | 10 | export default new Vuex.Store({ 11 | modules: { 12 | config, 13 | connection, 14 | main, 15 | servers, 16 | }, 17 | }); 18 | -------------------------------------------------------------------------------- /ui/src/store/modules/config.js: -------------------------------------------------------------------------------- 1 | import { isLocalStorageAvailable } from "../../util"; 2 | 3 | export default { 4 | namespaced: true, 5 | state: { 6 | darkTheme: false, 7 | readonly: false, 8 | lang: "en", 9 | supportedFeatures: [], 10 | showNavigationDrawer: false, 11 | }, 12 | getters: { 13 | developmentMode(state) { 14 | return ( 15 | state.supportedFeatures.includes("ALL_EVENTS") || 16 | !state.supportedFeatures.includes("AGGREGATED_EVENTS") 17 | ); 18 | }, 19 | hasAggregatedValues: (state) => { 20 | return state.supportedFeatures.includes("AGGREGATED_EVENTS"); 21 | }, 22 | }, 23 | mutations: { 24 | init(state) { 25 | if (isLocalStorageAvailable) { 26 | state.darkTheme = localStorage.getItem("dark_theme") === "true"; 27 | state.readonly = localStorage.getItem("readonly") === "true"; 28 | state.lang = localStorage.getItem("lang") || "en"; 29 | } 30 | }, 31 | selectTheme(state, darkTheme) { 32 | state.darkTheme = darkTheme; 33 | if (isLocalStorageAvailable) { 34 | localStorage.setItem("dark_theme", darkTheme); 35 | } 36 | }, 37 | selectLang(state, lang) { 38 | state.lang = lang; 39 | if (isLocalStorageAvailable) { 40 | localStorage.setItem("lang", lang); 41 | } 42 | }, 43 | toggleReadonly(state) { 44 | state.readonly = !state.readonly; 45 | if (isLocalStorageAvailable) { 46 | localStorage.setItem("readonly", state.readonly); 47 | } 48 | }, 49 | updateConfig(state, config) { 50 | state.supportedFeatures = config.supportedFeatures; 51 | }, 52 | toggleNavigationDrawer(state) { 53 | state.showNavigationDrawer = !state.showNavigationDrawer; 54 | }, 55 | }, 56 | }; 57 | -------------------------------------------------------------------------------- /ui/src/store/modules/connection.js: -------------------------------------------------------------------------------- 1 | import { isLocalStorageAvailable } from "../../util"; 2 | 3 | export default { 4 | namespaced: true, 5 | state: { 6 | serverUrl: "", 7 | wsOnly: false, 8 | path: "/socket.io", 9 | namespace: "/admin", 10 | parser: "default", 11 | sessionId: "", 12 | connected: false, 13 | }, 14 | mutations: { 15 | init(state) { 16 | if (isLocalStorageAvailable) { 17 | state.serverUrl = localStorage.getItem("server_url") || ""; 18 | if (state.serverUrl.endsWith("/admin")) { 19 | // for backward compatibility 20 | state.serverUrl = state.serverUrl.slice(0, -6); 21 | } else { 22 | state.namespace = localStorage.getItem("namespace") || "/admin"; 23 | } 24 | state.wsOnly = localStorage.getItem("ws_only") === "true"; 25 | state.sessionId = localStorage.getItem("session_id"); 26 | state.path = localStorage.getItem("path") || "/socket.io"; 27 | state.parser = localStorage.getItem("parser") || "default"; 28 | } 29 | }, 30 | saveConfig(state, { serverUrl, wsOnly, path, namespace, parser }) { 31 | state.serverUrl = serverUrl; 32 | state.wsOnly = wsOnly; 33 | state.path = path; 34 | state.namespace = namespace; 35 | state.parser = parser; 36 | if (isLocalStorageAvailable) { 37 | localStorage.setItem("server_url", serverUrl); 38 | localStorage.setItem("ws_only", wsOnly); 39 | localStorage.setItem("path", path); 40 | localStorage.setItem("namespace", namespace); 41 | localStorage.setItem("parser", parser); 42 | } 43 | }, 44 | saveSessionId(state, sessionId) { 45 | state.sessionId = sessionId; 46 | if (isLocalStorageAvailable) { 47 | localStorage.setItem("session_id", sessionId); 48 | } 49 | }, 50 | connect(state) { 51 | state.connected = true; 52 | }, 53 | disconnect(state) { 54 | state.connected = false; 55 | }, 56 | }, 57 | }; 58 | -------------------------------------------------------------------------------- /ui/src/store/modules/main.js: -------------------------------------------------------------------------------- 1 | import { find, merge, remove as silentlyRemove } from "lodash-es"; 2 | import { pushUniq, remove } from "@/util"; 3 | 4 | const TEN_MINUTES = 10 * 60 * 1000; 5 | 6 | const getOrCreateNamespace = (namespaces, name) => { 7 | let namespace = find(namespaces, { name }); 8 | if (namespace) { 9 | return namespace; 10 | } 11 | namespace = { 12 | name, 13 | sockets: [], 14 | rooms: [], 15 | events: [], 16 | }; 17 | namespaces.push(namespace); 18 | return namespace; 19 | }; 20 | 21 | const getOrCreateRoom = (namespace, name) => { 22 | let room = find(namespace.rooms, { name }); 23 | if (room) { 24 | return room; 25 | } 26 | room = { 27 | name, 28 | active: true, 29 | sockets: [], 30 | }; 31 | namespace.rooms.push(room); 32 | return room; 33 | }; 34 | 35 | const getOrCreateClient = (clients, id) => { 36 | let client = find(clients, { id }); 37 | if (client) { 38 | return client; 39 | } 40 | client = { 41 | id, 42 | connected: true, 43 | sockets: [], 44 | }; 45 | clients.push(client); 46 | return client; 47 | }; 48 | 49 | const addSocket = (state, socket) => { 50 | const namespace = getOrCreateNamespace(state.namespaces, socket.nsp); 51 | socket.connected = true; 52 | if (!find(namespace.sockets, { id: socket.id })) { 53 | namespace.sockets.push(socket); 54 | } 55 | 56 | socket.rooms.forEach((name) => { 57 | const room = getOrCreateRoom(namespace, name); 58 | room.isPrivate = name === socket.id; 59 | if (!find(room.sockets, { id: socket.id })) { 60 | room.sockets.push(socket); 61 | } 62 | }); 63 | 64 | const client = getOrCreateClient(state.clients, socket.clientId); 65 | if (!find(client.sockets, { id: socket.id })) { 66 | client.sockets.push(socket); 67 | } 68 | }; 69 | 70 | const MAX_ARRAY_LENGTH = 1000; 71 | let EVENT_COUNTER = 0; 72 | 73 | const pushEvents = (array, event) => { 74 | event.eventId = ++EVENT_COUNTER; // unique id 75 | array.push(event); 76 | if (array.length > MAX_ARRAY_LENGTH) { 77 | array.shift(); 78 | } 79 | }; 80 | 81 | // group events by each 10 seconds 82 | // see: https://www.chartjs.org/docs/latest/general/performance.html#decimation 83 | function roundedTimestamp(timestamp) { 84 | return timestamp - (timestamp % 10_000); 85 | } 86 | 87 | export default { 88 | namespaced: true, 89 | state: { 90 | namespaces: [], 91 | clients: [], 92 | selectedNamespace: null, 93 | aggregatedEvents: [], 94 | }, 95 | getters: { 96 | findSocketById: (state) => (nsp, id) => { 97 | const namespace = find(state.namespaces, { name: nsp }); 98 | if (namespace) { 99 | return find(namespace.sockets, { id }); 100 | } 101 | }, 102 | findClientById: (state) => (id) => { 103 | return find(state.clients, { id }); 104 | }, 105 | findRoomByName: (state) => (nsp, name) => { 106 | const namespace = find(state.namespaces, { name: nsp }); 107 | if (namespace) { 108 | return find(namespace.rooms, { name }); 109 | } 110 | }, 111 | findRoomsByNamespace: (state) => (nsp) => { 112 | const namespace = find(state.namespaces, { name: nsp }); 113 | return namespace ? namespace.rooms : []; 114 | }, 115 | sockets: (state) => { 116 | return state.selectedNamespace ? state.selectedNamespace.sockets : []; 117 | }, 118 | rooms: (state) => { 119 | return state.selectedNamespace ? state.selectedNamespace.rooms : []; 120 | }, 121 | events: (state) => { 122 | return state.selectedNamespace ? state.selectedNamespace.events : []; 123 | }, 124 | }, 125 | mutations: { 126 | selectNamespace(state, namespace) { 127 | state.selectedNamespace = namespace; 128 | }, 129 | onAllSockets(state, sockets) { 130 | state.namespaces.forEach((namespace) => { 131 | namespace.sockets.splice(0); 132 | namespace.rooms.splice(0); 133 | }); 134 | state.clients.splice(0); 135 | sockets.forEach((socket) => addSocket(state, socket)); 136 | if (!state.selectedNamespace) { 137 | state.selectedNamespace = 138 | find(state.namespaces, { name: "/" }) || state.namespaces[0]; 139 | } 140 | }, 141 | onSocketConnected(state, { timestamp, socket }) { 142 | addSocket(state, socket); 143 | const namespace = getOrCreateNamespace(state.namespaces, socket.nsp); 144 | pushEvents(namespace.events, { 145 | type: "connection", 146 | timestamp, 147 | id: socket.id, 148 | }); 149 | }, 150 | onSocketUpdated(state, socket) { 151 | const namespace = getOrCreateNamespace(state.namespaces, socket.nsp); 152 | const existingSocket = find(namespace.sockets, { id: socket.id }); 153 | if (existingSocket) { 154 | merge(existingSocket, socket); 155 | } 156 | }, 157 | onSocketDisconnected(state, { timestamp, nsp, id, reason }) { 158 | const namespace = getOrCreateNamespace(state.namespaces, nsp); 159 | const [socket] = remove(namespace.sockets, { id }); 160 | if (socket) { 161 | socket.connected = false; 162 | 163 | const client = getOrCreateClient(state.clients, socket.clientId); 164 | remove(client.sockets, { id }); 165 | if (client.sockets.length === 0) { 166 | client.connected = false; 167 | remove(state.clients, { id: socket.clientId }); 168 | } 169 | } 170 | pushEvents(namespace.events, { 171 | type: "disconnection", 172 | timestamp, 173 | id, 174 | args: reason, 175 | }); 176 | }, 177 | onRoomJoined(state, { nsp, room, id, timestamp }) { 178 | const namespace = getOrCreateNamespace(state.namespaces, nsp); 179 | const socket = find(namespace.sockets, { id }); 180 | if (socket) { 181 | pushUniq(socket.rooms, room); 182 | const _room = getOrCreateRoom(namespace, room); 183 | _room.sockets.push(socket); 184 | } 185 | pushEvents(namespace.events, { 186 | type: "room_joined", 187 | timestamp, 188 | id, 189 | args: room, 190 | }); 191 | }, 192 | onRoomLeft(state, { timestamp, nsp, room, id }) { 193 | const namespace = getOrCreateNamespace(state.namespaces, nsp); 194 | const socket = find(namespace.sockets, { id }); 195 | if (socket) { 196 | remove(socket.rooms, room); 197 | } 198 | const _room = getOrCreateRoom(namespace, room); 199 | remove(_room.sockets, { id }); 200 | if (_room.sockets.length === 0) { 201 | _room.active = false; 202 | remove(namespace.rooms, { name: room }); 203 | } 204 | pushEvents(namespace.events, { 205 | type: "room_left", 206 | timestamp, 207 | id, 208 | args: room, 209 | }); 210 | }, 211 | onServerStats(state, serverStats) { 212 | if (!serverStats.aggregatedEvents) { 213 | return; 214 | } 215 | for (const aggregatedEvent of serverStats.aggregatedEvents) { 216 | const timestamp = roundedTimestamp(aggregatedEvent.timestamp); 217 | const elem = find(state.aggregatedEvents, { 218 | timestamp, 219 | type: aggregatedEvent.type, 220 | subType: aggregatedEvent.subType, 221 | }); 222 | if (elem) { 223 | elem.count += aggregatedEvent.count; 224 | } else { 225 | state.aggregatedEvents.push({ 226 | timestamp, 227 | type: aggregatedEvent.type, 228 | subType: aggregatedEvent.subType, 229 | count: aggregatedEvent.count, 230 | }); 231 | } 232 | } 233 | silentlyRemove(state.aggregatedEvents, (elem) => { 234 | return elem.timestamp < Date.now() - TEN_MINUTES; 235 | }); 236 | }, 237 | onEventReceived(state, { timestamp, nsp, id, args }) { 238 | const namespace = getOrCreateNamespace(state.namespaces, nsp); 239 | const eventName = args.shift(); 240 | pushEvents(namespace.events, { 241 | type: "event_received", 242 | timestamp, 243 | id, 244 | eventName, 245 | args, 246 | }); 247 | }, 248 | onEventSent(state, { timestamp, nsp, id, args }) { 249 | const namespace = getOrCreateNamespace(state.namespaces, nsp); 250 | const eventName = args.shift(); 251 | pushEvents(namespace.events, { 252 | type: "event_sent", 253 | timestamp, 254 | id, 255 | eventName, 256 | args, 257 | }); 258 | }, 259 | }, 260 | }; 261 | -------------------------------------------------------------------------------- /ui/src/store/modules/servers.js: -------------------------------------------------------------------------------- 1 | import { find, merge } from "lodash-es"; 2 | import { remove } from "../../util"; 3 | 4 | const HEALTHY_THRESHOLD = 10000; 5 | 6 | export default { 7 | namespaced: true, 8 | state: { 9 | servers: [], 10 | }, 11 | getters: { 12 | namespaces(state) { 13 | const namespaces = {}; 14 | for (const server of state.servers) { 15 | if (server.namespaces) { 16 | for (const { name, socketsCount } of server.namespaces) { 17 | namespaces[name] = (namespaces[name] || 0) + socketsCount; 18 | } 19 | } 20 | } 21 | return Object.keys(namespaces).map((name) => { 22 | return { 23 | name, 24 | socketsCount: namespaces[name], 25 | }; 26 | }); 27 | }, 28 | }, 29 | mutations: { 30 | onServerStats(state, stats) { 31 | stats.lastPing = Date.now(); 32 | const server = find(state.servers, { serverId: stats.serverId }); 33 | if (server) { 34 | merge(server, stats); 35 | } else { 36 | stats.healthy = true; 37 | state.servers.push(stats); 38 | } 39 | }, 40 | removeServer(state, serverId) { 41 | remove(state.servers, { serverId }); 42 | }, 43 | updateState(state) { 44 | state.servers.forEach((server) => { 45 | server.healthy = Date.now() - server.lastPing < HEALTHY_THRESHOLD; 46 | }); 47 | }, 48 | }, 49 | }; 50 | -------------------------------------------------------------------------------- /ui/src/util.js: -------------------------------------------------------------------------------- 1 | import { findIndex } from "lodash-es"; 2 | 3 | const testLocalStorage = () => { 4 | const test = "test"; 5 | try { 6 | localStorage.setItem(test, test); 7 | localStorage.removeItem(test); 8 | return true; 9 | } catch (e) { 10 | return false; 11 | } 12 | }; 13 | 14 | export const isLocalStorageAvailable = testLocalStorage(); 15 | 16 | export function formatDuration(duration) { 17 | const d = Math.ceil(Math.max(duration, 0)); 18 | const days = Math.floor(d / 86400); 19 | const hours = Math.floor((d - days * 86400) / 3600); 20 | const minutes = Math.floor((d - days * 86400 - hours * 3600) / 60); 21 | const seconds = Math.ceil(d) - days * 86400 - hours * 3600 - minutes * 60; 22 | 23 | const output = []; 24 | if (days > 0) { 25 | output.push(days + "d"); 26 | } 27 | if (days > 0 || hours > 0) { 28 | output.push(hours + "h"); 29 | } 30 | if (days > 0 || hours > 0 || minutes > 0) { 31 | output.push(minutes + "m"); 32 | } 33 | output.push(seconds + "s"); 34 | return output.join(" "); 35 | } 36 | 37 | /** 38 | * lodash remove() does not play well with Vue.js 39 | */ 40 | export function remove(array, predicate) { 41 | const index = 42 | typeof predicate === "object" 43 | ? findIndex(array, predicate) 44 | : array.indexOf(predicate); 45 | return index === -1 ? [] : array.splice(index, 1); 46 | } 47 | 48 | export function pushUniq(array, elem) { 49 | if (!array.includes(elem)) { 50 | array.push(elem); 51 | } 52 | } 53 | 54 | export function percentage(value, total) { 55 | return total === 0 ? 0 : ((value / total) * 100).toFixed(1); 56 | } 57 | -------------------------------------------------------------------------------- /ui/src/views/Client.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 66 | 67 | 68 | -------------------------------------------------------------------------------- /ui/src/views/Clients.vue: -------------------------------------------------------------------------------- 1 | 51 | 52 | 126 | 127 | 132 | -------------------------------------------------------------------------------- /ui/src/views/Dashboard.vue: -------------------------------------------------------------------------------- 1 | 30 | 31 | 63 | -------------------------------------------------------------------------------- /ui/src/views/Events.vue: -------------------------------------------------------------------------------- 1 | 78 | 79 | 148 | 153 | -------------------------------------------------------------------------------- /ui/src/views/Room.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 59 | -------------------------------------------------------------------------------- /ui/src/views/Rooms.vue: -------------------------------------------------------------------------------- 1 | 71 | 72 | 173 | 174 | 179 | -------------------------------------------------------------------------------- /ui/src/views/Servers.vue: -------------------------------------------------------------------------------- 1 | 32 | 33 | 125 | -------------------------------------------------------------------------------- /ui/src/views/Socket.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 68 | 69 | 70 | -------------------------------------------------------------------------------- /ui/src/views/Sockets.vue: -------------------------------------------------------------------------------- 1 | 41 | 42 | 112 | 113 | 118 | -------------------------------------------------------------------------------- /ui/vue.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | publicPath: "./", 3 | 4 | configureWebpack: { 5 | node: false, // remove buffer polyfill 6 | }, 7 | chainWebpack: (config) => { 8 | config.plugin("html").tap((args) => { 9 | args[0].title = "Socket.IO Admin UI"; 10 | return args; 11 | }); 12 | config.plugin("define").tap((args) => { 13 | const version = require("./package.json").version; 14 | args[0]["process.env"]["VERSION"] = JSON.stringify(version); 15 | return args; 16 | }); 17 | // exclude moment package (included by chart.js@2) 18 | config.externals({ moment: "moment" }); 19 | }, 20 | 21 | pluginOptions: { 22 | i18n: { 23 | locale: "en", 24 | fallbackLocale: "en", 25 | localeDir: "locales", 26 | enableInSFC: false, 27 | }, 28 | }, 29 | 30 | transpileDependencies: ["vuetify"], 31 | }; 32 | --------------------------------------------------------------------------------