diff --git a/src/features/Classes/ClassesPage/index.jsx b/src/features/Classes/ClassesPage/index.jsx
new file mode 100644
index 00000000..d7c3dc08
--- /dev/null
+++ b/src/features/Classes/ClassesPage/index.jsx
@@ -0,0 +1,53 @@
+import React, { useEffect, useState } from 'react';
+import { useDispatch, useSelector } from 'react-redux';
+
+import Container from '@edx/paragon/dist/Container';
+import { Pagination } from '@edx/paragon';
+import ClassesTable from 'features/Classes/ClassesTable';
+
+import { updateCurrentPage } from 'features/Classes/data/slice';
+import { fetchClassesData } from 'features/Classes/data/thunks';
+import { initialPage } from 'features/constants';
+
+const ClassesPage = () => {
+ const selectedInstitution = useSelector((state) => state.main.selectedInstitution);
+ const stateClasses = useSelector((state) => state.classes);
+ const dispatch = useDispatch();
+ const [currentPage, setCurrentPage] = useState(initialPage);
+
+ useEffect(() => {
+ if (Object.keys(selectedInstitution).length > 0) {
+ dispatch(fetchClassesData(selectedInstitution.id, currentPage));
+ }
+ }, [currentPage, selectedInstitution, dispatch]); // eslint-disable-line react-hooks/exhaustive-deps
+
+ const handlePagination = (targetPage) => {
+ setCurrentPage(targetPage);
+ dispatch(updateCurrentPage(targetPage));
+ };
+
+ return (
+
+ Classes
+
+
+ {stateClasses.table.numPages > 1 && (
+
+ )}
+
+
+ );
+};
+
+export default ClassesPage;
diff --git a/src/features/Classes/ClassesTable/_test_/index.test.jsx b/src/features/Classes/ClassesTable/_test_/index.test.jsx
new file mode 100644
index 00000000..d1f48a60
--- /dev/null
+++ b/src/features/Classes/ClassesTable/_test_/index.test.jsx
@@ -0,0 +1,55 @@
+import React from 'react';
+import CoursesPage from 'features/Courses/CoursesPage';
+import { waitFor } from '@testing-library/react';
+import '@testing-library/jest-dom/extend-expect';
+import { renderWithProviders } from 'test-utils';
+
+jest.mock('@edx/frontend-platform/logging', () => ({
+ logError: jest.fn(),
+}));
+
+const mockStore = {
+ courses: {
+ table: {
+ data: [
+ {
+ masterCourseName: 'Demo Course 1',
+ numberOfClasses: 1,
+ missingClassesForInstructor: null,
+ numberOfStudents: 1,
+ numberOfPendingStudents: 1,
+ },
+ {
+ masterCourseName: 'Demo Course 2',
+ numberOfClasses: 1,
+ missingClassesForInstructor: 1,
+ numberOfStudents: 16,
+ numberOfPendingStudents: 0,
+ },
+ ],
+ count: 2,
+ num_pages: 1,
+ current_page: 1,
+ },
+ },
+};
+
+describe('CoursesPage', () => {
+ it('renders courses data and pagination', async () => {
+ const component = renderWithProviders(
+ ,
+ { preloadedState: mockStore },
+ );
+
+ waitFor(() => {
+ expect(component.container).toHaveTextContent('Demo Course 1');
+ expect(component.container).toHaveTextContent('Demo Course 2');
+ expect(component.container).toHaveTextContent('Ready');
+ expect(component.container).toHaveTextContent('Missing (1)');
+ expect(component.container).toHaveTextContent('Pending (1)');
+ expect(component.container).toHaveTextContent('Complete');
+ expect(component.container).toHaveTextContent('1/2');
+ expect(component.container).toHaveTextContent('16/16');
+ });
+ });
+});
diff --git a/src/features/Classes/ClassesTable/columns.jsx b/src/features/Classes/ClassesTable/columns.jsx
new file mode 100644
index 00000000..3825e49d
--- /dev/null
+++ b/src/features/Classes/ClassesTable/columns.jsx
@@ -0,0 +1,42 @@
+/* eslint-disable react/prop-types, no-nested-ternary */
+import React from 'react';
+import { format } from 'date-fns';
+
+const columns = [
+ {
+ Header: 'Course',
+ accessor: 'masterCourseName',
+ },
+ {
+ Header: 'Class',
+ accessor: 'className',
+ },
+ {
+ Header: 'Start Date',
+ accessor: 'startDate',
+ Cell: ({ row }) => (row.values.startDate ? format(row.values.startDate, 'MM/dd/yy') : ''),
+ },
+ {
+ Header: 'End Date',
+ accessor: 'endDate',
+ Cell: ({ row }) => (row.values.endDate ? format(row.values.endDate, 'MM/dd/yy') : ''),
+ },
+ {
+ Header: 'Students Enrolled',
+ accessor: 'numberOfStudents',
+ },
+ {
+ Header: 'Max',
+ accessor: 'maxStudents',
+ },
+ {
+ Header: 'Instructors',
+ accessor: ({ instructors }) => (
+
+ {instructors.map(instructor => - {`${instructor}`}
)}
+
+ ),
+ },
+];
+
+export { columns };
diff --git a/src/features/Classes/ClassesTable/index.jsx b/src/features/Classes/ClassesTable/index.jsx
new file mode 100644
index 00000000..f2c2eb33
--- /dev/null
+++ b/src/features/Classes/ClassesTable/index.jsx
@@ -0,0 +1,43 @@
+import React, { useMemo } from 'react';
+import PropTypes from 'prop-types';
+
+import { IntlProvider } from 'react-intl';
+import { Row, Col } from '@edx/paragon';
+import DataTable from '@edx/paragon/dist/DataTable';
+
+import { columns } from 'features/Classes/ClassesTable/columns';
+
+const ClassesTable = ({ data, count }) => {
+ const COLUMNS = useMemo(() => columns, []);
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+ClassesTable.propTypes = {
+ data: PropTypes.arrayOf(PropTypes.shape([])),
+ count: PropTypes.number,
+};
+
+ClassesTable.defaultProps = {
+ data: [],
+ count: 0,
+};
+
+export default ClassesTable;
diff --git a/src/features/Classes/_test_/index.test.jsx b/src/features/Classes/_test_/index.test.jsx
new file mode 100644
index 00000000..e69de29b
diff --git a/src/features/Classes/data/_test_/redux.test.js b/src/features/Classes/data/_test_/redux.test.js
new file mode 100644
index 00000000..e69de29b
diff --git a/src/features/Classes/data/index.js b/src/features/Classes/data/index.js
new file mode 100644
index 00000000..a6a37d7b
--- /dev/null
+++ b/src/features/Classes/data/index.js
@@ -0,0 +1,2 @@
+export { reducer } from 'features/Classes/data/slice';
+export { fetchClassesData } from 'features/Classes/data/thunks';
diff --git a/src/features/Classes/data/slice.js b/src/features/Classes/data/slice.js
new file mode 100644
index 00000000..fa58456b
--- /dev/null
+++ b/src/features/Classes/data/slice.js
@@ -0,0 +1,51 @@
+/* eslint-disable no-param-reassign */
+import { createSlice } from '@reduxjs/toolkit';
+import { RequestStatus } from 'features/constants';
+
+const initialState = {
+ table: {
+ currentPage: 1,
+ data: [],
+ status: RequestStatus.LOADING,
+ error: null,
+ numPages: 0,
+ count: 0,
+ },
+ filters: {},
+};
+
+export const classesSlice = createSlice({
+ name: 'classes',
+ initialState,
+ reducers: {
+ updateCurrentPage: (state, { payload }) => {
+ state.table.currentPage = payload;
+ },
+ fetchClassesDataRequest: (state) => {
+ state.table.status = RequestStatus.LOADING;
+ },
+ fetchClassesDataSuccess: (state, { payload }) => {
+ const { results, count, numPages } = payload;
+ state.table.status = RequestStatus.SUCCESS;
+ state.table.data = results;
+ state.table.numPages = numPages;
+ state.table.count = count;
+ },
+ fetchClassesDataFailed: (state) => {
+ state.table.status = RequestStatus.ERROR;
+ },
+ updateFilters: (state, { payload }) => {
+ state.filters = payload;
+ },
+ },
+});
+
+export const {
+ updateCurrentPage,
+ fetchClassesDataRequest,
+ fetchClassesDataSuccess,
+ fetchClassesDataFailed,
+ updateFilters,
+} = classesSlice.actions;
+
+export const { reducer } = classesSlice;
diff --git a/src/features/Classes/data/thunks.js b/src/features/Classes/data/thunks.js
new file mode 100644
index 00000000..0091b763
--- /dev/null
+++ b/src/features/Classes/data/thunks.js
@@ -0,0 +1,26 @@
+import { logError } from '@edx/frontend-platform/logging';
+import { camelCaseObject } from '@edx/frontend-platform';
+import {
+ fetchClassesDataRequest,
+ fetchClassesDataSuccess,
+ fetchClassesDataFailed,
+} from 'features/Classes/data/slice';
+import { getClassesByInstitution } from 'features/Common/data/api';
+
+function fetchClassesData(id, currentPage) {
+ return async (dispatch) => {
+ dispatch(fetchClassesDataRequest);
+
+ try {
+ const response = camelCaseObject(await getClassesByInstitution(id, '', true, '', currentPage));
+ dispatch(fetchClassesDataSuccess(response.data));
+ } catch (error) {
+ dispatch(fetchClassesDataFailed());
+ logError(error);
+ }
+ };
+}
+
+export {
+ fetchClassesData,
+};
diff --git a/src/features/Common/data/api.js b/src/features/Common/data/api.js
index 669dbfa3..8c7d9fdb 100644
--- a/src/features/Common/data/api.js
+++ b/src/features/Common/data/api.js
@@ -19,12 +19,12 @@ function getLicensesByInstitution(institutionId, limit, page = '') {
);
}
-function getClassesByInstitution(institutionId, courseName, limit = false, instructorsList = '') {
+function getClassesByInstitution(institutionId, courseName, limit = false, instructorsList = '', page = '') {
const encodedCourseName = encodeURIComponent(courseName);
return getAuthenticatedHttpClient().get(
`${getConfig().COURSE_OPERATIONS_API_V2_BASE_URL}/classes`
- + `/?limit=${limit}&institution_id=${institutionId}&course_name=${encodedCourseName}&instructors=${instructorsList}`,
+ + `/?limit=${limit}&institution_id=${institutionId}&course_name=${encodedCourseName}&instructors=${instructorsList}&page=${page}`,
);
}
diff --git a/src/features/Main/Sidebar/index.jsx b/src/features/Main/Sidebar/index.jsx
index 0c51f18b..2542398a 100644
--- a/src/features/Main/Sidebar/index.jsx
+++ b/src/features/Main/Sidebar/index.jsx
@@ -73,6 +73,17 @@ export const Sidebar = () => {
Courses
+
+
+
diff --git a/src/features/Main/index.jsx b/src/features/Main/index.jsx
index e56b6c8f..b7e6e994 100644
--- a/src/features/Main/index.jsx
+++ b/src/features/Main/index.jsx
@@ -14,6 +14,7 @@ import InstructorsPage from 'features/Instructors/InstructorsPage';
import CoursesPage from 'features/Courses/CoursesPage';
import DashboardPage from 'features/Dashboard/DashboardPage';
import LicensesPage from 'features/Licenses/LicensesPage';
+import ClassesPage from 'features/Classes/ClassesPage';
import { Select } from 'react-paragon-topaz';
import { fetchInstitutionData } from 'features/Main/data/thunks';
@@ -93,6 +94,9 @@ const Main = () => {
+
+
+
diff --git a/src/store.js b/src/store.js
index 1320240a..73e4d6ba 100644
--- a/src/store.js
+++ b/src/store.js
@@ -5,6 +5,7 @@ import { reducer as coursesReducer } from 'features/Courses/data';
import { reducer as studentsReducer } from 'features/Students/data';
import { reducer as dashboardReducer } from 'features/Dashboard/data';
import { reducer as licensesReducer } from 'features/Licenses/data';
+import { reducer as classesReducer } from 'features/Classes/data';
export function initializeStore(preloadedState = undefined) {
return configureStore({
@@ -15,6 +16,7 @@ export function initializeStore(preloadedState = undefined) {
students: studentsReducer,
dashboard: dashboardReducer,
licenses: licensesReducer,
+ classes: classesReducer,
},
preloadedState,
});