Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
14f7ad01b8 | ||
|
|
104cb30ef5 | ||
|
|
e9bc4cebe4 | ||
|
|
559180592c | ||
|
|
0f1f0ae89d | ||
|
|
2e725e0441 | ||
|
|
a1c2ccc539 | ||
|
|
a70ddd79f6 |
BIN
assets/edx-sm.png
Normal file
BIN
assets/edx-sm.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.2 KiB |
@@ -64,6 +64,28 @@ module.exports = Merge.smart(commonConfig, {
|
||||
test: /\.(woff2?|ttf|svg|eot)(\?v=\d+\.\d+\.\d+)?$/,
|
||||
loader: 'file-loader',
|
||||
},
|
||||
{
|
||||
test: /\.(jpe?g|png|gif|ico)(\?v=\d+\.\d+\.\d+)?$/,
|
||||
use: [
|
||||
'file-loader',
|
||||
{
|
||||
loader: 'image-webpack-loader',
|
||||
options: {
|
||||
optimizationlevel: 7,
|
||||
mozjpeg: {
|
||||
progressive: true,
|
||||
},
|
||||
gifsicle: {
|
||||
interlaced: false,
|
||||
},
|
||||
pngquant: {
|
||||
quality: '65-90',
|
||||
speed: 4,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
// Specify additional processing or side-effects done on the Webpack output bundles as a whole.
|
||||
@@ -80,7 +102,7 @@ module.exports = Merge.smart(commonConfig, {
|
||||
LOGIN_URL: 'http://localhost:18000/login',
|
||||
LOGOUT_URL: 'http://localhost:18000/login',
|
||||
CSRF_TOKEN_API_PATH: '/csrf/api/v1/token',
|
||||
REFRESH_ACCESS_TOKEN_ENDPOINT: 'http://localhost:18000/login',
|
||||
REFRESH_ACCESS_TOKEN_ENDPOINT: 'http://localhost:18000/login_refresh',
|
||||
DATA_API_BASE_URL: 'http://localhost:8000',
|
||||
// LMS_CLIENT_ID should match the lms DOT client application id your LMS container
|
||||
LMS_CLIENT_ID: 'login-service-client-id',
|
||||
|
||||
@@ -70,6 +70,28 @@ module.exports = Merge.smart(commonConfig, {
|
||||
test: /\.(woff2?|ttf|svg|eot)(\?v=\d+\.\d+\.\d+)?$/,
|
||||
loader: 'file-loader',
|
||||
},
|
||||
{
|
||||
test: /\.(jpe?g|png|gif|ico)(\?v=\d+\.\d+\.\d+)?$/,
|
||||
use: [
|
||||
'file-loader',
|
||||
{
|
||||
loader: 'image-webpack-loader',
|
||||
options: {
|
||||
optimizationlevel: 7,
|
||||
mozjpeg: {
|
||||
progressive: true,
|
||||
},
|
||||
gifsicle: {
|
||||
interlaced: false,
|
||||
},
|
||||
pngquant: {
|
||||
quality: '65-90',
|
||||
speed: 4,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
// New in Webpack 4. Replaces CommonChunksPlugin. Extract common modules among all chunks to one
|
||||
|
||||
11644
package-lock.json
generated
11644
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -69,6 +69,7 @@
|
||||
"html-webpack-plugin": "^3.0.3",
|
||||
"husky": "^0.14.3",
|
||||
"identity-obj-proxy": "^3.0.0",
|
||||
"image-webpack-loader": "^4.2.0",
|
||||
"jest": "^22.4.0",
|
||||
"node-sass": "^4.7.2",
|
||||
"react-dev-utils": "^5.0.0",
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
.spinner-overlay {
|
||||
position: absolute;
|
||||
position: fixed;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
top: 0;
|
||||
background-color: #999;
|
||||
opacity: 0.5;
|
||||
z-index: 99999;
|
||||
@@ -28,9 +29,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
.back-link{
|
||||
float:right;
|
||||
}
|
||||
.student-filters{
|
||||
display: flex;
|
||||
.label{
|
||||
|
||||
@@ -223,13 +223,13 @@ export default class Gradebook extends React.Component {
|
||||
return (
|
||||
<div className="d-flex justify-content-center">
|
||||
{ this.props.showSpinner && <div className="spinner-overlay"><Icon className={['fa', 'fa-spinner', 'fa-spin', 'fa-5x', 'color-black']} /></div>}
|
||||
<div className="card gradebook-container">
|
||||
<div className="card-body">
|
||||
<div className="gradebook-container">
|
||||
<div>
|
||||
<a
|
||||
href={this.lmsInstructorDashboardUrl(this.props.match.params.courseId)}
|
||||
className="back-link"
|
||||
className="mb-3"
|
||||
>
|
||||
Back to Dashboard
|
||||
{'<< Back to Dashboard'}
|
||||
</a>
|
||||
<h1>Gradebook</h1>
|
||||
<h3> {this.props.match.params.courseId}</h3>
|
||||
|
||||
30
src/components/Header/index.jsx
Normal file
30
src/components/Header/index.jsx
Normal file
@@ -0,0 +1,30 @@
|
||||
import React from 'react';
|
||||
import { Hyperlink } from '@edx/paragon';
|
||||
|
||||
import EdxLogo from '../../../assets/edx-sm.png';
|
||||
|
||||
export default class Header extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
mobileNavOpen: false,
|
||||
};
|
||||
}
|
||||
|
||||
renderLogo() {
|
||||
return (
|
||||
<img src={EdxLogo} alt="edX logo" height="30" width="60" />
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="mb-3">
|
||||
<header className="d-flex justify-content-center align-items-center p-3 border-bottom-blue">
|
||||
<Hyperlink content={this.renderLogo()} destination="https://www.edx.org" />
|
||||
<div />
|
||||
</header>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -17,7 +17,7 @@ const fetchAssignmentTypes = courseId => (
|
||||
.then((data) => {
|
||||
dispatch(gotAssignmentTypes(Object.keys(data.assignment_types)));
|
||||
})
|
||||
.catch((error) => {
|
||||
.catch(() => {
|
||||
dispatch(errorFetchingAssignmentTypes());
|
||||
});
|
||||
}
|
||||
|
||||
73
src/data/actions/assignmentTypes.test.js
Normal file
73
src/data/actions/assignmentTypes.test.js
Normal file
@@ -0,0 +1,73 @@
|
||||
import configureMockStore from 'redux-mock-store';
|
||||
import MockAdapter from 'axios-mock-adapter';
|
||||
import thunk from 'redux-thunk';
|
||||
|
||||
import apiClient from '../apiClient';
|
||||
import { configuration } from '../../config';
|
||||
import { fetchAssignmentTypes } from './assignmentTypes';
|
||||
import {
|
||||
STARTED_FETCHING_ASSIGNMENT_TYPES,
|
||||
GOT_ASSIGNMENT_TYPES,
|
||||
ERROR_FETCHING_ASSIGNMENT_TYPES,
|
||||
} from '../constants/actionTypes/assignmentTypes';
|
||||
|
||||
const mockStore = configureMockStore([thunk]);
|
||||
const axiosMock = new MockAdapter(apiClient);
|
||||
|
||||
describe('actions', () => {
|
||||
afterEach(() => {
|
||||
axiosMock.reset();
|
||||
});
|
||||
|
||||
describe('fetchAssignmentTypes', () => {
|
||||
const courseId = 'course-v1:edX+DemoX+Demo_Course';
|
||||
|
||||
it('dispatches success action after fetching fetchAssignmentTypes', () => {
|
||||
const responseData = {
|
||||
assignment_types: {
|
||||
Exam: {
|
||||
drop_count: 0,
|
||||
min_count: 1,
|
||||
short_label: 'Exam',
|
||||
type: 'Exam',
|
||||
weight: 0.25,
|
||||
},
|
||||
Homework: {
|
||||
drop_count: 1,
|
||||
min_count: 3,
|
||||
short_label: 'Ex',
|
||||
type: 'Homework',
|
||||
weight: 0.75,
|
||||
},
|
||||
},
|
||||
};
|
||||
const expectedActions = [
|
||||
{ type: STARTED_FETCHING_ASSIGNMENT_TYPES },
|
||||
{ type: GOT_ASSIGNMENT_TYPES, assignmentTypes: Object.keys(responseData.assignment_types) },
|
||||
];
|
||||
const store = mockStore();
|
||||
|
||||
axiosMock.onGet(`${configuration.LMS_BASE_URL}/api/grades/v1/gradebook/${courseId}/grading-info?graded_only=true`)
|
||||
.replyOnce(200, JSON.stringify(responseData));
|
||||
|
||||
return store.dispatch(fetchAssignmentTypes(courseId)).then(() => {
|
||||
expect(store.getActions()).toEqual(expectedActions);
|
||||
});
|
||||
});
|
||||
|
||||
it('dispatches failure action after fetching cohorts', () => {
|
||||
const expectedActions = [
|
||||
{ type: STARTED_FETCHING_ASSIGNMENT_TYPES },
|
||||
{ type: ERROR_FETCHING_ASSIGNMENT_TYPES },
|
||||
];
|
||||
const store = mockStore();
|
||||
|
||||
axiosMock.onGet(`${configuration.LMS_BASE_URL}/api/grades/v1/gradebook/${courseId}/grading-info?graded_only=true`)
|
||||
.replyOnce(500, JSON.stringify({}));
|
||||
|
||||
return store.dispatch(fetchAssignmentTypes(courseId)).then(() => {
|
||||
expect(store.getActions()).toEqual(expectedActions);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -17,7 +17,7 @@ const fetchCohorts = courseId => (
|
||||
.then((data) => {
|
||||
dispatch(gotCohorts(data.cohorts));
|
||||
})
|
||||
.catch((error) => {
|
||||
.catch(() => {
|
||||
dispatch(errorFetchingCohorts());
|
||||
});
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import MockAdapter from 'axios-mock-adapter';
|
||||
import thunk from 'redux-thunk';
|
||||
|
||||
import apiClient from '../apiClient';
|
||||
import { configuration } from '../../config';
|
||||
import { fetchCohorts } from './cohorts';
|
||||
import {
|
||||
STARTED_FETCHING_COHORTS,
|
||||
@@ -47,7 +48,7 @@ describe('actions', () => {
|
||||
];
|
||||
const store = mockStore();
|
||||
|
||||
axiosMock.onGet(`http://localhost:18000/courses/${courseId}/cohorts/`)
|
||||
axiosMock.onGet(`${configuration.LMS_BASE_URL}/courses/${courseId}/cohorts/`)
|
||||
.replyOnce(200, JSON.stringify(responseData));
|
||||
|
||||
return store.dispatch(fetchCohorts(courseId)).then(() => {
|
||||
@@ -62,7 +63,7 @@ describe('actions', () => {
|
||||
];
|
||||
const store = mockStore();
|
||||
|
||||
axiosMock.onGet(`http://localhost:18000/courses/${courseId}/cohorts/`)
|
||||
axiosMock.onGet(`${configuration.LMS_BASE_URL}/courses/${courseId}/cohorts/`)
|
||||
.replyOnce(500, JSON.stringify({}));
|
||||
|
||||
return store.dispatch(fetchCohorts(courseId)).then(() => {
|
||||
|
||||
142
src/data/actions/grades.test.js
Normal file
142
src/data/actions/grades.test.js
Normal file
@@ -0,0 +1,142 @@
|
||||
import configureMockStore from 'redux-mock-store';
|
||||
import MockAdapter from 'axios-mock-adapter';
|
||||
import thunk from 'redux-thunk';
|
||||
|
||||
import apiClient from '../apiClient';
|
||||
import { configuration } from '../../config';
|
||||
import { fetchGrades } from './grades';
|
||||
import {
|
||||
STARTED_FETCHING_GRADES,
|
||||
FINISHED_FETCHING_GRADES,
|
||||
ERROR_FETCHING_GRADES,
|
||||
GOT_GRADES,
|
||||
UPDATE_BANNER,
|
||||
} from '../constants/actionTypes/grades';
|
||||
import { sortAlphaAsc } from './utils';
|
||||
|
||||
|
||||
const mockStore = configureMockStore([thunk]);
|
||||
const axiosMock = new MockAdapter(apiClient);
|
||||
|
||||
describe('actions', () => {
|
||||
afterEach(() => {
|
||||
axiosMock.reset();
|
||||
});
|
||||
|
||||
describe('fetchGrades', () => {
|
||||
const courseId = 'course-v1:edX+DemoX+Demo_Course';
|
||||
const expectedCohort = 1;
|
||||
const expectedTrack = 'verified';
|
||||
const fetchGradesURL = `${configuration.LMS_BASE_URL}/api/grades/v1/gradebook/${courseId}/?page_size=10&cohort_id=${expectedCohort}&enrollment_mode=${expectedTrack}`;
|
||||
const responseData = {
|
||||
next: `${fetchGradesURL}&cursor=2344fda`,
|
||||
previous: null,
|
||||
results: [
|
||||
{
|
||||
course_id: courseId,
|
||||
email: 'user1@example.com',
|
||||
username: 'user1',
|
||||
user_id: 1,
|
||||
percent: 0.5,
|
||||
letter_grade: null,
|
||||
section_breakdown: [
|
||||
{
|
||||
subsection_name: 'Demo Course Overview',
|
||||
score_earned: 0,
|
||||
score_possible: 0,
|
||||
percent: 0,
|
||||
displayed_value: '0.00',
|
||||
grade_description: '(0.00/0.00)',
|
||||
},
|
||||
{
|
||||
subsection_name: 'Example Week 1: Getting Started',
|
||||
score_earned: 1,
|
||||
score_possible: 1,
|
||||
percent: 1,
|
||||
displayed_value: '1.00',
|
||||
grade_description: '(0.00/0.00)',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
course_id: courseId,
|
||||
email: 'user22@example.com',
|
||||
username: 'user22',
|
||||
user_id: 22,
|
||||
percent: 0,
|
||||
letter_grade: null,
|
||||
section_breakdown: [
|
||||
{
|
||||
subsection_name: 'Demo Course Overview',
|
||||
score_earned: 0,
|
||||
score_possible: 0,
|
||||
percent: 0,
|
||||
displayed_value: '0.00',
|
||||
grade_description: '(0.00/0.00)',
|
||||
},
|
||||
{
|
||||
subsection_name: 'Example Week 1: Getting Started',
|
||||
score_earned: 1,
|
||||
score_possible: 1,
|
||||
percent: 0,
|
||||
displayed_value: '0.00',
|
||||
grade_description: '(0.00/0.00)',
|
||||
},
|
||||
],
|
||||
}],
|
||||
};
|
||||
|
||||
it('dispatches success action after fetching grades', () => {
|
||||
const expectedActions = [
|
||||
{ type: STARTED_FETCHING_GRADES },
|
||||
{
|
||||
type: GOT_GRADES,
|
||||
grades: responseData.results.sort(sortAlphaAsc),
|
||||
cohort: expectedCohort,
|
||||
track: expectedTrack,
|
||||
headings: [
|
||||
{
|
||||
columnSortable: true,
|
||||
key: 'username',
|
||||
label: 'Username',
|
||||
onSort: expect.anything(),
|
||||
},
|
||||
{
|
||||
columnSortable: true,
|
||||
key: 'total',
|
||||
label: 'Total',
|
||||
onSort: expect.anything(),
|
||||
},
|
||||
],
|
||||
prev: responseData.previous,
|
||||
next: responseData.next,
|
||||
},
|
||||
{ type: FINISHED_FETCHING_GRADES },
|
||||
{ type: UPDATE_BANNER, showSuccess: false },
|
||||
];
|
||||
const store = mockStore();
|
||||
|
||||
axiosMock.onGet(fetchGradesURL)
|
||||
.replyOnce(200, JSON.stringify(responseData));
|
||||
|
||||
return store.dispatch(fetchGrades(courseId, expectedCohort, expectedTrack, false)).then(() => {
|
||||
expect(store.getActions()).toEqual(expectedActions);
|
||||
});
|
||||
});
|
||||
|
||||
it('dispatches failure action after fetching grades', () => {
|
||||
const expectedActions = [
|
||||
{ type: STARTED_FETCHING_GRADES },
|
||||
{ type: ERROR_FETCHING_GRADES },
|
||||
];
|
||||
const store = mockStore();
|
||||
|
||||
axiosMock.onGet(fetchGradesURL)
|
||||
.replyOnce(500, JSON.stringify({}));
|
||||
|
||||
return store.dispatch(fetchGrades(courseId, expectedCohort, expectedTrack, false)).then(() => {
|
||||
expect(store.getActions()).toEqual(expectedActions);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -17,7 +17,7 @@ const fetchTracks = courseId => (
|
||||
.then((data) => {
|
||||
dispatch(gotTracks(data.course_modes));
|
||||
})
|
||||
.catch((error) => {
|
||||
.catch(() => {
|
||||
dispatch(errorFetchingTracks());
|
||||
});
|
||||
}
|
||||
|
||||
80
src/data/actions/tracks.test.js
Normal file
80
src/data/actions/tracks.test.js
Normal file
@@ -0,0 +1,80 @@
|
||||
import configureMockStore from 'redux-mock-store';
|
||||
import MockAdapter from 'axios-mock-adapter';
|
||||
import thunk from 'redux-thunk';
|
||||
|
||||
import apiClient from '../apiClient';
|
||||
import { configuration } from '../../config';
|
||||
import { fetchTracks } from './tracks';
|
||||
import {
|
||||
STARTED_FETCHING_TRACKS,
|
||||
GOT_TRACKS,
|
||||
ERROR_FETCHING_TRACKS,
|
||||
} from '../constants/actionTypes/tracks';
|
||||
|
||||
const mockStore = configureMockStore([thunk]);
|
||||
const axiosMock = new MockAdapter(apiClient);
|
||||
|
||||
describe('actions', () => {
|
||||
afterEach(() => {
|
||||
axiosMock.reset();
|
||||
});
|
||||
|
||||
describe('fetchTracks', () => {
|
||||
const courseId = 'course-v1:edX+DemoX+Demo_Course';
|
||||
|
||||
it('dispatches success action after fetching tracks', () => {
|
||||
const responseData = {
|
||||
course_modes: [
|
||||
{
|
||||
slug: 'audit',
|
||||
name: 'Audit',
|
||||
min_price: 0,
|
||||
suggested_prices: '',
|
||||
currency: 'usd',
|
||||
expiration_datetime: null,
|
||||
description: null,
|
||||
sku: '68EFFFF',
|
||||
bulk_sku: null,
|
||||
},
|
||||
{
|
||||
slug: 'verified',
|
||||
name: 'Verified Certificate',
|
||||
min_price: 100,
|
||||
suggested_prices: '',
|
||||
currency: 'usd',
|
||||
expiration_datetime: '2021-05-04T18:08:12.644361Z',
|
||||
description: null,
|
||||
sku: '8CF08E5',
|
||||
bulk_sku: 'A5B6DBE',
|
||||
}],
|
||||
};
|
||||
const expectedActions = [
|
||||
{ type: STARTED_FETCHING_TRACKS },
|
||||
{ type: GOT_TRACKS, tracks: responseData.course_modes },
|
||||
];
|
||||
const store = mockStore();
|
||||
|
||||
axiosMock.onGet(`${configuration.LMS_BASE_URL}/api/enrollment/v1/course/${courseId}`)
|
||||
.replyOnce(200, JSON.stringify(responseData));
|
||||
|
||||
return store.dispatch(fetchTracks(courseId)).then(() => {
|
||||
expect(store.getActions()).toEqual(expectedActions);
|
||||
});
|
||||
});
|
||||
|
||||
it('dispatches failure action after fetching tracks', () => {
|
||||
const expectedActions = [
|
||||
{ type: STARTED_FETCHING_TRACKS },
|
||||
{ type: ERROR_FETCHING_TRACKS },
|
||||
];
|
||||
const store = mockStore();
|
||||
|
||||
axiosMock.onGet(`${configuration.LMS_BASE_URL}/api/enrollment/v1/course/${courseId}`)
|
||||
.replyOnce(500, JSON.stringify({}));
|
||||
|
||||
return store.dispatch(fetchTracks(courseId)).then(() => {
|
||||
expect(store.getActions()).toEqual(expectedActions);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -17,6 +17,7 @@ const cohorts = (state = initialState, action) => {
|
||||
return {
|
||||
...state,
|
||||
results: action.cohorts,
|
||||
finishedFetching: true,
|
||||
errorFetching: false,
|
||||
};
|
||||
case STARTED_FETCHING_COHORTS:
|
||||
|
||||
70
src/data/reducers/cohorts.test.js
Normal file
70
src/data/reducers/cohorts.test.js
Normal file
@@ -0,0 +1,70 @@
|
||||
import cohorts from './cohorts';
|
||||
import {
|
||||
STARTED_FETCHING_COHORTS,
|
||||
ERROR_FETCHING_COHORTS,
|
||||
GOT_COHORTS,
|
||||
} from '../constants/actionTypes/cohorts';
|
||||
|
||||
const initialState = {
|
||||
results: [],
|
||||
startedFetching: false,
|
||||
errorFetching: false,
|
||||
};
|
||||
|
||||
const cohortsData = [
|
||||
{
|
||||
assignment_type: 'manual',
|
||||
group_id: null,
|
||||
id: 1,
|
||||
name: 'default_group',
|
||||
user_count: 2,
|
||||
user_partition_id: null,
|
||||
},
|
||||
{
|
||||
assignment_type: 'auto',
|
||||
group_id: null,
|
||||
id: 2,
|
||||
name: 'auto_group',
|
||||
user_count: 5,
|
||||
user_partition_id: null,
|
||||
}];
|
||||
|
||||
describe('dashboardAnalytics reducer', () => {
|
||||
it('has initial state', () => {
|
||||
expect(cohorts(undefined, {})).toEqual(initialState);
|
||||
});
|
||||
|
||||
it('updates fetch cohorts request state', () => {
|
||||
const expected = {
|
||||
...initialState,
|
||||
startedFetching: true,
|
||||
};
|
||||
expect(cohorts(undefined, {
|
||||
type: STARTED_FETCHING_COHORTS,
|
||||
})).toEqual(expected);
|
||||
});
|
||||
|
||||
it('updates fetch cohorts success state', () => {
|
||||
const expected = {
|
||||
...initialState,
|
||||
results: cohortsData,
|
||||
errorFetching: false,
|
||||
finishedFetching: true,
|
||||
};
|
||||
expect(cohorts(undefined, {
|
||||
type: GOT_COHORTS,
|
||||
cohorts: cohortsData,
|
||||
})).toEqual(expected);
|
||||
});
|
||||
|
||||
it('updates fetch cohorts failure state', () => {
|
||||
const expected = {
|
||||
...initialState,
|
||||
errorFetching: true,
|
||||
finishedFetching: true,
|
||||
};
|
||||
expect(cohorts(undefined, {
|
||||
type: ERROR_FETCHING_COHORTS,
|
||||
})).toEqual(expected);
|
||||
});
|
||||
});
|
||||
@@ -6,17 +6,21 @@ import { Provider } from 'react-redux';
|
||||
|
||||
import apiClient from './data/apiClient';
|
||||
import GradebookPage from './containers/GradebookPage';
|
||||
import Header from './components/Header';
|
||||
import store from './data/store';
|
||||
import './App.scss';
|
||||
|
||||
const App = () => (
|
||||
<Provider store={store}>
|
||||
<Router>
|
||||
<main>
|
||||
<Switch>
|
||||
<Route exact path="/:courseId" component={GradebookPage} />
|
||||
</Switch>
|
||||
</main>
|
||||
<div>
|
||||
<Header />
|
||||
<main>
|
||||
<Switch>
|
||||
<Route exact path="/:courseId" component={GradebookPage} />
|
||||
</Switch>
|
||||
</main>
|
||||
</div>
|
||||
</Router>
|
||||
</Provider>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user