Skip to content

Commit

Permalink
single job page view (#598)
Browse files Browse the repository at this point in the history
* initial attempt: add JobPage

* add fetch job state to jobHandler

* clean up JobPage

* fix type mismatch

* fix type mismatch

* pr review changes

---------

Co-authored-by: Felix Mosheev <[email protected]>
  • Loading branch information
jamesgweber and felixmosh authored Aug 8, 2023
1 parent 626d078 commit 91994c5
Show file tree
Hide file tree
Showing 12 changed files with 160 additions and 21 deletions.
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'>;
}

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 @@ -11,6 +11,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 @@ -40,6 +44,16 @@ export const App = () => {
<>
<Suspense fallback={<Loader />}>
<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
73 changes: 73 additions & 0 deletions packages/ui/src/pages/JobPage/JobPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { useParams, useHistory, useLocation, Link } 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 { 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);

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}>
<Link to={`/queue/${queue.name}${search}`}>
<ArrowLeftIcon />
</Link>
<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>
);
};
6 changes: 6 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,9 @@ export const QueuePage = ({
<JobCard
key={job.id}
job={job}
jobUrlPath={`/queue/${encodeURIComponent(queue.name)}/${encodeURIComponent(
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

0 comments on commit 91994c5

Please sign in to comment.