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

PADV-949 Add licenses page #29

Merged
merged 2 commits into from
Jan 17, 2024
Merged
Show file tree
Hide file tree
Changes from all 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
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