diff --git a/packages/api/src/handlers/redisStats.ts b/packages/api/src/handlers/redisStats.ts index 107d89cb..fe454d95 100644 --- a/packages/api/src/handlers/redisStats.ts +++ b/packages/api/src/handlers/redisStats.ts @@ -1,5 +1,5 @@ import { parse as parseRedisInfo } from 'redis-info'; -import { BullBoardRequest, ControllerHandlerReturnType, RedisStats } from '../../typings/app'; +import { BullBoardRequest, ControllerHandlerReturnType, RedisStatsOptions } from '../../typings/app'; import { BaseAdapter } from '../queueAdapters/base'; function formatUptime(uptime: number) { @@ -20,11 +20,11 @@ function formatUptime(uptime: number) { return segments.join(', '); } -async function getStats(queue: BaseAdapter): Promise { - const redisInfoRaw = await queue.getRedisInfo(); - const redisInfo = parseRedisInfo(redisInfoRaw); +async function getStats(queue: BaseAdapter): Promise { + const { rawInfo, options } = await queue.getRedisInfo(); + const redisInfo = parseRedisInfo(rawInfo); - return { + const stats = { version: redisInfo.redis_version, mode: redisInfo.redis_mode, port: +redisInfo.tcp_port, @@ -41,6 +41,11 @@ async function getStats(queue: BaseAdapter): Promise { blocked: +redisInfo.blocked_clients, }, }; + + return { + stats, + options, + }; } export async function redisStatsHandler({ diff --git a/packages/api/src/queueAdapters/base.ts b/packages/api/src/queueAdapters/base.ts index 192020dc..bdce27c9 100644 --- a/packages/api/src/queueAdapters/base.ts +++ b/packages/api/src/queueAdapters/base.ts @@ -5,6 +5,7 @@ import { JobStatus, QueueAdapterOptions, QueueJob, + RedisRawInfoOptions, Status, } from '../../typings/app'; @@ -59,7 +60,7 @@ export abstract class BaseAdapter { public abstract getName(): string; - public abstract getRedisInfo(): Promise; + public abstract getRedisInfo(): Promise; public abstract isPaused(): Promise; diff --git a/packages/api/src/queueAdapters/bull.ts b/packages/api/src/queueAdapters/bull.ts index c29c45bd..a7ab12a5 100644 --- a/packages/api/src/queueAdapters/bull.ts +++ b/packages/api/src/queueAdapters/bull.ts @@ -4,6 +4,7 @@ import { JobCounts, JobStatus, QueueAdapterOptions, + RedisRawInfoOptions, Status, } from '../../typings/app'; import { STATUSES } from '../constants/statuses'; @@ -14,8 +15,12 @@ export class BullAdapter extends BaseAdapter { super({ ...options, allowCompletedRetries: false }); } - public getRedisInfo(): Promise { - return this.queue.client.info(); + public getRedisInfo(): Promise { + return this.queue.client.info() + .then((rawInfo) => ({ + rawInfo, + options: this.queue.client.options, + })); } public getName(): string { diff --git a/packages/api/src/queueAdapters/bullMQ.ts b/packages/api/src/queueAdapters/bullMQ.ts index 8e0ebce2..c23fc329 100644 --- a/packages/api/src/queueAdapters/bullMQ.ts +++ b/packages/api/src/queueAdapters/bullMQ.ts @@ -4,6 +4,7 @@ import { JobCounts, JobStatus, QueueAdapterOptions, + RedisRawInfoOptions, Status, } from '../../typings/app'; import { STATUSES } from '../constants/statuses'; @@ -14,9 +15,14 @@ export class BullMQAdapter extends BaseAdapter { super(options); } - public async getRedisInfo(): Promise { + public async getRedisInfo(): Promise { const client = await this.queue.client; - return client.info(); + const rawInfo = await client.info(); + + return { + rawInfo, + options: client.options, + } } public getName(): string { diff --git a/packages/api/tests/api/index.spec.ts b/packages/api/tests/api/index.spec.ts index dce28a25..3a22e2bc 100644 --- a/packages/api/tests/api/index.spec.ts +++ b/packages/api/tests/api/index.spec.ts @@ -235,7 +235,7 @@ describe('happy', () => { }); }); - it('should get redis stats', async () => { + it('should get redis stats and options', async () => { const paintQueue = new Queue('Paint', { connection }); queueList.push(paintQueue); @@ -250,8 +250,10 @@ describe('happy', () => { .expect(200) .then((res) => { const responseJson = JSON.parse(res.text); + const { stats, options } = responseJson; - expect(responseJson).toHaveProperty('version', expect.stringMatching(/\d+\.\d+\.\d+/)); + expect(stats).toHaveProperty('version', expect.stringMatching(/\d+\.\d+\.\d+/)); + expect(options).toHaveProperty('host', connection.host); }); }); }); diff --git a/packages/api/typings/app.ts b/packages/api/typings/app.ts index 1cd1ca76..fff08251 100644 --- a/packages/api/typings/app.ts +++ b/packages/api/typings/app.ts @@ -1,4 +1,5 @@ import { RedisInfo } from 'redis-info'; +import { RedisOptions as RedisOpts } from 'ioredis'; import { STATUSES } from '../src/constants/statuses'; import { BaseAdapter } from '../src/queueAdapters/base'; @@ -88,6 +89,18 @@ export interface RedisStats { }; } +export interface RedisOptions extends RedisOpts {} + +export type RedisStatsOptions = { + stats: RedisStats, + options: RedisOptions +} + +export type RedisRawInfoOptions = { + rawInfo: string, + options: RedisOptions +} + export interface AppJob { id: QueueJobJson['id']; name: QueueJobJson['name']; diff --git a/packages/ui/src/App.tsx b/packages/ui/src/App.tsx index 0140ba08..bd03d6b4 100644 --- a/packages/ui/src/App.tsx +++ b/packages/ui/src/App.tsx @@ -7,7 +7,6 @@ import { HeaderActions } from './components/HeaderActions/HeaderActions'; import { Loader } from './components/Loader/Loader'; import { Menu } from './components/Menu/Menu'; import { Title } from './components/Title/Title'; -import { useActiveQueue } from './hooks/useActiveQueue'; import { useConfirm } from './hooks/useConfirm'; import { useQueues } from './hooks/useQueues'; import { useScrollTopOnNav } from './hooks/useScrollTopOnNav'; @@ -28,8 +27,7 @@ const OverviewPageLazy = React.lazy(() => export const App = () => { useScrollTopOnNav(); - const { queues, actions: queueActions } = useQueues(); - const activeQueue = useActiveQueue({ queues }); + const { actions: queueActions } = useQueues(); const { confirmProps } = useConfirm(); useEffect(() => { @@ -39,7 +37,7 @@ export const App = () => { return ( <>
- + <Title /> <HeaderActions /> </Header> <main> @@ -48,7 +46,7 @@ export const App = () => { <Switch> <Route path="/queue/:name/:jobId" - render={() => <JobPageLazy queue={activeQueue || null} />} + render={() => <JobPageLazy />} /> <Route path="/queue/:name" render={() => <QueuePageLazy />} /> @@ -58,7 +56,7 @@ export const App = () => { <ConfirmModal {...confirmProps} /> </div> </main> - <Menu queues={queues} /> + <Menu /> <ToastContainer /> </> ); diff --git a/packages/ui/src/components/Menu/Menu.module.css b/packages/ui/src/components/Menu/Menu.module.css index e4a88c56..ff8271ee 100644 --- a/packages/ui/src/components/Menu/Menu.module.css +++ b/packages/ui/src/components/Menu/Menu.module.css @@ -60,6 +60,14 @@ border-left-color: #4abec7; } +.redisOpts { + border-top: 1px solid hsl(206, 9%, 25%); + padding-top: 1rem; + text-align: center; + font-size: smaller; + color: #828e97; +} + .appVersion { text-align: center; } diff --git a/packages/ui/src/components/Menu/Menu.tsx b/packages/ui/src/components/Menu/Menu.tsx index 6f2a0fc0..7a688739 100644 --- a/packages/ui/src/components/Menu/Menu.tsx +++ b/packages/ui/src/components/Menu/Menu.tsx @@ -1,15 +1,17 @@ -import { AppQueue } from '@bull-board/api/typings/app'; -import cn from 'clsx'; import React, { useState } from 'react'; import { useTranslation } from 'react-i18next'; import { NavLink } from 'react-router-dom'; import { useSelectedStatuses } from '../../hooks/useSelectedStatuses'; +import { useQueues } from './../../hooks/useQueues'; import { links } from '../../utils/links'; import { SearchIcon } from '../Icons/Search'; +import { MenuFooter } from './MenuFooter'; import s from './Menu.module.css'; -export const Menu = ({ queues }: { queues: AppQueue[] | null }) => { +export const Menu = () => { const { t } = useTranslation(); + const { queues } = useQueues(); + const selectedStatuses = useSelectedStatuses(); const [searchTerm, setSearchTerm] = useState(''); @@ -52,7 +54,8 @@ export const Menu = ({ queues }: { queues: AppQueue[] | null }) => { </ul> )} </nav> - <div className={cn(s.appVersion, s.secondary)}>{process.env.APP_VERSION}</div> + + <MenuFooter /> </aside> ); }; diff --git a/packages/ui/src/components/Menu/MenuFooter.tsx b/packages/ui/src/components/Menu/MenuFooter.tsx new file mode 100644 index 00000000..12f2e8b3 --- /dev/null +++ b/packages/ui/src/components/Menu/MenuFooter.tsx @@ -0,0 +1,21 @@ +import cn from 'clsx'; +import React from 'react'; +import { useRedisOptions } from '../../hooks/useRedisOptions'; +import s from './Menu.module.css'; + +export const MenuFooter = () => { + const options = useRedisOptions(); + + return ( + <div> + {options && ( + <p className={s.redisOpts}> + {`${options.host}:${options.port}:${options.db}`} + </p> + )} + <div className={cn(s.appVersion, s.secondary)}> + {process.env.APP_VERSION} + </div> + </div> + ); +}; diff --git a/packages/ui/src/components/RedisStatsModal/RedisStatsModal.tsx b/packages/ui/src/components/RedisStatsModal/RedisStatsModal.tsx index 0c41bcd1..fbf2530e 100644 --- a/packages/ui/src/components/RedisStatsModal/RedisStatsModal.tsx +++ b/packages/ui/src/components/RedisStatsModal/RedisStatsModal.tsx @@ -1,4 +1,4 @@ -import { RedisStats } from '@bull-board/api/typings/app'; +import { RedisStatsOptions } from '@bull-board/api/typings/app'; import formatBytes from 'pretty-bytes'; import React, { useState } from 'react'; import { useTranslation } from 'react-i18next'; @@ -8,8 +8,8 @@ import { Modal } from '../Modal/Modal'; import s from './RedisStatsModal.module.css'; const getMemoryUsage = ( - used?: RedisStats['memory']['used'], - total?: RedisStats['memory']['total'] + used?: RedisStatsOptions["stats"]['memory']['used'], + total?: RedisStatsOptions["stats"]['memory']['total'] ) => { if (used === undefined) { return '-'; @@ -30,15 +30,17 @@ export interface RedisStatsModalProps { export const RedisStatsModal = ({ open, onClose }: RedisStatsModalProps) => { const { t } = useTranslation(); - const [stats, setStats] = useState<RedisStats>(null as any); + const [info, setInfo] = useState<RedisStatsOptions>(null as any); const api = useApi(); - useInterval(() => api.getStats().then((stats) => setStats(stats)), 5000); + useInterval(() => api.getStats().then((stats) => setInfo(stats)), 5000); - if (!stats) { + if (!info) { return null; } + const { stats } = info; + const items = [ { title: t('REDIS.MEMORY_USAGE'), diff --git a/packages/ui/src/components/Title/Title.tsx b/packages/ui/src/components/Title/Title.tsx index 5b71d028..c563b7e5 100644 --- a/packages/ui/src/components/Title/Title.tsx +++ b/packages/ui/src/components/Title/Title.tsx @@ -1,18 +1,21 @@ import React from 'react'; import s from './Title.module.css'; +import { useActiveQueue } from '../../hooks/useActiveQueue'; -interface TitleProps { - name?: string; - description?: string; -} +export const Title = () => { + const queue = useActiveQueue(); -export const Title = ({ name, description }: TitleProps) => ( - <div className={s.queueTitle}> - {!!name && ( - <> - <h1 className={s.name}>{name}</h1> - {!!description && <p className={s.description}>{description}</p>} - </> - )} - </div> -); + if (!queue) + return <div/> + + return ( + <div className={s.queueTitle}> + {queue.name && ( + <> + <h1 className={s.name}>{queue.name}</h1> + {queue.description && <p className={s.description}>{queue.description}</p>} + </> + )} + </div> + ) +}; diff --git a/packages/ui/src/hooks/useActiveQueue.ts b/packages/ui/src/hooks/useActiveQueue.ts index 4be30a65..323aec8a 100644 --- a/packages/ui/src/hooks/useActiveQueue.ts +++ b/packages/ui/src/hooks/useActiveQueue.ts @@ -1,15 +1,17 @@ import { AppQueue } from '@bull-board/api/typings/app'; import { useActiveQueueName } from './useActiveQueueName'; -import { QueuesState } from './useQueues'; +import { useQueues } from './useQueues'; -export function useActiveQueue(data: Pick<QueuesState, 'queues'>): AppQueue | null { - const activeQueueName = useActiveQueueName(); - if (!data.queues) { +export function useActiveQueue(): AppQueue | null { + const { queues } = useQueues(); + + if (!queues) { return null; } - const activeQueue = data.queues.find((q) => q.name === activeQueueName); + const activeQueueName = useActiveQueueName(); + const activeQueue = queues.find((q) => q.name === activeQueueName); return activeQueue || null; } diff --git a/packages/ui/src/hooks/useRedisOptions.ts b/packages/ui/src/hooks/useRedisOptions.ts new file mode 100644 index 00000000..b998676b --- /dev/null +++ b/packages/ui/src/hooks/useRedisOptions.ts @@ -0,0 +1,16 @@ +import React from 'react'; +import { RedisOptions } from '@bull-board/api/typings/app'; +import { useApi } from './useApi'; + +export function useRedisOptions(): RedisOptions | undefined { + const [options, setOptions] = React.useState<RedisOptions>(); + const api = useApi(); + + React.useEffect(() => { + api.getStats().then(({ options }) => { + setOptions(options) + }) + }, []); + + return options; +} diff --git a/packages/ui/src/pages/JobPage/JobPage.tsx b/packages/ui/src/pages/JobPage/JobPage.tsx index c0e6cb88..ca17c199 100644 --- a/packages/ui/src/pages/JobPage/JobPage.tsx +++ b/packages/ui/src/pages/JobPage/JobPage.tsx @@ -1,4 +1,4 @@ -import { AppQueue, JobRetryStatus } from '@bull-board/api/typings/app'; +import { JobRetryStatus } from '@bull-board/api/typings/app'; import cn from 'clsx'; import React from 'react'; import { useTranslation } from 'react-i18next'; @@ -6,14 +6,17 @@ import { Link, useHistory } from 'react-router-dom'; import { ArrowLeftIcon } from '../../components/Icons/ArrowLeft'; import { JobCard } from '../../components/JobCard/JobCard'; import { StickyHeader } from '../../components/StickyHeader/StickyHeader'; +import { useActiveQueue } from '../../hooks/useActiveQueue'; import { useJob } from '../../hooks/useJob'; import { useSelectedStatuses } from '../../hooks/useSelectedStatuses'; import { links } from '../../utils/links'; import buttonS from '../../components/Button/Button.module.css'; -export const JobPage = ({ queue }: { queue: AppQueue | null }) => { +export const JobPage = () => { const { t } = useTranslation(); const history = useHistory(); + + const queue = useActiveQueue(); const { job, status, actions } = useJob(); const selectedStatuses = useSelectedStatuses(); diff --git a/packages/ui/src/pages/QueuePage/QueuePage.tsx b/packages/ui/src/pages/QueuePage/QueuePage.tsx index 60a2d9f0..521b4880 100644 --- a/packages/ui/src/pages/QueuePage/QueuePage.tsx +++ b/packages/ui/src/pages/QueuePage/QueuePage.tsx @@ -16,9 +16,9 @@ import { links } from '../../utils/links'; export const QueuePage = () => { const { t } = useTranslation(); const selectedStatus = useSelectedStatuses(); - const { actions, queues } = useQueues(); + const { actions } = useQueues(); const { actions: jobActions } = useJob(); - const queue = useActiveQueue({ queues }); + const queue = useActiveQueue(); actions.pollQueues(); if (!queue) { diff --git a/packages/ui/src/services/Api.ts b/packages/ui/src/services/Api.ts index 5975d57e..3c5fb7ad 100644 --- a/packages/ui/src/services/Api.ts +++ b/packages/ui/src/services/Api.ts @@ -2,7 +2,7 @@ import { AppJob, JobCleanStatus, JobRetryStatus, - RedisStats, + RedisStatsOptions, Status, } from '@bull-board/api/typings/app'; import { GetJobResponse, GetQueuesResponse } from '@bull-board/api/typings/responses'; @@ -91,7 +91,7 @@ export class Api { return this.axios.put(`/queues/${encodeURIComponent(queueName)}/empty`); } - public getStats(): Promise<RedisStats> { + public getStats(): Promise<RedisStatsOptions> { return this.axios.get(`/redis/stats`); }