From 52100ea9794765b306a676766cfc101c466a31e7 Mon Sep 17 00:00:00 2001 From: ahmad anwar <7552088+AhmedAnwarHafez@users.noreply.github.com> Date: Thu, 2 Jan 2025 11:58:05 +1300 Subject: [PATCH 1/8] feat: add delimiter property to queue adapter and app queue interfaces This change enhances the flexibility of queue configurations by allowing the specification of a delimiter. --- packages/api/src/handlers/queues.ts | 1 + packages/api/src/queueAdapters/base.ts | 2 ++ packages/api/typings/app.ts | 2 ++ 3 files changed, 5 insertions(+) diff --git a/packages/api/src/handlers/queues.ts b/packages/api/src/handlers/queues.ts index 49bf5cfbf..4de6b99f0 100644 --- a/packages/api/src/handlers/queues.ts +++ b/packages/api/src/handlers/queues.ts @@ -90,6 +90,7 @@ async function getAppQueues( allowCompletedRetries: queue.allowCompletedRetries, isPaused, type: queue.type, + delimiter: queue.delimiter, }; }) ); diff --git a/packages/api/src/queueAdapters/base.ts b/packages/api/src/queueAdapters/base.ts index 4973d1f02..79e26b2a9 100644 --- a/packages/api/src/queueAdapters/base.ts +++ b/packages/api/src/queueAdapters/base.ts @@ -15,6 +15,7 @@ export abstract class BaseAdapter { public readonly allowRetries: boolean; public readonly allowCompletedRetries: boolean; public readonly prefix: string; + public readonly delimiter: string; public readonly description: string; public readonly type: QueueType; private formatters = new Map any>(); @@ -27,6 +28,7 @@ export abstract class BaseAdapter { this.allowRetries = this.readOnlyMode ? false : options.allowRetries !== false; this.allowCompletedRetries = this.allowRetries && options.allowCompletedRetries !== false; this.prefix = options.prefix || ''; + this.delimiter = options.delimiter || ''; this.description = options.description || ''; this.type = type; } diff --git a/packages/api/typings/app.ts b/packages/api/typings/app.ts index bcedbbe6d..fcc3fc41a 100644 --- a/packages/api/typings/app.ts +++ b/packages/api/typings/app.ts @@ -31,6 +31,7 @@ export interface QueueAdapterOptions { allowRetries: boolean; prefix: string; description: string; + delimiter: string; } export type BullBoardQueues = Map; @@ -117,6 +118,7 @@ export interface AppJob { export type QueueType = 'bull' | 'bullmq'; export interface AppQueue { + delimiter: string; name: string; description?: string; counts: Record; From 79e489b64859d116edfdcf148d62d63ad99a3ef6 Mon Sep 17 00:00:00 2001 From: ahmad anwar <7552088+AhmedAnwarHafez@users.noreply.github.com> Date: Thu, 2 Jan 2025 12:15:09 +1300 Subject: [PATCH 2/8] feat(Menu): implement hierarchical queue display and enhance styling - Introduced a new `QueueTree` component to render queues in a hierarchical structure based on a delimiter. - Updated the `Menu` component to utilize the new `QueueTree` for displaying queues. - Enhanced CSS styles for the menu, including padding adjustments and added margin for nested levels. --- .../ui/src/components/Menu/Menu.module.css | 6 +- packages/ui/src/components/Menu/Menu.tsx | 77 ++++++++++++------- packages/ui/src/utils/toTree.ts | 34 ++++++++ 3 files changed, 90 insertions(+), 27 deletions(-) create mode 100644 packages/ui/src/utils/toTree.ts diff --git a/packages/ui/src/components/Menu/Menu.module.css b/packages/ui/src/components/Menu/Menu.module.css index 03f91f5ca..1254c5e4d 100644 --- a/packages/ui/src/components/Menu/Menu.module.css +++ b/packages/ui/src/components/Menu/Menu.module.css @@ -31,7 +31,11 @@ .menu { list-style: none; - padding: 0; + padding: 0.5rem 0; +} + +.menuLevel { + margin-left: 20px; } .menu li + li { diff --git a/packages/ui/src/components/Menu/Menu.tsx b/packages/ui/src/components/Menu/Menu.tsx index a991f6f28..05e5968c6 100644 --- a/packages/ui/src/components/Menu/Menu.tsx +++ b/packages/ui/src/components/Menu/Menu.tsx @@ -7,18 +7,22 @@ import { useQueues } from './../../hooks/useQueues'; import { links } from '../../utils/links'; import { SearchIcon } from '../Icons/Search'; import s from './Menu.module.css'; +import { AppQueueTree, toTree } from '../../utils/toTree'; export const Menu = () => { const { t } = useTranslation(); const { queues } = useQueues(); - - const selectedStatuses = useSelectedStatuses(); const [searchTerm, setSearchTerm] = useState(''); + const tree = toTree( + queues?.filter((queue) => + queue.name?.toLowerCase().includes(searchTerm?.toLowerCase() as string) + ) || [] + ); + return ( ); }; + +function QueueTree({ tree }: { tree: AppQueueTree }) { + const { t } = useTranslation(); + const selectedStatuses = useSelectedStatuses(); + + const keys = Object.keys(tree).sort(); + if (keys.length === 0) return null; + + return ( +
+ {keys.map((key) => { + const node = tree[key]; + const isLeafNode = Object.keys(node.children).length === 0; + + return isLeafNode && node.queue?.name ? ( +
+ + {key} + {node.queue?.isPaused && [ {t('MENU.PAUSED')} ]} + +
+ ) : ( +
+ {key} + +
+ ); + })} +
+ ); +} diff --git a/packages/ui/src/utils/toTree.ts b/packages/ui/src/utils/toTree.ts new file mode 100644 index 000000000..457347541 --- /dev/null +++ b/packages/ui/src/utils/toTree.ts @@ -0,0 +1,34 @@ +import { AppQueue } from '@bull-board/api/typings/app'; + +export interface AppQueueTree { + [key: string]: { + children: AppQueueTree; + queue?: AppQueue; + }; +} + +export function toTree(array: AppQueue[]): AppQueueTree { + const tree: AppQueueTree = {}; + array.forEach((item) => { + if (item.delimiter) { + const levels = item.name.split(item.delimiter); + let current = tree; + + levels.forEach((level, index) => { + current[level] = current[level] || { children: {} }; + + if (index === levels.length - 1) { + current[level].queue = item; + } + + current = current[level].children; + }); + } else { + tree[item.name] = { + children: {}, + queue: item, + }; + } + }); + return tree; +} From 46c2cf719fe141bc8b4d105349a26b96e5ad423a Mon Sep 17 00:00:00 2001 From: ahmad anwar <7552088+AhmedAnwarHafez@users.noreply.github.com> Date: Thu, 2 Jan 2025 12:15:51 +1300 Subject: [PATCH 3/8] refactor(example.ts): update queue names and add delimiter support (hierarchy support) --- example.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/example.ts b/example.ts index 3a27c33e2..ecb4fa633 100644 --- a/example.ts +++ b/example.ts @@ -53,7 +53,7 @@ const run = async () => { const app = express(); const exampleBull = createQueue3('ExampleBull'); - const exampleBullMq = createQueueMQ('ExampleBullMQ'); + const exampleBullMq = createQueueMQ('Examples.BullMQ'); const flow = new FlowProducer({ connection: redisOptions }); setupBullProcessor(exampleBull); // needed only for example proposes @@ -135,7 +135,10 @@ const run = async () => { serverAdapter.setBasePath('/ui'); createBullBoard({ - queues: [new BullMQAdapter(exampleBullMq), new BullAdapter(exampleBull)], + queues: [ + new BullMQAdapter(exampleBullMq, { delimiter: '.' }), + new BullAdapter(exampleBull, { delimiter: '.' }), + ], serverAdapter, }); From 8b3367cbda8561780906cc5637f21a0034cfc931 Mon Sep 17 00:00:00 2001 From: ahmad anwar <7552088+AhmedAnwarHafez@users.noreply.github.com> Date: Thu, 2 Jan 2025 15:51:45 +1300 Subject: [PATCH 4/8] feat(menu): added two queue examples to demonestrate grouping in the menu component --- example.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/example.ts b/example.ts index ecb4fa633..e8f65af29 100644 --- a/example.ts +++ b/example.ts @@ -54,6 +54,8 @@ const run = async () => { const exampleBull = createQueue3('ExampleBull'); const exampleBullMq = createQueueMQ('Examples.BullMQ'); + const newRegistration = createQueueMQ('Notifications.User.NewRegistration'); + const resetPassword = createQueueMQ('Notifications.User.ResetPassword'); const flow = new FlowProducer({ connection: redisOptions }); setupBullProcessor(exampleBull); // needed only for example proposes @@ -138,6 +140,8 @@ const run = async () => { queues: [ new BullMQAdapter(exampleBullMq, { delimiter: '.' }), new BullAdapter(exampleBull, { delimiter: '.' }), + new BullMQAdapter(newRegistration, { delimiter: '.' }), + new BullMQAdapter(resetPassword, { delimiter: '.' }), ], serverAdapter, }); From 3c9b19ad3b690749372dd933302de8987a2fee04 Mon Sep 17 00:00:00 2001 From: ahmad anwar <7552088+AhmedAnwarHafez@users.noreply.github.com> Date: Thu, 2 Jan 2025 15:56:42 +1300 Subject: [PATCH 5/8] style(Menu): update CSS for improved layout and padding --- packages/ui/src/components/Menu/Menu.module.css | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/packages/ui/src/components/Menu/Menu.module.css b/packages/ui/src/components/Menu/Menu.module.css index 1254c5e4d..11a4eb193 100644 --- a/packages/ui/src/components/Menu/Menu.module.css +++ b/packages/ui/src/components/Menu/Menu.module.css @@ -35,18 +35,23 @@ } .menuLevel { - margin-left: 20px; + margin-left: 0.5rem; } -.menu li + li { - border-top: 1px solid hsl(206, 9%, 25%); +.menuLevel details { + padding: 0.5rem 1rem; + cursor: pointer; +} + +.menuLevel details summary { + padding: 0.5rem 0; } .menu a { color: inherit; text-decoration: none; display: block; - padding: 1rem 1.25rem; + padding: 0.5rem 1rem; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; From 505ee95562ec030390ab1164560b7be7a337d472 Mon Sep 17 00:00:00 2001 From: ahmad anwar <7552088+AhmedAnwarHafez@users.noreply.github.com> Date: Thu, 2 Jan 2025 16:20:42 +1300 Subject: [PATCH 6/8] fix(example.ts): added an example with a different delimiter --- example.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/example.ts b/example.ts index e8f65af29..b2522f88d 100644 --- a/example.ts +++ b/example.ts @@ -55,7 +55,7 @@ const run = async () => { const exampleBull = createQueue3('ExampleBull'); const exampleBullMq = createQueueMQ('Examples.BullMQ'); const newRegistration = createQueueMQ('Notifications.User.NewRegistration'); - const resetPassword = createQueueMQ('Notifications.User.ResetPassword'); + const resetPassword = createQueueMQ('Notifications:User:ResetPassword'); const flow = new FlowProducer({ connection: redisOptions }); setupBullProcessor(exampleBull); // needed only for example proposes @@ -141,7 +141,7 @@ const run = async () => { new BullMQAdapter(exampleBullMq, { delimiter: '.' }), new BullAdapter(exampleBull, { delimiter: '.' }), new BullMQAdapter(newRegistration, { delimiter: '.' }), - new BullMQAdapter(resetPassword, { delimiter: '.' }), + new BullMQAdapter(resetPassword, { delimiter: ':' }), ], serverAdapter, }); From e3a31a50fa6160bf614be5cb145dc79dcf072207 Mon Sep 17 00:00:00 2001 From: ahmad anwar <7552088+AhmedAnwarHafez@users.noreply.github.com> Date: Fri, 3 Jan 2025 06:48:35 +1300 Subject: [PATCH 7/8] refactor: renamed variable --- packages/ui/src/utils/toTree.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/ui/src/utils/toTree.ts b/packages/ui/src/utils/toTree.ts index 457347541..45fa001bc 100644 --- a/packages/ui/src/utils/toTree.ts +++ b/packages/ui/src/utils/toTree.ts @@ -7,26 +7,26 @@ export interface AppQueueTree { }; } -export function toTree(array: AppQueue[]): AppQueueTree { +export function toTree(queues: AppQueue[]): AppQueueTree { const tree: AppQueueTree = {}; - array.forEach((item) => { - if (item.delimiter) { - const levels = item.name.split(item.delimiter); + queues.forEach((queue) => { + if (queue.delimiter) { + const levels = queue.name.split(queue.delimiter); let current = tree; levels.forEach((level, index) => { current[level] = current[level] || { children: {} }; if (index === levels.length - 1) { - current[level].queue = item; + current[level].queue = queue; } current = current[level].children; }); } else { - tree[item.name] = { + tree[queue.name] = { children: {}, - queue: item, + queue: queue, }; } }); From 17784b08f6f1b23fc75d3c6f926216c7b7d6feb1 Mon Sep 17 00:00:00 2001 From: ahmad anwar <7552088+AhmedAnwarHafez@users.noreply.github.com> Date: Fri, 3 Jan 2025 07:28:55 +1300 Subject: [PATCH 8/8] perf(Menu, toTree): improved toTree to use arrays instead of objects for improved performance --- packages/ui/src/components/Menu/Menu.tsx | 28 +++++------ packages/ui/src/utils/toTree.ts | 63 +++++++++++++++--------- 2 files changed, 52 insertions(+), 39 deletions(-) diff --git a/packages/ui/src/components/Menu/Menu.tsx b/packages/ui/src/components/Menu/Menu.tsx index 05e5968c6..ed8245723 100644 --- a/packages/ui/src/components/Menu/Menu.tsx +++ b/packages/ui/src/components/Menu/Menu.tsx @@ -7,7 +7,7 @@ import { useQueues } from './../../hooks/useQueues'; import { links } from '../../utils/links'; import { SearchIcon } from '../Icons/Search'; import s from './Menu.module.css'; -import { AppQueueTree, toTree } from '../../utils/toTree'; +import { AppQueueTreeNode, toTree } from '../../utils/toTree'; export const Menu = () => { const { t } = useTranslation(); @@ -51,34 +51,32 @@ export const Menu = () => { ); }; -function QueueTree({ tree }: { tree: AppQueueTree }) { +function QueueTree({ tree }: { tree: AppQueueTreeNode }) { const { t } = useTranslation(); const selectedStatuses = useSelectedStatuses(); - const keys = Object.keys(tree).sort(); - if (keys.length === 0) return null; + if (!tree.children.length) return null; return (
- {keys.map((key) => { - const node = tree[key]; - const isLeafNode = Object.keys(node.children).length === 0; + {tree.children.map((node) => { + const isLeafNode = !node.children.length; - return isLeafNode && node.queue?.name ? ( -
+ return isLeafNode ? ( +
- {key} + {node.name} {node.queue?.isPaused && [ {t('MENU.PAUSED')} ]}
) : ( -
- {key} - +
+ {node.name} +
); })} diff --git a/packages/ui/src/utils/toTree.ts b/packages/ui/src/utils/toTree.ts index 45fa001bc..e444756b6 100644 --- a/packages/ui/src/utils/toTree.ts +++ b/packages/ui/src/utils/toTree.ts @@ -1,34 +1,49 @@ import { AppQueue } from '@bull-board/api/typings/app'; -export interface AppQueueTree { - [key: string]: { - children: AppQueueTree; - queue?: AppQueue; - }; +export interface AppQueueTreeNode { + name: string; + queue?: AppQueue; + children: AppQueueTreeNode[]; } -export function toTree(queues: AppQueue[]): AppQueueTree { - const tree: AppQueueTree = {}; +export function toTree(queues: AppQueue[]): AppQueueTreeNode { + const root: AppQueueTreeNode = { + name: 'root', + children: [], + }; + queues.forEach((queue) => { - if (queue.delimiter) { - const levels = queue.name.split(queue.delimiter); - let current = tree; + if (!queue.delimiter) { + // If no delimiter, add as direct child to root + root.children.push({ + name: queue.name, + queue, + children: [], + }); + return; + } - levels.forEach((level, index) => { - current[level] = current[level] || { children: {} }; + const parts = queue.name.split(queue.delimiter); + let currentLevel = root.children; + let currentPath = ''; - if (index === levels.length - 1) { - current[level].queue = queue; - } + parts.forEach((part, index) => { + currentPath = currentPath ? `${currentPath}${queue.delimiter}${part}` : part; + let node = currentLevel.find((n) => n.name === part); - current = current[level].children; - }); - } else { - tree[queue.name] = { - children: {}, - queue: queue, - }; - } + if (!node) { + node = { + name: part, + children: [], + // Only set queue data if we're at the leaf node + ...(index === parts.length - 1 ? { queue } : {}), + }; + currentLevel.push(node); + } + + currentLevel = node.children; + }); }); - return tree; + + return root; }