Skip to content

Commit

Permalink
PADV-949 Add licenses page (#29)
Browse files Browse the repository at this point in the history
feat: Add licenses page
  • Loading branch information
AuraAlba authored Jan 17, 2024
1 parent d708d96 commit d6dbb65
Show file tree
Hide file tree
Showing 15 changed files with 354 additions and 9 deletions.
12 changes: 10 additions & 2 deletions src/features/Dashboard/DashboardPage/index.jsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,19 @@
import React, { useEffect, useState } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { useHistory } from 'react-router-dom';

import { Container } from '@edx/paragon';
import StudentsMetrics from 'features/Students/StudentsMetrics';
import LicensesTable from 'features/Licenses/LicensesTable';
import { Button } from 'react-paragon-topaz';

import { fetchLicensesData } from 'features/Dashboard/data';
import { updateActiveTab } from 'features/Main/data/slice';

import 'features/Dashboard/DashboardPage/index.scss';

const DashboardPage = () => {
const history = useHistory();
const dispatch = useDispatch();
const stateInstitution = useSelector((state) => state.main.institution.data);
const licenseData = useSelector((state) => state.dashboard.tableLicense.data);
Expand All @@ -20,6 +23,11 @@ const DashboardPage = () => {
// eslint-disable-next-line no-unused-expressions
stateInstitution.length > 0 ? idInstitution = stateInstitution[0].id : idInstitution = '';

const handleViewAllLicenses = () => {
history.push('/licenses');
dispatch(updateActiveTab('licenses'));
};

useEffect(() => {
if (licenseData.length > 5) {
// Return 5 licenses with fewest remaining seats
Expand All @@ -43,9 +51,9 @@ const DashboardPage = () => {
</h2>
<StudentsMetrics />
<div className="license-section">
<div className="d-flex justify-content-between">
<div className="d-flex justify-content-between px-4">
<h3>License inventory</h3>
<Button variant="outline-primary">View All</Button>
<Button onClick={handleViewAllLicenses} variant="outline-primary">View All</Button>
</div>
<LicensesTable data={dataTableLicense} />
</div>
Expand Down
2 changes: 1 addition & 1 deletion src/features/Dashboard/DashboardPage/index.scss
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,5 @@

.license-section {
background-color: $color-white;
padding: 2rem;
padding: 2rem 0;
}
68 changes: 68 additions & 0 deletions src/features/Licenses/LicensesPage/_test_/index.test.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import React from 'react';
import axios from 'axios';
import { render, waitFor } from '@testing-library/react';
import LicensesPage from 'features/Licenses/LicensesPage';
import '@testing-library/jest-dom/extend-expect';
import { Provider } from 'react-redux';
import { initializeStore } from 'store';

let store;

jest.mock('axios');

jest.mock('@edx/frontend-platform/logging', () => ({
logError: jest.fn(),
}));

const mockResponse = {
data: {
results: [
{
licenseName: 'License Name 1',
purchasedSeats: 20,
numberOfStudents: 6,
numberOfPendingStudents: 11,
},
{
licenseName: 'License Name 2',
purchasedSeats: 10,
numberOfStudents: 1,
numberOfPendingStudents: 5,
},
],
count: 2,
num_pages: 1,
current_page: 1,
},
};

describe('LicensesPage component', () => {
beforeEach(() => {
store = initializeStore();
});

test('renders licenses data components', () => {
axios.get.mockResolvedValue(mockResponse);

const component = render(
<Provider store={store}>
<LicensesPage />
</Provider>,
);

waitFor(() => {
expect(component.container).toHaveTextContent('License Pool');
expect(component.container).toHaveTextContent('License Name 1');
expect(component.container).toHaveTextContent('License Name 2');
expect(component.container).toHaveTextContent('Purchased');
expect(component.container).toHaveTextContent('20');
expect(component.container).toHaveTextContent('10');
expect(component.container).toHaveTextContent('Enrolled');
expect(component.container).toHaveTextContent('6');
expect(component.container).toHaveTextContent('1');
expect(component.container).toHaveTextContent('Remaining');
expect(component.container).toHaveTextContent('11');
expect(component.container).toHaveTextContent('5');
});
});
});
55 changes: 55 additions & 0 deletions src/features/Licenses/LicensesPage/index.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import React, { useEffect, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';

import Container from '@edx/paragon/dist/Container';
import LicensesTable from 'features/Licenses/LicensesTable';
import { Pagination } from '@edx/paragon';

import { fetchLicensesData } from 'features/Licenses/data';
import { updateCurrentPage } from 'features/Licenses/data/slice';
import { initialPage } from 'features/constants';

const LicensesPage = () => {
const dispatch = useDispatch();
const stateInstitution = useSelector((state) => state.main.institution.data);
const stateLicenses = useSelector((state) => state.licenses.table);
const [currentPage, setCurrentPage] = useState(initialPage);

let idInstitution = '';
// eslint-disable-next-line no-unused-expressions
stateInstitution.length > 0 ? idInstitution = stateInstitution[0].id : idInstitution = '';

const handlePagination = (targetPage) => {
setCurrentPage(targetPage);
dispatch(updateCurrentPage(targetPage));
};

useEffect(() => {
dispatch(fetchLicensesData(idInstitution));
}, [currentPage]); // eslint-disable-line react-hooks/exhaustive-deps

return (
<Container size="xl" className="px-4">
<h2 className="title-page">License pool inventory</h2>
<div className="page-content-container">
<LicensesTable
data={stateLicenses.data}
count={stateLicenses.count}
/>
{stateLicenses.numPages > 1 && (
<Pagination
paginationLabel="paginationNavigation"
pageCount={stateLicenses.numPages}
currentPage={currentPage}
onPageSelect={handlePagination}
variant="reduced"
className="mx-auto pagination-table"
size="small"
/>
)}
</div>
</Container>
);
};

export default LicensesPage;
2 changes: 1 addition & 1 deletion src/features/Licenses/LicensesTable/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ const LicensesTable = ({ data, count }) => {
return (
<IntlProvider locale="en">
<Row className="justify-content-center my-4 my-3 mx-0">
<Col xs={12} className="p-0">
<Col xs={12} className="px-4">
<DataTable
isSortable
columns={COLUMNS}
Expand Down
95 changes: 95 additions & 0 deletions src/features/Licenses/data/_test_/redux.test.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import MockAdapter from 'axios-mock-adapter';
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
import { initializeMockApp } from '@edx/frontend-platform/testing';
import { fetchLicensesData } from 'features/Licenses/data';
import { updateCurrentPage } from 'features/Licenses/data/slice';
import { executeThunk } from 'test-utils';
import { initializeStore } from 'store';

let axiosMock;
let store;

describe('Licenses redux tests', () => {
beforeEach(() => {
initializeMockApp({
authenticatedUser: {
userId: 1,
username: 'testuser',
administrator: true,
roles: [],
},
});
axiosMock = new MockAdapter(getAuthenticatedHttpClient());

store = initializeStore();
});

afterEach(() => {
axiosMock.reset();
});

test('successful fetch licenses data', async () => {
const licensesApiUrl = `${process.env.COURSE_OPERATIONS_API_V2_BASE_URL}/license-pool/?limit=true&institution_id=1`;
const mockResponse = {
results: [
{
licenseName: 'License Name 1',
purchasedSeats: 20,
numberOfStudents: 6,
numberOfPendingStudents: 11,
},
{
licenseName: 'License Name 2',
purchasedSeats: 10,
numberOfStudents: 1,
numberOfPendingStudents: 5,
},
],
count: 2,
num_pages: 1,
current_page: 1,
};
axiosMock.onGet(licensesApiUrl)
.reply(200, mockResponse);

expect(store.getState().licenses.table.status)
.toEqual('loading');

await executeThunk(fetchLicensesData(1), store.dispatch, store.getState);

expect(store.getState().licenses.table.data)
.toEqual(mockResponse.results);

expect(store.getState().licenses.table.status)
.toEqual('success');
});

test('failed fetch licenses data', async () => {
const licensesApiUrl = `${process.env.COURSE_OPERATIONS_API_V2_BASE_URL}/license-pool/?limit=false&institution_id=1`;
axiosMock.onGet(licensesApiUrl)
.reply(500);

expect(store.getState().licenses.table.status)
.toEqual('loading');

await executeThunk(fetchLicensesData(1), store.dispatch, store.getState);

expect(store.getState().licenses.table.data)
.toEqual([]);

expect(store.getState().licenses.table.status)
.toEqual('error');
});

test('update current page', () => {
const newPage = 2;
const intialState = store.getState().courses.table;
const expectState = {
...intialState,
currentPage: newPage,
};

store.dispatch(updateCurrentPage(newPage));
expect(store.getState().licenses.table).toEqual(expectState);
});
});
2 changes: 2 additions & 0 deletions src/features/Licenses/data/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { reducer } from 'features/Licenses/data/slice';
export { fetchLicensesData } from 'features/Licenses/data/thunks';
46 changes: 46 additions & 0 deletions src/features/Licenses/data/slice.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/* 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,
},
};

export const licensesSlice = createSlice({
name: 'licenses',
initialState,
reducers: {
updateCurrentPage: (state, { payload }) => {
state.table.currentPage = payload;
},
fetchLicensesDataRequest: (state) => {
state.table.status = RequestStatus.LOADING;
},
fetchLicensesDataSuccess: (state, { payload }) => {
const { results, count, numPages } = payload;
state.table.status = RequestStatus.SUCCESS;
state.table.data = results;
state.table.numPages = numPages;
state.table.count = count;
},
fetchLicensesDataFailed: (state) => {
state.table.status = RequestStatus.ERROR;
},
},
});

export const {
updateCurrentPage,
fetchLicensesDataRequest,
fetchLicensesDataSuccess,
fetchLicensesDataFailed,
} = licensesSlice.actions;

export const { reducer } = licensesSlice;
26 changes: 26 additions & 0 deletions src/features/Licenses/data/thunks.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { logError } from '@edx/frontend-platform/logging';
import { camelCaseObject } from '@edx/frontend-platform';

import { getLicensesByInstitution } from 'features/Common/data/api';
import {
fetchLicensesDataRequest,
fetchLicensesDataSuccess,
fetchLicensesDataFailed,
} from 'features/Licenses/data/slice';

function fetchLicensesData(id) {
return async (dispatch) => {
dispatch(fetchLicensesDataRequest());
try {
const response = camelCaseObject(await getLicensesByInstitution(id, true));
dispatch(fetchLicensesDataSuccess(response.data));
} catch (error) {
dispatch(fetchLicensesDataFailed());
logError(error);
}
};
}

export {
fetchLicensesData,
};
14 changes: 13 additions & 1 deletion src/features/Main/Sidebar/_test_/index.test.jsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import React from 'react';
import { render, fireEvent } from '@testing-library/react';
import { Provider } from 'react-redux';
import { Sidebar } from 'features/Main/Sidebar';
import '@testing-library/jest-dom/extend-expect';
import { initializeStore } from 'store';

let store;

const mockHistoryPush = jest.fn();

Expand All @@ -16,8 +20,16 @@ jest.mock('react-router', () => ({
}));

describe('Sidebar', () => {
beforeEach(() => {
store = initializeStore();
});

it('should render properly', () => {
const { getByRole } = render(<Sidebar />);
const { getByRole } = render(
<Provider store={store}>
<Sidebar />
</Provider>,
);

const studentsTabButton = getByRole('button', { name: /students/i });
expect(studentsTabButton).toBeInTheDocument();
Expand Down
Loading

0 comments on commit d6dbb65

Please sign in to comment.