Skip to content

Commit 0aa1bce

Browse files
feat: adding pending task section on course info page
1 parent 88d3700 commit 0aa1bce

File tree

15 files changed

+525
-76
lines changed

15 files changed

+525
-76
lines changed

src/components/ObjectCell.tsx

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { parseObject } from '../utils/formatters';
2+
3+
interface ObjectCellProps {
4+
value: Record<string, any> | null,
5+
}
6+
7+
const ObjectCell = ({ value }: ObjectCellProps) => {
8+
return (
9+
<pre style={{ whiteSpace: 'pre-wrap', wordBreak: 'break-word' }}>
10+
{parseObject(value ?? '')}
11+
</pre>
12+
);
13+
};
14+
15+
export { ObjectCell };
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
import { screen, waitFor } from '@testing-library/react';
2+
import { PendingTasks } from './PendingTasks';
3+
import { usePendingTasks } from '../data/apiHook';
4+
import { renderWithProviders } from '../testUtils';
5+
6+
jest.mock('../data/apiHook');
7+
8+
const mockUsePendingTasks = usePendingTasks as jest.MockedFunction<typeof usePendingTasks>;
9+
10+
describe('PendingTasks', () => {
11+
const mockFetchTasks = jest.fn();
12+
13+
beforeEach(() => {
14+
jest.clearAllMocks();
15+
mockUsePendingTasks.mockReturnValue({
16+
mutate: mockFetchTasks,
17+
data: undefined,
18+
isPending: false,
19+
} as any);
20+
});
21+
22+
it('should render the collapsible pending tasks section', () => {
23+
renderWithProviders(<PendingTasks />);
24+
25+
expect(screen.getByText('Pending Tasks')).toBeInTheDocument();
26+
expect(screen.getByRole('button')).toBeInTheDocument();
27+
});
28+
29+
it('should show loading skeleton when tasks are being fetched', async () => {
30+
mockUsePendingTasks.mockReturnValue({
31+
mutate: mockFetchTasks,
32+
data: undefined,
33+
isPending: true,
34+
} as any);
35+
36+
const { container } = renderWithProviders(<PendingTasks />);
37+
const toggleButton = screen.getByRole('button');
38+
await waitFor(() => toggleButton.click());
39+
40+
expect(screen.queryByText('No tasks currently running.')).not.toBeInTheDocument();
41+
expect(screen.queryByRole('table')).not.toBeInTheDocument();
42+
expect(screen.queryByText('Task Type')).not.toBeInTheDocument();
43+
44+
const skeletons = container.querySelectorAll('.react-loading-skeleton');
45+
expect(skeletons).toHaveLength(3);
46+
});
47+
48+
it('should display no tasks message when tasks array is empty', async () => {
49+
mockUsePendingTasks.mockReturnValue({
50+
mutate: mockFetchTasks,
51+
data: [],
52+
isPending: false,
53+
} as any);
54+
55+
renderWithProviders(<PendingTasks />);
56+
const toggleButton = screen.getByRole('button');
57+
await waitFor(() => toggleButton.click());
58+
59+
expect(screen.getByText('No tasks currently running.')).toBeInTheDocument();
60+
});
61+
62+
it('should render data table with tasks when data is available', async () => {
63+
const mockTasks = [
64+
{
65+
taskType: 'grade_course',
66+
taskInput: 'course data',
67+
taskId: '12345',
68+
requester: '[email protected]',
69+
taskState: 'SUCCESS',
70+
created: '2023-01-01',
71+
taskOutput: 'output.csv',
72+
duration: '5 minutes',
73+
status: 'Completed',
74+
taskMessage: 'Task completed successfully',
75+
},
76+
];
77+
78+
mockUsePendingTasks.mockReturnValue({
79+
mutate: mockFetchTasks,
80+
data: mockTasks,
81+
isPending: false,
82+
} as any);
83+
84+
renderWithProviders(<PendingTasks />);
85+
const toggleButton = screen.getByRole('button');
86+
await waitFor(() => toggleButton.click());
87+
88+
expect(screen.getByText('Task Type')).toBeInTheDocument();
89+
expect(screen.getByText('Task ID')).toBeInTheDocument();
90+
expect(screen.getByText('grade_course')).toBeInTheDocument();
91+
expect(screen.getByText('12345')).toBeInTheDocument();
92+
});
93+
94+
it('should fetch tasks on component mount', async () => {
95+
renderWithProviders(<PendingTasks />);
96+
97+
await waitFor(() => {
98+
expect(mockFetchTasks).toHaveBeenCalledTimes(1);
99+
});
100+
});
101+
});

src/components/PendingTasks.tsx

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import { useIntl } from '@openedx/frontend-base';
2+
import { Collapsible, DataTable, Icon, Skeleton } from '@openedx/paragon';
3+
import { useEffect, useMemo } from 'react';
4+
import { messages } from './messages';
5+
import { ExpandLess, ExpandMore } from '@openedx/paragon/icons';
6+
import { usePendingTasks } from '../data/apiHook';
7+
import { useParams } from 'react-router';
8+
import { ObjectCell } from './ObjectCell';
9+
import { PendingTask, TableCellValue } from '../types';
10+
11+
const PendingTasks = () => {
12+
const intl = useIntl();
13+
const { courseId = '' } = useParams();
14+
const { mutate: fetchTasks, data: tasks, isPending } = usePendingTasks(courseId);
15+
16+
const tableColumns = useMemo(() => [
17+
{ accessor: 'taskType', Header: intl.formatMessage(messages.taskTypeColumnName) },
18+
{ accessor: 'taskInput', Header: intl.formatMessage(messages.taskInputColumnName), Cell: ({ row }: TableCellValue<PendingTask>) => <ObjectCell value={row.original.taskInput} /> },
19+
{ accessor: 'taskId', Header: intl.formatMessage(messages.taskIdColumnName) },
20+
{ accessor: 'requester', Header: intl.formatMessage(messages.requesterColumnName) },
21+
{ accessor: 'taskState', Header: intl.formatMessage(messages.taskStateColumnName) },
22+
{ accessor: 'created', Header: intl.formatMessage(messages.createdColumnName) },
23+
{ accessor: 'taskOutput', Header: intl.formatMessage(messages.taskOutputColumnName), Cell: ({ row }: TableCellValue<PendingTask>) => <ObjectCell value={row.original.taskOutput} /> },
24+
{ accessor: 'durationSec', Header: intl.formatMessage(messages.durationColumnName) },
25+
{ accessor: 'status', Header: intl.formatMessage(messages.statusColumnName) },
26+
{ accessor: 'taskMessage', Header: intl.formatMessage(messages.taskMessageColumnName) },
27+
], [intl]);
28+
29+
useEffect(() => {
30+
fetchTasks();
31+
}, [fetchTasks]);
32+
33+
const renderContent = () => {
34+
if (isPending) {
35+
return <Skeleton count={3} />;
36+
}
37+
38+
if (tasks?.length === 0) {
39+
return <div className="my-3">{intl.formatMessage(messages.noTasksMessage)}</div>;
40+
}
41+
42+
return (
43+
<DataTable
44+
columns={tableColumns}
45+
data={tasks}
46+
isLoading={isPending}
47+
RowStatusComponent={() => null}
48+
/>
49+
);
50+
};
51+
52+
return (
53+
<Collapsible.Advanced
54+
className="mt-4 pt-4 border-top"
55+
styling="basic"
56+
>
57+
<Collapsible.Trigger
58+
className="collapsible-trigger d-flex border-0 align-items-center text-decoration-none"
59+
>
60+
<div className="d-flex">
61+
<h3>{intl.formatMessage(messages.pendingTasksTitle)}</h3>
62+
</div>
63+
64+
<Collapsible.Visible whenClosed>
65+
<div className="pl-2 d-flex">
66+
<Icon src={ExpandMore} />
67+
</div>
68+
</Collapsible.Visible>
69+
<Collapsible.Visible whenOpen>
70+
<div className="pl-2 d-flex">
71+
<Icon src={ExpandLess} />
72+
</div>
73+
</Collapsible.Visible>
74+
</Collapsible.Trigger>
75+
<Collapsible.Body>
76+
{renderContent() }
77+
</Collapsible.Body>
78+
</Collapsible.Advanced>
79+
);
80+
};
81+
82+
export { PendingTasks };

src/components/messages.ts

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import { defineMessages } from '@openedx/frontend-base';
2+
3+
const messages = defineMessages({
4+
pendingTasksTitle: {
5+
id: 'instruct.pendingTasks.section.title',
6+
defaultMessage: 'Pending Tasks',
7+
description: 'Title for the pending tasks section',
8+
},
9+
noTasksMessage: {
10+
id: 'instruct.pendingTasks.section.noTasks',
11+
defaultMessage: 'No tasks currently running.',
12+
description: 'Message displayed when there are no pending tasks',
13+
},
14+
taskTypeColumnName: {
15+
id: 'instruct.pendingTasks.table.column.taskType',
16+
defaultMessage: 'Task Type',
17+
description: 'Column name for task type in pending tasks table',
18+
},
19+
taskInputColumnName: {
20+
id: 'instruct.pendingTasks.table.column.taskInput',
21+
defaultMessage: 'Task Input',
22+
description: 'Column name for task input in pending tasks table',
23+
},
24+
taskIdColumnName: {
25+
id: 'instruct.pendingTasks.table.column.taskId',
26+
defaultMessage: 'Task ID',
27+
description: 'Column name for task ID in pending tasks table',
28+
},
29+
requesterColumnName: {
30+
id: 'instruct.pendingTasks.table.column.requester',
31+
defaultMessage: 'Requester',
32+
description: 'Column name for requester in pending tasks table',
33+
},
34+
taskStateColumnName: {
35+
id: 'instruct.pendingTasks.table.column.taskState',
36+
defaultMessage: 'Task State',
37+
description: 'Column name for task state in pending tasks table',
38+
},
39+
createdColumnName: {
40+
id: 'instruct.pendingTasks.table.column.created',
41+
defaultMessage: 'Created',
42+
description: 'Column name for created date in pending tasks table',
43+
},
44+
taskOutputColumnName: {
45+
id: 'instruct.pendingTasks.table.column.taskOutput',
46+
defaultMessage: 'Task Output',
47+
description: 'Column name for task output in pending tasks table',
48+
},
49+
durationColumnName: {
50+
id: 'instruct.pendingTasks.table.column.duration',
51+
defaultMessage: 'Duration (sec)',
52+
description: 'Column name for duration in pending tasks table',
53+
},
54+
statusColumnName: {
55+
id: 'instruct.pendingTasks.table.column.status',
56+
defaultMessage: 'Status',
57+
description: 'Column name for status in pending tasks table',
58+
},
59+
taskMessageColumnName: {
60+
id: 'instruct.pendingTasks.table.column.taskMessage',
61+
defaultMessage: 'Task Message',
62+
description: 'Column name for task message in pending tasks table',
63+
},
64+
});
65+
66+
export { messages };

src/courseInfo/CourseInfoPage.test.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,4 +30,9 @@ describe('CourseInfoPage', () => {
3030
renderComponent();
3131
expect(screen.getByText('General Course Info Component')).toBeInTheDocument();
3232
});
33+
34+
it('renders pending tasks section', () => {
35+
renderComponent();
36+
expect(screen.getByText('Pending Tasks')).toBeInTheDocument();
37+
});
3338
});

src/courseInfo/CourseInfoPage.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
import { Container } from '@openedx/paragon';
22
import { GeneralCourseInfo } from './components/generalCourseInfo';
3+
import { PendingTasks } from '../components/PendingTasks';
34

45
const CourseInfoPage = () => {
56
return (
67
<Container className="mt-4.5 mb-4" fluid="xl">
78
<GeneralCourseInfo />
9+
<PendingTasks />
810
</Container>
911
);
1012
};

0 commit comments

Comments
 (0)