diff --git a/packages/ui/src/App.tsx b/packages/ui/src/App.tsx
new file mode 100644
index 00000000..55a13941
--- /dev/null
+++ b/packages/ui/src/App.tsx
@@ -0,0 +1,69 @@
+import React, { Suspense } from 'react';
+import { Route, Switch } from 'react-router-dom';
+import { ToastContainer } from 'react-toastify';
+import { ConfirmModal } from './components/ConfirmModal/ConfirmModal';
+import { Header } from './components/Header/Header';
+import { HeaderActions } from './components/HeaderActions/HeaderActions';
+import { Menu } from './components/Menu/Menu';
+import { Title } from './components/Title/Title';
+import { useActiveQueue } from './hooks/useActiveQueue';
+import { useScrollTopOnNav } from './hooks/useScrollTopOnNav';
+import { useStore } from './hooks/useStore';
+
+const QueuePageLazy = React.lazy(() =>
+ import('./pages/QueuePage/QueuePage').then(({ QueuePage }) => ({ default: QueuePage }))
+);
+
+const OverviewPageLazy = React.lazy(() =>
+ import('./pages/OverviewPage/OverviewPage').then(({ OverviewPage }) => ({
+ default: OverviewPage,
+ }))
+);
+
+export const App = () => {
+ useScrollTopOnNav();
+ const { state, actions, selectedStatuses, confirmProps } = useStore();
+ const activeQueue = useActiveQueue(state.data);
+
+ return (
+ <>
+
+
+
+ {state.loading ? (
+ 'Loading...'
+ ) : (
+ <>
+ 'Loading...'}>
+
+ (
+
+ )}
+ />
+
+ }
+ />
+
+
+
+ >
+ )}
+
+
+
+
+ >
+ );
+};
diff --git a/packages/ui/src/components/App.tsx b/packages/ui/src/components/App.tsx
deleted file mode 100644
index f60c9ddf..00000000
--- a/packages/ui/src/components/App.tsx
+++ /dev/null
@@ -1,60 +0,0 @@
-import React from 'react';
-import { Redirect, Route, Switch } from 'react-router-dom';
-import { ToastContainer } from 'react-toastify';
-import { useActiveQueue } from '../hooks/useActiveQueue';
-import { useScrollTopOnNav } from '../hooks/useScrollTopOnNav';
-import { useStore } from '../hooks/useStore';
-import { ConfirmModal } from './ConfirmModal/ConfirmModal';
-import { Header } from './Header/Header';
-import { HeaderActions } from './HeaderActions/HeaderActions';
-import { Menu } from './Menu/Menu';
-import { QueuePage } from './QueuePage/QueuePage';
-import { QueueTitle } from './QueueTitle/QueueTitle';
-
-export const App = () => {
- useScrollTopOnNav();
- const { state, actions, selectedStatuses, confirmProps } = useStore();
- const activeQueue = useActiveQueue(state.data);
-
- return (
- <>
-
-
-
- {state.loading ? (
- 'Loading...'
- ) : (
- <>
-
- (
-
- )}
- />
-
-
- {!!state.data &&
- Array.isArray(state.data?.queues) &&
- state.data.queues.length > 0 && (
-
- )}
-
-
-
- >
- )}
-
-
-
-
- >
- );
-};
diff --git a/packages/ui/src/components/Card/Card.module.css b/packages/ui/src/components/Card/Card.module.css
new file mode 100644
index 00000000..0dc2e62d
--- /dev/null
+++ b/packages/ui/src/components/Card/Card.module.css
@@ -0,0 +1,8 @@
+.card {
+ background-color: #fff;
+ box-shadow: 0 1px 1px 0 rgba(60, 75, 100, 0.14), 0 2px 1px -1px rgba(60, 75, 100, 0.12),
+ 0 1px 3px 0 rgba(60, 75, 100, 0.2);
+ border-radius: 0.25rem;
+ padding: 1em;
+ display: flex;
+}
diff --git a/packages/ui/src/components/Card/Card.tsx b/packages/ui/src/components/Card/Card.tsx
new file mode 100644
index 00000000..b75e1674
--- /dev/null
+++ b/packages/ui/src/components/Card/Card.tsx
@@ -0,0 +1,10 @@
+import cn from 'clsx';
+import React, { PropsWithChildren } from 'react';
+import s from './Card.module.css';
+
+interface ICardProps {
+ className?: string;
+}
+export const Card = ({ children, className }: PropsWithChildren) => (
+ {children}
+);
diff --git a/packages/ui/src/components/Header/Header.module.css b/packages/ui/src/components/Header/Header.module.css
index 77a44924..4c684a34 100644
--- a/packages/ui/src/components/Header/Header.module.css
+++ b/packages/ui/src/components/Header/Header.module.css
@@ -28,6 +28,7 @@
z-index: 2;
display: flex;
justify-content: center;
+ text-decoration: none;
}
.header > .logo > .img {
diff --git a/packages/ui/src/components/Header/Header.tsx b/packages/ui/src/components/Header/Header.tsx
index 64c9fd1f..ccf8f3d7 100644
--- a/packages/ui/src/components/Header/Header.tsx
+++ b/packages/ui/src/components/Header/Header.tsx
@@ -1,5 +1,6 @@
import cn from 'clsx';
import React, { PropsWithChildren } from 'react';
+import { NavLink } from 'react-router-dom';
import { useUIConfig } from '../../hooks/useUIConfig';
import { getStaticPath } from '../../utils/getStaticPath';
import s from './Header.module.css';
@@ -11,7 +12,7 @@ export const Header = ({ children }: PropsWithChildren) => {
return (
-
+
{!!logoPath && (
) => {
/>
)}
{boardTitle}
-
+
{children}
);
diff --git a/packages/ui/src/components/JobCard/JobCard.tsx b/packages/ui/src/components/JobCard/JobCard.tsx
index eefa73d6..c83ff2cc 100644
--- a/packages/ui/src/components/JobCard/JobCard.tsx
+++ b/packages/ui/src/components/JobCard/JobCard.tsx
@@ -1,4 +1,5 @@
import React from 'react';
+import { Card } from '../Card/Card';
import { Details } from './Details/Details';
import { JobActions } from './JobActions/JobActions';
import s from './JobCard.module.css';
@@ -23,7 +24,7 @@ interface JobCardProps {
const greenStatuses = [STATUSES.active, STATUSES.completed];
export const JobCard = ({ job, status, actions, readOnlyMode, allowRetries }: JobCardProps) => (
-
+
#{job.id}
@@ -57,5 +58,5 @@ export const JobCard = ({ job, status, actions, readOnlyMode, allowRetries }: Jo
)}
-
+
);
diff --git a/packages/ui/src/components/JobCard/Progress/Progress.tsx b/packages/ui/src/components/JobCard/Progress/Progress.tsx
index 55cdbcaf..f89fe95f 100644
--- a/packages/ui/src/components/JobCard/Progress/Progress.tsx
+++ b/packages/ui/src/components/JobCard/Progress/Progress.tsx
@@ -12,34 +12,32 @@ export const Progress = ({
percentage: number;
status: Status;
className?: string;
-}) => {
- return (
-
- );
-};
+}) => (
+
+);
diff --git a/packages/ui/src/components/QueueCard/QueueCard.module.css b/packages/ui/src/components/QueueCard/QueueCard.module.css
new file mode 100644
index 00000000..990eac7e
--- /dev/null
+++ b/packages/ui/src/components/QueueCard/QueueCard.module.css
@@ -0,0 +1,4 @@
+.queueCard {
+ flex-direction: column;
+ gap: 0.5rem;
+}
diff --git a/packages/ui/src/components/QueueCard/QueueCard.tsx b/packages/ui/src/components/QueueCard/QueueCard.tsx
new file mode 100644
index 00000000..97f92600
--- /dev/null
+++ b/packages/ui/src/components/QueueCard/QueueCard.tsx
@@ -0,0 +1,16 @@
+import { AppQueue } from '@bull-board/api/dist/typings/app';
+import React from 'react';
+import { Card } from '../Card/Card';
+import { QueueStats } from './QueueStats/QueueStats';
+import s from './QueueCard.module.css';
+
+interface IQueueCardProps {
+ queue: AppQueue;
+}
+
+export const QueueCard = ({ queue }: IQueueCardProps) => (
+
+ {queue.name}
+
+
+);
diff --git a/packages/ui/src/components/QueueCard/QueueStats/QueueStats.module.css b/packages/ui/src/components/QueueCard/QueueStats/QueueStats.module.css
new file mode 100644
index 00000000..b243513a
--- /dev/null
+++ b/packages/ui/src/components/QueueCard/QueueStats/QueueStats.module.css
@@ -0,0 +1,47 @@
+.stats {
+ display: flex;
+ white-space: nowrap;
+ gap: 1rem;
+ align-items: center;
+}
+
+.progressBar {
+ width: 100%;
+ display: flex;
+ overflow: hidden;
+ height: 1rem;
+ border-radius: 9999px;
+ background-color: #e5e7eb;
+ line-height: 1;
+ font-size: 0.75rem;
+ color: #fff;
+ text-align: center;
+}
+
+.progressBar > div {
+ padding: 0.125rem;
+}
+
+.progressBar > div + div {
+ border-left: 2px solid #fff;
+}
+
+.waiting {
+ background-color: var(--waiting);
+}
+
+.completed {
+ background-color: var(--completed);
+}
+
+.failed {
+ background-color: var(--failed);
+}
+
+.active {
+ background-color: var(--active);
+}
+
+.delayed {
+ background-color: var(--delayed);
+}
diff --git a/packages/ui/src/components/QueueCard/QueueStats/QueueStats.tsx b/packages/ui/src/components/QueueCard/QueueStats/QueueStats.tsx
new file mode 100644
index 00000000..8ac62a7b
--- /dev/null
+++ b/packages/ui/src/components/QueueCard/QueueStats/QueueStats.tsx
@@ -0,0 +1,39 @@
+import { AppQueue } from '@bull-board/api/dist/typings/app';
+import React from 'react';
+import { queueStatsStatusList } from '../../../constants/queue-stats-status';
+import s from './QueueStats.module.css';
+
+interface IQueueStatsProps {
+ queue: AppQueue;
+}
+
+export const QueueStats = ({ queue }: IQueueStatsProps) => {
+ const total = queueStatsStatusList.reduce((result, status) => result + queue.counts[status], 0);
+
+ return (
+
+
+ {queueStatsStatusList
+ .filter((status) => queue.counts[status] > 0)
+ .map((status) => {
+ const value = queue.counts[status];
+
+ return (
+
+ {value}
+
+ );
+ })}
+
+
{total} Jobs
+
+ );
+};
diff --git a/packages/ui/src/components/QueueTitle/QueueTitle.tsx b/packages/ui/src/components/QueueTitle/QueueTitle.tsx
deleted file mode 100644
index 7bb0bb77..00000000
--- a/packages/ui/src/components/QueueTitle/QueueTitle.tsx
+++ /dev/null
@@ -1,17 +0,0 @@
-import { AppQueue } from '@bull-board/api/typings/app';
-import React from 'react';
-import s from './QueueTitle.module.css';
-
-interface QueueTitleProps {
- queue: Pick | null;
-}
-export const QueueTitle = ({ queue }: QueueTitleProps) => (
-
- {!!queue && (
- <>
-
{queue.name}
- {!!queue.description &&
{queue.description}
}
- >
- )}
-
-);
diff --git a/packages/ui/src/components/StatusLegend/StatusLegend.module.css b/packages/ui/src/components/StatusLegend/StatusLegend.module.css
new file mode 100644
index 00000000..99ef57d0
--- /dev/null
+++ b/packages/ui/src/components/StatusLegend/StatusLegend.module.css
@@ -0,0 +1,41 @@
+.legend {
+ display: flex;
+ list-style: none;
+ padding: 0;
+ margin: 0;
+ text-transform: uppercase;
+}
+
+.legend > li:before {
+ content: '';
+ background-color: var(--item-bg);
+ border-radius: 50%;
+ width: 0.5rem;
+ height: 0.5rem;
+ display: inline-block;
+ margin-right: 0.5rem;
+}
+
+.legend > li + li {
+ margin-left: 2rem;
+}
+
+.waiting {
+ --item-bg: var(--waiting);
+}
+
+.active {
+ --item-bg: var(--active);
+}
+
+.failed {
+ --item-bg: var(--failed);
+}
+
+.completed {
+ --item-bg: var(--completed);
+}
+
+.delayed {
+ --item-bg: var(--delayed);
+}
diff --git a/packages/ui/src/components/StatusLegend/StatusLegend.tsx b/packages/ui/src/components/StatusLegend/StatusLegend.tsx
new file mode 100644
index 00000000..464528ee
--- /dev/null
+++ b/packages/ui/src/components/StatusLegend/StatusLegend.tsx
@@ -0,0 +1,13 @@
+import { queueStatsStatusList } from '../../constants/queue-stats-status';
+import React from 'react';
+import s from './StatusLegend.module.css';
+
+export const StatusLegend = () => (
+
+ {queueStatsStatusList.map((status) => (
+ -
+ {status}
+
+ ))}
+
+);
diff --git a/packages/ui/src/components/QueueTitle/QueueTitle.module.css b/packages/ui/src/components/Title/Title.module.css
similarity index 100%
rename from packages/ui/src/components/QueueTitle/QueueTitle.module.css
rename to packages/ui/src/components/Title/Title.module.css
diff --git a/packages/ui/src/components/Title/Title.tsx b/packages/ui/src/components/Title/Title.tsx
new file mode 100644
index 00000000..5b71d028
--- /dev/null
+++ b/packages/ui/src/components/Title/Title.tsx
@@ -0,0 +1,18 @@
+import React from 'react';
+import s from './Title.module.css';
+
+interface TitleProps {
+ name?: string;
+ description?: string;
+}
+
+export const Title = ({ name, description }: TitleProps) => (
+
+ {!!name && (
+ <>
+
{name}
+ {!!description &&
{description}
}
+ >
+ )}
+
+);
diff --git a/packages/ui/src/constants/queue-stats-status.ts b/packages/ui/src/constants/queue-stats-status.ts
new file mode 100644
index 00000000..9bba2515
--- /dev/null
+++ b/packages/ui/src/constants/queue-stats-status.ts
@@ -0,0 +1,9 @@
+import { STATUSES } from '@bull-board/api/dist/src/constants/statuses';
+
+export const queueStatsStatusList = [
+ STATUSES.active,
+ STATUSES.waiting,
+ STATUSES.completed,
+ STATUSES.failed,
+ STATUSES.delayed,
+];
diff --git a/packages/ui/src/index.css b/packages/ui/src/index.css
index a4227333..c1421405 100644
--- a/packages/ui/src/index.css
+++ b/packages/ui/src/index.css
@@ -1,6 +1,12 @@
:root {
--menu-width: 260px;
--header-height: 80px;
+
+ --failed: #f56565;
+ --completed: #48bb78;
+ --waiting: #e3a008;
+ --active: #1c64f2;
+ --delayed: #9061f9;
}
body {
diff --git a/packages/ui/src/index.tsx b/packages/ui/src/index.tsx
index b3666121..6fd920ab 100644
--- a/packages/ui/src/index.tsx
+++ b/packages/ui/src/index.tsx
@@ -1,7 +1,7 @@
import React from 'react';
import { render } from 'react-dom';
import { BrowserRouter } from 'react-router-dom';
-import { App } from './components/App';
+import { App } from './App';
import { ApiContext } from './hooks/useApi';
import './index.css';
import { UIConfigContext } from './hooks/useUIConfig';
diff --git a/packages/ui/src/pages/OverviewPage/OverviewPage.module.css b/packages/ui/src/pages/OverviewPage/OverviewPage.module.css
new file mode 100644
index 00000000..97f15145
--- /dev/null
+++ b/packages/ui/src/pages/OverviewPage/OverviewPage.module.css
@@ -0,0 +1,8 @@
+.overview {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
+ grid-gap: 2rem;
+ list-style: none;
+ margin: 2rem 0 0;
+ padding: 0;
+}
diff --git a/packages/ui/src/pages/OverviewPage/OverviewPage.tsx b/packages/ui/src/pages/OverviewPage/OverviewPage.tsx
new file mode 100644
index 00000000..abbf177f
--- /dev/null
+++ b/packages/ui/src/pages/OverviewPage/OverviewPage.tsx
@@ -0,0 +1,22 @@
+import { AppQueue } from '@bull-board/api/dist/typings/app';
+import React from 'react';
+import { QueueCard } from '../../components/QueueCard/QueueCard';
+import { StatusLegend } from '../../components/StatusLegend/StatusLegend';
+import s from './OverviewPage.module.css';
+
+interface IOverviewPageProps {
+ queues: AppQueue[] | undefined;
+}
+
+export const OverviewPage = (props: IOverviewPageProps) => (
+
+
+
+ {props.queues?.map((queue) => (
+ -
+
+
+ ))}
+
+
+);
diff --git a/packages/ui/src/components/QueuePage/QueuePage.module.css b/packages/ui/src/pages/QueuePage/QueuePage.module.css
similarity index 100%
rename from packages/ui/src/components/QueuePage/QueuePage.module.css
rename to packages/ui/src/pages/QueuePage/QueuePage.module.css
diff --git a/packages/ui/src/components/QueuePage/QueuePage.tsx b/packages/ui/src/pages/QueuePage/QueuePage.tsx
similarity index 86%
rename from packages/ui/src/components/QueuePage/QueuePage.tsx
rename to packages/ui/src/pages/QueuePage/QueuePage.tsx
index 464f0abd..3b0cc4de 100644
--- a/packages/ui/src/components/QueuePage/QueuePage.tsx
+++ b/packages/ui/src/pages/QueuePage/QueuePage.tsx
@@ -1,11 +1,11 @@
+import { AppQueue, JobRetryStatus } from '@bull-board/api/typings/app';
import React from 'react';
+import { JobCard } from '../../components/JobCard/JobCard';
+import { Pagination } from '../../components/Pagination/Pagination';
+import { QueueActions } from '../../components/QueueActions/QueueActions';
+import { StatusMenu } from '../../components/StatusMenu/StatusMenu';
import { Store } from '../../hooks/useStore';
-import { JobCard } from '../JobCard/JobCard';
-import { QueueActions } from '../QueueActions/QueueActions';
-import { StatusMenu } from '../StatusMenu/StatusMenu';
import s from './QueuePage.module.css';
-import { AppQueue, JobRetryStatus } from '@bull-board/api/typings/app';
-import { Pagination } from '../Pagination/Pagination';
export const QueuePage = ({
selectedStatus,