Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: sort queues by # of jobs with a particular status on the dashboard #875

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions packages/api/typings/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,8 @@ export interface AppQueue {
type: QueueType;
}

export type QueueSortKey = 'alphabetical' | keyof AppQueue['counts'];

export type HTTPMethod = 'get' | 'post' | 'put' | 'patch';
export type HTTPStatus = 200 | 204 | 404 | 405 | 500;

Expand Down Expand Up @@ -213,6 +215,7 @@ export type UIConfig = Partial<{
boardTitle: string;
boardLogo: { path: string; width?: number | string; height?: number | string };
miscLinks: Array<IMiscLink>;
queueSortOptions: Array<{ key: string; label: string }>;
favIcon: FavIcon;
locale: { lng?: string };
dateFormats?: DateFormats;
Expand Down
12 changes: 12 additions & 0 deletions packages/ui/src/components/Icons/Sort.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import React from 'react';

export const SortIcon = () => (
<svg fill="#000000" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 490 490">
<g>
<polygon points="85.877,154.014 85.877,428.309 131.706,428.309 131.706,154.014 180.497,221.213 217.584,194.27 108.792,44.46
0,194.27 37.087,221.213 "/>
<polygon points="404.13,335.988 404.13,61.691 358.301,61.691 358.301,335.99 309.503,268.787 272.416,295.73 381.216,445.54
490,295.715 452.913,268.802 "/>
</g>
</svg>
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { QueueSortKey, UIConfig } from '@bull-board/api/typings/app';
import { Item, Portal, Root, Trigger } from '@radix-ui/react-dropdown-menu';
import React from 'react';
import { DropdownContent } from '../DropdownContent/DropdownContent';
import { SortIcon } from '../Icons/Sort';
import { Button } from '../Button/Button';

type QueueSortingDropdownProps = {
sortOptions: UIConfig['queueSortOptions'];
className: string;
sortHandler: (sortKey: QueueSortKey) => void;
};

export const QueueSortingDropdown = ({ sortOptions = [], className, sortHandler }: QueueSortingDropdownProps) => {
const [selectedSort, setSelectedSort] = React.useState(sortOptions[0].key);

return (
<Root>
<Trigger asChild>
<Button className={className}>
<SortIcon />
{sortOptions.find((option) => option.key === selectedSort)?.label}
</Button>
</Trigger>

<Portal>
<DropdownContent align="end">
{sortOptions.map((option) => (
<Item key={option.key} asChild onSelect={() => {
setSelectedSort(option.key);
sortHandler(option.key as QueueSortKey);
}}>
<p>{option.label}</p>
</Item>
))}
</DropdownContent>
</Portal>
</Root>
);
};
32 changes: 27 additions & 5 deletions packages/ui/src/hooks/useQueues.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { JobCleanStatus, JobRetryStatus } from '@bull-board/api/typings/app';
import { JobCleanStatus, JobRetryStatus, QueueSortKey } from '@bull-board/api/typings/app';
import { GetQueuesResponse } from '@bull-board/api/typings/responses';
import { useCallback } from 'react';
import { useCallback, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { create } from 'zustand';
import { QueueActions } from '../../typings/app';
Expand All @@ -26,6 +26,7 @@ const useQueuesStore = create<QueuesState>((set) => ({
}));

export function useQueues(): Omit<QueuesState, 'updateQueues'> & { actions: QueueActions } {
const [activeQueueSortKey, setActiveQueueSortKey] = useState<QueueSortKey>('alphabetical');
const query = useQuery();
const { t } = useTranslation();
const api = useApi();
Expand All @@ -52,16 +53,23 @@ export function useQueues(): Omit<QueuesState, 'updateQueues'> & { actions: Queu
jobsPerPage,
})
.then((data) => {
setState(data.queues);
const sortedQueues = data.queues ? [...data.queues].sort((a, b) => {
if (activeQueueSortKey === 'alphabetical') {
return a.name.localeCompare(b.name);
}

return b.counts[activeQueueSortKey] - a.counts[activeQueueSortKey];
}) : [];
setState(sortedQueues);
})
// eslint-disable-next-line no-console
.catch((error) => console.error('Failed to poll', error)),
[activeQueueName, jobsPerPage, selectedStatuses]
[activeQueueName, jobsPerPage, selectedStatuses, activeQueueSortKey]
);

const pollQueues = () =>
useInterval(updateQueues, pollingInterval > 0 ? pollingInterval * 1000 : null, [
selectedStatuses,
selectedStatuses, activeQueueSortKey
]);

const withConfirmAndUpdate = getConfirmFor(updateQueues, openConfirm);
Expand Down Expand Up @@ -115,6 +123,19 @@ export function useQueues(): Omit<QueuesState, 'updateQueues'> & { actions: Queu
jobOptions: Record<any, any>
) => withConfirmAndUpdate(() => api.addJob(queueName, jobName, jobData, jobOptions), '', false);

const sortQueues = useCallback((sortKey: QueueSortKey) => {
setActiveQueueSortKey(sortKey);
const sortedQueues = queues ? [...queues].sort((a, b) => {
if (sortKey === 'alphabetical') {
return a.name.localeCompare(b.name);
}

return b.counts[sortKey] - a.counts[sortKey];
}) : [];

setState(sortedQueues);
}, [queues]); // Added dependency array

return {
queues,
loading,
Expand All @@ -128,6 +149,7 @@ export function useQueues(): Omit<QueuesState, 'updateQueues'> & { actions: Queu
resumeQueue,
emptyQueue,
addJob,
sortQueues,
},
};
}
12 changes: 11 additions & 1 deletion packages/ui/src/pages/OverviewPage/OverviewPage.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
grid-gap: 2rem;
list-style: none;
margin: 2rem 0 0;
margin: 1.5rem 0 0;
padding: 0;
}

Expand All @@ -21,3 +21,13 @@
}
}
}

.dropdown {
margin: 1.5rem 100% 0;
transform: translateX(-100%);
position: relative;
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.5rem;
}
21 changes: 21 additions & 0 deletions packages/ui/src/pages/OverviewPage/OverviewPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import { useQuery } from '../../hooks/useQuery';
import { useQueues } from '../../hooks/useQueues';
import { links } from '../../utils/links';
import s from './OverviewPage.module.css';
import { QueueSortingDropdown } from '../../components/QueueSortingDropdown/QueueSortingDropdown';
import { QueueSortKey } from '@bull-board/api/typings/app';

export const OverviewPage = () => {
const { t } = useTranslation();
Expand All @@ -18,10 +20,29 @@ export const OverviewPage = () => {
const selectedStatus = query.get('status') as Status;
const queuesToView =
queues?.filter((queue) => !selectedStatus || queue.counts[selectedStatus] > 0) || [];

const sortHandler = (sortKey: QueueSortKey) => {
actions.sortQueues(sortKey);
}

return (
<section>
<StatusLegend />

<QueueSortingDropdown sortOptions={
[
{ key: 'alphabetical', label: t('DASHBOARD.SORTING.ALPHABETICAL') },
{ key: 'failed', label: t('DASHBOARD.SORTING.FAILED') },
{ key: 'completed', label: t('DASHBOARD.SORTING.COMPLETED') },
{ key: 'active', label: t('DASHBOARD.SORTING.ACTIVE') },
{ key: 'waiting', label: t('DASHBOARD.SORTING.WAITING') },
{ key: 'delayed', label: t('DASHBOARD.SORTING.DELAYED') },
]
}
sortHandler={sortHandler}
className={s.dropdown}
/>

{queuesToView.length > 0 && (
<ul className={s.overview}>
{queuesToView.map((queue) => (
Expand Down
10 changes: 9 additions & 1 deletion packages/ui/src/static/locales/en-US/messages.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,15 @@
"DASHBOARD": {
"JOBS_COUNT_one": "{{count}} Job",
"JOBS_COUNT": "{{count}} Jobs",
"NO_FILTERED_MESSAGE": "There are no queues that have a job with \"{{status}}\" status.\n<lnk>Click here</lnk> to clear the filter."
"NO_FILTERED_MESSAGE": "There are no queues that have a job with \"{{status}}\" status.\n<lnk>Click here</lnk> to clear the filter.",
"SORTING": {
"FAILED": "Failed Jobs",
"DELAYED": "Delayed Jobs",
"WAITING": "Waiting Jobs",
"ACTIVE": "Active Jobs",
"COMPLETED": "Completed Jobs",
"ALPHABETICAL": "Alphabetical (A-Z)"
}
},
"JOB": {
"DELAY_CHANGED": "*Delay changed; the new run time is currently unknown",
Expand Down
2 changes: 2 additions & 0 deletions packages/ui/typings/app.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
AppQueue,
JobCleanStatus,
JobRetryStatus,
QueueSortKey,
Status,
} from '@bull-board/api/typings/app';

Expand All @@ -25,6 +26,7 @@ export interface QueueActions {
jobData: any,
jobOptions: any
) => () => Promise<void>;
sortQueues: (sortKey: QueueSortKey) => void;
}

export interface JobActions {
Expand Down