From afcea5d9e1c6b7e62ac571e693425f41c4114aad Mon Sep 17 00:00:00 2001 From: Felix Mosheev <9304194+felixmosh@users.noreply.github.com> Date: Thu, 15 Dec 2022 18:13:23 +0200 Subject: [PATCH] feat: Allow to pass uiConfig from server config to client. (#504) feat: Allow to change board title feat: Allow to change board logo closes #503 --- example.ts | 7 +++- packages/api/src/index.ts | 9 +++-- packages/api/typings/app.ts | 13 ++++++- packages/express/src/ExpressAdapter.ts | 17 +++++++-- packages/fastify/src/FastifyAdapter.ts | 12 ++++++- packages/hapi/src/HapiAdapter.ts | 13 ++++++- packages/koa/src/KoaAdapter.ts | 13 ++++++- .../src/components/Header/Header.module.css | 11 ++++-- packages/ui/src/components/Header/Header.tsx | 36 +++++++++++++------ packages/ui/src/hooks/useUIConfig.ts | 8 +++++ packages/ui/src/index.ejs | 1 + packages/ui/src/index.tsx | 10 ++++-- packages/ui/webpack.config.js | 1 + 13 files changed, 124 insertions(+), 27 deletions(-) create mode 100644 packages/ui/src/hooks/useUIConfig.ts diff --git a/example.ts b/example.ts index 25974fec..bad3049c 100644 --- a/example.ts +++ b/example.ts @@ -2,7 +2,12 @@ import * as Bull from 'bull'; import Queue3 from 'bull'; import { Queue as QueueMQ, QueueScheduler, Worker } from 'bullmq'; import express from 'express'; -import { ExpressAdapter, createBullBoard, BullMQAdapter, BullAdapter } from '@bull-board/express/src'; +import { + ExpressAdapter, + createBullBoard, + BullMQAdapter, + BullAdapter, +} from '@bull-board/express/src'; const redisOptions = { port: 6379, diff --git a/packages/api/src/index.ts b/packages/api/src/index.ts index 68f4d72d..e762578d 100644 --- a/packages/api/src/index.ts +++ b/packages/api/src/index.ts @@ -1,16 +1,18 @@ +import path from 'path'; +import { BoardOptions, IServerAdapter } from '../typings/app'; +import { errorHandler } from './handlers/error'; import { BaseAdapter } from './queueAdapters/base'; -import { IServerAdapter } from '../typings/app'; import { getQueuesApi } from './queuesApi'; -import path from 'path'; import { appRoutes } from './routes'; -import { errorHandler } from './handlers/error'; export function createBullBoard({ queues, serverAdapter, + options = { uiConfig: {} }, }: { queues: ReadonlyArray; serverAdapter: IServerAdapter; + options?: BoardOptions; }) { const { bullBoardQueues, setQueues, replaceQueues, addQueue, removeQueue } = getQueuesApi(queues); const uiBasePath = path.dirname(eval(`require.resolve('@bull-board/ui/package.json')`)); @@ -19,6 +21,7 @@ export function createBullBoard({ .setQueues(bullBoardQueues) .setViewsPath(path.join(uiBasePath, 'dist')) .setStaticPath('/static', path.join(uiBasePath, 'dist/static')) + .setUIConfig(options.uiConfig) .setEntryRoute(appRoutes.entryPoint) .setErrorHandler(errorHandler) .setApiRoutes(appRoutes.api); diff --git a/packages/api/typings/app.ts b/packages/api/typings/app.ts index 8b425099..6f0aad03 100644 --- a/packages/api/typings/app.ts +++ b/packages/api/typings/app.ts @@ -1,6 +1,6 @@ import { RedisInfo } from 'redis-info'; -import { BaseAdapter } from '../src/queueAdapters/base'; import { STATUSES } from '../src/constants/statuses'; +import { BaseAdapter } from '../src/queueAdapters/base'; export type JobCleanStatus = 'completed' | 'wait' | 'active' | 'delayed' | 'failed'; @@ -148,6 +148,8 @@ export interface IServerAdapter { setErrorHandler(handler: (error: Error) => ControllerHandlerReturnType): IServerAdapter; setApiRoutes(routes: AppControllerRoute[]): IServerAdapter; + + setUIConfig(config: UIConfig): IServerAdapter; } export interface Pagination { @@ -159,3 +161,12 @@ export interface Pagination { } export type FormatterField = 'data' | 'returnValue' | 'name'; + +export type BoardOptions = { + uiConfig: UIConfig; +}; + +export type UIConfig = Partial<{ + boardTitle: string; + boardLogo: { path: string; width?: number | string; height?: number | string }; +}>; diff --git a/packages/express/src/ExpressAdapter.ts b/packages/express/src/ExpressAdapter.ts index 8e8a8669..7db97d65 100644 --- a/packages/express/src/ExpressAdapter.ts +++ b/packages/express/src/ExpressAdapter.ts @@ -5,6 +5,7 @@ import { ControllerHandlerReturnType, HTTPMethod, IServerAdapter, + UIConfig, } from '@bull-board/api/dist/typings/app'; import ejs from 'ejs'; import express, { Express, NextFunction, Request, Response, Router } from 'express'; @@ -15,6 +16,7 @@ export class ExpressAdapter implements IServerAdapter { private basePath = ''; private bullBoardQueues: BullBoardQueues | undefined; private errorHandler: ((error: Error) => ControllerHandlerReturnType) | undefined; + private uiConfig: UIConfig = {}; constructor() { this.app = express(); @@ -88,8 +90,14 @@ export class ExpressAdapter implements IServerAdapter { const viewHandler = (_req: Request, res: Response) => { const basePath = this.basePath.endsWith('/') ? this.basePath : `${this.basePath}/`; - - res.render(name, { basePath }); + const uiConfig = JSON.stringify(this.uiConfig) + .replace(//g, '\\u003e'); + + res.render(name, { + basePath, + uiConfig, + }); }; this.app[routeDef.method](routeDef.route, viewHandler); @@ -101,6 +109,11 @@ export class ExpressAdapter implements IServerAdapter { return this; } + setUIConfig(config: UIConfig = {}): ExpressAdapter { + this.uiConfig = config; + return this; + } + public getRouter(): any { return this.app; } diff --git a/packages/fastify/src/FastifyAdapter.ts b/packages/fastify/src/FastifyAdapter.ts index 600b4963..2e9d1812 100644 --- a/packages/fastify/src/FastifyAdapter.ts +++ b/packages/fastify/src/FastifyAdapter.ts @@ -4,6 +4,7 @@ import { BullBoardQueues, ControllerHandlerReturnType, IServerAdapter, + UIConfig, } from '@bull-board/api/dist/typings/app'; import fastifyStatic from '@fastify/static'; @@ -25,6 +26,7 @@ export class FastifyAdapter implements IServerAdapter { private viewPath: string | undefined; private entryRoute: { method: HTTPMethods; routes: string[]; filename: string } | undefined; private apiRoutes: Array | undefined; + private uiConfig: UIConfig = {}; public setBasePath(path: string): FastifyAdapter { this.basePath = path; @@ -82,6 +84,11 @@ export class FastifyAdapter implements IServerAdapter { return this; } + public setUIConfig(config: UIConfig = {}): FastifyAdapter { + this.uiConfig = config; + return this; + } + public registerPlugin() { return (fastify: FastifyInstance, _opts: { basePath: string }, next: (err?: Error) => void) => { if (!this.statics) { @@ -117,8 +124,11 @@ export class FastifyAdapter implements IServerAdapter { url, handler: (_req, reply) => { const basePath = this.basePath.endsWith('/') ? this.basePath : `${this.basePath}/`; + const uiConfig = JSON.stringify(this.uiConfig) + .replace(//g, '\\u003e'); - return reply.view(filename, { basePath }); + return reply.view(filename, { basePath, uiConfig }); }, }) ); diff --git a/packages/hapi/src/HapiAdapter.ts b/packages/hapi/src/HapiAdapter.ts index 3b611121..1b41a156 100644 --- a/packages/hapi/src/HapiAdapter.ts +++ b/packages/hapi/src/HapiAdapter.ts @@ -4,6 +4,7 @@ import { BullBoardQueues, ControllerHandlerReturnType, IServerAdapter, + UIConfig, } from '@bull-board/api/dist/typings/app'; import { PluginBase, PluginPackage } from '@hapi/hapi'; import Vision from '@hapi/vision'; @@ -24,6 +25,7 @@ export class HapiAdapter implements IServerAdapter { private viewPath: string | undefined; private entryRoute: AppViewRoute | undefined; private apiRoutes: HapiRouteDef[] | undefined; + private uiConfig: UIConfig = {}; public setBasePath(path: string): HapiAdapter { this.basePath = path; @@ -76,6 +78,11 @@ export class HapiAdapter implements IServerAdapter { return this; } + public setUIConfig(config: UIConfig = {}): HapiAdapter { + this.uiConfig = config; + return this; + } + public registerPlugin(): PluginBase & PluginPackage { return { pkg: require('../package.json'), @@ -127,7 +134,11 @@ export class HapiAdapter implements IServerAdapter { handler: (_request, h) => { const { name } = handler(); const basePath = this.basePath.endsWith('/') ? this.basePath : `${this.basePath}/`; - return h.view(name, { basePath }); + const uiConfig = JSON.stringify(this.uiConfig) + .replace(//g, '\\u003e'); + + return h.view(name, { basePath, uiConfig }); }, }) ); diff --git a/packages/koa/src/KoaAdapter.ts b/packages/koa/src/KoaAdapter.ts index 9c0caaab..cacbf26c 100644 --- a/packages/koa/src/KoaAdapter.ts +++ b/packages/koa/src/KoaAdapter.ts @@ -4,6 +4,7 @@ import { BullBoardQueues, ControllerHandlerReturnType, IServerAdapter, + UIConfig, } from '@bull-board/api/dist/typings/app'; import Koa from 'koa'; @@ -21,6 +22,7 @@ export class KoaAdapter implements IServerAdapter { private viewPath: string | undefined; private entryRoute: AppViewRoute | undefined; private apiRoutes: AppControllerRoute[] | undefined; + private uiConfig: UIConfig = {}; public setBasePath(path: string): KoaAdapter { this.basePath = path; @@ -60,6 +62,11 @@ export class KoaAdapter implements IServerAdapter { return this; } + public setUIConfig(config: UIConfig = {}): KoaAdapter { + this.uiConfig = config; + return this; + } + public registerPlugin(options: Partial<{ mount: string }> = { mount: this.basePath }) { if (!this.statics) { throw new Error(`Please call 'setStaticPath' before using 'registerPlugin'`); @@ -110,7 +117,11 @@ export class KoaAdapter implements IServerAdapter { router[method](path, async (ctx) => { const { name } = handler(); const basePath = this.basePath.endsWith('/') ? this.basePath : `${this.basePath}/`; - await (ctx as any).render(name, { basePath }); + const uiConfig = JSON.stringify(this.uiConfig) + .replace(//g, '\\u003e'); + + await (ctx as any).render(name, { basePath, uiConfig }); }); }); diff --git a/packages/ui/src/components/Header/Header.module.css b/packages/ui/src/components/Header/Header.module.css index 9d56856a..d49c3242 100644 --- a/packages/ui/src/components/Header/Header.module.css +++ b/packages/ui/src/components/Header/Header.module.css @@ -26,12 +26,17 @@ border-bottom: 1px solid rgba(0, 0, 0, 0.4); box-shadow: 0 2px 5px 0 rgba(0, 0, 0, 0.1); z-index: 2; + display: flex; + justify-content: center; } -.header > .logo > img { - width: 1.2em; +.header > .logo > .img { margin-right: 0.3em; - margin-bottom: -0.2em; +} + +.header > .logo > .img.default { + width: 1.2em; + margin-bottom: 0.2em; } .header .content { diff --git a/packages/ui/src/components/Header/Header.tsx b/packages/ui/src/components/Header/Header.tsx index 18724165..ac182252 100644 --- a/packages/ui/src/components/Header/Header.tsx +++ b/packages/ui/src/components/Header/Header.tsx @@ -1,16 +1,30 @@ import cn from 'clsx'; import React, { PropsWithChildren } from 'react'; +import { useUIConfig } from '../../hooks/useUIConfig'; import s from './Header.module.css'; import { getStaticPath } from '../../utils/getStaticPath'; -export const Header = ({ children }: PropsWithChildren) => ( -
-
- Bull Dashboard - Bull Dashboard -
-
- {children} -
-
-); +export const Header = ({ children }: PropsWithChildren) => { + const uiConfig = useUIConfig(); + const logoPath = uiConfig.boardLogo?.path ?? getStaticPath('/images/logo.svg'); + const boardTitle = uiConfig.boardTitle ?? 'Bull Dashboard'; + return ( +
+
+ {!!logoPath && ( + {boardTitle} + )} + {boardTitle} +
+
+ {children} +
+
+ ); +}; diff --git a/packages/ui/src/hooks/useUIConfig.ts b/packages/ui/src/hooks/useUIConfig.ts new file mode 100644 index 00000000..bb74e601 --- /dev/null +++ b/packages/ui/src/hooks/useUIConfig.ts @@ -0,0 +1,8 @@ +import { UIConfig } from '@bull-board/api/dist/typings/app'; +import React, { useContext } from 'react'; + +export const UIConfigContext = React.createContext(null as any); + +export function useUIConfig() { + return useContext(UIConfigContext); +} diff --git a/packages/ui/src/index.ejs b/packages/ui/src/index.ejs index 417ab179..74fa1a76 100644 --- a/packages/ui/src/index.ejs +++ b/packages/ui/src/index.ejs @@ -11,6 +11,7 @@ +
Loading...
diff --git a/packages/ui/src/index.tsx b/packages/ui/src/index.tsx index 2bb7ab0b..b3666121 100644 --- a/packages/ui/src/index.tsx +++ b/packages/ui/src/index.tsx @@ -4,6 +4,7 @@ import { BrowserRouter } from 'react-router-dom'; import { App } from './components/App'; import { ApiContext } from './hooks/useApi'; import './index.css'; +import { UIConfigContext } from './hooks/useUIConfig'; import { Api } from './services/Api'; import './theme.css'; import './toastify.css'; @@ -11,12 +12,15 @@ import './toastify.css'; const basePath = ((window as any).__basePath__ = document.head.querySelector('base')?.getAttribute('href') || ''); const api = new Api({ basePath }); +const uiConfig = JSON.parse(document.getElementById('__UI_CONFIG__')?.textContent || '{}'); render( - - - + + + + + , document.getElementById('root') ); diff --git a/packages/ui/webpack.config.js b/packages/ui/webpack.config.js index a2702b7d..b320dc8f 100644 --- a/packages/ui/webpack.config.js +++ b/packages/ui/webpack.config.js @@ -96,6 +96,7 @@ module.exports = { template: './src/index.ejs', templateParameters: { basePath, + uiConfig: '<%- uiConfig %>', }, inject: 'body', }),