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

single job page view #598

Merged
merged 9 commits into from
Aug 8, 2023
Merged
Show file tree
Hide file tree
Changes from 5 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
22 changes: 22 additions & 0 deletions packages/api/src/handlers/job.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { BullBoardRequest, ControllerHandlerReturnType, QueueJob } from '../../typings/app';
import { queueProvider } from '../providers/queue';
import { jobProvider } from '../providers/job';

async function getJobState(
_req: BullBoardRequest,
job: QueueJob
): Promise<ControllerHandlerReturnType> {
const state = await job.getState();

return {
status: 200,
body: {
job,
state,
},
};
}

export const jobHandler = queueProvider(jobProvider(getJobState), {
skipReadOnlyModeCheck: true,
});
11 changes: 2 additions & 9 deletions packages/api/src/providers/job.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,8 @@
import {
BullBoardRequest,
ControllerHandlerReturnType,
QueueJob,
} from '../../typings/app';
import { BullBoardRequest, ControllerHandlerReturnType, QueueJob } from '../../typings/app';
import { BaseAdapter } from '../queueAdapters/base';

export function jobProvider(
next: (
req: BullBoardRequest,
job: QueueJob
) => Promise<ControllerHandlerReturnType>
next: (req: BullBoardRequest, job: QueueJob) => Promise<ControllerHandlerReturnType>
) {
return async (
req: BullBoardRequest,
Expand Down
10 changes: 8 additions & 2 deletions packages/api/src/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,20 @@ import { cleanJobHandler } from './handlers/cleanJob';
import { emptyQueueHandler } from './handlers/emptyQueue';
import { entryPoint } from './handlers/entryPoint';
import { jobLogsHandler } from './handlers/jobLogs';
import { jobHandler } from './handlers/job';
import { pauseQueueHandler } from './handlers/pauseQueue';
import { promoteJobHandler } from './handlers/promotJob';
import { queuesHandler } from './handlers/queues';
import { redisStatsHandler } from './handlers/redisStats';
import { resumeQueueHandler } from './handlers/resumeQueue';
import { retryAllHandler } from './handlers/retryAll';
import { retryJobHandler } from './handlers/retryJob';
import { promoteAllHandler } from "./handlers/promoteAll";
import { promoteAllHandler } from './handlers/promoteAll';

export const appRoutes: AppRouteDefs = {
entryPoint: {
method: 'get',
route: ['/', '/queue/:queueName'],
route: ['/', '/queue/:queueName', '/queue/:queueName/:jobId'],
handler: entryPoint,
},
api: [
Expand All @@ -27,6 +28,11 @@ export const appRoutes: AppRouteDefs = {
route: '/api/queues/:queueName/:jobId/logs',
handler: jobLogsHandler,
},
{
method: 'get',
route: '/api/queues/:queueName/:jobId',
handler: jobHandler,
},
{
method: 'put',
route: '/api/queues/:queueName/retry/:queueStatus',
Expand Down
2 changes: 2 additions & 0 deletions packages/api/typings/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ export interface QueueJob {
retry(state?: JobRetryStatus): Promise<void>;

toJSON(): QueueJobJson;

getState(): Promise<Status | 'stuck' | 'waiting-children' | 'unknown'>;
Copy link
Contributor Author

@jamesgweber jamesgweber Jul 5, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Apparently this is the full union of states 🤷‍♂️ - idk if this is going to cause problems, any advice here would be greatly appreciated.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do you need to specify all of the statuses (which the board not supports yet, as far as i know, bull doesn't support waiting-children as well)

This interface must be a unify version between bullmq & bull

Copy link
Contributor Author

@jamesgweber jamesgweber Jul 6, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm getting a build error unless I include them all.
https://gist.github.com/jamesgweber/cbf1e8bbf29f169964d37b6adb8c405a

bull board might not support it but { Job } from 'bull'; does and it expects that continuity

Another possible option is getState(): Promise<Status | any>; 🙅

}

export interface QueueJobJson {
Expand Down
14 changes: 14 additions & 0 deletions packages/ui/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ import { useActiveQueue } from './hooks/useActiveQueue';
import { useScrollTopOnNav } from './hooks/useScrollTopOnNav';
import { useStore } from './hooks/useStore';

const JobPageLazy = React.lazy(() =>
import('./pages/JobPage/JobPage').then(({ JobPage }) => ({ default: JobPage }))
);

const QueuePageLazy = React.lazy(() =>
import('./pages/QueuePage/QueuePage').then(({ QueuePage }) => ({ default: QueuePage }))
);
Expand Down Expand Up @@ -39,6 +43,16 @@ export const App = () => {
<>
<Suspense fallback={() => 'Loading...'}>
<Switch>
<Route
path="/queue/:name/:jobId"
render={() => (
<JobPageLazy
queue={activeQueue || null}
actions={actions}
selectedStatus={selectedStatuses}
/>
)}
/>
<Route
path="/queue/:name"
render={() => (
Expand Down
19 changes: 17 additions & 2 deletions packages/ui/src/components/JobCard/JobCard.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import React from 'react';
import { NavLink } from 'react-router-dom';
import { Card } from '../Card/Card';
import { Details } from './Details/Details';
import { JobActions } from './JobActions/JobActions';
Expand All @@ -10,6 +11,7 @@ import { STATUSES } from '@bull-board/api/dist/src/constants/statuses';

interface JobCardProps {
job: AppJob;
jobUrlPath?: string;
status: Status;
readOnlyMode: boolean;
allowRetries: boolean;
Expand All @@ -23,10 +25,23 @@ interface JobCardProps {

const greenStatuses = [STATUSES.active, STATUSES.completed];

export const JobCard = ({ job, status, actions, readOnlyMode, allowRetries }: JobCardProps) => (
export const JobCard = ({
job,
status,
actions,
readOnlyMode,
allowRetries,
jobUrlPath,
}: JobCardProps) => (
<Card className={s.card}>
<div className={s.sideInfo}>
<span title={`#${job.id}`}>#{job.id}</span>
{jobUrlPath ? (
<NavLink to={jobUrlPath}>
<span title={`#${job.id}`}>#{job.id}</span>
</NavLink>
) : (
<span title={`#${job.id}`}>#{job.id}</span>
)}
<Timeline job={job} status={status} />
</div>
<div className={s.contentWrapper}>
Expand Down
8 changes: 4 additions & 4 deletions packages/ui/src/hooks/useActiveQueueName.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@ import { useLocation } from 'react-router-dom';
export function useActiveQueueName(): string {
const { pathname } = useLocation();

const match = matchPath<{ name: string }>(pathname, {
path: '/queue/:name',
exact: true,
strict: true,
const match = matchPath<{ name: string; jobId: string }>(pathname, {
path: ['/queue/:name', '/queue/:name/:jobId'],
exact: false,
strict: false,
});

return decodeURIComponent(match?.params.name || '');
Expand Down
5 changes: 4 additions & 1 deletion packages/ui/src/hooks/useStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { useInterval } from './useInterval';
import { useQuery } from './useQuery';
import { useSelectedStatuses } from './useSelectedStatuses';
import { useSettingsStore } from './useSettings';
import { STATUSES } from "@bull-board/api/dist/src/constants/statuses";
import { STATUSES } from '@bull-board/api/dist/src/constants/statuses';

type State = {
data: null | GetQueuesResponse;
Expand Down Expand Up @@ -148,6 +148,8 @@ export const useStore = (): Store => {
const getJobLogs = (queueName: string) => (job: AppJob) => () =>
api.getJobLogs(queueName, job.id);

const getJob = (queueName: string) => (jobId: string) => () => api.getJob(queueName, jobId);

return {
state,
actions: {
Expand All @@ -158,6 +160,7 @@ export const useStore = (): Store => {
cleanJob,
cleanAll,
getJobLogs,
getJob,
pauseQueue,
resumeQueue,
emptyQueue,
Expand Down
74 changes: 74 additions & 0 deletions packages/ui/src/pages/JobPage/JobPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { useParams, useHistory, useLocation } from 'react-router-dom';
import { AppJob, AppQueue, JobRetryStatus, Status } from '@bull-board/api/typings/app';
import React, { useState } from 'react';
import { Store } from '../../hooks/useStore';
import s from '../QueuePage/QueuePage.module.css';
import { JobCard } from '../../components/JobCard/JobCard';
import { ArrowLeftIcon } from '../../components/Icons/ArrowLeft';
import { Button } from '../../components/JobCard/Button/Button';
import { useInterval } from '../../hooks/useInterval';

export const JobPage = ({
actions,
queue,
selectedStatus,
}: {
queue: AppQueue | null;
actions: Store['actions'];
selectedStatus: Store['selectedStatuses'];
}) => {
const { search } = useLocation();
const history = useHistory();
const { name, jobId } = useParams<any>();
const [job, setJob] = useState<AppJob>();
const [status, setStatus] = useState<Status>(selectedStatus[queue?.name || '']);

useInterval(() => {
fetchJob();
}, 5000);
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should use users config of interval time
Do we pause the interval of the QueuePage?

Copy link
Contributor Author

@jamesgweber jamesgweber Aug 3, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should use users config of interval time

idk what this means - if you can point me to an example I'm happy to make the change

Do we pause the interval of the QueuePage?

no - I just let the polling continue because it keeps everything in sync and it's simple. traffic is not a real concern for an app like this imo. You probably wouldn't have 1000s of concurrent users.


const fetchJob = async () => {
const { job, state } = await actions.getJob(name)(jobId)();
setJob(job);
setStatus(state);
};

if (!queue) {
return <section>Queue Not found</section>;
}

if (!job) {
return <section>Job Not found</section>;
}

const cleanJob = async () => {
await actions.cleanJob(queue?.name)(job)();
history.push(`/queue/${queue.name}`);
};

return (
<section>
<div className={s.stickyHeader}>
<div className={s.actionContainer}>
<Button onClick={() => history.push(`/queue/${queue.name}${search}`)}>
<ArrowLeftIcon />
</Button>
<div>Status: {status.toLocaleUpperCase()}</div>
</div>
</div>
<JobCard
key={job.id}
job={job}
status={status}
actions={{
cleanJob,
promoteJob: actions.promoteJob(queue?.name)(job),
retryJob: actions.retryJob(queue?.name, status as JobRetryStatus)(job),
getJobLogs: actions.getJobLogs(queue?.name)(job),
}}
readOnlyMode={queue?.readOnlyMode}
allowRetries={(job.isFailed || queue.allowCompletedRetries) && queue?.allowRetries}
/>
</section>
);
};
4 changes: 4 additions & 0 deletions packages/ui/src/pages/QueuePage/QueuePage.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { AppQueue, JobRetryStatus } from '@bull-board/api/typings/app';
import React from 'react';
import { useLocation } from 'react-router-dom';
import { JobCard } from '../../components/JobCard/JobCard';
import { Pagination } from '../../components/Pagination/Pagination';
import { QueueActions } from '../../components/QueueActions/QueueActions';
Expand All @@ -16,6 +17,8 @@ export const QueuePage = ({
actions: Store['actions'];
selectedStatus: Store['selectedStatuses'];
}) => {
const { search } = useLocation();

if (!queue) {
return <section>Queue Not found</section>;
}
Expand Down Expand Up @@ -47,6 +50,7 @@ export const QueuePage = ({
<JobCard
key={job.id}
job={job}
jobUrlPath={`/queue/${encodeURIComponent(queue.name)}/${job.id}${search}`}
status={status}
actions={{
cleanJob: actions.cleanJob(queue?.name)(job),
Expand Down
10 changes: 7 additions & 3 deletions packages/ui/src/services/Api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,7 @@ export class Api {
}

public promoteAll(queueName: string): Promise<void> {
return this.axios.put(
`/queues/${encodeURIComponent(queueName)}/promote`
);
return this.axios.put(`/queues/${encodeURIComponent(queueName)}/promote`);
}

public cleanAll(queueName: string, status: JobCleanStatus): Promise<void> {
Expand Down Expand Up @@ -75,6 +73,12 @@ export class Api {
);
}

public getJob(queueName: string, jobId: AppJob['id']): Promise<any> {
return this.axios.get(
`/queues/${encodeURIComponent(queueName)}/${encodeURIComponent(`${jobId}`)}`
);
}

public pauseQueue(queueName: string) {
return this.axios.put(`/queues/${encodeURIComponent(queueName)}/pause`);
}
Expand Down
1 change: 1 addition & 0 deletions packages/ui/typings/app.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export interface QueueActions {
retryJob: (queueName: string, status: JobRetryStatus) => (job: AppJob) => () => Promise<void>;
cleanJob: (queueName: string) => (job: AppJob) => () => Promise<void>;
getJobLogs: (queueName: string) => (job: AppJob) => () => Promise<string[]>;
getJob: (queueName: string) => (jobId: string) => () => Promise<any>;
retryAll: (queueName: string, status: JobRetryStatus) => () => Promise<void>;
promoteAll: (queueName: string) => () => Promise<void>;
cleanAll: (queueName: string, status: JobCleanStatus) => () => Promise<void>;
Expand Down