Skip to content

Commit

Permalink
feat: Allow to pass uiConfig from server config to client. (#504)
Browse files Browse the repository at this point in the history
feat: Allow to change board title
feat: Allow to change board logo

closes #503
  • Loading branch information
felixmosh authored Dec 15, 2022
1 parent a0646a9 commit afcea5d
Show file tree
Hide file tree
Showing 13 changed files with 124 additions and 27 deletions.
7 changes: 6 additions & 1 deletion example.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
9 changes: 6 additions & 3 deletions packages/api/src/index.ts
Original file line number Diff line number Diff line change
@@ -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<BaseAdapter>;
serverAdapter: IServerAdapter;
options?: BoardOptions;
}) {
const { bullBoardQueues, setQueues, replaceQueues, addQueue, removeQueue } = getQueuesApi(queues);
const uiBasePath = path.dirname(eval(`require.resolve('@bull-board/ui/package.json')`));
Expand All @@ -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);
Expand Down
13 changes: 12 additions & 1 deletion packages/api/typings/app.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand Down Expand Up @@ -148,6 +148,8 @@ export interface IServerAdapter {
setErrorHandler(handler: (error: Error) => ControllerHandlerReturnType): IServerAdapter;

setApiRoutes(routes: AppControllerRoute[]): IServerAdapter;

setUIConfig(config: UIConfig): IServerAdapter;
}

export interface Pagination {
Expand All @@ -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 };
}>;
17 changes: 15 additions & 2 deletions packages/express/src/ExpressAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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();
Expand Down Expand Up @@ -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, '\\u003c')
.replace(/>/g, '\\u003e');

res.render(name, {
basePath,
uiConfig,
});
};

this.app[routeDef.method](routeDef.route, viewHandler);
Expand All @@ -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;
}
Expand Down
12 changes: 11 additions & 1 deletion packages/fastify/src/FastifyAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
BullBoardQueues,
ControllerHandlerReturnType,
IServerAdapter,
UIConfig,
} from '@bull-board/api/dist/typings/app';

import fastifyStatic from '@fastify/static';
Expand All @@ -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<FastifyRouteDef> | undefined;
private uiConfig: UIConfig = {};

public setBasePath(path: string): FastifyAdapter {
this.basePath = path;
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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, '\\u003c')
.replace(/>/g, '\\u003e');

return reply.view(filename, { basePath });
return reply.view(filename, { basePath, uiConfig });
},
})
);
Expand Down
13 changes: 12 additions & 1 deletion packages/hapi/src/HapiAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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;
Expand Down Expand Up @@ -76,6 +78,11 @@ export class HapiAdapter implements IServerAdapter {
return this;
}

public setUIConfig(config: UIConfig = {}): HapiAdapter {
this.uiConfig = config;
return this;
}

public registerPlugin(): PluginBase<any> & PluginPackage {
return {
pkg: require('../package.json'),
Expand Down Expand Up @@ -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, '\\u003c')
.replace(/>/g, '\\u003e');

return h.view(name, { basePath, uiConfig });
},
})
);
Expand Down
13 changes: 12 additions & 1 deletion packages/koa/src/KoaAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
BullBoardQueues,
ControllerHandlerReturnType,
IServerAdapter,
UIConfig,
} from '@bull-board/api/dist/typings/app';

import Koa from 'koa';
Expand All @@ -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;
Expand Down Expand Up @@ -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'`);
Expand Down Expand Up @@ -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, '\\u003c')
.replace(/>/g, '\\u003e');

await (ctx as any).render(name, { basePath, uiConfig });
});
});

Expand Down
11 changes: 8 additions & 3 deletions packages/ui/src/components/Header/Header.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
36 changes: 25 additions & 11 deletions packages/ui/src/components/Header/Header.tsx
Original file line number Diff line number Diff line change
@@ -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<any>) => (
<header className={s.header}>
<div className={s.logo}>
<img src={getStaticPath('/images/logo.svg')} alt="Bull Dashboard" />
Bull Dashboard
</div>
<div className={cn(s.content, { [s.positionRight]: React.Children.count(children) === 1 })}>
{children}
</div>
</header>
);
export const Header = ({ children }: PropsWithChildren<any>) => {
const uiConfig = useUIConfig();
const logoPath = uiConfig.boardLogo?.path ?? getStaticPath('/images/logo.svg');
const boardTitle = uiConfig.boardTitle ?? 'Bull Dashboard';
return (
<header className={s.header}>
<div className={s.logo}>
{!!logoPath && (
<img
src={logoPath}
className={cn(s.img, { [s.default]: !uiConfig.boardLogo })}
width={uiConfig.boardLogo?.width}
height={uiConfig.boardLogo?.height}
alt={boardTitle}
/>
)}
{boardTitle}
</div>
<div className={cn(s.content, { [s.positionRight]: React.Children.count(children) === 1 })}>
{children}
</div>
</header>
);
};
8 changes: 8 additions & 0 deletions packages/ui/src/hooks/useUIConfig.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { UIConfig } from '@bull-board/api/dist/typings/app';
import React, { useContext } from 'react';

export const UIConfigContext = React.createContext<UIConfig>(null as any);

export function useUIConfig() {
return useContext(UIConfigContext);
}
1 change: 1 addition & 0 deletions packages/ui/src/index.ejs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
<link href="https://fonts.googleapis.com/css2?family=Ubuntu:wght@300;400;500&display=swap" rel="stylesheet">
</head>
<body>
<script id="__UI_CONFIG__" type="application/json"><%= uiConfig %></script>
<div id="root">Loading...</div>
</body>
</html>
10 changes: 7 additions & 3 deletions packages/ui/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,23 @@ 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';

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(
<BrowserRouter basename={basePath}>
<ApiContext.Provider value={api}>
<App />
</ApiContext.Provider>
<UIConfigContext.Provider value={uiConfig}>
<ApiContext.Provider value={api}>
<App />
</ApiContext.Provider>
</UIConfigContext.Provider>
</BrowserRouter>,
document.getElementById('root')
);
1 change: 1 addition & 0 deletions packages/ui/webpack.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ module.exports = {
template: './src/index.ejs',
templateParameters: {
basePath,
uiConfig: '<%- uiConfig %>',
},
inject: 'body',
}),
Expand Down

0 comments on commit afcea5d

Please sign in to comment.