├── .github
└── workflows
│ └── codeql.yml
├── .gitignore
├── .npmignore
├── .prettierrc
├── CHANGELOG.md
├── LICENSE
├── README.md
├── global.d.ts
├── index.ts
├── logo.png
├── package-lock.json
├── package.json
├── screenshots
├── README.md
├── screenshot-00001.png
├── screenshot-00002.png
├── screenshot-00003.png
├── screenshot-00004.png
├── screenshot-00005.png
├── screenshot-00006.png
├── screenshot-00007.png
└── screenshot-00008.png
├── src
├── client
│ ├── components
│ │ ├── AcknowledgedMessages
│ │ │ └── AcknowledgedMessages.tsx
│ │ ├── App
│ │ │ ├── App.tsx
│ │ │ ├── AppPage.tsx
│ │ │ └── style.css
│ │ ├── Consumer
│ │ │ ├── Consumer.tsx
│ │ │ ├── ConsumerPage.tsx
│ │ │ ├── ConsumerRates.tsx
│ │ │ └── ConsumerResourcesUsage.tsx
│ │ ├── DeadLetteredMessages
│ │ │ └── DeadLetteredMessages.tsx
│ │ ├── Exchange
│ │ │ ├── BindQueue
│ │ │ │ ├── BindQueue.tsx
│ │ │ │ └── FormModal.tsx
│ │ │ ├── DeleteExchange
│ │ │ │ └── DeleteExchange.tsx
│ │ │ ├── Exchange.tsx
│ │ │ ├── ExchangePage.tsx
│ │ │ └── UnbindQueue
│ │ │ │ └── UnbindQueue.tsx
│ │ ├── Home
│ │ │ ├── CreateExchange
│ │ │ │ ├── CreateExchange.tsx
│ │ │ │ └── FormModal.tsx
│ │ │ ├── CreateQueue
│ │ │ │ ├── CreateQueue.tsx
│ │ │ │ └── FormModal.tsx
│ │ │ └── Home.tsx
│ │ ├── LeftPanel
│ │ │ ├── Exchanges
│ │ │ │ ├── Exchanges.tsx
│ │ │ │ └── ExchangesPage.tsx
│ │ │ ├── LeftPanel.tsx
│ │ │ ├── Logo
│ │ │ │ ├── Logo.tsx
│ │ │ │ └── logo.png
│ │ │ ├── Queues
│ │ │ │ ├── Queues.tsx
│ │ │ │ └── QueuesPage.tsx
│ │ │ └── Scheduled
│ │ │ │ ├── Scheduled.tsx
│ │ │ │ └── ScheduledPage.tsx
│ │ ├── PendingMessages
│ │ │ └── PendingMessages.tsx
│ │ ├── Queue
│ │ │ ├── MessageRates.tsx
│ │ │ ├── Queue.tsx
│ │ │ ├── QueuePage.tsx
│ │ │ └── RateLimiting
│ │ │ │ ├── RateLimiting.tsx
│ │ │ │ ├── RateLimitingPage.tsx
│ │ │ │ └── SetRateLimit
│ │ │ │ ├── FormModal.tsx
│ │ │ │ └── SetRateLimit.tsx
│ │ ├── QueueConsumers
│ │ │ ├── QueueConsumers.tsx
│ │ │ └── QueueConsumersPage.tsx
│ │ ├── Queues
│ │ │ ├── DeleteNamespace
│ │ │ │ └── DeleteNamespace.tsx
│ │ │ ├── DeleteQueue
│ │ │ │ └── DeleteQueue.tsx
│ │ │ ├── Queues.tsx
│ │ │ └── QueuesPage.tsx
│ │ ├── ScheduledMessages
│ │ │ └── ScheduledMessages.tsx
│ │ └── common
│ │ │ ├── Breadcrumbs.tsx
│ │ │ ├── ErrorBoundary.tsx
│ │ │ ├── Errors
│ │ │ ├── AnErrorOccurred.tsx
│ │ │ ├── PageNotFound.tsx
│ │ │ └── exclamation-circle-fill.svg
│ │ │ ├── Footer
│ │ │ ├── Footer.tsx
│ │ │ ├── FooterPage.tsx
│ │ │ └── style.css
│ │ │ ├── Messages
│ │ │ ├── MessageOptions
│ │ │ │ ├── Delete.tsx
│ │ │ │ ├── MessageOptions.tsx
│ │ │ │ ├── Requeue.tsx
│ │ │ │ └── RequeueWithPriority
│ │ │ │ │ ├── FormBody.tsx
│ │ │ │ │ ├── FormHandler.tsx
│ │ │ │ │ └── RequeueWithPriority.tsx
│ │ │ ├── Messages.tsx
│ │ │ └── MessagesPage.tsx
│ │ │ ├── Modal.tsx
│ │ │ ├── ModalLink.tsx
│ │ │ ├── Notification.tsx
│ │ │ ├── Paginator.tsx
│ │ │ ├── Query.tsx
│ │ │ └── TimeSeriesChart
│ │ │ ├── Chart.tsx
│ │ │ ├── LiveTimeSeries.tsx
│ │ │ ├── Navigation
│ │ │ ├── Navigation.tsx
│ │ │ ├── Scroll.tsx
│ │ │ └── WindowSize.tsx
│ │ │ ├── QueryTimeSeries.tsx
│ │ │ ├── TimeSeriesChart.tsx
│ │ │ └── UPlotChartEngine.tsx
│ ├── hooks
│ │ ├── useParams.ts
│ │ ├── useQuery.ts
│ │ ├── useSelector.ts
│ │ ├── useUrlParams.ts
│ │ └── useWebsocketSubscription.ts
│ ├── index.html
│ ├── index.tsx
│ ├── polyfill
│ │ └── polyfill.ts
│ ├── routes
│ │ ├── common.ts
│ │ ├── index.tsx
│ │ └── routes
│ │ │ ├── acknowledged-messages.ts
│ │ │ ├── consumer.ts
│ │ │ ├── consumers.ts
│ │ │ ├── dead-lettered-messages.ts
│ │ │ ├── exchange.ts
│ │ │ ├── home.ts
│ │ │ ├── index.ts
│ │ │ ├── pending-messages.ts
│ │ │ ├── queue.ts
│ │ │ ├── queues.ts
│ │ │ └── scheduled-messages.ts
│ ├── store
│ │ ├── components
│ │ │ ├── Exchange
│ │ │ │ ├── action.ts
│ │ │ │ ├── reducer.ts
│ │ │ │ └── state.ts
│ │ │ ├── LeftPanel
│ │ │ │ ├── Exchanges
│ │ │ │ │ ├── action.ts
│ │ │ │ │ ├── reducer.ts
│ │ │ │ │ └── state.ts
│ │ │ │ ├── reducer.ts
│ │ │ │ └── state.ts
│ │ │ ├── reducer.ts
│ │ │ └── state.ts
│ │ ├── index.ts
│ │ ├── notifications
│ │ │ ├── action.ts
│ │ │ ├── reducer.ts
│ │ │ └── state.ts
│ │ ├── state.ts
│ │ └── websocket-main-stream
│ │ │ ├── action.ts
│ │ │ ├── reducer.ts
│ │ │ └── state.ts
│ ├── tools
│ │ ├── start-monitor-server.ts
│ │ └── utils.ts
│ └── transport
│ │ ├── endpoints.ts
│ │ ├── http
│ │ └── api
│ │ │ ├── common
│ │ │ └── IMessage.ts
│ │ │ ├── delete-message.ts
│ │ │ ├── delete-queue.ts
│ │ │ ├── exchanges.ts
│ │ │ ├── get-messages.ts
│ │ │ ├── index.ts
│ │ │ ├── namespaces.ts
│ │ │ ├── purge-messages.ts
│ │ │ ├── queue-rate-limiting.ts
│ │ │ ├── requeue-message.ts
│ │ │ ├── save-queue.ts
│ │ │ └── time-series.ts
│ │ └── websocket
│ │ ├── streams
│ │ ├── consumerHeartbeatStream.ts
│ │ ├── mainStream.ts
│ │ ├── queueConsumersStream.ts
│ │ ├── queueOnlineConsumersStream.ts
│ │ └── rateStream.ts
│ │ └── websocket.ts
└── server
│ └── middleware.ts
├── tsconfig-server.json
├── tsconfig.json
└── webpack.config.js
/.github/workflows/codeql.yml:
--------------------------------------------------------------------------------
1 | # For most projects, this workflow file will not need changing; you simply need
2 | # to commit it to your repository.
3 | #
4 | # You may wish to alter this file to override the set of languages analyzed,
5 | # or to provide custom queries or build logic.
6 | #
7 | # ******** NOTE ********
8 | # We have attempted to detect the languages in your repository. Please check
9 | # the `language` matrix defined below to confirm you have the correct set of
10 | # supported CodeQL languages.
11 | #
12 | name: "code quality"
13 |
14 | on:
15 | push:
16 | branches: [ "master" ]
17 | pull_request:
18 | # The branches below must be a subset of the branches above
19 | branches: [ "master" ]
20 |
21 | jobs:
22 | analyze:
23 | name: Analyze
24 | runs-on: ubuntu-latest
25 | permissions:
26 | actions: read
27 | contents: read
28 | security-events: write
29 |
30 | strategy:
31 | fail-fast: false
32 | matrix:
33 | language: [ 'javascript' ]
34 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
35 | # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support
36 |
37 | steps:
38 | - name: Checkout repository
39 | uses: actions/checkout@v3
40 |
41 | # Initializes the CodeQL tools for scanning.
42 | - name: Initialize CodeQL
43 | uses: github/codeql-action/init@v2
44 | with:
45 | languages: ${{ matrix.language }}
46 | # If you wish to specify custom queries, you can do so here or in a config file.
47 | # By default, queries listed here will override any specified in a config file.
48 | # Prefix the list here with "+" to use these queries and those in the config file.
49 |
50 | # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
51 | # queries: security-extended,security-and-quality
52 |
53 |
54 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
55 | # If this step fails, then you should remove it and run the build manually (see below)
56 | - name: Autobuild
57 | uses: github/codeql-action/autobuild@v2
58 |
59 | # ℹ️ Command-line programs to run using the OS shell.
60 | # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
61 |
62 | # If the Autobuild fails above, remove it and uncomment the following three lines.
63 | # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance.
64 |
65 | # - run: |
66 | # echo "Run, Build Application using script"
67 | # ./location_of_script_within_repo/buildscript.sh
68 |
69 | - name: Perform CodeQL Analysis
70 | uses: github/codeql-action/analyze@v2
71 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # compiled output
2 | /dist
3 | /node_modules
4 | /docs/coverage
5 |
6 | # Logs
7 | logs
8 | *.log
9 | npm-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
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | .idea
2 | src/**
3 | .gitignore
4 | tsconfig-server.json
5 | webpack.config.js
6 | .prettierrc
7 | tsconfig.json
8 | index.ts
9 | .npmignore
10 | !types/**
11 | screenshots
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "arrowParens": "always",
3 | "semi": true,
4 | "useTabs": false,
5 | "tabWidth": 4,
6 | "bracketSpacing": true,
7 | "singleQuote": true,
8 | "trailingComma": "none",
9 | "printWidth": 120
10 | }
11 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2017-2023 Weyoss
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining
4 | a copy of this software and associated documentation files (the
5 | "Software"), to deal in the Software without restriction, including
6 | without limitation the rights to use, copy, modify, merge, publish,
7 | distribute, sublicense, and/or sell copies of the Software, and to
8 | permit persons to whom the Software is furnished to do so, subject to
9 | the following conditions:
10 |
11 | The above copyright notice and this permission notice shall be
12 | included in all copies or substantial portions of the Software.
13 |
14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
A simple high-performance Redis message queue for Node.js.
4 |
5 |
6 | > **Important Notice**
7 | >
8 | > This repository has been archived and is no longer maintained. The RedisSMQ Monitor functionality has been moved to the main [redis-smq repository](https://github.com/weyoss/redis-smq). Please refer to the main repository for the latest updates and documentation.
9 | >
10 | > **What this means:**
11 | >
12 | > - No further issues or pull requests will be accepted in this repository
13 | > - All future updates and improvements will be made in the main redis-smq repository
14 | > - Please direct all questions and contributions to the main repository
15 |
16 | # RedisSMQ Monitor Client
17 |
18 |
19 |
20 |
21 |
22 |
23 | RedisSMQ Monitor Client is a Web UI for the [RedisSMQ Monitor](https://github.com/weyoss/redis-smq-monitor) application.
24 |
25 | From your browser, it allows you to manage RedisSMQ in real-time. The Web UI is shipped with the `redis-smq-monitor`
26 | package. To start using it, you need to install and configure the [RedisSMQ Monitor](https://github.com/weyoss/redis-smq-monitor).
27 |
28 | 
29 |
30 | [More screenshots can be found here](./screenshots).
31 |
32 | ## License
33 |
34 | [MIT](https://github.com/weyoss/redis-smq-monitor-client/blob/master/LICENSE)
--------------------------------------------------------------------------------
/global.d.ts:
--------------------------------------------------------------------------------
1 | declare module '*.css';
2 | declare module '*.png';
3 | declare module '*.svg';
4 | declare const basePath: string;
5 |
--------------------------------------------------------------------------------
/index.ts:
--------------------------------------------------------------------------------
1 | export { Middleware } from './src/server/middleware';
2 |
--------------------------------------------------------------------------------
/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/weyoss/redis-smq-monitor-client/17f4bbbd280154ef4d258406fad90195583bff2d/logo.png
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "redis-smq-monitor-client",
3 | "description": "A Web UI for the RedisSMQ Monitor application",
4 | "version": "7.3.1",
5 | "author": "Weyoss ",
6 | "license": "MIT",
7 | "keywords": [
8 | "redis",
9 | "message queue",
10 | "message-queue",
11 | "message",
12 | "queue",
13 | "job queue",
14 | "job-queue",
15 | "jobs",
16 | "redis-smq",
17 | "priority",
18 | "priority queue",
19 | "priority-queue",
20 | "scheduler",
21 | "broker",
22 | "message broker",
23 | "message-broker"
24 | ],
25 | "repository": {
26 | "type": "git",
27 | "url": "https://github.com/weyoss/redis-smq-monitor-client.git"
28 | },
29 | "homepage": "https://github.com/weyoss/redis-smq-monitor-client",
30 | "bugs": {
31 | "url": "https://github.com/weyoss/redis-smq-monitor-client/issues"
32 | },
33 | "main": "dist/index.js",
34 | "types": "dist/index.d.ts",
35 | "scripts": {
36 | "test": "echo \"Error: no test specified\" && exit 1",
37 | "start:dev": "run-p start:server start:webpack",
38 | "start:server": "TS_NODE_PROJECT='tsconfig-server.json' nodemon src/client/tools/start-monitor-server.ts",
39 | "start:webpack": "webpack serve --progress --open --mode development",
40 | "prebuild": "npm run build:clean",
41 | "build": "run-p build:client build:server",
42 | "build:clean": "rimraf ./dist && mkdir ./dist",
43 | "build:client": "NODE_ENV=production webpack --progress",
44 | "build:server": "NODE_ENV=production tsc --project tsconfig-server.json",
45 | "prepublishOnly": "npm run build"
46 | },
47 | "devDependencies": {
48 | "@babel/core": "7.11.6",
49 | "@types/koa": "2.13.4",
50 | "@types/koa-send": "4.1.3",
51 | "@types/lodash": "4.14.176",
52 | "@types/node": "13.13.21",
53 | "@types/react": "16.14.20",
54 | "@types/react-bootstrap": "0.32.29",
55 | "@types/react-dom": "16.9.14",
56 | "@types/react-redux": "7.1.9",
57 | "@types/react-router": "5.1.8",
58 | "@types/react-router-dom": "5.1.5",
59 | "@types/redux-immutable-state-invariant": "2.1.1",
60 | "axios": "0.24.0",
61 | "babel-loader": "8.2.2",
62 | "bootstrap": "5.1.3",
63 | "clean-webpack-plugin": "4.0.0",
64 | "css-loader": "6.2.0",
65 | "file-loader": "6.2.0",
66 | "formik": "2.2.9",
67 | "html-loader": "2.1.2",
68 | "html-webpack-plugin": "5.3.2",
69 | "lodash": "4.17.21",
70 | "mini-css-extract-plugin": "2.1.0",
71 | "nodemon": "^2.0.20",
72 | "npm-run-all": "4.1.5",
73 | "prettier": "2.1.2",
74 | "query-string": "7.0.1",
75 | "react": "16.14.0",
76 | "react-bootstrap": "2.1.0",
77 | "react-dom": "16.14.0",
78 | "react-hot-loader": "4.12.21",
79 | "react-redux": "7.2.1",
80 | "react-router": "5.2.0",
81 | "react-router-dom": "5.2.0",
82 | "redux": "4.0.5",
83 | "redux-immutable-state-invariant": "2.1.0",
84 | "redux-thunk": "2.3.0",
85 | "rimraf": "3.0.2",
86 | "socket.io-client": "4.5.4",
87 | "string-replace-loader": "3.1.0",
88 | "style-loader": "1.2.1",
89 | "ts-loader": "6.2.2",
90 | "ts-node": "9.0.0",
91 | "typescript": "4.5.2",
92 | "uplot": "1.6.18",
93 | "url-loader": "4.1.0",
94 | "webpack": "5.76.3",
95 | "webpack-cli": "5.0.1",
96 | "webpack-dev-server": "4.13.1"
97 | },
98 | "dependencies": {
99 | "@types/ejs": "3.1.0",
100 | "ejs": "3.1.7",
101 | "koa": "2.13.1",
102 | "koa-send": "5.0.1"
103 | }
104 | }
105 |
--------------------------------------------------------------------------------
/screenshots/README.md:
--------------------------------------------------------------------------------
1 | 
2 | 
3 | 
4 | 
5 | 
6 | 
7 | 
8 | 
9 |
10 |
--------------------------------------------------------------------------------
/screenshots/screenshot-00001.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/weyoss/redis-smq-monitor-client/17f4bbbd280154ef4d258406fad90195583bff2d/screenshots/screenshot-00001.png
--------------------------------------------------------------------------------
/screenshots/screenshot-00002.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/weyoss/redis-smq-monitor-client/17f4bbbd280154ef4d258406fad90195583bff2d/screenshots/screenshot-00002.png
--------------------------------------------------------------------------------
/screenshots/screenshot-00003.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/weyoss/redis-smq-monitor-client/17f4bbbd280154ef4d258406fad90195583bff2d/screenshots/screenshot-00003.png
--------------------------------------------------------------------------------
/screenshots/screenshot-00004.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/weyoss/redis-smq-monitor-client/17f4bbbd280154ef4d258406fad90195583bff2d/screenshots/screenshot-00004.png
--------------------------------------------------------------------------------
/screenshots/screenshot-00005.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/weyoss/redis-smq-monitor-client/17f4bbbd280154ef4d258406fad90195583bff2d/screenshots/screenshot-00005.png
--------------------------------------------------------------------------------
/screenshots/screenshot-00006.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/weyoss/redis-smq-monitor-client/17f4bbbd280154ef4d258406fad90195583bff2d/screenshots/screenshot-00006.png
--------------------------------------------------------------------------------
/screenshots/screenshot-00007.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/weyoss/redis-smq-monitor-client/17f4bbbd280154ef4d258406fad90195583bff2d/screenshots/screenshot-00007.png
--------------------------------------------------------------------------------
/screenshots/screenshot-00008.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/weyoss/redis-smq-monitor-client/17f4bbbd280154ef4d258406fad90195583bff2d/screenshots/screenshot-00008.png
--------------------------------------------------------------------------------
/src/client/components/AcknowledgedMessages/AcknowledgedMessages.tsx:
--------------------------------------------------------------------------------
1 | import { RouteComponentProps, withRouter } from 'react-router';
2 | import React, { useCallback } from 'react';
3 | import QueueMessages from '../common/Messages/Messages';
4 | import {
5 | requeueAcknowledgedMessage,
6 | purgeAcknowledgedMessages,
7 | deleteQueueAcknowledgedMessage,
8 | getQueueAcknowledgedMessages
9 | } from '../../transport/http/api';
10 | import { IQueueRouteParams } from '../../routes/routes/queue';
11 |
12 | const AcknowledgedMessages: React.FC> = (props) => {
13 | const { namespace, queueName } = props.match.params;
14 | const FetchQueueMessagesRequestFactory = useCallback((skip: number, take: number) => {
15 | return () => getQueueAcknowledgedMessages(namespace, queueName, skip, take);
16 | }, []);
17 | const DeleteQueueMessageRequestFactory = useCallback((messageId: string, sequenceId: number) => {
18 | return () => deleteQueueAcknowledgedMessage(namespace, queueName, messageId, sequenceId);
19 | }, []);
20 | const RequeueQueueMessageRequestFactory = useCallback((messageId: string, sequenceId: number) => {
21 | return () => requeueAcknowledgedMessage(namespace, queueName, messageId, sequenceId);
22 | }, []);
23 | const deleteMessagesRequestCallback = useCallback(() => purgeAcknowledgedMessages(namespace, queueName), []);
24 | return (
25 | <>
26 |
27 | {queueName}@{namespace} / Acknowledged messages
28 |
29 |
35 | >
36 | );
37 | };
38 |
39 | export default withRouter(AcknowledgedMessages);
40 |
--------------------------------------------------------------------------------
/src/client/components/App/App.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from 'react';
2 | import { Socket } from 'socket.io-client';
3 | import AppPage from './AppPage';
4 | import Websocket from '../../transport/websocket/websocket';
5 | import { useDispatch } from 'react-redux';
6 | import { IStoreState } from '../../store/state';
7 | import useSelector from '../../hooks/useSelector';
8 | import { INotificationsState } from '../../store/notifications/state';
9 | import { EWebsocketMainStreamStatus, IWebsocketMainStreamState } from '../../store/websocket-main-stream/state';
10 | import { setLoadingAction, setPayloadAction } from '../../store/websocket-main-stream/action';
11 | import { TWebsocketMainStreamPayload } from '../../transport/websocket/streams/mainStream';
12 |
13 | const App = () => {
14 | const websocketMainStreamState = useSelector(
15 | (state) => state.websocketMainStream
16 | );
17 | const notificationsState = useSelector((state) => state.notifications);
18 | const dispatch = useDispatch();
19 | useEffect(() => {
20 | if (websocketMainStreamState.status === EWebsocketMainStreamStatus.INIT) {
21 | dispatch(setLoadingAction());
22 | Websocket()
23 | .then((socket: Socket) => {
24 | socket.on('streamMain', (payload: TWebsocketMainStreamPayload) => {
25 | dispatch(setPayloadAction(payload));
26 | });
27 | })
28 | .catch((e: Error) => {
29 | throw e;
30 | });
31 | }
32 | }, []);
33 | return ;
34 | };
35 |
36 | export default App;
37 |
--------------------------------------------------------------------------------
/src/client/components/App/AppPage.tsx:
--------------------------------------------------------------------------------
1 | import 'bootstrap/dist/css/bootstrap.min.css';
2 | import './style.css';
3 |
4 | import React from 'react';
5 | import Footer from '../common/Footer/Footer';
6 | import Routes from '../../routes';
7 | import { BrowserRouter } from 'react-router-dom';
8 | import { Spinner } from 'react-bootstrap';
9 | import { INotificationsState } from '../../store/notifications/state';
10 | import Notification from '../common/Notification';
11 | import { EWebsocketMainStreamStatus, IWebsocketMainStreamState } from '../../store/websocket-main-stream/state';
12 | import LeftPanel from '../LeftPanel/LeftPanel';
13 |
14 | interface IProps {
15 | websocketMainStreamState: IWebsocketMainStreamState;
16 | notificationsState: INotificationsState;
17 | }
18 |
19 | const Page: React.FC = (props) => {
20 | const { websocketMainStreamState, notificationsState } = props;
21 | const { status } = websocketMainStreamState;
22 | if (status === EWebsocketMainStreamStatus.INIT) {
23 | return (
24 | <>
25 | Initializing...
26 |
27 | >
28 | );
29 | }
30 | if (status === EWebsocketMainStreamStatus.LOADING) {
31 | return (
32 | <>
33 | Waiting for upstream data...
34 |
35 | >
36 | );
37 | }
38 | return (
39 | <>
40 |
41 |
47 |
48 | >
49 | );
50 | };
51 |
52 | const AppPage: React.FC = (props) => {
53 | const path = basePath !== '/' ? basePath : undefined;
54 | return (
55 |
56 |
57 |
58 | );
59 | };
60 |
61 | export default AppPage;
62 |
--------------------------------------------------------------------------------
/src/client/components/App/style.css:
--------------------------------------------------------------------------------
1 | a {
2 | text-decoration: none;
3 | }
4 |
5 | .btn-link {
6 | text-decoration: none;
7 | }
8 |
9 | .display-4 {
10 | font-size: 2rem;
11 | font-weight: 440;
12 | margin-bottom: 1rem;
13 | }
14 |
15 | .display-5 {
16 | font-size: 1.6rem;
17 | font-weight: 440;
18 | margin-bottom: 1rem;
19 | }
20 |
21 | .display-6 {
22 | font-size: 1.4rem;
23 | font-weight: 440;
24 | margin-bottom: 1rem;
25 | }
26 |
27 | .table-hover tbody tr:hover td, .table-hover tbody tr:hover th {
28 | background-color: #e6f2ff;
29 | }
30 |
31 | .table thead tr th {
32 | font-weight: 540;
33 | line-height: 1;
34 | }
35 |
36 | #app {
37 | margin: 0;
38 | padding: 1.5rem;
39 | }
40 |
41 | #app .mainContainer {
42 | display: flex;
43 | flex-wrap: nowrap;
44 | }
45 |
46 | #app .mainContainer > div {
47 | display: block;
48 | width: 0;
49 | }
50 |
51 | #app .mainContainer .page {
52 | padding: 1.5rem;
53 | background: #EEE;
54 | flex: 1;
55 | }
56 |
57 | #app .mainContainer .leftPanel {
58 | flex-basis: 20rem;
59 | max-width: 20rem;
60 | min-width: 20rem;
61 | width: 20rem;
62 | flex-grow: 0;
63 | flex-shrink: 0;
64 | background: #f8f8f8;
65 | padding: 1.5rem;
66 | }
67 |
68 |
--------------------------------------------------------------------------------
/src/client/components/Consumer/Consumer.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { RouteComponentProps } from 'react-router';
3 | import { IConsumerRouteParams } from '../../routes/routes/consumer';
4 | import ConsumerPage from './ConsumerPage';
5 | import { TConsumerHeartbeatStreamPayload } from '../../transport/websocket/streams/consumerHeartbeatStream';
6 | import useWebsocketSubscription from '../../hooks/useWebsocketSubscription';
7 |
8 | const Consumer: React.FC> = ({ match }) => {
9 | const { namespace, queueName, consumerId } = match.params;
10 | const { isLoading, data: heartbeat } = useWebsocketSubscription(
11 | `streamConsumerHeartbeat:${consumerId}`,
12 | 0
13 | );
14 | return (
15 |
22 | );
23 | };
24 |
25 | export default Consumer;
26 |
--------------------------------------------------------------------------------
/src/client/components/Consumer/ConsumerPage.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Spinner } from 'react-bootstrap';
3 | import { IConsumerRouteParams } from '../../routes/routes/consumer';
4 | import { TConsumerHeartbeatStreamPayload } from '../../transport/websocket/streams/consumerHeartbeatStream';
5 | import ConsumerRates from './ConsumerRates';
6 | import ConsumerResourcesUsage from './ConsumerResourcesUsage';
7 |
8 | interface IProps extends IConsumerRouteParams {
9 | heartbeat: TConsumerHeartbeatStreamPayload | null;
10 | isLoading: boolean;
11 | }
12 |
13 | const Render: React.FC = ({ isLoading, heartbeat, namespace, queueName, consumerId }) => {
14 | if (isLoading) {
15 | return ;
16 | }
17 | if (!heartbeat) {
18 | return Consumer does not exists or went offline.
;
19 | }
20 |
21 | return (
22 |
23 |
Rates
24 |
25 | RAM & CPU
26 |
27 |
28 | );
29 | };
30 |
31 | const ConsumerPage: React.FC = (props) => {
32 | const { consumerId } = props;
33 | return (
34 | <>
35 | Consumer ID {consumerId}
36 |
37 | >
38 | );
39 | };
40 |
41 | export default ConsumerPage;
42 |
--------------------------------------------------------------------------------
/src/client/components/Consumer/ConsumerRates.tsx:
--------------------------------------------------------------------------------
1 | import React, { useCallback, useMemo } from 'react';
2 | import useUrlParams from '../../hooks/useUrlParams';
3 | import { Nav } from 'react-bootstrap';
4 | import TimeSeriesChart from '../common/TimeSeriesChart/TimeSeriesChart';
5 | import {
6 | getConsumerAcknowledgedTimeSeries,
7 | getConsumerDeadLetteredTimeSeries
8 | } from '../../transport/http/api/time-series';
9 |
10 | enum ENavigationTab {
11 | ACKNOWLEDGED = 'acknowledged',
12 | DEAD_LETTERED = 'dead-lettered'
13 | }
14 |
15 | const ConsumerRates: React.FC<{ namespace: string; queueName: string; consumerId: string }> = ({
16 | namespace,
17 | queueName,
18 | consumerId
19 | }) => {
20 | const { getUrlParam, setUrlParam } = useUrlParams();
21 |
22 | const activeTab = useMemo(() => getUrlParam('rates') ?? ENavigationTab.ACKNOWLEDGED, [
23 | location.pathname,
24 | location.search
25 | ]);
26 |
27 | const FetchAcknowledgedTimeSeries = useCallback(
28 | (from: number, to: number) => () => getConsumerAcknowledgedTimeSeries(consumerId, from, to),
29 | [namespace, queueName, consumerId]
30 | );
31 |
32 | const FetchDeadLetteredTimeSeries = useCallback(
33 | (from: number, to: number) => () => getConsumerDeadLetteredTimeSeries(consumerId, from, to),
34 | [namespace, queueName, consumerId]
35 | );
36 |
37 | return (
38 | <>
39 |
40 |
41 | setUrlParam('rates', ENavigationTab.ACKNOWLEDGED)}
43 | active={activeTab === ENavigationTab.ACKNOWLEDGED}
44 | >
45 | Acknowledged
46 |
47 |
48 |
49 | setUrlParam('rates', ENavigationTab.DEAD_LETTERED)}
51 | active={activeTab === ENavigationTab.DEAD_LETTERED}
52 | >
53 | Dead-lettered
54 |
55 |
56 |
57 | {activeTab === ENavigationTab.ACKNOWLEDGED && (
58 |
64 | )}
65 | {activeTab === ENavigationTab.DEAD_LETTERED && (
66 |
72 | )}
73 | >
74 | );
75 | };
76 |
77 | export default ConsumerRates;
78 |
--------------------------------------------------------------------------------
/src/client/components/Consumer/ConsumerResourcesUsage.tsx:
--------------------------------------------------------------------------------
1 | import { Table } from 'react-bootstrap';
2 | import { bytesToMB } from '../../tools/utils';
3 | import React from 'react';
4 | import { TConsumerHeartbeatStreamPayload } from '../../transport/websocket/streams/consumerHeartbeatStream';
5 |
6 | const ConsumerResourcesUsage: React.FC = ({ data }) => {
7 | return (
8 | <>
9 |
10 | Note: Sometimes the CPU usage is not accurate and does not match the real CPU usage. Therefore it should
11 | be regarded just as an indicative value.
12 |
13 |
14 |
15 |
16 | RAM (RSS, MB)
17 | CPU Usage (%)
18 |
19 |
20 |
21 |
22 | {bytesToMB(data.ram.usage.rss)}
23 | {data.cpu.percentage}
24 |
25 |
26 |
27 | >
28 | );
29 | };
30 |
31 | export default ConsumerResourcesUsage;
32 |
--------------------------------------------------------------------------------
/src/client/components/DeadLetteredMessages/DeadLetteredMessages.tsx:
--------------------------------------------------------------------------------
1 | import { RouteComponentProps, withRouter } from 'react-router';
2 | import React, { useCallback } from 'react';
3 | import QueueMessages from '../common/Messages/Messages';
4 | import {
5 | requeueDeadLetteredMessage,
6 | purgeDeadLetteredMessages,
7 | deleteQueueDeadLetteredMessage,
8 | getQueueDeadLetteredMessages
9 | } from '../../transport/http/api';
10 | import { IQueueRouteParams } from '../../routes/routes/queue';
11 |
12 | const DeadLetteredMessages: React.FC> = (props) => {
13 | const { namespace, queueName } = props.match.params;
14 | const FetchQueueMessagesRequestFactory = useCallback((skip: number, take: number) => {
15 | return () => getQueueDeadLetteredMessages(namespace, queueName, skip, take);
16 | }, []);
17 | const DeleteQueueMessageRequestFactory = useCallback((messageId: string, sequenceId: number) => {
18 | return () => deleteQueueDeadLetteredMessage(namespace, queueName, messageId, sequenceId);
19 | }, []);
20 | const RequeueQueueMessageRequestFactory = useCallback((messageId: string, sequenceId: number) => {
21 | return () => requeueDeadLetteredMessage(namespace, queueName, messageId, sequenceId);
22 | }, []);
23 | const deleteMessagesRequestCallback = useCallback(() => purgeDeadLetteredMessages(namespace, queueName), []);
24 | return (
25 | <>
26 |
27 | {queueName}@{namespace} / Dead-lettered messages
28 |
29 |
35 | >
36 | );
37 | };
38 |
39 | export default withRouter(DeadLetteredMessages);
40 |
--------------------------------------------------------------------------------
/src/client/components/Exchange/BindQueue/BindQueue.tsx:
--------------------------------------------------------------------------------
1 | import React, { useCallback, useEffect, useState } from 'react';
2 | import FormModal from './FormModal';
3 | import { Button } from 'react-bootstrap';
4 | import { IMessageQueue } from '../../../transport/http/api/common/IMessage';
5 | import useSelector from '../../../hooks/useSelector';
6 | import { IStoreState } from '../../../store/state';
7 | import { IWebsocketMainStreamState } from '../../../store/websocket-main-stream/state';
8 | import { addNotificationAction } from '../../../store/notifications/action';
9 | import { ENotificationType } from '../../../store/notifications/state';
10 | import { useDispatch } from 'react-redux';
11 | import { bindQueue } from '../../../transport/http/api/exchanges';
12 | import { reloadAction } from '../../../store/components/Exchange/action';
13 |
14 | export interface IBindQueueProps {
15 | exchangeName: string;
16 | }
17 |
18 | const BindQueue: React.FC = ({ exchangeName }) => {
19 | const dispatch = useDispatch();
20 | const [queues, setQueues] = useState([]);
21 | const { payload } = useSelector(
22 | (state) => state.websocketMainStream
23 | ); const [openHandler, setOpenHandler] = useState(false);
24 | const closeHandler = useCallback(() => setOpenHandler(false), []);
25 | useEffect(() => {
26 | if (payload.queuesCount) {
27 | const q: IMessageQueue[] = [];
28 | const { queues } = payload;
29 | for(const ns in queues) {
30 | for (const name in queues[ns]) {
31 | q.push({ name, ns });
32 | }
33 | }
34 | setQueues(q);
35 | }
36 | }, [payload.queuesCount]);
37 | const bindQueuesFn = useCallback(() => {
38 | if (queues.length) setOpenHandler(true);
39 | else {
40 | dispatch(addNotificationAction(`At least one queue is required. Please create a queue first.`, ENotificationType.ERROR));
41 | }
42 | }, [queues, openHandler]);
43 |
44 | const RequestFactory = useCallback((queue: string) => {
45 | const [name, ns] = queue.split('@');
46 | return () => bindQueue({ ns, name }, exchangeName);
47 | }, [exchangeName]);
48 |
49 | const requestSuccessCallback = useCallback(() => {
50 | dispatch(addNotificationAction(`Queue has been successfully bound.`, ENotificationType.SUCCESS));
51 | dispatch(reloadAction());
52 | }, []);
53 |
54 | return (
55 | <>
56 |
57 | Bind Queue
58 |
59 | {openHandler && (
60 |
61 | )}
62 | >
63 | );
64 | };
65 |
66 | export default BindQueue;
67 |
--------------------------------------------------------------------------------
/src/client/components/Exchange/BindQueue/FormModal.tsx:
--------------------------------------------------------------------------------
1 | import React, { useCallback, useEffect, useRef } from 'react';
2 | import { IBindQueueProps } from './BindQueue';
3 | import useQuery, { EQueryStatus, TQueryRequest } from '../../../hooks/useQuery';
4 | import { Field, Form, Formik, FormikProps } from 'formik';
5 | import { FormGroup, FormLabel, FormSelect, Spinner } from 'react-bootstrap';
6 | import { FieldProps } from 'formik/dist/Field';
7 | import Modal from '../../common/Modal';
8 | import { IMessageQueue } from '../../../transport/http/api/common/IMessage';
9 |
10 | interface IFormFields {
11 | queue: string;
12 | }
13 |
14 | interface IHandlerProps extends IBindQueueProps {
15 | RequestFactory: (queue: string) => TQueryRequest;
16 | requestSuccessCallback: () => void;
17 | closeHandlerCallback: () => void;
18 | queues: IMessageQueue[];
19 | exchangeName: string;
20 | }
21 |
22 | const FormModal: React.FC = ({ queues, RequestFactory, requestSuccessCallback, closeHandlerCallback }) => {
23 | const formRef = useRef | null>(null);
24 | const query = useQuery();
25 |
26 | useEffect(() => {
27 | if (query.state.status === EQueryStatus.SUCCESS) {
28 | requestSuccessCallback();
29 | closeHandlerCallback();
30 | } else if (query.state.status === EQueryStatus.ERROR) {
31 | closeHandlerCallback();
32 | }
33 | }, [query.state.status]);
34 |
35 | const onSubmit = useCallback(() => {
36 | const form = formRef.current;
37 | if (form) {
38 | // Fixing submitForm bug (always get resolved when the form is invalid)
39 | // See formik/issues/1580
40 | form.submitForm()
41 | .then(form.validateForm)
42 | .then((errors) => {
43 | if (!Object.keys(errors).length) {
44 | query.sendQuery(RequestFactory(form.values.queue));
45 | }
46 | })
47 | .catch((e) => {
48 | console.log(e);
49 | });
50 | }
51 | }, []);
52 |
53 | const validateQueue = (key?: string): string | void => {
54 | if (!key || !key.length) {
55 | return 'Required'
56 | }
57 | };
58 |
59 | return (
60 |
61 | {query.state.status === EQueryStatus.LOADING ? (
62 |
63 | ) : (
64 | void 0}>
65 | {({ errors, touched }) => (
66 |
88 | )}
89 |
90 | )}
91 |
92 | );
93 | };
94 |
95 | export default FormModal;
96 |
--------------------------------------------------------------------------------
/src/client/components/Exchange/DeleteExchange/DeleteExchange.tsx:
--------------------------------------------------------------------------------
1 | import React, { useCallback, useEffect } from 'react';
2 | import { Button } from 'react-bootstrap';
3 | import { addNotificationAction } from '../../../store/notifications/action';
4 | import { ENotificationType } from '../../../store/notifications/state';
5 | import { useDispatch } from 'react-redux';
6 | import { deleteExchange } from '../../../transport/http/api/exchanges';
7 | import useQuery, { EQueryStatus } from '../../../hooks/useQuery';
8 | import { useHistory } from 'react-router';
9 |
10 | export interface IDeleteExchangeProps {
11 | exchangeName: string;
12 | }
13 |
14 | const DeleteExchange: React.FC = ({ exchangeName }) => {
15 | const dispatch = useDispatch();
16 | const query = useQuery();
17 | const history = useHistory();
18 | useEffect(() => {
19 | if (query.state.status === EQueryStatus.SUCCESS) {
20 | dispatch(addNotificationAction(`Exchange has been successfully deleted.`, ENotificationType.SUCCESS));
21 | history.push('/');
22 | }
23 | }, [query.state.status]);
24 | const deleteExchangeRequest = useCallback(() => {
25 | query.sendQuery(() => deleteExchange(exchangeName));
26 | }, [exchangeName]);
27 | return (
28 | <>
29 |
30 | Delete exchange
31 |
32 | >
33 | );
34 | };
35 |
36 | export default DeleteExchange;
37 |
--------------------------------------------------------------------------------
/src/client/components/Exchange/Exchange.tsx:
--------------------------------------------------------------------------------
1 | import React, { useCallback } from 'react';
2 | import { RouteComponentProps } from 'react-router';
3 | import { IExchangeRouteParams } from '../../routes/routes/exchange';
4 | import ExchangePage from './ExchangePage';
5 | import { getExchangeQueues } from '../../transport/http/api/exchanges';
6 | import Query from '../common/Query';
7 | import useSelector from '../../hooks/useSelector';
8 | import { IStoreState } from '../../store/state';
9 | import { IExchangeState } from '../../store/components/Exchange/state';
10 |
11 | const Exchange: React.FC> = ({ match }) => {
12 | const { version } = useSelector((state) => {
13 | return state.components.Exchange
14 | });
15 | const { name } = match.params;
16 | const request = useCallback(() => getExchangeQueues(name), [name]);
17 | return (
18 |
19 |
20 | {({ state }) => }
21 |
22 |
23 | );
24 | }
25 |
26 | export default Exchange;
--------------------------------------------------------------------------------
/src/client/components/Exchange/ExchangePage.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { IMessageQueue } from '../../transport/http/api/common/IMessage';
3 | import BindQueue from './BindQueue/BindQueue';
4 | import UnbindQueue from './UnbindQueue/UnbindQueue';
5 | import { Badge, ListGroup } from 'react-bootstrap';
6 | import DeleteExchange from './DeleteExchange/DeleteExchange';
7 | import { queue } from '../../routes/routes';
8 | import { Link } from 'react-router-dom';
9 |
10 | interface IExchangePageProps {
11 | exchangeName: string;
12 | queues: IMessageQueue[];
13 | }
14 |
15 |
16 | const ExchangeQueues: React.FC = ({ exchangeName, queues }) => {
17 | if (!queues.length) {
18 | return The exchange has not yet any bound queue.
19 | }
20 | return (
21 |
22 | {
23 | queues.map((q, index) => {
24 | return
29 |
30 | {q.name}@{q.ns}
31 |
32 |
33 |
34 |
35 |
36 |
37 | })
38 | }
39 |
40 | )
41 | }
42 |
43 | const ExchangePage: React.FC = ({ exchangeName, queues }) => {
44 | return <>
45 |
46 |
{exchangeName}
47 |
48 |
49 |
50 |
51 |
52 |
53 | >
54 | }
55 |
56 | export default ExchangePage;
--------------------------------------------------------------------------------
/src/client/components/Exchange/UnbindQueue/UnbindQueue.tsx:
--------------------------------------------------------------------------------
1 | import React, { useCallback, useEffect } from 'react';
2 | import { Button } from 'react-bootstrap';
3 | import { IMessageQueue } from '../../../transport/http/api/common/IMessage';
4 | import { addNotificationAction } from '../../../store/notifications/action';
5 | import { ENotificationType } from '../../../store/notifications/state';
6 | import { useDispatch } from 'react-redux';
7 | import { unbindQueue } from '../../../transport/http/api/exchanges';
8 | import { reloadAction } from '../../../store/components/Exchange/action';
9 | import useQuery, { EQueryStatus } from '../../../hooks/useQuery';
10 |
11 | export interface IBindQueueProps {
12 | exchangeName: string;
13 | queue: IMessageQueue;
14 | }
15 |
16 | const UnbindQueue: React.FC = ({ exchangeName, queue }) => {
17 | const dispatch = useDispatch();
18 | const query = useQuery();
19 | useEffect(() => {
20 | if (query.state.status === EQueryStatus.SUCCESS) {
21 | dispatch(addNotificationAction(`Queue has been successfully unbound.`, ENotificationType.SUCCESS));
22 | dispatch(reloadAction()); }
23 | }, [query.state.status]);
24 | const unbindQueueRequest = useCallback(() => {
25 | query.sendQuery(() => unbindQueue(queue, exchangeName));
26 | }, [exchangeName, queue]);
27 | return (
28 | <>
29 |
30 | Unbind Queue
31 |
32 | >
33 | );
34 | };
35 |
36 | export default UnbindQueue;
37 |
--------------------------------------------------------------------------------
/src/client/components/Home/CreateExchange/CreateExchange.tsx:
--------------------------------------------------------------------------------
1 | import React, { useCallback, useState } from 'react';
2 | import FormModal from './FormModal';
3 | import { Button } from 'react-bootstrap';
4 | import { addNotificationAction } from '../../../store/notifications/action';
5 | import { ENotificationType } from '../../../store/notifications/state';
6 | import { useDispatch } from 'react-redux';
7 | import { createExchange } from '../../../transport/http/api/exchanges';
8 | import { reloadAction } from '../../../store/components/LeftPanel/Exchanges/action';
9 |
10 | const CreateExchange: React.FC = () => {
11 | const dispatch = useDispatch();
12 | const [openHandler, setOpenHandler] = useState(false);
13 | const closeHandler = useCallback(() => setOpenHandler(false), []);
14 | const RequestFactory = useCallback(
15 | (exchangeName) => () => createExchange(exchangeName),
16 | []
17 | );
18 | const requestSuccessCallback = useCallback(() => {
19 | dispatch(addNotificationAction(`Exchange has been successfully created.`, ENotificationType.SUCCESS));
20 | dispatch(reloadAction());
21 | }, []);
22 | return (
23 | <>
24 | setOpenHandler(true)} className={'me-3'}>
25 | Create Exchange
26 |
27 | {openHandler && (
28 |
29 | )}
30 | >
31 | );
32 | };
33 |
34 | export default CreateExchange;
35 |
--------------------------------------------------------------------------------
/src/client/components/Home/CreateExchange/FormModal.tsx:
--------------------------------------------------------------------------------
1 | import React, { useCallback, useEffect, useRef } from 'react';
2 | import useQuery, { EQueryStatus, TQueryRequest } from '../../../hooks/useQuery';
3 | import { Field, Form, Formik, FormikProps } from 'formik';
4 | import { FormControl, FormGroup, FormLabel, Spinner } from 'react-bootstrap';
5 | import { FieldProps } from 'formik/dist/Field';
6 | import Modal from '../../common/Modal';
7 |
8 | interface IFormFields {
9 | exchangeName: string;
10 | }
11 |
12 | interface IFormModalProps {
13 | RequestFactory: (exchangeName: string) => TQueryRequest;
14 | requestSuccessCallback: () => void;
15 | closeHandlerCallback: () => void;
16 | onSubmitCallback: (values: any) => void;
17 | }
18 |
19 | const FormModal: React.FC = ({ RequestFactory, requestSuccessCallback, closeHandlerCallback }) => {
20 | const formRef = useRef | null>(null);
21 | const query = useQuery();
22 |
23 | useEffect(() => {
24 | if (query.state.status === EQueryStatus.SUCCESS) {
25 | requestSuccessCallback();
26 | closeHandlerCallback();
27 | } else if (query.state.status === EQueryStatus.ERROR) {
28 | closeHandlerCallback();
29 | }
30 | }, [query.state.status]);
31 |
32 | const onSubmit = useCallback(() => {
33 | const form = formRef.current;
34 | if (form) {
35 | // Fixing submitForm bug (always get resolved when the form is invalid)
36 | // See formik/issues/1580
37 | form.submitForm()
38 | .then(form.validateForm)
39 | .then((errors) => {
40 | if (!Object.keys(errors).length) {
41 | query.sendQuery(RequestFactory(form.values.exchangeName));
42 | }
43 | })
44 | .catch((e) => {
45 | console.log(e);
46 | });
47 | }
48 | }, []);
49 |
50 | const validateExchangeName = (key?: string): string | void => {
51 | if (!key || !key.length) {
52 | return 'Required'
53 | }
54 | const filtered = key
55 | .toLowerCase()
56 | .replace(/(?:[a-z][a-z0-9]?)+(?:[-_.]?[a-z0-9])*/, '');
57 | if (filtered.length) {
58 | return 'Valid characters are letters (a-z) and numbers (0-9). (-_) are allowed between alphanumerics. Use a dot (.) to denote hierarchies.';
59 | }
60 | };
61 |
62 | return (
63 |
64 | {query.state.status === EQueryStatus.LOADING ? (
65 |
66 | ) : (
67 | void 0}>
68 | {({ errors, touched }) => (
69 |
86 | )}
87 |
88 | )}
89 |
90 | );
91 | };
92 |
93 | export default FormModal;
94 |
--------------------------------------------------------------------------------
/src/client/components/Home/CreateQueue/CreateQueue.tsx:
--------------------------------------------------------------------------------
1 | import React, { useCallback, useState } from 'react';
2 | import FormModal from './FormModal';
3 | import { Button } from 'react-bootstrap';
4 | import { addNotificationAction } from '../../../store/notifications/action';
5 | import { ENotificationType } from '../../../store/notifications/state';
6 | import { useDispatch } from 'react-redux';
7 | import { saveQueue } from '../../../transport/http/api/save-queue';
8 |
9 | const CreateQueue: React.FC = () => {
10 | const dispatch = useDispatch();
11 | const [openHandler, setOpenHandler] = useState(false);
12 | const closeHandler = useCallback(() => setOpenHandler(false), []);
13 | const RequestFactory = useCallback(
14 | (queue, type) => () => saveQueue(queue, type),
15 | []
16 | );
17 | const requestSuccessCallback = useCallback(() => {
18 | dispatch(addNotificationAction(`Queue has been successfully created.`, ENotificationType.SUCCESS));
19 | }, []);
20 | return (
21 | <>
22 | setOpenHandler(true)} className={'me-3'}>
23 | Create Queue
24 |
25 | {openHandler && (
26 |
27 | )}
28 | >
29 | );
30 | };
31 |
32 | export default CreateQueue;
33 |
--------------------------------------------------------------------------------
/src/client/components/Home/Home.tsx:
--------------------------------------------------------------------------------
1 | import React, { useCallback, useMemo } from 'react';
2 | import { Nav } from 'react-bootstrap';
3 | import { RouteComponentProps } from 'react-router';
4 | import {
5 | getGlobalAcknowledgedTimeSeries,
6 | getGlobalDeadLetteredTimeSeries,
7 | getGlobalPublishedTimeSeries
8 | } from '../../transport/http/api/time-series';
9 | import TimeSeriesChart from '../common/TimeSeriesChart/TimeSeriesChart';
10 | import useUrlParams from '../../hooks/useUrlParams';
11 | import CreateExchange from './CreateExchange/CreateExchange';
12 | import CreateQueue from './CreateQueue/CreateQueue';
13 |
14 | enum ENavigationTab {
15 | ACKNOWLEDGED = 'acknowledged',
16 | DEAD_LETTERED = 'dead-lettered',
17 | PUBLISHED = 'published'
18 | }
19 |
20 | const Home: React.FC = ({ location }) => {
21 | const { getUrlParam, setUrlParam } = useUrlParams();
22 |
23 | const activeTab = useMemo(() => getUrlParam('rates') ?? ENavigationTab.ACKNOWLEDGED, [
24 | location.pathname,
25 | location.search
26 | ]);
27 |
28 | const FetchAcknowledgedTimeSeries = useCallback(
29 | (from: number, to: number) => () => getGlobalAcknowledgedTimeSeries(from, to),
30 | []
31 | );
32 |
33 | const FetchDeadLetteredTimeSeries = useCallback(
34 | (from: number, to: number) => () => getGlobalDeadLetteredTimeSeries(from, to),
35 | []
36 | );
37 |
38 | const FetchPublishedTimeSeries = useCallback(
39 | (from: number, to: number) => () => getGlobalPublishedTimeSeries(from, to),
40 | []
41 | );
42 |
43 | return (
44 | <>
45 |
46 |
47 |
48 |
49 | Global Rates
50 |
51 | The following metrics are gathered from all existing queues in the system. Select a specific queue from
52 | the queue listing, to view its metrics.
53 |
54 |
55 |
56 | setUrlParam('rates', ENavigationTab.ACKNOWLEDGED)}
58 | active={activeTab === ENavigationTab.ACKNOWLEDGED}
59 | >
60 | Acknowledged
61 |
62 |
63 |
64 | setUrlParam('rates', ENavigationTab.DEAD_LETTERED)}
66 | active={activeTab === ENavigationTab.DEAD_LETTERED}
67 | >
68 | Dead-lettered
69 |
70 |
71 |
72 | setUrlParam('rates', ENavigationTab.PUBLISHED)}
74 | active={activeTab === ENavigationTab.PUBLISHED}
75 | >
76 | Published
77 |
78 |
79 |
80 | {activeTab === ENavigationTab.ACKNOWLEDGED && (
81 |
87 | )}
88 | {activeTab === ENavigationTab.DEAD_LETTERED && (
89 |
95 | )}
96 | {activeTab === ENavigationTab.PUBLISHED && (
97 |
103 | )}
104 | >
105 | );
106 | };
107 |
108 | export default Home;
109 |
--------------------------------------------------------------------------------
/src/client/components/LeftPanel/Exchanges/Exchanges.tsx:
--------------------------------------------------------------------------------
1 | import { ExchangesPage } from './ExchangesPage';
2 | import React, { useCallback } from 'react';
3 | import Query from '../../common/Query';
4 | import { getExchanges } from '../../../transport/http/api/exchanges';
5 | import { useParams } from '../../../hooks/useParams';
6 | import { exchange } from '../../../routes/routes';
7 | import { IExchangeRouteParams } from '../../../routes/routes/exchange';
8 | import useSelector from '../../../hooks/useSelector';
9 | import { IStoreState } from '../../../store/state';
10 | import { IExchangesState } from '../../../store/components/LeftPanel/Exchanges/state';
11 |
12 | export const Exchanges = () => {
13 | const { version } = useSelector((state) => {
14 | return state.components.LeftPanel.Exchanges
15 | });
16 | const request = useCallback(() => getExchanges(), []);
17 | const matchedParams: Partial | undefined = useParams(exchange.path);
18 | return (
19 |
20 |
21 | {({ state }) => }
22 |
23 |
24 | );
25 | }
26 |
27 | export default Exchanges;
--------------------------------------------------------------------------------
/src/client/components/LeftPanel/Exchanges/ExchangesPage.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import * as routes from '../../../routes/routes';
3 | import { Link } from 'react-router-dom';
4 |
5 | interface IExchangesPageProps {
6 | fanOuts: string[];
7 | selectedExchange: string | undefined;
8 | }
9 |
10 | const List: React.FC = ({ fanOuts, selectedExchange }) => {
11 | if (!fanOuts.length) return No fanout exchanges yet.
12 | const items: React.ReactElement[] = [];
13 | for(const name of fanOuts) {
14 | const className = selectedExchange === name ? 'active ' : '';
15 | items.push(
16 | {name})
21 | }
22 | return ({items}
)
23 | }
24 |
25 | export const ExchangesPage: React.FC = (props) => {
26 | return (
27 |
28 |
Exchanges
29 |
30 |
31 | )
32 | }
--------------------------------------------------------------------------------
/src/client/components/LeftPanel/LeftPanel.tsx:
--------------------------------------------------------------------------------
1 | import Logo from './Logo/Logo';
2 | import Queues from './Queues/Queues';
3 | import Scheduled from './Scheduled/Scheduled';
4 | import React from 'react';
5 | import Exchanges from './Exchanges/Exchanges';
6 |
7 | const LeftPanel = () => {
8 | return (
9 |
10 |
11 |
12 |
13 |
14 |
15 | )
16 | }
17 |
18 | export default LeftPanel;
--------------------------------------------------------------------------------
/src/client/components/LeftPanel/Logo/Logo.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Link } from 'react-router-dom';
3 | import logo from './logo.png';
4 | import { home } from '../../../routes/routes';
5 |
6 | export default () => (
7 |
8 |
9 |
10 |
11 |
12 | );
13 |
--------------------------------------------------------------------------------
/src/client/components/LeftPanel/Logo/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/weyoss/redis-smq-monitor-client/17f4bbbd280154ef4d258406fad90195583bff2d/src/client/components/LeftPanel/Logo/logo.png
--------------------------------------------------------------------------------
/src/client/components/LeftPanel/Queues/Queues.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import QueuesPage from './QueuesPage';
3 | import { IStoreState } from '../../../store/state';
4 | import useSelector from '../../../hooks/useSelector';
5 | import { IWebsocketMainStreamState } from '../../../store/websocket-main-stream/state';
6 | import { useParams } from '../../../hooks/useParams';
7 | import { queue, queues } from '../../../routes/routes';
8 | import { IQueuesRouteParams } from '../../../routes/routes/queues';
9 | import { IQueueRouteParams } from '../../../routes/routes/queue';
10 |
11 | const Queues = () => {
12 | const { payload } = useSelector(
13 | (state) => state.websocketMainStream
14 | );
15 | // This component is not a child of the Router, so we can not access current route parameters.
16 | // This is a workaround to get the parameters.
17 | const params: Partial | Partial | undefined = useParams(queues.path) || useParams(queue.path) ;
18 | return (
19 | <>
20 | Queues
21 |
24 | >
25 | );
26 | };
27 |
28 | export default Queues;
29 |
--------------------------------------------------------------------------------
/src/client/components/LeftPanel/Queues/QueuesPage.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Link } from 'react-router-dom';
3 | import * as routes from '../../../routes/routes';
4 | import { TWebsocketMainStreamPayload } from '../../../transport/websocket/streams/mainStream';
5 |
6 | interface IProps {
7 | websocketMainStreamPayload: TWebsocketMainStreamPayload;
8 | selectedNamespace: string | undefined;
9 | }
10 |
11 | const QueuesPage: React.FC = ({ websocketMainStreamPayload, selectedNamespace }) => {
12 | const data = [];
13 | for (const ns in websocketMainStreamPayload.queues) {
14 | const totalQueues = Object.keys(websocketMainStreamPayload.queues[ns]).length;
15 | const className = selectedNamespace === ns ? 'active ' : '';
16 | data.push(
17 |
22 | {ns} {totalQueues} queue(s)
23 |
24 | );
25 | }
26 | return {
27 | data.length?
28 |
{data}
:
29 |
No queues here yet.
30 | }
31 |
32 | };
33 |
34 | export default QueuesPage;
35 |
--------------------------------------------------------------------------------
/src/client/components/LeftPanel/Scheduled/Scheduled.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import useSelector from '../../../hooks/useSelector';
3 | import { IStoreState } from '../../../store/state';
4 | import ScheduledPage from './ScheduledPage';
5 | import { IWebsocketMainStreamState } from '../../../store/websocket-main-stream/state';
6 |
7 | const Scheduled = () => {
8 | const { payload } = useSelector(
9 | (state) => state.websocketMainStream
10 | );
11 | return ;
12 | };
13 |
14 | export default Scheduled;
15 |
--------------------------------------------------------------------------------
/src/client/components/LeftPanel/Scheduled/ScheduledPage.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Link } from 'react-router-dom';
3 | import * as routes from '../../../routes/routes';
4 |
5 | interface IProps {
6 | count: number;
7 | }
8 |
9 | const ScheduledPage: React.FC = ({ count }) => {
10 | return (
11 |
12 |
Scheduled
13 | { count ? (
14 |
15 |
19 | Messages {count}
20 |
21 |
22 | ) : (
23 |
No scheduled messages yet.
24 | )}
25 |
26 | );
27 | };
28 |
29 | export default ScheduledPage;
30 |
--------------------------------------------------------------------------------
/src/client/components/PendingMessages/PendingMessages.tsx:
--------------------------------------------------------------------------------
1 | import { RouteComponentProps, withRouter } from 'react-router';
2 | import React, { useCallback } from 'react';
3 | import QueueMessages from '../common/Messages/Messages';
4 | import { purgePendingMessages, deleteQueuePendingMessage, getQueuePendingMessages } from '../../transport/http/api';
5 | import { IQueueRouteParams } from '../../routes/routes/queue';
6 |
7 | const PendingMessages: React.FC> = (props) => {
8 | const { namespace, queueName } = props.match.params;
9 | const FetchQueueMessagesRequestFactory = useCallback((skip: number, take: number) => {
10 | return () => getQueuePendingMessages(namespace, queueName, skip, take);
11 | }, []);
12 | const DeleteQueueMessageRequestFactory = useCallback((messageId: string, sequenceId: number) => {
13 | return () => deleteQueuePendingMessage(namespace, queueName, messageId, sequenceId);
14 | }, []);
15 | const deleteMessagesRequestCallback = useCallback(() => purgePendingMessages(namespace, queueName), []);
16 |
17 | return (
18 | <>
19 |
20 | {queueName}@{namespace} / Pending messages
21 |
22 |
27 | >
28 | );
29 | };
30 |
31 | export default withRouter(PendingMessages);
32 |
--------------------------------------------------------------------------------
/src/client/components/Queue/MessageRates.tsx:
--------------------------------------------------------------------------------
1 | import React, { useCallback, useEffect, useState } from 'react';
2 | import useUrlParams from '../../hooks/useUrlParams';
3 | import { Nav, Spinner } from 'react-bootstrap';
4 | import TimeSeriesChart from '../common/TimeSeriesChart/TimeSeriesChart';
5 | import {
6 | getQueueAcknowledgedTimeSeries,
7 | getQueueDeadLetteredTimeSeries,
8 | getQueuePublishedTimeSeries
9 | } from '../../transport/http/api/time-series';
10 |
11 | enum ENavigationTab {
12 | ACKNOWLEDGED = 'acknowledged',
13 | DEAD_LETTERED = 'dead-lettered',
14 | PUBLISHED = 'published'
15 | }
16 |
17 | const MessageRates: React.FC<{ namespace: string; queueName: string }> = ({ queueName, namespace }) => {
18 | const { getUrlParam, setUrlParam } = useUrlParams();
19 | const [activeTab, setActiveTab] = useState(null);
20 |
21 | const FetchAcknowledgedTimeSeries = useCallback(
22 | (from: number, to: number) => () => getQueueAcknowledgedTimeSeries(namespace, queueName, from, to),
23 | [namespace, queueName]
24 | );
25 |
26 | const FetchDeadLetteredTimeSeries = useCallback(
27 | (from: number, to: number) => () => getQueueDeadLetteredTimeSeries(namespace, queueName, from, to),
28 | [namespace, queueName]
29 | );
30 |
31 | const FetchPublishedTimeSeries = useCallback(
32 | (from: number, to: number) => () => getQueuePublishedTimeSeries(namespace, queueName, from, to),
33 | [namespace, queueName]
34 | );
35 |
36 | useEffect(() => {
37 | setActiveTab(null);
38 | setTimeout(() => {
39 | const tab = getUrlParam('rates') ?? ENavigationTab.ACKNOWLEDGED;
40 | setActiveTab(tab as ENavigationTab);
41 | }, 250);
42 | }, [location.pathname]);
43 |
44 | useEffect(() => {
45 | const tab = getUrlParam('rates') ?? ENavigationTab.ACKNOWLEDGED;
46 | setActiveTab(tab as ENavigationTab);
47 | }, [location.search]);
48 |
49 | if (!activeTab) {
50 | return ;
51 | }
52 |
53 | return (
54 | <>
55 |
56 |
57 | setUrlParam('rates', ENavigationTab.ACKNOWLEDGED)}
59 | active={activeTab === ENavigationTab.ACKNOWLEDGED}
60 | >
61 | Acknowledged
62 |
63 |
64 |
65 | setUrlParam('rates', ENavigationTab.DEAD_LETTERED)}
67 | active={activeTab === ENavigationTab.DEAD_LETTERED}
68 | >
69 | Dead-lettered
70 |
71 |
72 |
73 | setUrlParam('rates', ENavigationTab.PUBLISHED)}
75 | active={activeTab === ENavigationTab.PUBLISHED}
76 | >
77 | Published
78 |
79 |
80 |
81 | {activeTab === ENavigationTab.ACKNOWLEDGED && (
82 |
88 | )}
89 | {activeTab === ENavigationTab.DEAD_LETTERED && (
90 |
96 | )}
97 | {activeTab === ENavigationTab.PUBLISHED && (
98 |
104 | )}
105 | >
106 | );
107 | };
108 |
109 | export default MessageRates;
110 |
--------------------------------------------------------------------------------
/src/client/components/Queue/Queue.tsx:
--------------------------------------------------------------------------------
1 | import React, { useCallback } from 'react';
2 | import { IStoreState } from '../../store/state';
3 | import QueuePage from './QueuePage';
4 | import { useDispatch, useSelector } from 'react-redux';
5 | import { RouteComponentProps, useHistory } from 'react-router';
6 | import { IQueueRouteParams } from '../../routes/routes/queue';
7 | import { TWebsocketMainStreamPayloadQueue } from '../../transport/websocket/streams/mainStream';
8 | import { addNotificationAction } from '../../store/notifications/action';
9 | import { ENotificationType } from '../../store/notifications/state';
10 | import { deleteQueue } from '../../transport/http/api/delete-queue';
11 |
12 | const Queue: React.FC> = ({ match }) => {
13 | const { namespace, queueName } = match.params;
14 | const dispatch = useDispatch();
15 | const history = useHistory();
16 | const selectedQueue = useSelector((state) => {
17 | const queues = state.websocketMainStream.payload.queues;
18 | return queues[namespace] && queues[namespace][queueName];
19 | });
20 | const deleteQueueRequestCallback = useCallback(() => deleteQueue(namespace, queueName), [namespace, queueName]);
21 | const onDeleteQueueSuccessCallback = useCallback(() => {
22 | dispatch(
23 | addNotificationAction(
24 | `Queue [${queueName}@${namespace}] has been successfully deleted.`,
25 | ENotificationType.SUCCESS
26 | )
27 | );
28 | history.push('/');
29 | }, [namespace, queueName]);
30 | return (
31 |
36 | );
37 | };
38 |
39 | export default Queue;
40 |
--------------------------------------------------------------------------------
/src/client/components/Queue/QueuePage.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import * as routes from '../../routes/routes';
3 | import { Link } from 'react-router-dom';
4 | import { Badge, ListGroup } from 'react-bootstrap';
5 | import { EQueueType, TWebsocketMainStreamPayloadQueue } from '../../transport/websocket/streams/mainStream';
6 | import MessageRates from './MessageRates';
7 | import { TQueryRequest } from '../../hooks/useQuery';
8 | import ModalLink from '../common/ModalLink';
9 | import RateLimiting from './RateLimiting/RateLimiting';
10 |
11 | interface IProps {
12 | queue: TWebsocketMainStreamPayloadQueue | undefined;
13 | deleteQueueRequestCallback: TQueryRequest;
14 | deleteQueueRequestSuccessCallback: () => void;
15 | }
16 |
17 | const QueuePage: React.FC = ({ queue, deleteQueueRequestCallback, deleteQueueRequestSuccessCallback }) => {
18 | if (!queue) {
19 | return (
20 |
21 |
Queue not found!
22 |
The queue you are looking for does not exists.
23 |
24 | );
25 | }
26 | const {
27 | ns,
28 | name,
29 | type,
30 | acknowledgedMessagesCount,
31 | deadLetteredMessagesCount,
32 | pendingMessagesCount,
33 | consumersCount
34 | } = queue;
35 |
36 | return (
37 |
38 |
39 | {name}@{ns} ({type === EQueueType.LIFO_QUEUE ? 'LIFO Queue' : (type === EQueueType.FIFO_QUEUE ? 'FIFO Queue' : 'Priority Queue')})
40 |
41 |
42 |
49 | Are you sure you want to delete this message queue?
50 |
51 |
52 | The queue will be deleted from the system alongside with its messages (acknowledged,
53 | pending, dead-lettered).
54 |
55 |
56 | Before confirming, make sure that this queue is not used by a consumer or a producer.
57 |
58 | }
59 | modalTitle={'Queue Deletion'}
60 | />
61 |
62 |
Rates
63 |
64 |
Queue Rate Limiting
65 |
66 |
Messages
67 |
68 |
76 | Pending messages
77 |
78 | {pendingMessagesCount}
79 |
80 |
88 | Acknowledged messages
89 |
90 | {acknowledgedMessagesCount}
91 |
92 |
100 | Dead-lettered messages
101 |
102 | {deadLetteredMessagesCount}
103 |
104 |
105 |
Consumers
106 |
107 |
115 | Consumers
116 |
117 | {consumersCount}
118 |
119 |
120 |
121 | );
122 | };
123 |
124 | export default QueuePage;
125 |
--------------------------------------------------------------------------------
/src/client/components/Queue/RateLimiting/RateLimiting.tsx:
--------------------------------------------------------------------------------
1 | import React, { useCallback, useMemo, useState } from 'react';
2 | import {
3 | clearQueueRateLimit,
4 | getQueueRateLimit,
5 | setQueueRateLimit
6 | } from '../../../transport/http/api/queue-rate-limiting';
7 | import Query from '../../common/Query';
8 | import QueueRateLimitPage from './RateLimitingPage';
9 | import { useDispatch } from 'react-redux';
10 | import { addNotificationAction } from '../../../store/notifications/action';
11 | import { ENotificationType } from '../../../store/notifications/state';
12 |
13 | const RateLimiting: React.FC<{ name: string; ns: string }> = ({ name, ns }) => {
14 | const [reload, setReload] = useState(Date.now());
15 | /**
16 | * Mapping (ns, name) -> (reload)
17 | *
18 | * Explicitly using useMemo instead of useEffect
19 | * When the QueueRateLimiting component is mounted for the first time, useEffect will always get triggered and will cause (reload) to be updated a second time
20 | * On the other hand useMemo is only dependant on (ns, name) changes
21 | */
22 | useMemo(() => {
23 | setReload(Date.now());
24 | }, [ns, name]);
25 | const dispatch = useDispatch();
26 | const request = useMemo(() => () => getQueueRateLimit(ns, name), [reload]);
27 | const clearRateLimitRequestCallback = useCallback(() => clearQueueRateLimit(ns, name), [reload]);
28 | const setRateLimitRequestCallback = useCallback(
29 | (limit: number, interval: number) => () => setQueueRateLimit(ns, name, interval, limit),
30 | [reload]
31 | );
32 | const onSetRateLimitSuccess = useCallback(() => {
33 | dispatch(addNotificationAction(`Queue rate limit has been successfully set.`, ENotificationType.SUCCESS));
34 | setReload(Date.now());
35 | }, [reload]);
36 | const onClearRateLimitSuccess = useCallback(() => {
37 | dispatch(addNotificationAction(`Queue rate limit has been successfully cleared.`, ENotificationType.SUCCESS));
38 | setReload(Date.now());
39 | }, [reload]);
40 | return useMemo(
41 | () => (
42 |
43 | {({ state }) => {
44 | return (
45 |
52 | );
53 | }}
54 |
55 | ),
56 | [reload]
57 | );
58 | };
59 |
60 | export default RateLimiting;
61 |
--------------------------------------------------------------------------------
/src/client/components/Queue/RateLimiting/RateLimitingPage.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { TGetQueueRateLimitResponse } from '../../../transport/http/api/queue-rate-limiting';
3 | import { TQueryRequest } from '../../../hooks/useQuery';
4 | import SetRateLimit from './SetRateLimit/SetRateLimit';
5 | import ModalLink from '../../common/ModalLink';
6 |
7 | const RateLimitingPage: React.FC<{
8 | rateLimit: TGetQueueRateLimitResponse['data'];
9 | clearRateLimitRequestCallback: TQueryRequest;
10 | clearRateLimitRequestSuccessCallback: () => void;
11 | setRateLimitRequestCallback: (limit: number, interval: number) => TQueryRequest;
12 | setRateLimitRequestSuccessCallback: () => void;
13 | }> = ({
14 | rateLimit,
15 | clearRateLimitRequestCallback,
16 | clearRateLimitRequestSuccessCallback,
17 | setRateLimitRequestCallback,
18 | setRateLimitRequestSuccessCallback
19 | }) => {
20 | const params = () => {
21 | if (rateLimit) {
22 | return (
23 |
24 | Limit: {rateLimit.limit}, Interval: {rateLimit.interval}
25 |
26 | );
27 | }
28 | return Unlimited ;
29 | };
30 | return (
31 | <>
32 | {params()}
33 |
34 |
38 | {rateLimit && (
39 | Are you sure you want to clear the rate limit?
}
45 | modalTitle={'Clear Queue Rate Limit'}
46 | />
47 | )}
48 |
49 | >
50 | );
51 | };
52 |
53 | export default RateLimitingPage;
54 |
--------------------------------------------------------------------------------
/src/client/components/Queue/RateLimiting/SetRateLimit/FormModal.tsx:
--------------------------------------------------------------------------------
1 | import React, { useCallback, useEffect, useRef } from 'react';
2 | import { ISetQueueRateLimitProps } from './SetRateLimit';
3 | import useQuery, { EQueryStatus } from '../../../../hooks/useQuery';
4 | import Modal from '../../../common/Modal';
5 | import { Field, Form, Formik, FormikProps } from 'formik';
6 | import { FormControl, FormGroup, FormLabel, Spinner } from 'react-bootstrap';
7 | import { FieldProps } from 'formik/dist/Field';
8 |
9 | interface IFormFields {
10 | interval: string;
11 | limit: string;
12 | }
13 |
14 | interface IHandlerProps extends ISetQueueRateLimitProps {
15 | closeHandlerCallback: () => void;
16 | onSubmitCallback: (values: any) => void;
17 | }
18 |
19 | const FormModal: React.FC = ({ RequestFactory, requestSuccessCallback, closeHandlerCallback }) => {
20 | const formRef = useRef>(null);
21 | const query = useQuery();
22 |
23 | useEffect(() => {
24 | if (query.state.status === EQueryStatus.SUCCESS) {
25 | requestSuccessCallback();
26 | closeHandlerCallback();
27 | } else if (query.state.status === EQueryStatus.ERROR) {
28 | closeHandlerCallback();
29 | }
30 | }, [query.state.status]);
31 |
32 | const onSubmit = useCallback(() => {
33 | const form = formRef.current;
34 | if (form) {
35 | // Fixing submitForm bug (always get resolved when the form is invalid)
36 | // See formik/issues/1580
37 | form.submitForm()
38 | .then(form.validateForm)
39 | .then((errors) => {
40 | if (!Object.keys(errors).length) {
41 | query.sendQuery(RequestFactory(Number(form.values.limit), Number(form.values.interval)));
42 | }
43 | })
44 | .catch((e) => {
45 | console.log(e);
46 | });
47 | }
48 | }, []);
49 |
50 | const validateInterval = (interval: string): string | void => {
51 | if (!interval) {
52 | return 'Required';
53 | }
54 | const intervalValue = Number(interval);
55 | if (isNaN(intervalValue) || intervalValue < 1000) {
56 | return 'Interval must be greater than or equal to 1000';
57 | }
58 | };
59 |
60 | const validateLimit = (limit: string): string | void => {
61 | if (!limit) {
62 | return 'Required';
63 | }
64 | const limitValue = Number(limit);
65 | if (isNaN(limitValue) || limitValue < 1) {
66 | return 'Limit must be greater than or equal to 1';
67 | }
68 | };
69 |
70 | return (
71 |
72 | {query.state.status === EQueryStatus.LOADING ? (
73 |
74 | ) : (
75 | void 0}>
76 | {({ errors, touched }) => (
77 |
115 | )}
116 |
117 | )}
118 |
119 | );
120 | };
121 |
122 | export default FormModal;
123 |
--------------------------------------------------------------------------------
/src/client/components/Queue/RateLimiting/SetRateLimit/SetRateLimit.tsx:
--------------------------------------------------------------------------------
1 | import React, { useCallback, useState } from 'react';
2 | import FormModal from './FormModal';
3 | import { TQueryRequest } from '../../../../hooks/useQuery';
4 | import { Button } from 'react-bootstrap';
5 |
6 | export interface ISetQueueRateLimitProps {
7 | RequestFactory: (limit: number, interval: number) => TQueryRequest;
8 | requestSuccessCallback: () => void;
9 | }
10 |
11 | const SetRateLimit: React.FC = (props) => {
12 | const [openHandler, setOpenHandler] = useState(false);
13 | const closeHandler = useCallback(() => setOpenHandler(false), []);
14 | return (
15 | <>
16 | setOpenHandler(true)} className={'me-3'}>
17 | Set queue rate limit
18 |
19 | {openHandler && (
20 |
21 | )}
22 | >
23 | );
24 | };
25 |
26 | export default SetRateLimit;
27 |
--------------------------------------------------------------------------------
/src/client/components/QueueConsumers/QueueConsumers.tsx:
--------------------------------------------------------------------------------
1 | import React, { useCallback } from 'react';
2 | import { IQueueRouteParams } from '../../routes/routes/queue';
3 | import { RouteComponentProps } from 'react-router';
4 | import { consumer } from '../../routes/routes';
5 | import useWebsocketSubscription from '../../hooks/useWebsocketSubscription';
6 | import { TQueueOnlineConsumersStreamPayload } from '../../transport/websocket/streams/queueOnlineConsumersStream';
7 | import QueueConsumersPage from './QueueConsumersPage';
8 |
9 | const QueueConsumers: React.FC> = ({ match }) => {
10 | const { namespace, queueName } = match.params;
11 | const getConsumerLink = useCallback((consumerId: string) => {
12 | return consumer.getLink({ queueName, namespace, consumerId });
13 | }, []);
14 | const { isLoading: isQueueConsumersStreamLoading, data: queueConsumersStreamPayload } = useWebsocketSubscription>(`streamQueueConsumers:${namespace}:${queueName}`, 0);
15 | const { isLoading: isQueueOnlineConsumersStreamLoading, data: queueOnlineConsumersStreamPayload } = useWebsocketSubscription<
16 | TQueueOnlineConsumersStreamPayload
17 | >(`streamQueueOnlineConsumers:${namespace}:${queueName}`, 0);
18 | return (
19 | <>
20 |
21 | {queueName}@{namespace} / Consumers
22 |
23 |
30 | >
31 | );
32 | };
33 |
34 | export default QueueConsumers;
35 |
--------------------------------------------------------------------------------
/src/client/components/QueueConsumers/QueueConsumersPage.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Link } from 'react-router-dom';
3 | import { Spinner, Table } from 'react-bootstrap';
4 | import { TWebsocketQueueConsumersPayloadConsumerInfo } from '../../transport/websocket/streams/queueConsumersStream';
5 | import { TQueueOnlineConsumersStreamPayload } from '../../transport/websocket/streams/queueOnlineConsumersStream';
6 |
7 | // interface IQueueConsumersListingPageProps extends Omit
8 | interface IQueueConsumersPageProps {
9 | queueConsumers: Record;
10 | onlineConsumers: TQueueOnlineConsumersStreamPayload;
11 | isQueueConsumersStreamLoading: boolean;
12 | isQueueOnlineConsumersStreamLoading: boolean;
13 | getConsumerLink: (id: string) => string;
14 | }
15 |
16 | const QueueConsumersPage: React.FC = ({
17 | isQueueConsumersStreamLoading,
18 | isQueueOnlineConsumersStreamLoading,
19 | queueConsumers,
20 | onlineConsumers,
21 | getConsumerLink,
22 | }) => {
23 | if (isQueueConsumersStreamLoading || isQueueOnlineConsumersStreamLoading) {
24 | return
25 | }
26 | if (!Object.keys(queueConsumers).length) {
27 | return No consumers yet.
;
28 | }
29 | return (
30 |
31 |
32 |
33 |
34 | ID
35 |
36 | PID /
37 |
38 | Hostname
39 |
40 |
41 | IP
42 |
43 | Address
44 |
45 |
46 | Started
47 |
48 | at
49 |
50 | Status
51 |
52 |
53 | {
54 | Object.keys(queueConsumers).map((id) => {
55 | const item: TWebsocketQueueConsumersPayloadConsumerInfo = JSON.parse(queueConsumers[id]);
56 | const isOnline = onlineConsumers.ids.includes(id);
57 | const { pid, hostname, ipAddress, createdAt } = item;
58 | return (
59 |
60 |
61 | {isOnline ? (
62 |
63 | {id}
64 |
65 | ) : (
66 | id
67 | )}
68 |
69 |
70 | {pid} /
71 |
72 | {hostname}
73 |
74 | {ipAddress.length ? ipAddress.map((ip, index) => {ip}
) : 'NA'}
75 | {new Date(createdAt).toUTCString()}
76 | {isOnline ? 'online' : 'offline'}
77 |
78 | );
79 | })
80 | }
81 |
82 |
83 | );
84 | };
85 |
86 | export default QueueConsumersPage;
87 |
--------------------------------------------------------------------------------
/src/client/components/Queues/DeleteNamespace/DeleteNamespace.tsx:
--------------------------------------------------------------------------------
1 | import ModalLink from '../../common/ModalLink';
2 | import React, { useCallback } from 'react';
3 | import { useDispatch } from 'react-redux';
4 | import { useHistory } from 'react-router';
5 | import { deleteNamespace } from '../../../transport/http/api';
6 | import { addNotificationAction } from '../../../store/notifications/action';
7 | import { ENotificationType } from '../../../store/notifications/state';
8 |
9 | interface IDeleteNamespaceProps {
10 | namespace: string;
11 | }
12 |
13 | const DeleteNamespace: React.FC = ({ namespace }) => {
14 | const dispatch = useDispatch();
15 | const history = useHistory();
16 | const deleteNamespaceRequestCallback = useCallback(() => deleteNamespace(namespace), [namespace]);
17 | const deleteNamespaceRequestSuccessCallback = useCallback(() => {
18 | dispatch(addNotificationAction(`Namespace (${namespace}) has been successfully deleted.`, ENotificationType.SUCCESS));
19 | history.push('/');
20 | }, [namespace]);
21 | return
28 | Are you sure you want to delete this namespace?
29 |
30 |
31 | The namespace will be deleted from the system alongside with its queues.
32 |
33 |
34 | Before confirming, make sure that this namespace is not used by a message handler.
35 |
36 | }
37 | modalTitle={`Deleting namespace [${namespace}]`}
38 | />
39 | }
40 |
41 | export default DeleteNamespace;
--------------------------------------------------------------------------------
/src/client/components/Queues/DeleteQueue/DeleteQueue.tsx:
--------------------------------------------------------------------------------
1 | import React, { useCallback, useEffect } from 'react';
2 | import { Button } from 'react-bootstrap';
3 | import { addNotificationAction } from '../../../store/notifications/action';
4 | import { ENotificationType } from '../../../store/notifications/state';
5 | import { useDispatch } from 'react-redux';
6 | import useQuery, { EQueryStatus } from '../../../hooks/useQuery';
7 | import { useHistory } from 'react-router';
8 | import { IMessageQueue } from '../../../transport/http/api/common/IMessage';
9 | import { deleteQueue } from '../../../transport/http/api/delete-queue';
10 | import { queues } from '../../../routes/routes';
11 |
12 | export interface IDeleteQueueProps {
13 | queue: IMessageQueue;
14 | }
15 |
16 | const DeleteQueue: React.FC = ({ queue }) => {
17 | const dispatch = useDispatch();
18 | const query = useQuery();
19 | const history = useHistory();
20 | useEffect(() => {
21 | if (query.state.status === EQueryStatus.SUCCESS) {
22 | dispatch(addNotificationAction(`Queue ${queue.name}@${queue.ns} has been successfully deleted.`, ENotificationType.SUCCESS));
23 | history.push(queues.getLink({namespace: queue.ns}));
24 | }
25 | }, [query.state.status]);
26 | const deleteQueueRequest = useCallback(() => {
27 | query.sendQuery(() => deleteQueue(queue.ns, queue.name));
28 | }, [`${queue.name}@${queue.ns}`]);
29 | return (
30 | <>
31 |
32 | Delete queue
33 |
34 | >
35 | );
36 | };
37 |
38 | export default DeleteQueue;
39 |
--------------------------------------------------------------------------------
/src/client/components/Queues/Queues.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from 'react';
2 | import QueuesPage from './QueuesPage';
3 | import useSelector from '../../hooks/useSelector';
4 | import { IStoreState } from '../../store/state';
5 | import { IWebsocketMainStreamState } from '../../store/websocket-main-stream/state';
6 | import { IMessageQueue } from '../../transport/http/api/common/IMessage';
7 | import { RouteComponentProps } from 'react-router';
8 | import { IQueuesRouteParams } from '../../routes/routes/queues';
9 |
10 | const Queues: React.FC> = ({ match }) => {
11 | const { namespace } = match.params;
12 | const [queues, setQueues] = useState([]);
13 | const { payload } = useSelector(
14 | (state) => state.websocketMainStream
15 | );
16 | useEffect(() => {
17 | const list: IMessageQueue[] = [];
18 | for(const ns in payload.queues)
19 | if (ns === namespace) for (const name in payload.queues[ns]) list.push({ name, ns });
20 | setQueues(list);
21 | }, [payload, namespace]);
22 | return <>
23 | Queues under namespace [{namespace}]
24 |
25 | >
26 | }
27 |
28 | export default Queues;
--------------------------------------------------------------------------------
/src/client/components/Queues/QueuesPage.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { IMessageQueue } from '../../transport/http/api/common/IMessage';
3 | import { Badge, ListGroup } from 'react-bootstrap';
4 | import DeleteQueue from './DeleteQueue/DeleteQueue';
5 | import { Link } from 'react-router-dom';
6 | import { queue } from '../../routes/routes';
7 | import DeleteNamespace from './DeleteNamespace/DeleteNamespace';
8 |
9 | interface IQueuesPageProps {
10 | namespace: string;
11 | queues: IMessageQueue[];
12 | }
13 |
14 |
15 | const QueuesPage: React.FC = ({ namespace, queues }) => {
16 | return (
17 | <>
18 |
19 | {
20 | queues.length? (
21 |
22 | {
23 | queues.map((q, index) => {
24 | return
29 |
30 | {q.name}@{q.ns}
31 |
32 |
33 |
34 |
35 |
36 |
37 | })
38 | }
39 |
40 | ) : (
41 | No queues found.
42 | )
43 | }
44 | >
45 | );
46 | }
47 |
48 |
49 | export default QueuesPage;
--------------------------------------------------------------------------------
/src/client/components/ScheduledMessages/ScheduledMessages.tsx:
--------------------------------------------------------------------------------
1 | import { withRouter } from 'react-router';
2 | import React, { useCallback } from 'react';
3 | import QueueMessages from '../common/Messages/Messages';
4 | import { purgeScheduledMessages } from '../../transport/http/api';
5 | import { getScheduledMessages } from '../../transport/http/api';
6 | import { deleteScheduledMessage } from '../../transport/http/api';
7 |
8 | const ScheduledMessages: React.FC = () => {
9 | const FetchQueueMessagesRequestFactory = useCallback((skip: number, take: number) => {
10 | return () => getScheduledMessages(skip, take);
11 | }, []);
12 | const DeleteQueueMessageRequestFactory = useCallback((messageId: string, sequenceId: number) => {
13 | return () => deleteScheduledMessage(messageId, sequenceId);
14 | }, []);
15 |
16 | return (
17 | <>
18 | Scheduled Messages
19 |
24 | >
25 | );
26 | };
27 |
28 | export default withRouter(ScheduledMessages);
29 |
--------------------------------------------------------------------------------
/src/client/components/common/Breadcrumbs.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import * as routes from '../../routes/routes';
3 | import { RouteComponentProps, RouteProps } from 'react-router';
4 | import { Link } from 'react-router-dom';
5 | import { IParameterizedRouteProps } from '../../routes/common';
6 |
7 | const relative = (child: string, parent: string) => {
8 | if (parent === '/') return true;
9 | child = child.replace(/^\/+|\/+$/g, '');
10 | parent = parent.replace(/^\/+|\/+$/g, '');
11 | if (child === parent) return true;
12 | return parent
13 | .replace(/^\/+|\/+$/g, '')
14 | .split('/')
15 | .filter((i) => i.length)
16 | .every((token, idx) => child.split('/')[idx] === token);
17 | };
18 |
19 | const Breadcrumbs: React.FC = ({ match }) => {
20 | const crumbs = Object.values>(routes)
21 | .filter(({ path }: RouteProps) => typeof path === 'string' && relative(match.path, path))
22 | .map(({ path, ...rest }) => ({
23 | path: Object.keys(match.params).length
24 | ? Object.keys(match.params).reduce(
25 | (path: string, param) => path.replace(`:${param}`, (match.params as any)[param]),
26 | path as string
27 | )
28 | : path,
29 | ...rest
30 | }))
31 | .sort((a, b) => {
32 | return a.path.length - b.path.length;
33 | });
34 | if (crumbs.length <= 1) {
35 | return null;
36 | }
37 | return (
38 |
39 | {crumbs.map((item, index) => (
40 |
41 | /
42 |
43 | {item.caption}
44 |
45 |
46 | ))}
47 |
48 | );
49 | };
50 |
51 | export default Breadcrumbs;
52 |
--------------------------------------------------------------------------------
/src/client/components/common/ErrorBoundary.tsx:
--------------------------------------------------------------------------------
1 | import React, { ErrorInfo, PropsWithChildren } from 'react';
2 |
3 | export interface ErrorBoundaryStateInterface {
4 | error: Error | null;
5 | errorInfo: ErrorInfo | null;
6 | }
7 |
8 | export default class ErrorBoundary extends React.Component, ErrorBoundaryStateInterface> {
9 | state = { error: null, errorInfo: null };
10 |
11 | componentDidCatch(error: Error, errorInfo: ErrorInfo) {
12 | this.setState({
13 | error,
14 | errorInfo
15 | });
16 | // Report error here
17 | }
18 |
19 | render() {
20 | const { children } = this.props;
21 | if (this.state.errorInfo !== null) {
22 | const { componentStack } = (this.state.errorInfo as unknown) as ErrorInfo;
23 | const errorStr = ((this.state.error as unknown) as Error).toString();
24 | return (
25 | <>
26 | Something went wrong.
27 |
28 | Error
29 | {errorStr}
30 |
31 | ErrorInfo
32 | {componentStack}
33 | >
34 | );
35 | }
36 | return children;
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/src/client/components/common/Errors/AnErrorOccurred.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import exclamation from './exclamation-circle-fill.svg';
3 | import { Container } from 'react-bootstrap';
4 |
5 | const AnErrorOccurred: React.FC<{ message?: string }> = ({ message }) => {
6 | return (
7 |
8 | An HTTP error occurred
9 | {message && {message}
}
10 |
11 |
12 |
13 |
14 | );
15 | };
16 |
17 | export default AnErrorOccurred;
18 |
--------------------------------------------------------------------------------
/src/client/components/common/Errors/PageNotFound.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const PageNotFound: React.FC = () => {
4 | return (
5 |
6 |
Page Not Found
7 |
The page your are looking for does not exist.
8 |
9 | );
10 | };
11 |
12 | export default PageNotFound;
13 |
--------------------------------------------------------------------------------
/src/client/components/common/Errors/exclamation-circle-fill.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/src/client/components/common/Footer/Footer.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import FooterPage from './FooterPage';
3 | import pkg from '../../../../../package.json';
4 |
5 | const Footer = () => {
6 | return ;
7 | };
8 |
9 | export default Footer;
10 |
--------------------------------------------------------------------------------
/src/client/components/common/Footer/FooterPage.tsx:
--------------------------------------------------------------------------------
1 | import './style.css';
2 | import React from 'react';
3 |
4 | interface IProps {
5 | version: string;
6 | license: string;
7 | }
8 |
9 | const FooterPage: React.FC = ({ version, license }) => (
10 |
11 | {`RedisSMQ Monitor v${version}`}
12 |
13 | ©{' '}
14 |
15 | Weyoss
16 | {' '}
17 | 2017 - {new Date().getFullYear()}. Licensed under{' '}
18 |
19 | {license}
20 |
21 | .
22 |
23 | );
24 |
25 | export default FooterPage;
26 |
--------------------------------------------------------------------------------
/src/client/components/common/Footer/style.css:
--------------------------------------------------------------------------------
1 | footer {
2 | display: block;
3 | font-size: 0.8rem;
4 | color: lightslategray;
5 | margin-top: 1rem;
6 | }
--------------------------------------------------------------------------------
/src/client/components/common/Messages/MessageOptions/Delete.tsx:
--------------------------------------------------------------------------------
1 | import { TQueryRequest } from '../../../../hooks/useQuery';
2 | import React, { useCallback } from 'react';
3 | import { useDispatch } from 'react-redux';
4 | import { addNotificationAction } from '../../../../store/notifications/action';
5 | import { ENotificationType } from '../../../../store/notifications/state';
6 | import ModalLink from '../../ModalLink';
7 |
8 | interface IProps {
9 | messageId: string;
10 | successCallback: () => void;
11 | RequestFactory: TQueryRequest;
12 | }
13 |
14 | const Delete: React.FC = ({ messageId, RequestFactory, successCallback }) => {
15 | const dispatch = useDispatch();
16 | const onSuccess = useCallback(() => {
17 | dispatch(
18 | addNotificationAction(`Message ID ${messageId} has been successfully deleted.`, ENotificationType.SUCCESS)
19 | );
20 | successCallback();
21 | }, [messageId]);
22 | return (
23 | Are you sure you want to delete this message?}
29 | modalTitle={`Message Deletion`}
30 | />
31 | );
32 | };
33 |
34 | export default Delete;
35 |
--------------------------------------------------------------------------------
/src/client/components/common/Messages/MessageOptions/MessageOptions.tsx:
--------------------------------------------------------------------------------
1 | import { DropdownButton } from 'react-bootstrap';
2 | import Delete from './Delete';
3 | import React from 'react';
4 | import { TQueryRequest } from '../../../../hooks/useQuery';
5 | import Requeue from './Requeue';
6 |
7 | export interface IMessageOptionsSharedProps {
8 | DeleteMessageRequestFactory: (messageId: string, sequenceId: number) => TQueryRequest;
9 | deleteMessageSuccessCallback: () => void;
10 | RequeueMessageRequestFactory?: (messageId: string, sequenceId: number) => TQueryRequest;
11 | requeueMessageSuccessCallback?: () => void;
12 | }
13 |
14 | interface IMessageOptionsProps extends IMessageOptionsSharedProps {
15 | messageId: string;
16 | sequenceId: number;
17 | }
18 |
19 | const MessageOptions: React.FC = ({
20 | DeleteMessageRequestFactory,
21 | deleteMessageSuccessCallback,
22 | RequeueMessageRequestFactory,
23 | requeueMessageSuccessCallback,
24 | messageId,
25 | sequenceId
26 | }) => {
27 | const options: JSX.Element[] = [];
28 | if (RequeueMessageRequestFactory && requeueMessageSuccessCallback) {
29 | options.push(
30 |
36 | );
37 | }
38 | options.push(
39 |
45 | );
46 | return (
47 |
48 | {options}
49 |
50 | );
51 | };
52 |
53 | export default MessageOptions;
54 |
--------------------------------------------------------------------------------
/src/client/components/common/Messages/MessageOptions/Requeue.tsx:
--------------------------------------------------------------------------------
1 | import React, { useCallback } from 'react';
2 | import { useDispatch } from 'react-redux';
3 | import { addNotificationAction } from '../../../../store/notifications/action';
4 | import { ENotificationType } from '../../../../store/notifications/state';
5 | import ModalLink from '../../ModalLink';
6 | import { TQueryRequest } from '../../../../hooks/useQuery';
7 |
8 | interface IProps {
9 | messageId: string;
10 | RequestFactory: TQueryRequest;
11 | successCallback: () => void;
12 | }
13 |
14 | const Requeue: React.FC = ({ messageId, RequestFactory, successCallback }) => {
15 | const dispatch = useDispatch();
16 | const onSuccess = useCallback(() => {
17 | dispatch(
18 | addNotificationAction(`Message ID ${messageId} has been successfully re-queued.`, ENotificationType.SUCCESS)
19 | );
20 | successCallback();
21 | }, [messageId]);
22 | return (
23 | Are you sure you want to re-queue this message?}
29 | modalTitle={`Message Re-queuing`}
30 | />
31 | );
32 | };
33 |
34 | export default Requeue;
35 |
--------------------------------------------------------------------------------
/src/client/components/common/Messages/MessageOptions/RequeueWithPriority/FormBody.tsx:
--------------------------------------------------------------------------------
1 | import React, { ChangeEvent } from 'react';
2 | import { Alert, Form, Spinner } from 'react-bootstrap';
3 | import { EQueryStatus, IQueryState } from '../../../../../hooks/useQuery';
4 | import { EMessagePriority } from '../../../../../transport/http/api/common/IMessage';
5 |
6 | interface IProps {
7 | messageId: string;
8 | messagePriority: EMessagePriority;
9 | onSelectPriority: (event: ChangeEvent) => void;
10 | queryState: IQueryState;
11 | }
12 |
13 | const FormBody: React.FC = ({ messageId, messagePriority, onSelectPriority, queryState }) => {
14 | return (
15 | <>
16 | {queryState.status === EQueryStatus.LOADING ? (
17 |
18 | ) : (
19 | <>
20 | {queryState.errorMessage && {queryState.errorMessage} }
21 |
22 |
23 | Message ID
24 |
25 |
26 |
27 |
28 | Select message priority
29 |
35 | Lowest
36 | Very Low
37 | Low
38 | Normal
39 | Above Normal
40 | High
41 | Very High
42 | Highest
43 |
44 |
45 | >
46 | )}
47 | >
48 | );
49 | };
50 |
51 | export default FormBody;
52 |
--------------------------------------------------------------------------------
/src/client/components/common/Messages/MessageOptions/RequeueWithPriority/FormHandler.tsx:
--------------------------------------------------------------------------------
1 | import React, { ChangeEvent, useCallback, useEffect, useState } from 'react';
2 | import { useDispatch } from 'react-redux';
3 | import useQuery, { EQueryStatus } from '../../../../../hooks/useQuery';
4 | import { addNotificationAction } from '../../../../../store/notifications/action';
5 | import { ENotificationType } from '../../../../../store/notifications/state';
6 | import Modal from '../../../Modal';
7 | import FormBody from './FormBody';
8 | import { IRequeueMessageWithPriorityProps } from './RequeueWithPriority';
9 | import { EMessagePriority } from '../../../../../transport/http/api/common/IMessage';
10 |
11 | interface IHandlerProps extends IRequeueMessageWithPriorityProps {
12 | closeHandlerCallback: () => void;
13 | onSubmitCallback: () => void;
14 | }
15 |
16 | const FormHandler: React.FC = ({
17 | messageId,
18 | sequenceId,
19 | RequeueMessageRequestFactory,
20 | requeueMessageSuccessCallback,
21 | closeHandlerCallback
22 | }) => {
23 | const [priority, setPriority] = useState(EMessagePriority.NORMAL);
24 | const dispatch = useDispatch();
25 | const query = useQuery();
26 | useEffect(() => {
27 | if (query.state.status === EQueryStatus.SUCCESS || query.state.status === EQueryStatus.ERROR) {
28 | if (query.state.status === EQueryStatus.SUCCESS) {
29 | dispatch(
30 | addNotificationAction(
31 | `Message ID ${messageId} successfully re-queued with priority.`,
32 | ENotificationType.SUCCESS
33 | )
34 | );
35 | requeueMessageSuccessCallback();
36 | }
37 | closeHandlerCallback();
38 | }
39 | }, [query.state]);
40 |
41 | const onSelectPriority = useCallback((event: ChangeEvent) => {
42 | setPriority(Number(event.target.value));
43 | }, []);
44 |
45 | const onSubmit = useCallback(() => {
46 | query.sendQuery(RequeueMessageRequestFactory(messageId, sequenceId, priority));
47 | }, []);
48 |
49 | return (
50 |
56 |
62 |
63 | );
64 | };
65 |
66 | export default FormHandler;
67 |
--------------------------------------------------------------------------------
/src/client/components/common/Messages/MessageOptions/RequeueWithPriority/RequeueWithPriority.tsx:
--------------------------------------------------------------------------------
1 | import { TQueryRequest } from '../../../../../hooks/useQuery';
2 | import React, { useCallback, useState } from 'react';
3 | import FormHandler from './FormHandler';
4 | import { EMessagePriority } from '../../../../../transport/http/api/common/IMessage';
5 |
6 | export interface IRequeueMessageWithPriorityProps {
7 | messageId: string;
8 | sequenceId: number;
9 | RequeueMessageRequestFactory: (
10 | messageId: string,
11 | sequenceId: number,
12 | priority: EMessagePriority
13 | ) => TQueryRequest;
14 | requeueMessageSuccessCallback: () => void;
15 | }
16 |
17 | const RequeueWithPriority: React.FC = (props) => {
18 | const [openHandler, setOpenHandler] = useState(false);
19 | const closeHandler = useCallback(() => setOpenHandler(false), []);
20 | return (
21 | <>
22 | setOpenHandler(true)}
26 | >
27 | Re-queue message with priority
28 |
29 | {openHandler && (
30 |
31 | )}
32 | >
33 | );
34 | };
35 |
36 | export default RequeueWithPriority;
37 |
--------------------------------------------------------------------------------
/src/client/components/common/Messages/Messages.tsx:
--------------------------------------------------------------------------------
1 | import React, { useCallback, useEffect, useMemo, useState } from 'react';
2 | import { RouteComponentProps, useHistory, withRouter } from 'react-router';
3 | import queryString from 'query-string';
4 | import MessagesPage from './MessagesPage';
5 | import { TQueryRequest } from '../../../hooks/useQuery';
6 | import Query from '../Query';
7 | import { TPaginatedHTTPResponse } from '../../../transport/http/api';
8 | import { IQueueRouteParams } from '../../../routes/routes/queue';
9 | import { IMessage } from 'client/transport/http/api/common/IMessage';
10 |
11 | interface IProps extends RouteComponentProps {
12 | FetchQueueMessagesRequestFactory: (
13 | skip: number,
14 | take: number
15 | ) => TQueryRequest>;
16 | DeleteQueueMessageRequestFactory(messageId: string, sequenceId: number): TQueryRequest;
17 | RequeueMessageRequestFactory?: (messageId: string, sequenceId: number) => TQueryRequest;
18 | deleteMessagesRequestCallback: TQueryRequest;
19 | }
20 |
21 | const getPaginationParams = (path: string, take = 10) => {
22 | const { page } = queryString.parse(path);
23 | const pageNumber = typeof page === 'string' && Number(page) > 1 ? Number(page) : 1;
24 | const skip = (pageNumber - 1) * take;
25 | return {
26 | skip,
27 | take,
28 | page: pageNumber
29 | };
30 | };
31 |
32 | const QueueMessages: React.FC = ({
33 | location,
34 | deleteMessagesRequestCallback,
35 | FetchQueueMessagesRequestFactory,
36 | DeleteQueueMessageRequestFactory,
37 | RequeueMessageRequestFactory
38 | }) => {
39 | const [paginationParams, setPaginationParams] = useState<{ skip: number; take: number; page: number }>(
40 | getPaginationParams(location.search)
41 | );
42 |
43 | // Request fn
44 | const request = useMemo(() => FetchQueueMessagesRequestFactory(paginationParams.skip, paginationParams.take), [
45 | paginationParams
46 | ]);
47 |
48 | // Handling location update
49 | useEffect(() => {
50 | const params = getPaginationParams(location.search);
51 | if (params.page !== paginationParams.page) setPaginationParams(params);
52 | }, [location.search]);
53 |
54 | // Pagination navigation
55 | const history = useHistory();
56 | const onSelectPageCallback = useCallback((page: number) => {
57 | const params = queryString.parse(location.search);
58 | params.page = String(page);
59 | history.push({
60 | search: queryString.stringify(params)
61 | });
62 | }, []);
63 |
64 | const onMessageOperationSuccessCallback = useCallback(() => {
65 | // force fetching messages with new sequence IDs
66 | setPaginationParams(() => ({
67 | ...paginationParams
68 | }));
69 | }, []);
70 |
71 | return (
72 |
73 | {({ state }) => {
74 | return (
75 |
86 | );
87 | }}
88 |
89 | );
90 | };
91 |
92 | export default withRouter(QueueMessages);
93 |
--------------------------------------------------------------------------------
/src/client/components/common/Messages/MessagesPage.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import Paginator from '../Paginator';
3 | import MessageOptions, { IMessageOptionsSharedProps } from './MessageOptions/MessageOptions';
4 | import { Table } from 'react-bootstrap';
5 | import { TQueryRequest } from '../../../hooks/useQuery';
6 | import { IMessage } from '../../../transport/http/api/common/IMessage';
7 | import ModalLink from '../ModalLink';
8 |
9 | interface IProps extends IMessageOptionsSharedProps {
10 | messages: { total: number; items: { message: IMessage; sequenceId: number }[] };
11 | pageParams: {
12 | skip: number;
13 | take: number;
14 | page: number;
15 | };
16 | onSelectPageCallback: (page: number) => void;
17 | deleteMessagesRequestCallback: TQueryRequest;
18 | deleteMessagesRequestSuccessCallback: () => void;
19 | }
20 |
21 | const MessagesPage: React.FC = (props) => {
22 | const {
23 | messages,
24 | onSelectPageCallback,
25 | pageParams,
26 | deleteMessagesRequestCallback,
27 | deleteMessagesRequestSuccessCallback,
28 | ...rest
29 | } = props;
30 | const [activeMessageId, setActiveMessageId] = useState(null);
31 | if (!messages.total) {
32 | return Empty message list
;
33 | }
34 | return (
35 | <>
36 |
37 | Are you sure you want to delete all messages?}
43 | modalTitle={'Deleting all messages'}
44 | />
45 |
46 |
47 |
48 |
49 | ID
50 | Message
51 | Options
52 |
53 |
54 |
55 | {messages.items.map(({ message, sequenceId }) => {
56 | const mid = `${message.messageState.uuid}${sequenceId ? `-${sequenceId}` : ''}`;
57 | return (
58 |
59 | {message.messageState.uuid}
60 |
61 | {activeMessageId === mid ? (
62 | <>
63 |
64 | {
{JSON.stringify(message, undefined, 2)} }
65 |
66 | setActiveMessageId(null)}
70 | >
71 | ↑
72 |
73 |
74 |
75 | >
76 | ) : (
77 | <>
78 |
79 | {JSON.stringify(message.body)}{' '}
80 | setActiveMessageId(mid)}
83 | >
84 | ↓
85 |
86 |
87 | >
88 | )}
89 |
90 |
91 |
96 |
97 |
98 | );
99 | })}
100 |
101 |
102 |
108 | >
109 | );
110 | };
111 |
112 | export default MessagesPage;
113 |
--------------------------------------------------------------------------------
/src/client/components/common/Modal.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Button, Modal as BaseModal } from 'react-bootstrap';
3 |
4 | export interface IModalState {
5 | title: string;
6 | onSubmit: () => void;
7 | onCancel: () => void;
8 | cancelCaption?: string;
9 | submitCaption?: string;
10 | children: JSX.Element;
11 | }
12 |
13 | const Modal: React.FC = (props) => {
14 | const { title, onSubmit, onCancel, children, submitCaption, cancelCaption } = props;
15 | return (
16 |
17 |
18 | {title}
19 |
20 | {children}
21 |
22 |
23 | {cancelCaption ?? 'Cancel'}
24 |
25 |
26 | {submitCaption ?? 'Submit'}
27 |
28 |
29 |
30 | );
31 | };
32 |
33 | export default Modal;
34 |
--------------------------------------------------------------------------------
/src/client/components/common/ModalLink.tsx:
--------------------------------------------------------------------------------
1 | import { Button } from 'react-bootstrap';
2 | import React, { useCallback, useEffect, useState } from 'react';
3 | import useQuery, { EQueryStatus, TQueryRequest } from '../../hooks/useQuery';
4 | import Modal from './Modal';
5 | import { ButtonProps } from 'react-bootstrap/Button';
6 |
7 | interface IProps {
8 | onSuccess: () => void;
9 | request: TQueryRequest;
10 | btnCaption: string;
11 | modalBody: JSX.Element;
12 | modalTitle: string;
13 | variant?: ButtonProps['variant'];
14 | className?: string;
15 | }
16 |
17 | const ModalLink: React.FC = ({
18 | className,
19 | btnCaption,
20 | modalTitle,
21 | modalBody,
22 | request,
23 | onSuccess,
24 | variant = 'link'
25 | }) => {
26 | const [showModal, setShowModal] = useState(false);
27 | const query = useQuery();
28 |
29 | useEffect(() => {
30 | if (query.state.status === EQueryStatus.SUCCESS) {
31 | onSuccess();
32 | }
33 | }, [query.state.status]);
34 |
35 | const onSubmit = useCallback(() => {
36 | setShowModal(false);
37 | query.sendQuery(request);
38 | }, []);
39 |
40 | const onCancel = useCallback(() => {
41 | setShowModal(false);
42 | }, []);
43 |
44 | return (
45 | <>
46 | setShowModal(true)}>
47 | {btnCaption}
48 |
49 | {showModal && (
50 |
57 | {modalBody}
58 |
59 | )}
60 | >
61 | );
62 | };
63 |
64 | export default ModalLink;
65 |
--------------------------------------------------------------------------------
/src/client/components/common/Notification.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { INotificationsState } from '../../store/notifications/state';
3 | import { Alert } from 'react-bootstrap';
4 | import { useDispatch } from 'react-redux';
5 | import { closeNotificationAction } from '../../store/notifications/action';
6 |
7 | const Notification: React.FC = (props) => {
8 | const { stack } = props;
9 | const dispatch = useDispatch();
10 | return (
11 | <>
12 | {stack.map((notification, idx) => (
13 | dispatch(closeNotificationAction(notification.id))}
17 | dismissible
18 | >
19 | {notification.text}
20 |
21 | ))}{' '}
22 | >
23 | );
24 | };
25 |
26 | export default Notification;
27 |
--------------------------------------------------------------------------------
/src/client/components/common/Paginator.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | interface IProps {
4 | totalItems: number;
5 | onPageChange: (page: number) => void;
6 | currentPage: number;
7 | itemsPerPage: number;
8 | }
9 |
10 | function calculatePages(totalItems: number, currentPage: number, itemsPerPage: number, chunkSize = 15) {
11 | const totalPages = Math.ceil(totalItems / itemsPerPage);
12 | let rangeStart = 1;
13 | let rangeEnd = totalPages;
14 | if (totalPages > chunkSize) {
15 | const pivot = Math.ceil(chunkSize / 2);
16 | if (currentPage > pivot) {
17 | if (currentPage + pivot <= totalPages) {
18 | rangeStart = currentPage - pivot + 1;
19 | if (2 * pivot === chunkSize) rangeEnd = currentPage + pivot;
20 | else rangeEnd = currentPage + pivot - 1;
21 | } else {
22 | if (2 * pivot === chunkSize) {
23 | rangeStart = totalPages - 2 * pivot + 1;
24 | } else {
25 | rangeStart = totalPages - 2 * (pivot - 1);
26 | }
27 | rangeEnd = totalPages;
28 | }
29 | } else rangeEnd = rangeStart + chunkSize - 1;
30 | }
31 | return new Array(rangeEnd - rangeStart + 1).fill(0).map((_, index) => rangeStart + index);
32 | }
33 |
34 | const Paginator: React.FC = (props) => {
35 | const { totalItems, currentPage, itemsPerPage, onPageChange } = props;
36 | const pages = calculatePages(totalItems, currentPage, itemsPerPage);
37 | return (
38 |
39 | {pages.map((page) => (
40 |
41 | onPageChange(page)}
45 | >
46 | {page}
47 |
48 |
49 | ))}
50 |
51 | );
52 | };
53 |
54 | export default Paginator;
55 |
--------------------------------------------------------------------------------
/src/client/components/common/Query.tsx:
--------------------------------------------------------------------------------
1 | import React, { ReactElement, useEffect } from 'react';
2 | import useQuery, { EQueryStatus, IQueryState } from '../../hooks/useQuery';
3 | import { AxiosResponse } from 'axios';
4 | import { Spinner } from 'react-bootstrap';
5 | import AnErrorOccurred from './Errors/AnErrorOccurred';
6 |
7 | interface IProps {
8 | request: () => Promise;
9 | children(props: { state: IQueryState }): ReactElement;
10 | }
11 |
12 | const Query: React.FC = ({ request, children }) => {
13 | const { state, sendQuery } = useQuery();
14 | useEffect(() => {
15 | sendQuery(request);
16 | }, [request]);
17 | if ([EQueryStatus.LOADING, EQueryStatus.IDLE].includes(state.status)) {
18 | return ;
19 | }
20 | if (state.status === EQueryStatus.SUCCESS) {
21 | return children({ state });
22 | }
23 | return ;
24 | };
25 |
26 | export default Query;
27 |
--------------------------------------------------------------------------------
/src/client/components/common/TimeSeriesChart/Chart.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useMemo } from 'react';
2 | import {
3 | TRateStream,
4 | TRateStreamItem
5 | } from '../../../transport/websocket/streams/rateStream';
6 | import UPlotChartEngine, { IUPlotChartProps } from './UPlotChartEngine';
7 |
8 | const formatTimeSeries = (data: TRateStream, label: string, scale: string) => {
9 | const formatted: IUPlotChartProps = {
10 | lines: [],
11 | series: [[]]
12 | };
13 | const lineData: number[] = [];
14 | data.forEach((i: TRateStreamItem) => {
15 | formatted.series[0].push(i.timestamp);
16 | lineData.push(i.value);
17 | });
18 | formatted.series.push(lineData);
19 | formatted.lines.push({
20 | color: '#0c5efe',
21 | fill: '#0ca7fe',
22 | scale,
23 | label
24 | });
25 | return formatted;
26 | };
27 |
28 | const Chart: React.FC<{
29 | timeSeries: TRateStream;
30 | scale: string;
31 | label: string;
32 | onReady: () => void;
33 | }> = ({ timeSeries, scale, label, onReady }) => {
34 | const { lines, series } = useMemo(() => formatTimeSeries(timeSeries, label, scale), [timeSeries, scale, label]);
35 | useEffect(() => {
36 | onReady();
37 | }, []);
38 | return ;
39 | };
40 |
41 | export default Chart;
42 |
--------------------------------------------------------------------------------
/src/client/components/common/TimeSeriesChart/LiveTimeSeries.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from 'react';
2 | import { Spinner } from 'react-bootstrap';
3 | import Chart from './Chart';
4 | import { TRateStream } from '../../../transport/websocket/streams/rateStream';
5 | import Websocket from '../../../transport/websocket/websocket';
6 | import { Socket } from 'socket.io-client';
7 |
8 | export enum ELiveTimeSeriesLoadingProgress {
9 | CONNECTING,
10 | WAITING_FOR_DATA,
11 | DONE
12 | }
13 |
14 | export const LiveTimeSeries: React.FC<{
15 | stream: string;
16 | scale: string;
17 | label: string;
18 | onReady(): void;
19 | }> = ({ stream, scale, label, onReady }) => {
20 | const [liveTimeSeries, setLiveTimeSeries] = useState<{
21 | progress: ELiveTimeSeriesLoadingProgress;
22 | payload: TRateStream;
23 | }>({
24 | progress: ELiveTimeSeriesLoadingProgress.CONNECTING,
25 | payload: []
26 | });
27 | useEffect(() => {
28 | setLiveTimeSeries((prev) => {
29 | return {
30 | ...prev,
31 | progress: ELiveTimeSeriesLoadingProgress.CONNECTING
32 | };
33 | });
34 | Websocket()
35 | .then((socket: Socket) => {
36 | setLiveTimeSeries({
37 | ...liveTimeSeries,
38 | progress: ELiveTimeSeriesLoadingProgress.WAITING_FOR_DATA
39 | });
40 | socket.on(stream, (payload: { timestamp: number; value: number }[]) => {
41 | setLiveTimeSeries((prev) => {
42 | return {
43 | ...prev,
44 | progress: ELiveTimeSeriesLoadingProgress.DONE,
45 | payload: payload
46 | };
47 | });
48 | });
49 | })
50 | .catch((e: Error) => {
51 | throw e;
52 | });
53 | return () => {
54 | Websocket().then((socket) => {
55 | socket.removeAllListeners(stream);
56 | });
57 | };
58 | }, [stream]);
59 | if (liveTimeSeries.progress !== ELiveTimeSeriesLoadingProgress.DONE) {
60 | const progress =
61 | liveTimeSeries.progress === ELiveTimeSeriesLoadingProgress.CONNECTING
62 | ? 'Connecting'
63 | : 'Waiting for live stream data';
64 | return (
65 | <>
66 | {progress}...
67 |
68 | >
69 | );
70 | }
71 | return ;
72 | };
73 |
--------------------------------------------------------------------------------
/src/client/components/common/TimeSeriesChart/Navigation/Navigation.tsx:
--------------------------------------------------------------------------------
1 | import React, { ReactElement, useCallback, useState } from 'react';
2 | import WindowSize, { EWindowSize } from './WindowSize';
3 | import Scroll from './Scroll';
4 |
5 | export interface INavigationProps {
6 | children(props: { state: INavigationState; setReady: () => void }): ReactElement;
7 | }
8 |
9 | export interface INavigationState {
10 | isLoading: boolean;
11 | windowSize: EWindowSize | null;
12 | offset: number;
13 | from: number;
14 | to: number;
15 | }
16 |
17 | const Navigation: React.FC = ({ children }) => {
18 | const [state, setState] = useState({
19 | offset: 0,
20 | from: 0,
21 | to: 0,
22 | windowSize: null,
23 | isLoading: true
24 | });
25 | const setReady = useCallback(() => {
26 | setState((prevState) => {
27 | return {
28 | ...prevState,
29 | isLoading: false
30 | };
31 | });
32 | }, [state]);
33 | return (
34 | <>
35 | { }
36 | {children({ state, setReady })}
37 | { }
38 | >
39 | );
40 | };
41 |
42 | export default Navigation;
43 |
--------------------------------------------------------------------------------
/src/client/components/common/TimeSeriesChart/Navigation/Scroll.tsx:
--------------------------------------------------------------------------------
1 | import React, { useCallback, useEffect } from 'react';
2 | import { INavigationState } from './Navigation';
3 | import { EWindowSize } from './WindowSize';
4 |
5 | export enum ENavigationScrollDirection {
6 | RESET,
7 | LEFT,
8 | RIGHT
9 | }
10 |
11 | const Scroll: React.FC<{
12 | state: INavigationState;
13 | setNavigationState: React.Dispatch>;
14 | }> = ({ state, setNavigationState }) => {
15 | const scrollTo = useCallback(
16 | (direction: ENavigationScrollDirection, scrollWindow?: EWindowSize) => {
17 | setNavigationState((prev) => {
18 | return {
19 | ...prev,
20 | isLoading: true
21 | };
22 | });
23 | if (direction === ENavigationScrollDirection.RESET) {
24 | setNavigationState((prev) => {
25 | return {
26 | ...prev,
27 | offset: 0,
28 | to: 0,
29 | from: 0,
30 | windowSize: null
31 | };
32 | });
33 | } else {
34 | const window = scrollWindow || state.windowSize || EWindowSize.M1;
35 | const useOffset = scrollWindow ? 0 : state.offset;
36 | const scrollBy = window * (direction === ENavigationScrollDirection.RIGHT ? 1 : -1);
37 | const offset = useOffset + scrollBy;
38 | if (offset >= 0) {
39 | setNavigationState((prev) => {
40 | return {
41 | ...prev,
42 | offset: 0,
43 | to: 0,
44 | from: 0,
45 | windowSize: null
46 | };
47 | });
48 | } else {
49 | let from: number, to: number;
50 | const useTo = scrollWindow ? 0 : state.to;
51 | const useFrom = scrollWindow ? 0 : state.from;
52 | if (direction === ENavigationScrollDirection.RIGHT) {
53 | from = useTo;
54 | to = useTo + scrollBy;
55 | } else {
56 | to = useFrom > 0 ? useFrom : Math.ceil(Date.now() / 1000);
57 | from = to + scrollBy;
58 | }
59 | setNavigationState((prev) => {
60 | return {
61 | ...prev,
62 | offset,
63 | windowSize: window,
64 | from,
65 | to
66 | };
67 | });
68 | }
69 | }
70 | },
71 | [state]
72 | );
73 | useEffect(() => {
74 | if (state.windowSize) {
75 | scrollTo(ENavigationScrollDirection.LEFT, state.windowSize);
76 | }
77 | }, [state.windowSize]);
78 | if (state.isLoading) {
79 | return <>>;
80 | }
81 | return (
82 |
83 |
84 | scrollTo(ENavigationScrollDirection.LEFT)}
88 | >
89 | ← Scroll left
90 |
91 |
92 |
93 | scrollTo(ENavigationScrollDirection.RESET)}
97 | >
98 | Live stream
99 |
100 |
101 |
102 | scrollTo(ENavigationScrollDirection.RIGHT)}
106 | >
107 | Scroll right →
108 |
109 |
110 |
111 | );
112 | };
113 |
114 | export default Scroll;
115 |
--------------------------------------------------------------------------------
/src/client/components/common/TimeSeriesChart/Navigation/WindowSize.tsx:
--------------------------------------------------------------------------------
1 | import React, { useCallback } from 'react';
2 | import { INavigationState } from './Navigation';
3 |
4 | export interface IWindowSizeProps {
5 | state: INavigationState;
6 | setNavigationState: React.Dispatch>;
7 | }
8 |
9 | export enum EWindowSize {
10 | M1 = 60,
11 | M5 = 5 * 60,
12 | M15 = 15 * 60,
13 | M30 = 30 * 60,
14 | H1 = 60 * 60
15 | }
16 |
17 | const WindowSize: React.FC = ({ state, setNavigationState }) => {
18 | const setWindowSize = useCallback(
19 | (window: EWindowSize) => {
20 | setNavigationState((prev) => {
21 | return {
22 | ...prev,
23 | windowSize: window
24 | };
25 | });
26 | },
27 | [state]
28 | );
29 | if (state.isLoading) {
30 | return <>>;
31 | }
32 | return (
33 |
34 |
35 | setWindowSize(EWindowSize.M1)}
39 | >
40 | 1m
41 |
42 |
43 |
44 | setWindowSize(EWindowSize.M5)}
48 | >
49 | 5m
50 |
51 |
52 |
53 | setWindowSize(EWindowSize.M15)}
57 | >
58 | 15m
59 |
60 |
61 |
62 | setWindowSize(EWindowSize.M30)}
66 | >
67 | 30m
68 |
69 |
70 |
71 | setWindowSize(EWindowSize.H1)}
75 | >
76 | 1h
77 |
78 |
79 |
80 | );
81 | };
82 |
83 | export default WindowSize;
84 |
--------------------------------------------------------------------------------
/src/client/components/common/TimeSeriesChart/QueryTimeSeries.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Query from '../Query';
3 | import Chart from './Chart';
4 | import { ITimeSeriesChartProps } from './TimeSeriesChart';
5 |
6 | export const QueryTimeSeries: React.FC<{
7 | RequestFactory: ITimeSeriesChartProps['FetchTimeSeriesRequestFactory'];
8 | scale: string;
9 | label: string;
10 | from: number;
11 | to: number;
12 | onReady: () => void;
13 | }> = ({ RequestFactory, scale, label, from, to, onReady }) => {
14 | return (
15 | <>
16 |
17 | {({ state }) => {
18 | return ;
19 | }}
20 |
21 | >
22 | );
23 | };
24 |
--------------------------------------------------------------------------------
/src/client/components/common/TimeSeriesChart/TimeSeriesChart.tsx:
--------------------------------------------------------------------------------
1 | import React, { useMemo } from 'react';
2 | import { TQueryRequest } from '../../../hooks/useQuery';
3 | import { TGetTimeSeriesHTTPResponse } from '../../../transport/http/api/time-series';
4 | import Navigation from './Navigation/Navigation';
5 | import { LiveTimeSeries } from './LiveTimeSeries';
6 | import { QueryTimeSeries } from './QueryTimeSeries';
7 |
8 | export interface ITimeSeriesChartProps {
9 | label: string;
10 | scale: string;
11 | stream: string;
12 | FetchTimeSeriesRequestFactory: (from: number, to: number) => TQueryRequest;
13 | }
14 |
15 | const TimeSeriesChart: React.FC = ({ label, scale, stream, FetchTimeSeriesRequestFactory }) => {
16 | return (
17 |
18 |
19 | {({ state, setReady }) => {
20 | return useMemo(() => {
21 | if (state.offset === 0) {
22 | return ;
23 | }
24 | return (
25 |
33 | );
34 | }, [label, scale, stream, state.offset, state.from, state.to]);
35 | }}
36 |
37 |
38 | );
39 | };
40 |
41 | export default TimeSeriesChart;
42 |
--------------------------------------------------------------------------------
/src/client/components/common/TimeSeriesChart/UPlotChartEngine.tsx:
--------------------------------------------------------------------------------
1 | import React, { createRef, useEffect, useMemo, useState } from 'react';
2 | import UPlot, { AlignedData } from 'uplot';
3 | import 'uplot/dist/uPlot.min.css';
4 |
5 | const throttle = (func: Function, duration: number) => {
6 | let wait = false;
7 | return function (...args: unknown[]) {
8 | if (!wait) {
9 | func.apply(null, args);
10 | wait = true;
11 | setTimeout(function () {
12 | wait = false;
13 | }, duration);
14 | }
15 | };
16 | };
17 |
18 | const debounce = (func: (...ars: unknown[]) => unknown, duration: number) => {
19 | let timeout: ReturnType;
20 | return function (...args: unknown[]) {
21 | const effect = () => {
22 | clearTimeout(timeout);
23 | return func.apply(null, args);
24 | };
25 | clearTimeout(timeout);
26 | timeout = setTimeout(effect, duration);
27 | };
28 | };
29 |
30 | export interface IUPlotChartProps {
31 | series: AlignedData;
32 | lines: IUPlotChartPropsLine[];
33 | }
34 |
35 | export interface IUPlotChartPropsLine {
36 | label: string;
37 | scale: string;
38 | color: string;
39 | fill: string;
40 | }
41 |
42 | const UPlotChartEngine: React.FC = ({ series, lines }) => {
43 | const [uPlotInstance, setUPlotInstance] = useState(null);
44 | const plotRef = useMemo(() => createRef(), []);
45 |
46 | useEffect(() => {
47 | if (uPlotInstance) {
48 | uPlotInstance.destroy();
49 | setUPlotInstance(null);
50 | }
51 | if (plotRef.current) {
52 | const htmlElement = plotRef.current;
53 | const opts: UPlot.Options = {
54 | width: 700,
55 | height: 300,
56 | series: [
57 | {
58 | points: {
59 | show: false
60 | }
61 | },
62 | ...lines.map((i) => ({
63 | label: i.label,
64 | stroke: i.color,
65 | scale: i.scale,
66 | points: {
67 | show: false
68 | },
69 | spanGaps: false,
70 | fill: i.fill
71 | }))
72 | ],
73 | axes: [
74 | {},
75 | ...lines.map((value, index) => {
76 | const side = index > 0 ? 1 : 3;
77 | return {
78 | scale: value.scale,
79 | side,
80 | grid: { show: true }
81 | };
82 | })
83 | ]
84 | };
85 |
86 | const instance = new UPlot(
87 | { ...opts, width: htmlElement.clientWidth, height: htmlElement.clientHeight - 50 },
88 | series,
89 | htmlElement
90 | );
91 | setUPlotInstance(instance);
92 | }
93 | return () => {
94 | uPlotInstance?.destroy();
95 | setUPlotInstance(null);
96 | };
97 | }, [lines, series]);
98 |
99 | useEffect(() => {
100 | debounce(() => {
101 | if (uPlotInstance && plotRef.current && plotRef.current.parentElement) {
102 | uPlotInstance.setSize({ width: 0, height: 0 });
103 | const holder = plotRef.current.parentElement;
104 | const { clientWidth, clientHeight } = holder;
105 | uPlotInstance.setSize({ width: clientWidth, height: clientHeight - 50 });
106 | }
107 | }, 500);
108 | }, [window.innerWidth, window.innerHeight]);
109 |
110 | return (
111 |
120 | );
121 | };
122 |
123 | export default UPlotChartEngine;
124 |
--------------------------------------------------------------------------------
/src/client/hooks/useParams.ts:
--------------------------------------------------------------------------------
1 | import { matchPath, useLocation } from 'react-router';
2 |
3 | export function useParams(path: string) {
4 | const { pathname } = useLocation()
5 | const match = matchPath(pathname, { path })
6 | return match?.params
7 | }
8 |
--------------------------------------------------------------------------------
/src/client/hooks/useQuery.ts:
--------------------------------------------------------------------------------
1 | import { useCallback, useState } from 'react';
2 | import { AxiosError, AxiosResponse } from 'axios';
3 | import { useDispatch } from 'react-redux';
4 | import { addNotificationAction } from '../store/notifications/action';
5 | import { ENotificationType } from '../store/notifications/state';
6 |
7 | export enum EQueryStatus {
8 | IDLE,
9 | SUCCESS,
10 | ERROR,
11 | LOADING
12 | }
13 |
14 | export interface IQueryState {
15 | status: EQueryStatus;
16 | data?: T;
17 | errorDetails?: Record;
18 | errorMessage?: string;
19 | }
20 |
21 | export type TQueryRequest = () => Promise>;
22 |
23 | const useQuery = () => {
24 | const [state, setState] = useState>({ status: EQueryStatus.IDLE });
25 | const dispatch = useDispatch();
26 | const sendQuery = useCallback((request: TQueryRequest) => {
27 | setState({
28 | ...state,
29 | status: EQueryStatus.LOADING,
30 | data: undefined,
31 | errorDetails: undefined,
32 | errorMessage: undefined
33 | });
34 | request()
35 | .then(({ status, data }) => {
36 | setState({
37 | ...state,
38 | status: EQueryStatus.SUCCESS,
39 | data
40 | });
41 | })
42 | .catch(function (error: AxiosError) {
43 | if (error.response) {
44 | const msg = error.response.data?.error?.message ?? error.response.statusText;
45 | const errorMessage = `Request failed with status ${error.response.status}: ${msg}`;
46 | dispatch(addNotificationAction(errorMessage, ENotificationType.ERROR));
47 | setState({
48 | ...state,
49 | status: EQueryStatus.ERROR,
50 | errorMessage,
51 | errorDetails: error.response.status === 422 ? error.response.data.error : undefined
52 | });
53 | console.log(error.response.data);
54 | console.log(error.response.status);
55 | console.log(error.response.headers);
56 | } else if (error.request) {
57 | const message = `No response was received from the server after sending an XHR request.`;
58 | dispatch(addNotificationAction(message, ENotificationType.ERROR));
59 | setState({
60 | ...state,
61 | status: EQueryStatus.ERROR,
62 | errorMessage: `Could not receive any response from the server.`
63 | });
64 | console.log(error.request);
65 | } else {
66 | dispatch(
67 | addNotificationAction(
68 | `An error occurred while setting up an request. This is probably a configuration error.`,
69 | ENotificationType.ERROR
70 | )
71 | );
72 | setState({
73 | ...state,
74 | status: EQueryStatus.ERROR,
75 | errorMessage: error.message
76 | });
77 | console.log('Error', error.message);
78 | }
79 | console.log(error.config);
80 | });
81 | }, []);
82 | return { state, sendQuery };
83 | };
84 |
85 | export default useQuery;
86 |
--------------------------------------------------------------------------------
/src/client/hooks/useSelector.ts:
--------------------------------------------------------------------------------
1 | import { useSelector as useReduxSelector } from 'react-redux';
2 | import { isEqual } from 'lodash';
3 |
4 | const useSelector: typeof useReduxSelector = (selector) => {
5 | return useReduxSelector(selector, isEqual);
6 | };
7 |
8 | export default useSelector;
9 |
--------------------------------------------------------------------------------
/src/client/hooks/useUrlParams.ts:
--------------------------------------------------------------------------------
1 | import { useCallback } from 'react';
2 | import { useHistory } from 'react-router';
3 | import queryString from 'query-string';
4 |
5 | const useUrlParams = () => {
6 | const history = useHistory();
7 |
8 | const setUrlParam = useCallback(
9 | (key: string, value: string) => {
10 | const params = queryString.parse(location.search);
11 | params[key] = value;
12 | history.push({
13 | search: queryString.stringify(params)
14 | });
15 | },
16 | [location.search]
17 | );
18 |
19 | const getUrlParam = useCallback(
20 | (key: string): string | undefined => {
21 | const params = queryString.parse(location.search);
22 | const value = params[key];
23 | if (typeof value === 'string') return value;
24 | return undefined;
25 | },
26 | [location.search]
27 | );
28 |
29 | return { setUrlParam, getUrlParam };
30 | };
31 |
32 | export default useUrlParams;
33 |
--------------------------------------------------------------------------------
/src/client/hooks/useWebsocketSubscription.ts:
--------------------------------------------------------------------------------
1 | import { Socket } from 'socket.io-client';
2 | import Websocket from '../transport/websocket/websocket';
3 | import { useEffect, useState } from 'react';
4 |
5 | const useWebsocketSubscription = (stream: string, timeout: number) => {
6 | const [data, setData] = useState(null);
7 | const [isLoading, setIsLoading] = useState(true);
8 | useEffect(() => {
9 | let timer: ReturnType | null = null;
10 | const runTimeout = (socket: Socket) => {
11 | timer = setTimeout(() => {
12 | socket.removeAllListeners(stream);
13 | if (isLoading) setIsLoading(false);
14 | setData(null);
15 | }, 5000);
16 | };
17 | const cancelTimeout = () => {
18 | if (timer) clearTimeout(timer);
19 | if (isLoading) setIsLoading(false);
20 | };
21 | Websocket()
22 | .then((socket: Socket) => {
23 | timeout && runTimeout(socket);
24 | socket.on(stream, (payload: T) => {
25 | cancelTimeout();
26 | setData(payload);
27 | timeout && runTimeout(socket);
28 | });
29 | })
30 | .catch((e: Error) => {
31 | console.error(e);
32 | });
33 | return () => {
34 | cancelTimeout();
35 | Websocket().then((socket) => {
36 | socket.removeAllListeners(stream);
37 | });
38 | };
39 | }, [stream, timeout]);
40 | return {
41 | isLoading,
42 | data
43 | };
44 | };
45 |
46 | export default useWebsocketSubscription;
47 |
--------------------------------------------------------------------------------
/src/client/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | RedisSMQ Monitor
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/src/client/index.tsx:
--------------------------------------------------------------------------------
1 | // polyfill (needed to fix matchMedia for safari < 14)
2 | // See https://github.com/leeoniya/uPlot/issues/538
3 | import './polyfill/polyfill';
4 |
5 | import React from 'react';
6 | import { render } from 'react-dom';
7 | import { Provider as ReduxProvider } from 'react-redux';
8 | import configureStore from './store';
9 | import App from './components/App/App';
10 | import ErrorBoundary from './components/common/ErrorBoundary';
11 |
12 | const store = configureStore();
13 |
14 | render(
15 |
16 |
17 |
18 |
19 | ,
20 | document.getElementById('app')
21 | );
22 | export { TGetPendingMessagesWithPriorityHTTPResponse } from './transport/http/api';
23 | export { TGetQueueMessagesHTTPResponse } from './transport/http/api';
24 | export { TPaginatedHTTPResponse } from './transport/http/api';
25 |
--------------------------------------------------------------------------------
/src/client/polyfill/polyfill.ts:
--------------------------------------------------------------------------------
1 | let windowMatchMedia = window.matchMedia;
2 | window.matchMedia = (mediaQueryString) => {
3 | let wmm = windowMatchMedia(mediaQueryString);
4 | if (!wmm.addEventListener) {
5 | wmm.addEventListener = (
6 | type: K,
7 | listener: (this: any, ev: MediaQueryListEventMap[K]) => any
8 | ) => {
9 | wmm.addListener(listener);
10 | };
11 | wmm.removeEventListener = (
12 | type: K,
13 | listener: (this: any, ev: MediaQueryListEventMap[K]) => any
14 | ) => {
15 | wmm.removeListener(listener);
16 | };
17 | }
18 | return wmm;
19 | };
20 |
--------------------------------------------------------------------------------
/src/client/routes/common.ts:
--------------------------------------------------------------------------------
1 | import { generatePath, matchPath, RouteProps } from 'react-router';
2 | import * as routes from './routes';
3 |
4 | export interface IParameterizedRouteProps> extends RouteProps {
5 | path: string;
6 | getLink: (params: RouteParameters) => string;
7 | caption: string;
8 | }
9 |
10 | export function matchRouteParams(routeKey: keyof typeof routes, location: string) {
11 | const routeProps = routes[routeKey];
12 | const match = matchPath(location, routeProps);
13 | if (match) return match.params as T;
14 | return match;
15 | }
16 |
17 | export function matchRoute(path: string) {
18 | for (const key in routes) {
19 | const routeProps = routes[key as keyof typeof routes];
20 | const match = matchPath(path, routeProps);
21 | if (match) return match;
22 | }
23 | return null;
24 | }
25 |
26 | export const ParameterizedRoute = = Record>(
27 | def: Omit, 'getLink'>
28 | ): IParameterizedRouteProps => ({
29 | ...def,
30 | getLink(params): string {
31 | return generatePath(this.path, params);
32 | }
33 | });
34 |
--------------------------------------------------------------------------------
/src/client/routes/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Route, Switch } from 'react-router-dom';
3 | import Breadcrumbs from '../components/common/Breadcrumbs';
4 | import * as routes from './routes';
5 | import PageNotFound from '../components/common/Errors/PageNotFound';
6 |
7 | const Routes: React.FC = () => {
8 | return (
9 |
10 | {Object.keys(routes).map((key) => {
11 | const { component: Component, ...rest } = (routes as any)[key];
12 | return (
13 | (
17 | <>
18 |
19 |
20 | >
21 | )}
22 | />
23 | );
24 | })}
25 |
26 |
27 | );
28 | };
29 |
30 | export default Routes;
31 |
--------------------------------------------------------------------------------
/src/client/routes/routes/acknowledged-messages.ts:
--------------------------------------------------------------------------------
1 | import { ParameterizedRoute } from '../common';
2 | import QueueAcknowledgedMessages from '../../components/AcknowledgedMessages/AcknowledgedMessages';
3 | import { IQueueRouteParams } from './queue';
4 |
5 | export const acknowledgedMessages = ParameterizedRoute({
6 | path: '/namespaces/:namespace/queues/:queueName/acknowledged-messages',
7 | exact: true,
8 | component: QueueAcknowledgedMessages,
9 | caption: 'Acknowledged messages'
10 | });
11 |
--------------------------------------------------------------------------------
/src/client/routes/routes/consumer.ts:
--------------------------------------------------------------------------------
1 | import { ParameterizedRoute } from '../common';
2 | import Consumer from '../../components/Consumer/Consumer';
3 | import { IQueueRouteParams } from './queue';
4 |
5 | export interface IConsumerRouteParams extends IQueueRouteParams {
6 | consumerId: string;
7 | }
8 |
9 | export const consumer = ParameterizedRoute({
10 | path: '/namespaces/:namespace/queues/:queueName/consumers/:consumerId',
11 | exact: true,
12 | component: Consumer,
13 | caption: 'Consumer'
14 | });
15 |
--------------------------------------------------------------------------------
/src/client/routes/routes/consumers.ts:
--------------------------------------------------------------------------------
1 | import { ParameterizedRoute } from '../common';
2 | import { IQueueRouteParams } from './queue';
3 | import QueueConsumers from '../../components/QueueConsumers/QueueConsumers';
4 |
5 | export const consumers = ParameterizedRoute({
6 | path: '/namespaces/:namespace/queues/:queueName/consumers',
7 | exact: true,
8 | component: QueueConsumers,
9 | caption: 'Consumers'
10 | });
11 |
--------------------------------------------------------------------------------
/src/client/routes/routes/dead-lettered-messages.ts:
--------------------------------------------------------------------------------
1 | import { ParameterizedRoute } from '../common';
2 | import QueueDeadLetteredMessages from '../../components/DeadLetteredMessages/DeadLetteredMessages';
3 | import { IQueueRouteParams } from './queue';
4 |
5 | export const deadLetteredMessages = ParameterizedRoute({
6 | path: '/namespaces/:namespace/queues/:queueName/dead-lettered-messages',
7 | exact: true,
8 | component: QueueDeadLetteredMessages,
9 | caption: 'Dead-lettered messages'
10 | });
11 |
--------------------------------------------------------------------------------
/src/client/routes/routes/exchange.ts:
--------------------------------------------------------------------------------
1 | import { ParameterizedRoute } from '../common';
2 | import Exchange from '../../components/Exchange/Exchange';
3 |
4 | export interface IExchangeRouteParams {
5 | name: string;
6 | }
7 |
8 | export const exchange = ParameterizedRoute({
9 | path: '/exchanges/:name',
10 | exact: true,
11 | component: Exchange,
12 | caption: 'Exchange'
13 | });
14 |
--------------------------------------------------------------------------------
/src/client/routes/routes/home.ts:
--------------------------------------------------------------------------------
1 | import { ParameterizedRoute } from '../common';
2 | import Home from '../../components/Home/Home';
3 |
4 | export const home = ParameterizedRoute({
5 | path: '/',
6 | exact: true,
7 | component: Home,
8 | caption: 'Home'
9 | });
10 |
--------------------------------------------------------------------------------
/src/client/routes/routes/index.ts:
--------------------------------------------------------------------------------
1 | export { home } from './home';
2 | export { queue } from './queue';
3 | export { consumer } from './consumer';
4 | export { deadLetteredMessages } from './dead-lettered-messages';
5 | export { pendingMessages } from './pending-messages';
6 | export { acknowledgedMessages } from './acknowledged-messages';
7 | export { scheduledMessages } from './scheduled-messages';
8 | export { consumers } from './consumers';
9 | export { exchange } from './exchange';
10 | export { queues } from './queues';
11 |
--------------------------------------------------------------------------------
/src/client/routes/routes/pending-messages.ts:
--------------------------------------------------------------------------------
1 | import { ParameterizedRoute } from '../common';
2 | import QueuePendingMessages from '../../components/PendingMessages/PendingMessages';
3 | import { IQueueRouteParams } from './queue';
4 |
5 | export const pendingMessages = ParameterizedRoute({
6 | path: '/namespaces/:namespace/queues/:queueName/pending-messages',
7 | exact: true,
8 | component: QueuePendingMessages,
9 | caption: 'Pending messages'
10 | });
11 |
--------------------------------------------------------------------------------
/src/client/routes/routes/queue.ts:
--------------------------------------------------------------------------------
1 | import { ParameterizedRoute } from '../common';
2 | import Queue from '../../components/Queue/Queue';
3 |
4 | export interface IQueueRouteParams {
5 | queueName: string;
6 | namespace: string;
7 | }
8 |
9 | export const queue = ParameterizedRoute({
10 | path: '/namespaces/:namespace/queues/:queueName',
11 | exact: true,
12 | component: Queue,
13 | caption: 'Queue'
14 | });
15 |
--------------------------------------------------------------------------------
/src/client/routes/routes/queues.ts:
--------------------------------------------------------------------------------
1 | import { ParameterizedRoute } from '../common';
2 | import Queues from '../../components/Queues/Queues';
3 |
4 | export interface IQueuesRouteParams {
5 | namespace: string;
6 | }
7 |
8 | export const queues = ParameterizedRoute({
9 | path: '/namespaces/:namespace/queues',
10 | component: Queues,
11 | caption: 'Queues'
12 | });
13 |
--------------------------------------------------------------------------------
/src/client/routes/routes/scheduled-messages.ts:
--------------------------------------------------------------------------------
1 | import { ParameterizedRoute } from '../common';
2 | import ScheduledMessages from '../../components/ScheduledMessages/ScheduledMessages';
3 |
4 | export const scheduledMessages = ParameterizedRoute({
5 | path: '/scheduled-messages',
6 | exact: true,
7 | component: ScheduledMessages,
8 | caption: 'Scheduled messages'
9 | });
10 |
--------------------------------------------------------------------------------
/src/client/store/components/Exchange/action.ts:
--------------------------------------------------------------------------------
1 | export enum EActionType {
2 | RELOAD = 'RELOAD',
3 | }
4 |
5 | export function reloadAction() {
6 | return {
7 | type: EActionType.RELOAD,
8 | };
9 | }
10 |
--------------------------------------------------------------------------------
/src/client/store/components/Exchange/reducer.ts:
--------------------------------------------------------------------------------
1 | import { Reducer } from 'redux';
2 | import { EActionType } from './action';
3 | import {
4 | IExchangeState, initialExchangeState,
5 | } from './state';
6 |
7 | export const exchangeReducer: Reducer = (
8 | state = initialExchangeState,
9 | action
10 | ) => {
11 | const { type } = action;
12 | if (type === EActionType.RELOAD) {
13 | return {
14 | ...state,
15 | version: state.version + 1
16 | };
17 | }
18 | return state;
19 | };
20 |
21 |
--------------------------------------------------------------------------------
/src/client/store/components/Exchange/state.ts:
--------------------------------------------------------------------------------
1 | export interface IExchangeState {
2 | version: number;
3 | }
4 |
5 | export const initialExchangeState: IExchangeState = {
6 | version: 0,
7 | };
8 |
--------------------------------------------------------------------------------
/src/client/store/components/LeftPanel/Exchanges/action.ts:
--------------------------------------------------------------------------------
1 | export enum EActionType {
2 | RELOAD = 'RELOAD',
3 | }
4 |
5 | export function reloadAction() {
6 | return {
7 | type: EActionType.RELOAD,
8 | };
9 | }
10 |
--------------------------------------------------------------------------------
/src/client/store/components/LeftPanel/Exchanges/reducer.ts:
--------------------------------------------------------------------------------
1 | import { Reducer } from 'redux';
2 | import { EActionType } from './action';
3 | import { IExchangesState, initialExchangesState } from './state';
4 |
5 | export const exchangesReducer: Reducer = (
6 | state = initialExchangesState,
7 | action
8 | ) => {
9 | const { type } = action;
10 | if (type === EActionType.RELOAD) {
11 | return {
12 | ...state,
13 | version: state.version + 1
14 | };
15 | }
16 | return state;
17 | };
18 |
19 |
--------------------------------------------------------------------------------
/src/client/store/components/LeftPanel/Exchanges/state.ts:
--------------------------------------------------------------------------------
1 | export interface IExchangesState {
2 | version: number;
3 | }
4 |
5 | export const initialExchangesState: IExchangesState = {
6 | version: 0,
7 | };
8 |
--------------------------------------------------------------------------------
/src/client/store/components/LeftPanel/reducer.ts:
--------------------------------------------------------------------------------
1 | import { combineReducers } from 'redux';
2 | import { exchangesReducer } from './Exchanges/reducer';
3 | import { ILeftPanelState } from './state';
4 |
5 | export const leftPanelReducer = combineReducers({
6 | Exchanges: exchangesReducer,
7 | })
--------------------------------------------------------------------------------
/src/client/store/components/LeftPanel/state.ts:
--------------------------------------------------------------------------------
1 | import { IExchangesState } from './Exchanges/state';
2 |
3 | export interface ILeftPanelState {
4 | Exchanges: IExchangesState;
5 | }
--------------------------------------------------------------------------------
/src/client/store/components/reducer.ts:
--------------------------------------------------------------------------------
1 | import { combineReducers } from 'redux';
2 | import { exchangeReducer } from './Exchange/reducer';
3 | import { leftPanelReducer } from './LeftPanel/reducer';
4 | import { IComponentsState } from './state';
5 |
6 | export const componentsReducer = combineReducers({
7 | Exchange: exchangeReducer,
8 | LeftPanel: leftPanelReducer,
9 | })
--------------------------------------------------------------------------------
/src/client/store/components/state.ts:
--------------------------------------------------------------------------------
1 | import { ILeftPanelState } from './LeftPanel/state';
2 | import { IExchangeState } from './Exchange/state';
3 |
4 | export interface IComponentsState {
5 | LeftPanel: ILeftPanelState;
6 | Exchange: IExchangeState;
7 | }
8 |
9 | export const initialComponentsState: IComponentsState = {
10 | LeftPanel: {
11 | Exchanges: {
12 | version: 0,
13 | }
14 | },
15 | Exchange: {
16 | version: 0,
17 | }
18 | }
--------------------------------------------------------------------------------
/src/client/store/index.ts:
--------------------------------------------------------------------------------
1 | import { combineReducers, Store, createStore, applyMiddleware } from 'redux';
2 | import reduxImmutableStateInvariant from 'redux-immutable-state-invariant';
3 | import thunk from 'redux-thunk';
4 | import { IStoreState } from './state';
5 | import { notificationsReducer } from './notifications/reducer';
6 | import { initialNotificationsState } from './notifications/state';
7 | import { websocketMainStreamReducer } from './websocket-main-stream/reducer';
8 | import { initialWebsocketMainStreamState } from './websocket-main-stream/state';
9 | import { componentsReducer } from './components/reducer';
10 | import { initialComponentsState } from './components/state';
11 |
12 | const createRootReducer = () =>
13 | combineReducers({
14 | websocketMainStream: websocketMainStreamReducer,
15 | notifications: notificationsReducer,
16 | components: componentsReducer,
17 | });
18 |
19 | export const initialState: IStoreState = {
20 | websocketMainStream: initialWebsocketMainStreamState,
21 | notifications: initialNotificationsState,
22 | components: initialComponentsState,
23 | };
24 |
25 | export default function configureStore(state: IStoreState = initialState): Store {
26 | const middleware = applyMiddleware(reduxImmutableStateInvariant(), thunk);
27 | return createStore(createRootReducer(), state, middleware);
28 | }
29 |
--------------------------------------------------------------------------------
/src/client/store/notifications/action.ts:
--------------------------------------------------------------------------------
1 | import { ENotificationType } from './state';
2 |
3 | export enum EActionType {
4 | ADD_NOTIFICATION = 'ADD_NOTIFICATION',
5 | CLOSE_NOTIFICATION = 'CLOSE_NOTIFICATION'
6 | }
7 |
8 | export function addNotificationAction(text: string, type: ENotificationType) {
9 | return {
10 | type: EActionType.ADD_NOTIFICATION,
11 | params: {
12 | notification: {
13 | id: Date.now(),
14 | text,
15 | type
16 | }
17 | }
18 | };
19 | }
20 |
21 | export function closeNotificationAction(id: number) {
22 | return {
23 | type: EActionType.CLOSE_NOTIFICATION,
24 | params: {
25 | id
26 | }
27 | };
28 | }
29 |
--------------------------------------------------------------------------------
/src/client/store/notifications/reducer.ts:
--------------------------------------------------------------------------------
1 | import { Reducer } from 'redux';
2 | import { EActionType } from './action';
3 | import { initialNotificationsState, INotificationsState } from './state';
4 |
5 | export const notificationsReducer: Reducer = (state = initialNotificationsState, action) => {
6 | const { type } = action;
7 | if (type === EActionType.ADD_NOTIFICATION) {
8 | const { notification } = action.params;
9 | return {
10 | ...state,
11 | stack: [
12 | ...state.stack,
13 | {
14 | ...notification,
15 | id: Date.now()
16 | }
17 | ]
18 | };
19 | }
20 | if (type === EActionType.CLOSE_NOTIFICATION) {
21 | const { id } = action.params;
22 | return {
23 | ...state,
24 | stack: state.stack.filter((i) => i.id !== id)
25 | };
26 | }
27 | return state;
28 | };
29 |
--------------------------------------------------------------------------------
/src/client/store/notifications/state.ts:
--------------------------------------------------------------------------------
1 | export enum ENotificationType {
2 | SUCCESS = 'success',
3 | ERROR = 'danger'
4 | }
5 |
6 | export interface INotification {
7 | id: number;
8 | text: string;
9 | type: ENotificationType;
10 | }
11 |
12 | export interface INotificationsState {
13 | stack: INotification[];
14 | }
15 |
16 | export const initialNotificationsState: INotificationsState = {
17 | stack: []
18 | };
19 |
--------------------------------------------------------------------------------
/src/client/store/state.ts:
--------------------------------------------------------------------------------
1 | import { INotificationsState } from './notifications/state';
2 | import { IWebsocketMainStreamState } from './websocket-main-stream/state';
3 | import { IComponentsState } from './components/state';
4 |
5 | export interface IStoreState {
6 | websocketMainStream: IWebsocketMainStreamState;
7 | notifications: INotificationsState;
8 | components: IComponentsState;
9 | }
10 |
--------------------------------------------------------------------------------
/src/client/store/websocket-main-stream/action.ts:
--------------------------------------------------------------------------------
1 | import { TWebsocketMainStreamPayload } from '../../transport/websocket/streams/mainStream';
2 |
3 | export enum EActionType {
4 | SET_LOADING = 'SET_LOADING',
5 | SET_PAYLOAD = 'SET_PAYLOAD'
6 | }
7 |
8 | export function setPayloadAction(payload: TWebsocketMainStreamPayload) {
9 | return {
10 | type: EActionType.SET_PAYLOAD,
11 | payload
12 | };
13 | }
14 |
15 | export function setLoadingAction() {
16 | return {
17 | type: EActionType.SET_LOADING
18 | };
19 | }
20 |
--------------------------------------------------------------------------------
/src/client/store/websocket-main-stream/reducer.ts:
--------------------------------------------------------------------------------
1 | import { Reducer } from 'redux';
2 | import { EActionType } from './action';
3 | import { EWebsocketMainStreamStatus, initialWebsocketMainStreamState, IWebsocketMainStreamState } from './state';
4 |
5 | export const websocketMainStreamReducer: Reducer = (
6 | state = initialWebsocketMainStreamState,
7 | action
8 | ) => {
9 | const { type } = action;
10 | if (type === EActionType.SET_PAYLOAD) {
11 | const payload = action.payload;
12 | return {
13 | ...state,
14 | status: EWebsocketMainStreamStatus.LOADED,
15 | payload: {
16 | ...payload
17 | }
18 | };
19 | }
20 | if (type === EActionType.SET_LOADING) {
21 | return {
22 | ...state,
23 | status: EWebsocketMainStreamStatus.LOADING
24 | };
25 | }
26 | return state;
27 | };
28 |
29 |
--------------------------------------------------------------------------------
/src/client/store/websocket-main-stream/state.ts:
--------------------------------------------------------------------------------
1 | import { TWebsocketMainStreamPayload } from '../../transport/websocket/streams/mainStream';
2 |
3 |
4 | export enum EWebsocketMainStreamStatus {
5 | INIT,
6 | LOADING,
7 | LOADED,
8 | }
9 |
10 | export interface IWebsocketMainStreamState {
11 | status: EWebsocketMainStreamStatus;
12 | payload: TWebsocketMainStreamPayload;
13 | }
14 |
15 | export const initialWebsocketMainStreamState: IWebsocketMainStreamState = {
16 | status: EWebsocketMainStreamStatus.INIT,
17 | payload: {
18 | consumersCount: 0,
19 | queuesCount: 0,
20 | pendingMessagesCount: 0,
21 | deadLetteredMessagesCount: 0,
22 | scheduledMessagesCount: 0,
23 | acknowledgedMessagesCount: 0,
24 | queues: {}
25 | }
26 | };
27 |
--------------------------------------------------------------------------------
/src/client/tools/start-monitor-server.ts:
--------------------------------------------------------------------------------
1 | // Using redis-smq-monitor from a local repository
2 | // Fix this according to your preferences
3 | import { RedisClientName } from '../../../../redis-smq-common/dist/types';
4 | import { MonitorServer } from '../../../../redis-smq-monitor';
5 | import { TConfig } from '../../../../redis-smq-monitor/dist/types';
6 | import { logger } from '../../../../redis-smq-common';
7 |
8 | export const config: TConfig = {
9 | redis: {
10 | client: RedisClientName.IOREDIS,
11 | options: {
12 | host: '127.0.0.1',
13 | port: 6379
14 | }
15 | },
16 | logger: {
17 | enabled: true
18 | },
19 | server: {
20 | port: 3000,
21 | host: '127.0.0.1'
22 | }
23 | };
24 |
25 | logger.setLogger(console);
26 |
27 | const server = MonitorServer.createInstance(config);
28 | server.listen();
29 |
--------------------------------------------------------------------------------
/src/client/tools/utils.ts:
--------------------------------------------------------------------------------
1 | export function formatBytes(bytes: number, decimals = 2) {
2 | if (bytes === 0) return '0 Bytes';
3 |
4 | const k = 1024;
5 | const dm = decimals < 0 ? 0 : decimals;
6 | const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
7 |
8 | const i = Math.floor(Math.log(bytes) / Math.log(k));
9 |
10 | return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
11 | }
12 |
13 | export function bytesToMB(bytes: number, decimals = 2) {
14 | if (bytes === 0) return 0;
15 | return parseFloat((bytes / Math.pow(1024, 2)).toFixed(decimals));
16 | }
17 |
--------------------------------------------------------------------------------
/src/client/transport/endpoints.ts:
--------------------------------------------------------------------------------
1 | export const HOST = process.env.API_URL ?? `${window.location.protocol}//${window.location.host}`;
2 | export const API_URL = basePath === '/' ? HOST : `${HOST}${basePath}`;
3 |
--------------------------------------------------------------------------------
/src/client/transport/http/api/common/IMessage.ts:
--------------------------------------------------------------------------------
1 | export enum EMessagePriority {
2 | LOWEST = 7,
3 | VERY_LOW = 6,
4 | LOW = 5,
5 | NORMAL = 4,
6 | ABOVE_NORMAL = 3,
7 | HIGH = 2,
8 | VERY_HIGH = 1,
9 | HIGHEST = 0
10 | }
11 |
12 | export interface IMessageQueue {
13 | name: string;
14 | ns: string;
15 | }
16 |
17 | export interface IMessageState {
18 | uuid: string;
19 | publishedAt: number | null;
20 | scheduledAt: number | null;
21 | scheduledCronFired: boolean;
22 | scheduledRepeatCount: number;
23 | attempts: number;
24 | nextScheduledDelay: number;
25 | nextRetryDelay: number;
26 | expired: boolean;
27 | }
28 |
29 | export enum EExchangeType {
30 | DIRECT,
31 | FANOUT,
32 | TOPIC,
33 | }
34 |
35 | export type TTopicParams = {
36 | topic: string;
37 | ns: string;
38 | };
39 |
40 | export interface IExchangeParams<
41 | TBindingParams,
42 | TBindingType extends EExchangeType,
43 | > {
44 | exchangeTag: string;
45 | destinationQueue: IMessageQueue | null;
46 | bindingParams: TBindingParams;
47 | type: TBindingType;
48 | }
49 |
50 | export type IDirectExchangeParams = IExchangeParams<
51 | IMessageQueue | string,
52 | EExchangeType.DIRECT
53 | >;
54 |
55 | export type IFanOutExchangeParams = IExchangeParams<
56 | string,
57 | EExchangeType.FANOUT
58 | >;
59 |
60 | export type ITopicExchangeParams = IExchangeParams<
61 | TTopicParams | string,
62 | EExchangeType.TOPIC
63 | >;
64 |
65 | export interface IMessage {
66 | createdAt: number;
67 | ttl: number;
68 | retryThreshold: number;
69 | retryDelay: number;
70 | consumeTimeout: number;
71 | body: unknown;
72 | scheduledCron: string | null;
73 | scheduledDelay: number | null;
74 | scheduledRepeatPeriod: number | null;
75 | scheduledRepeat: number;
76 | priority: number | null;
77 | queue: IMessageQueue | null;
78 | messageState: IMessageState;
79 | exchange:
80 | | IDirectExchangeParams
81 | | ITopicExchangeParams
82 | | IFanOutExchangeParams
83 | | null;
84 | }
85 |
86 |
--------------------------------------------------------------------------------
/src/client/transport/http/api/delete-message.ts:
--------------------------------------------------------------------------------
1 | import axios from 'axios';
2 | import { API_URL } from '../../endpoints';
3 |
4 | export const deleteQueueDeadLetteredMessage = async (
5 | ns: string,
6 | queueName: string,
7 | messageId: string,
8 | sequenceId: number
9 | ) => {
10 | return axios.delete(
11 | `${API_URL}/api/ns/${ns}/queues/${queueName}/dead-lettered-messages/${messageId}?sequenceId=${sequenceId}`
12 | );
13 | };
14 |
15 | export const deleteQueuePendingMessage = async (
16 | ns: string,
17 | queueName: string,
18 | messageId: string,
19 | sequenceId: number
20 | ) => {
21 | return axios.delete(
22 | `${API_URL}/api/ns/${ns}/queues/${queueName}/pending-messages/${messageId}?sequenceId=${sequenceId}`
23 | );
24 | };
25 |
26 | export const deleteQueueAcknowledgedMessage = async (
27 | ns: string,
28 | queueName: string,
29 | messageId: string,
30 | sequenceId: number
31 | ) => {
32 | return axios.delete(
33 | `${API_URL}/api/ns/${ns}/queues/${queueName}/acknowledged-messages/${messageId}?sequenceId=${sequenceId}`
34 | );
35 | };
36 |
37 | export const deleteScheduledMessage = async (messageId: string, sequenceId: number) => {
38 | return axios.delete(`${API_URL}/api/main/scheduled-messages/${messageId}?sequenceId=${sequenceId}`);
39 | };
40 |
--------------------------------------------------------------------------------
/src/client/transport/http/api/delete-queue.ts:
--------------------------------------------------------------------------------
1 | import axios from 'axios';
2 | import { API_URL } from '../../endpoints';
3 |
4 | export const deleteQueue = async (ns: string, queueName: string) => {
5 | return axios.delete(`${API_URL}/api/ns/${ns}/queues/${queueName}`);
6 | };
7 |
--------------------------------------------------------------------------------
/src/client/transport/http/api/exchanges.ts:
--------------------------------------------------------------------------------
1 | import axios from 'axios';
2 | import { API_URL } from '../../endpoints';
3 | import { IMessageQueue } from './common/IMessage';
4 |
5 | export const getExchanges = async () => {
6 | return axios.get(
7 | `${API_URL}/api/exchanges`
8 | );
9 | };
10 |
11 | export const getExchangeQueues = async (exchangeName: string) => {
12 | return axios.get(
13 | `${API_URL}/api/exchanges/${exchangeName}/queues`
14 | );
15 | };
16 |
17 | export const createExchange = async (exchangeName: string) => {
18 | return axios.post(
19 | `${API_URL}/api/exchanges`,
20 | { exchangeName }
21 | );
22 | };
23 |
24 | export const bindQueue = async (queue: IMessageQueue, exchangeName: string) => {
25 | return axios.post(
26 | `${API_URL}/api/exchanges/${exchangeName}/bind`,
27 | { queue }
28 | );
29 | }
30 |
31 | export const unbindQueue = async (queue: IMessageQueue, exchangeName: string) => {
32 | return axios.post(
33 | `${API_URL}/api/exchanges/${exchangeName}/unbind`,
34 | { queue }
35 | );
36 | }
37 |
38 | export const deleteExchange = async (exchangeName: string) => {
39 | return axios.delete(
40 | `${API_URL}/api/exchanges/${exchangeName}`
41 | );
42 | };
--------------------------------------------------------------------------------
/src/client/transport/http/api/get-messages.ts:
--------------------------------------------------------------------------------
1 | import axios from 'axios';
2 | import { IHTTPResponse } from './index';
3 | import { IMessage } from './common/IMessage';
4 | import { API_URL } from '../../endpoints';
5 |
6 | export type TPaginatedHTTPResponse = IHTTPResponse<{
7 | total: number;
8 | items: T[];
9 | }>;
10 |
11 | export type TGetQueueMessagesHTTPResponse = TPaginatedHTTPResponse<{
12 | sequenceId: number;
13 | message: IMessage;
14 | }>;
15 |
16 | export type TGetPendingMessagesWithPriorityHTTPResponse = TPaginatedHTTPResponse;
17 |
18 | export const getScheduledMessages = async (skip: number, take: number) => {
19 | return axios.get(
20 | `${API_URL}/api/main/scheduled-messages?skip=${skip}&take=${take}`
21 | );
22 | };
23 |
24 | export const getQueuePendingMessagesWithPriority = async (
25 | ns: string,
26 | queueName: string,
27 | skip: number,
28 | take: number
29 | ) => {
30 | return axios.get(
31 | `${API_URL}/api/ns/${ns}/queues/${queueName}/pending-messages-with-priority?skip=${skip}&take=${take}`
32 | );
33 | };
34 |
35 | export const getQueuePendingMessages = async (ns: string, queueName: string, skip: number, take: number) => {
36 | return axios.get(
37 | `${API_URL}/api/ns/${ns}/queues/${queueName}/pending-messages?skip=${skip}&take=${take}`
38 | );
39 | };
40 |
41 | export const getQueueAcknowledgedMessages = async (ns: string, queueName: string, skip: number, take: number) => {
42 | return await axios.get(
43 | `${API_URL}/api/ns/${ns}/queues/${queueName}/acknowledged-messages?skip=${skip}&take=${take}`
44 | );
45 | };
46 |
47 | export const getQueueDeadLetteredMessages = async (ns: string, queueName: string, skip: number, take: number) => {
48 | return axios.get(
49 | `${API_URL}/api/ns/${ns}/queues/${queueName}/dead-lettered-messages?skip=${skip}&take=${take}`
50 | );
51 | };
52 |
--------------------------------------------------------------------------------
/src/client/transport/http/api/index.ts:
--------------------------------------------------------------------------------
1 | export * from './delete-message';
2 | export * from './get-messages';
3 | export * from './purge-messages';
4 | export * from './requeue-message';
5 | export * from './namespaces';
6 |
7 | export interface IHTTPResponse {
8 | data: T;
9 | }
10 |
--------------------------------------------------------------------------------
/src/client/transport/http/api/namespaces.ts:
--------------------------------------------------------------------------------
1 | import axios from 'axios';
2 | import { API_URL } from '../../endpoints';
3 |
4 | export const deleteNamespace = async (ns: string) => {
5 | return axios.delete(`${API_URL}/api/ns/${ns}`);
6 | };
7 |
--------------------------------------------------------------------------------
/src/client/transport/http/api/purge-messages.ts:
--------------------------------------------------------------------------------
1 | import axios from 'axios';
2 | import { API_URL } from '../../endpoints';
3 |
4 | export const purgeAcknowledgedMessages = async (ns: string, queueName: string) => {
5 | return axios.delete(`${API_URL}/api/ns/${ns}/queues/${queueName}/pending-messages`);
6 | };
7 |
8 | export const purgeDeadLetteredMessages = async (ns: string, queueName: string) => {
9 | return axios.delete(`${API_URL}/api/ns/${ns}/queues/${queueName}/dead-lettered-messages`);
10 | };
11 |
12 | export const purgePendingMessages = async (ns: string, queueName: string) => {
13 | return axios.delete(`${API_URL}/api/ns/${ns}/queues/${queueName}/pending-messages`);
14 | };
15 |
16 | export const purgeScheduledMessages = async () => {
17 | return axios.delete(`${API_URL}/api/main/scheduled-messages`);
18 | };
19 |
--------------------------------------------------------------------------------
/src/client/transport/http/api/queue-rate-limiting.ts:
--------------------------------------------------------------------------------
1 | import axios from 'axios';
2 | import { IHTTPResponse } from './index';
3 | import { API_URL } from '../../endpoints';
4 |
5 | export type TGetQueueRateLimitResponse = IHTTPResponse<{
6 | interval: number;
7 | limit: number;
8 | } | null>;
9 |
10 | export const setQueueRateLimit = async (ns: string, queueName: string, interval: number, limit: number) => {
11 | return axios.post(`${API_URL}/api/ns/${ns}/queues/${queueName}/rate-limit`, { interval, limit });
12 | };
13 |
14 | export const clearQueueRateLimit = async (ns: string, queueName: string) => {
15 | return axios.delete(`${API_URL}/api/ns/${ns}/queues/${queueName}/rate-limit`);
16 | };
17 |
18 | export const getQueueRateLimit = async (ns: string, queueName: string) => {
19 | return axios.get(`${API_URL}/api/ns/${ns}/queues/${queueName}/rate-limit`);
20 | };
21 |
--------------------------------------------------------------------------------
/src/client/transport/http/api/requeue-message.ts:
--------------------------------------------------------------------------------
1 | import axios from 'axios';
2 | import { API_URL } from '../../endpoints';
3 |
4 | export const requeueDeadLetteredMessage = async (
5 | ns: string,
6 | queueName: string,
7 | messageId: string,
8 | sequenceId: number
9 | ) => {
10 | return axios.post(
11 | `${API_URL}/api/ns/${ns}/queues/${queueName}/dead-lettered-messages/${messageId}/requeue?sequenceId=${sequenceId}`
12 | );
13 | };
14 |
15 | export const requeueAcknowledgedMessage = async (
16 | ns: string,
17 | queueName: string,
18 | messageId: string,
19 | sequenceId: number
20 | ) => {
21 | return axios.post(
22 | `${API_URL}/api/ns/${ns}/queues/${queueName}/acknowledged-messages/${messageId}/requeue?sequenceId=${sequenceId}`
23 | );
24 | };
25 |
--------------------------------------------------------------------------------
/src/client/transport/http/api/save-queue.ts:
--------------------------------------------------------------------------------
1 | import axios from 'axios';
2 | import { API_URL } from '../../endpoints';
3 |
4 | export const saveQueue = async (queue: { name: string; ns?: string }, type: number) => {
5 | return axios.post(`${API_URL}/api/queues`, { ...queue, type });
6 | };
--------------------------------------------------------------------------------
/src/client/transport/http/api/time-series.ts:
--------------------------------------------------------------------------------
1 | import axios from 'axios';
2 | import { IHTTPResponse } from './index';
3 | import { API_URL } from '../../endpoints';
4 |
5 | export type TGetTimeSeriesHTTPResponse = IHTTPResponse<
6 | {
7 | timestamp: number;
8 | value: number;
9 | }[]
10 | >;
11 |
12 | export const getQueueAcknowledgedTimeSeries = async (ns: string, queueName: string, from: number, to: number) => {
13 | return axios.get(
14 | `${API_URL}/api/ns/${ns}/queues/${queueName}/time-series/acknowledged?from=${from}&to=${to}`
15 | );
16 | };
17 |
18 | export const getQueueDeadLetteredTimeSeries = async (ns: string, queueName: string, from: number, to: number) => {
19 | return axios.get(
20 | `${API_URL}/api/ns/${ns}/queues/${queueName}/time-series/dead-lettered?from=${from}&to=${to}`
21 | );
22 | };
23 |
24 | export const getQueuePublishedTimeSeries = async (ns: string, queueName: string, from: number, to: number) => {
25 | return axios.get(
26 | `${API_URL}/api/ns/${ns}/queues/${queueName}/time-series/published?from=${from}&to=${to}`
27 | );
28 | };
29 |
30 | export const getGlobalAcknowledgedTimeSeries = async (from: number, to: number) => {
31 | return axios.get(`${API_URL}/api/main/time-series/acknowledged?from=${from}&to=${to}`);
32 | };
33 |
34 | export const getGlobalDeadLetteredTimeSeries = async (from: number, to: number) => {
35 | return axios.get(`${API_URL}/api/main/time-series/dead-lettered?from=${from}&to=${to}`);
36 | };
37 |
38 | export const getGlobalPublishedTimeSeries = async (from: number, to: number) => {
39 | return axios.get(`${API_URL}/api/main/time-series/published?from=${from}&to=${to}`);
40 | };
41 |
42 | export const getConsumerAcknowledgedTimeSeries = async (consumerId: string, from: number, to: number) => {
43 | return axios.get(
44 | `${API_URL}/api/consumers/${consumerId}/time-series/acknowledged?from=${from}&to=${to}`
45 | );
46 | };
47 |
48 | export const getConsumerDeadLetteredTimeSeries = async (consumerId: string, from: number, to: number) => {
49 | return axios.get(
50 | `${API_URL}/api/consumers/${consumerId}/time-series/dead-lettered?from=${from}&to=${to}`
51 | );
52 | };
53 |
--------------------------------------------------------------------------------
/src/client/transport/websocket/streams/consumerHeartbeatStream.ts:
--------------------------------------------------------------------------------
1 | export type TConsumerHeartbeatStreamPayload = {
2 | timestamp: number;
3 | data: {
4 | ram: { usage: NodeJS.MemoryUsage; free: number; total: number };
5 | cpu: { user: number; system: number; percentage: string };
6 | };
7 | };
--------------------------------------------------------------------------------
/src/client/transport/websocket/streams/mainStream.ts:
--------------------------------------------------------------------------------
1 | export type TWebsocketMainStreamPayload = {
2 | scheduledMessagesCount: number;
3 | queuesCount: number;
4 | deadLetteredMessagesCount: number;
5 | acknowledgedMessagesCount: number;
6 | pendingMessagesCount: number;
7 | consumersCount: number;
8 | queues: {
9 | [ns: string]: {
10 | [queueName: string]: TWebsocketMainStreamPayloadQueue;
11 | };
12 | };
13 | };
14 |
15 | export type TWebsocketMainStreamPayloadQueue = {
16 | name: string;
17 | ns: string;
18 | priorityQueuing: boolean;
19 | type: EQueueType;
20 | rateLimit: {
21 | interval: number;
22 | limit: number;
23 | } | null;
24 | deadLetteredMessagesCount: number;
25 | acknowledgedMessagesCount: number;
26 | pendingMessagesCount: number;
27 | consumersCount: number;
28 | };
29 |
30 | export enum EQueueType {
31 | LIFO_QUEUE,
32 | FIFO_QUEUE,
33 | PRIORITY_QUEUE,
34 | }
35 |
--------------------------------------------------------------------------------
/src/client/transport/websocket/streams/queueConsumersStream.ts:
--------------------------------------------------------------------------------
1 | export type TWebsocketQueueConsumersPayload = {
2 | [id: string]: TWebsocketQueueConsumersPayloadConsumerInfo;
3 | };
4 |
5 | export type TWebsocketQueueConsumersPayloadConsumerInfo = {
6 | ipAddress: string[];
7 | hostname: string;
8 | pid: number;
9 | createdAt: number;
10 | }
--------------------------------------------------------------------------------
/src/client/transport/websocket/streams/queueOnlineConsumersStream.ts:
--------------------------------------------------------------------------------
1 | export type TQueueOnlineConsumersStreamPayload = {
2 | ids: string[];
3 | };
4 |
--------------------------------------------------------------------------------
/src/client/transport/websocket/streams/rateStream.ts:
--------------------------------------------------------------------------------
1 | export type TRateStream = TRateStreamItem[];
2 |
3 | export interface TRateStreamItem {
4 | timestamp: number;
5 | value: number;
6 | }
7 |
--------------------------------------------------------------------------------
/src/client/transport/websocket/websocket.ts:
--------------------------------------------------------------------------------
1 | import { io, Socket } from 'socket.io-client';
2 | import { HOST } from '../endpoints';
3 |
4 | let socket: Socket | null = null;
5 |
6 | const connect = async (): Promise => {
7 | console.log('Trying to connect to WS server...');
8 | return new Promise((resolve, reject) => {
9 | const ws = io(HOST, { path: `${basePath === '/' ? '' : basePath}/socket.io/` });
10 | ws.once('connect', () => {
11 | console.log('Successfully connected to WS server.');
12 | resolve(ws);
13 | });
14 | ws.once('connect_error', (e: Error) => {
15 | console.error('An error occurred while trying to connect to WS server.');
16 | reject(e);
17 | });
18 | });
19 | };
20 |
21 | const Websocket = async () => {
22 | if (!socket) {
23 | socket = await connect();
24 | socket.once('disconnect', () => {
25 | socket = null;
26 | });
27 | }
28 | return socket;
29 | };
30 |
31 | export default Websocket;
32 |
--------------------------------------------------------------------------------
/src/server/middleware.ts:
--------------------------------------------------------------------------------
1 | import Koa from 'koa';
2 | import send from 'koa-send';
3 | import { promises, constants } from 'fs';
4 | import { join, posix } from 'path';
5 | import { tmpdir } from 'os';
6 | import { renderFile } from 'ejs';
7 |
8 | const assetsDir = join(__dirname, '..', '..');
9 | let tmpDir: string | null = null;
10 |
11 | async function serveIndex(ctx: Koa.ParameterizedContext, basePath: string): Promise {
12 | if (!tmpDir) {
13 | tmpDir = await promises.mkdtemp(join(tmpdir(), 'redis-smq-monitor-'));
14 | }
15 | const indexHTML = join(tmpDir, 'index.html');
16 | try {
17 | await promises.access(indexHTML, constants.F_OK);
18 | } catch {
19 | const html = await renderFile(join(assetsDir, 'assets', 'index.html'), { basePath });
20 | await promises.writeFile(indexHTML, html);
21 | }
22 | await send(ctx, 'index.html', { root: tmpDir });
23 | }
24 |
25 | export const Middleware = (ignorePaths: string[] = [], basePath = '/'): Koa.Middleware => async (ctx, next) => {
26 | // remove trailing "/" and transform relative paths to absolute paths
27 | const normalizedBasePath = basePath === '/' ? basePath : posix.resolve('/', basePath);
28 | if (
29 | !ignorePaths.length ||
30 | ignorePaths.find((i) => {
31 | const item = posix.resolve('/', i);
32 | return ctx.path.indexOf(item) === 0;
33 | }) === undefined
34 | ) {
35 | await ('/' === ctx.path || ctx.path.indexOf('/assets/') !== 0
36 | ? serveIndex(ctx, normalizedBasePath)
37 | : send(ctx, ctx.path, { root: assetsDir }));
38 | } else await next();
39 | };
40 |
--------------------------------------------------------------------------------
/tsconfig-server.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "module": "commonjs",
4 | "declaration": true,
5 | "removeComments": true,
6 | "emitDecoratorMetadata": true,
7 | "experimentalDecorators": true,
8 | "allowSyntheticDefaultImports": true,
9 | "esModuleInterop": true,
10 | "target": "es6",
11 | "sourceMap": true,
12 | "outDir": "./dist",
13 | "incremental": true,
14 | "strict": true,
15 | "skipLibCheck": true,
16 | "strictPropertyInitialization": true
17 | },
18 | "exclude": ["./node_modules", "dist", "**/*spec.ts", "examples", "src/client", "dev-server"]
19 | }
20 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "sourceMap": true,
4 | "target": "es6",
5 | "jsx": "react",
6 | "module": "esnext",
7 | "moduleResolution": "node",
8 | "experimentalDecorators": true,
9 | "allowSyntheticDefaultImports": true,
10 | "declaration": false,
11 | "removeComments": true,
12 | "noImplicitReturns": true,
13 | "noUnusedLocals": false,
14 | "strict": true,
15 | "outDir": "dist",
16 | "lib": ["es6", "es7", "dom"],
17 | "baseUrl": "src",
18 | "resolveJsonModule": true,
19 | "rootDir": "./",
20 | },
21 | "exclude": ["dist", "build", "node_modules", "src/server"]
22 | }
23 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | const webpack = require('webpack');
2 | const path = require('path');
3 | const pkg = require('./package.json');
4 |
5 | // variables
6 | const isProduction = process.env.NODE_ENV === 'production';
7 | const sourcePath = path.join(__dirname, './src/client');
8 | const outPath = path.join(__dirname, './dist');
9 |
10 | // Only prod and dev environments are supported
11 | const env = isProduction ? 'production' : 'development';
12 |
13 | // plugins
14 | const HtmlWebpackPlugin = require('html-webpack-plugin');
15 | const MiniCssExtractPlugin = require('mini-css-extract-plugin');
16 | const { CleanWebpackPlugin } = require('clean-webpack-plugin');
17 |
18 | module.exports = {
19 | mode: env,
20 | context: sourcePath,
21 | entry: {
22 | app: './index.tsx'
23 | },
24 | output: {
25 | path: `${outPath}/assets/`,
26 | publicPath: isProduction ? 'assets/' : '/',
27 | filename: isProduction ? '[contenthash].js' : '[chunkhash].js',
28 | chunkFilename: isProduction ? '[name].[contenthash].js' : '[name].[chunkhash].js'
29 | },
30 | target: 'web',
31 | resolve: {
32 | extensions: ['.js', '.ts', '.tsx']
33 | },
34 | module: {
35 | rules: [
36 | {
37 | test: /\.tsx?$/,
38 | use: [
39 | !isProduction && {
40 | loader: 'babel-loader',
41 | options: { plugins: ['react-hot-loader/babel'] }
42 | },
43 | 'ts-loader'
44 | ].filter(Boolean)
45 | },
46 | {
47 | test: /\.css$/,
48 | use: [
49 | isProduction ? MiniCssExtractPlugin.loader : 'style-loader',
50 | {
51 | loader: 'css-loader',
52 | options: {
53 | sourceMap: !isProduction,
54 | importLoaders: 1
55 | }
56 | }
57 | ]
58 | },
59 | // static assets
60 | {
61 | test: /\.html$/,
62 | use: [
63 | !isProduction && {
64 | loader: 'string-replace-loader',
65 | options: {
66 | search: '<%= basePath; %>',
67 | replace: '/',
68 | flags: 'g'
69 | }
70 | },
71 | {
72 | loader: 'html-loader'
73 | }
74 | ].filter(Boolean)
75 | },
76 | { test: /\.(a?png|svg)$/, use: 'url-loader?limit=10000' },
77 | {
78 | test: /\.(jpe?g|gif|bmp|mp3|mp4|ogg|wav|eot|ttf|woff|woff2)$/,
79 | use: 'file-loader'
80 | }
81 | ]
82 | },
83 | optimization: {
84 | splitChunks: {
85 | chunks: 'all'
86 | },
87 | runtimeChunk: true
88 | },
89 | plugins: [
90 | new webpack.EnvironmentPlugin({
91 | NODE_ENV: env,
92 | API_URL: isProduction ? null : 'http://localhost:3000',
93 | DEBUG: !isProduction
94 | }),
95 | new CleanWebpackPlugin(),
96 | new MiniCssExtractPlugin({
97 | filename: '[name].[contenthash].css'
98 | }),
99 | new HtmlWebpackPlugin({
100 | template: './index.html',
101 | minify: {
102 | minifyJS: true,
103 | minifyCSS: true,
104 | minifyURLs: true,
105 | removeComments: true,
106 | removeRedundantAttributes: true,
107 | removeStyleLinkTypeAttributes: true,
108 | useShortDoctype: true,
109 | collapseWhitespace: true,
110 | collapseInlineTagWhitespace: true
111 | },
112 | meta: {
113 | title: pkg.name,
114 | description: pkg.description,
115 | keywords: Array.isArray(pkg.keywords) ? pkg.keywords.join(',') : undefined
116 | }
117 | })
118 | ],
119 | devServer: {
120 | static: {
121 | directory: sourcePath
122 | },
123 | hot: true,
124 | historyApiFallback: true,
125 | client: {
126 | logging: 'log'
127 | },
128 | headers: {
129 | 'Access-Control-Allow-Origin': '*',
130 | 'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, PATCH, OPTIONS',
131 | 'Access-Control-Allow-Headers': 'X-Requested-With, content-type, Authorization'
132 | }
133 | },
134 | devtool: isProduction ? false : 'cheap-module-source-map'
135 | };
136 |
--------------------------------------------------------------------------------