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

[BD-05] [BB-2963] Improve Instructor dashboard #2

Draft
wants to merge 5 commits into
base: master
Choose a base branch
from
Draft
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
2 changes: 1 addition & 1 deletion src/components/value-or-loading/ValueOrLoading.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ function LoadingOrValue({ value }) {
}

LoadingOrValue.propTypes = {
value: PropType.element,
value: PropType.node,
};

LoadingOrValue.defaultProps = {
Expand Down
12 changes: 12 additions & 0 deletions src/data/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,15 @@ export const oraDataShape = PropTypes.objectOf(PropTypes.shape({
done: PropTypes.number,
status: PropTypes.string,
}));

export const oraSummaryDataShape = PropTypes.objectOf(PropTypes.shape({
units: PropTypes.number,
assessments: PropTypes.number,
total: PropTypes.number,
training: PropTypes.number,
peer: PropTypes.number,
self: PropTypes.number,
waiting: PropTypes.number,
staff: PropTypes.number,
done: PropTypes.number,
}));
33 changes: 22 additions & 11 deletions src/ora-dashboard/OraDashboard.jsx
Original file line number Diff line number Diff line change
@@ -1,26 +1,37 @@
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import React from 'react';
import { useSelector } from 'react-redux';
import { Spinner } from '@edx/paragon';
import SummaryTable from './summary-table/SummaryTable';
import DataTable from './data-table/DataTable';
import { oraDataShape } from '../data/constants';
import { oraDataShape, oraSummaryDataShape, RequestStatus } from '../data/constants';
import { loadingStatus } from './data/selectors';
import messages from './messages';

function OraDashboard({ intl, data, summary }) {
const status = useSelector(loadingStatus);

function OraDashboard({ data }) {
return (
<main>
<div className="container-fluid">
<h1>Open Responses</h1>
<SummaryTable />
<DataTable data={data} />
<h1>{intl.formatMessage(messages.section_heading)}</h1>
{status === RequestStatus.IN_PROGRESS && <Spinner animation="border" variant="primary" />}
{status === RequestStatus.SUCCESSFUL && (
<>
<SummaryTable data={summary} />
<DataTable data={data} />
</>
)}

</div>
</main>
);
}

OraDashboard.propTypes = {
data: oraDataShape,
};

OraDashboard.defaultProps = {
data: {},
intl: intlShape.isRequired,
data: oraDataShape.isRequired,
summary: oraSummaryDataShape.isRequired,
};

export default OraDashboard;
export default injectIntl(OraDashboard);
6 changes: 4 additions & 2 deletions src/ora-dashboard/OraDashboardContainer.jsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,22 @@
import React, { useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { useParams } from 'react-router';
import { selectOraBlocks } from './data/selectors';
import { selectOraBlocks, selectSummary } from './data/selectors';
import { fetchOraBlocks } from './data/thunks';
import OraDashboard from './OraDashboard';

export default function OraDashboardContainer() {
const { courseId } = useParams();
const dispatch = useDispatch();
const oraBlocks = useSelector(selectOraBlocks);
const summary = useSelector(selectSummary);

useEffect(() => {
// The courseId from the URL is the course we WANT to load.
dispatch(fetchOraBlocks(courseId));
}, [courseId]);

return (
<OraDashboard data={oraBlocks} />
<OraDashboard data={oraBlocks} summary={summary} />
);
}
151 changes: 101 additions & 50 deletions src/ora-dashboard/data-table/DataTable.jsx
Original file line number Diff line number Diff line change
@@ -1,67 +1,118 @@
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import React, { useEffect } from 'react';
import { Table } from '@edx/paragon';
import { useDispatch } from 'react-redux';
import { useParams } from 'react-router';
import LoadingOrValue from '../../components/value-or-loading/ValueOrLoading';
import messages from './messages';
import { fetchOraReports } from '../data/thunks';
import { oraDataShape } from '../../data/constants';
import EmbedORAModal from '../embed-ora-modal/EmbedORAModal';

/**
* Sort implementation for Paragon Table
* @param {any} firstElement
* @param {any} secondElement
* @param {string} key
* @param {string} direction
*/
function sort(firstElement, secondElement, key, direction) {
const directionIsAsc = direction === 'asc';

if (firstElement[key] > secondElement[key]) {
return directionIsAsc ? 1 : -1;
} if (firstElement[key] < secondElement[key]) {
return directionIsAsc ? -1 : 1;
}
return 0;
}

function DataTable({ intl, data }) {
const { courseId } = useParams();
const dispatch = useDispatch();
useEffect(() => {
Object.keys(data).map((blockId) => data[blockId].status || dispatch(fetchOraReports(courseId, blockId)));
if (Object.keys(data).length > 0) {
const blockId = Object.keys(data)[0];
if (!data[blockId].status) {
dispatch(fetchOraReports(courseId, blockId));
}
}
}, [courseId, data]);

// create a copy of data for sortable Table
const sortableData = Object.values(data).slice().map(item => {
const action = <EmbedORAModal usageKey={item.id} title={item.name} buttonText="Manage" />;
return { ...item, actions: action };
});

// define Table columns
const columns = [
{
label: intl.formatMessage(messages.unit_name),
key: 'vertical',
columnSortable: true,
},
{
label: intl.formatMessage(messages.assessment),
key: 'name',
columnSortable: true,
},
{
label: intl.formatMessage(messages.total_responses),
key: 'total',
columnSortable: true,
},
{
label: intl.formatMessage(messages.training),
key: 'training',
columnSortable: true,
},
{
label: intl.formatMessage(messages.peer),
key: 'peer',
columnSortable: true,
},
{
label: intl.formatMessage(messages.self),
key: 'self',
columnSortable: true,
},
{
label: intl.formatMessage(messages.waiting),
key: 'waiting',
columnSortable: true,
},
{
label: intl.formatMessage(messages.staff),
key: 'staff',
columnSortable: true,
},
{
label: intl.formatMessage(messages.final_grade_received),
key: 'done',
columnSortable: true,
},
{
label: '',
key: 'actions',
},
];

return (
<table className="table">
<thead>
<tr>
<th>
{intl.formatMessage(messages.unit_name)}
</th>
<th>
{intl.formatMessage(messages.assessment)}
</th>
<th>
{intl.formatMessage(messages.total_responses)}
</th>
<th>
{intl.formatMessage(messages.training)}
</th>
<th>
{intl.formatMessage(messages.peer)}
</th>
<th>
{intl.formatMessage(messages.self)}
</th>
<th>
{intl.formatMessage(messages.waiting)}
</th>
<th>
{intl.formatMessage(messages.staff)}
</th>
<th>
{intl.formatMessage(messages.final_grade_received)}
</th>
</tr>
</thead>
<tbody>
{Object.values(data).map(((block) => (
<tr key={block.id}>
<td>{block.vertical}</td>
<td>{block.name}</td>
<td><LoadingOrValue value={block.total} /></td>
<td><LoadingOrValue value={block.training} /></td>
<td><LoadingOrValue value={block.peer} /></td>
<td><LoadingOrValue value={block.self} /></td>
<td><LoadingOrValue value={block.waiting} /></td>
<td><LoadingOrValue value={block.staff} /></td>
<td><LoadingOrValue value={block.done} /></td>
</tr>
)))}
</tbody>
</table>
<div>
<Table
data={sortableData}
columns={columns.map(column => ({
...column,
onSort(direction) {
sortableData.sort(
(firstElement, secondElement) => sort(firstElement, secondElement, column.key, direction),
);
},
}))}
tableSortable
/>
{/* {iframes} */}
</div>
);
}
DataTable.propTypes = {
Expand Down
2 changes: 2 additions & 0 deletions src/ora-dashboard/data/selectors.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
/* eslint-disable import/prefer-default-export */
export const selectOraBlocks = state => state.blocks;

export const selectSummary = state => state.summary;

export const loadingStatus = state => state.status;
58 changes: 55 additions & 3 deletions src/ora-dashboard/data/slices.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,61 @@ function oraBlocksFromResponse(response) {
return oraBlocks;
}

/**
* Given a get_ora2_responses API response, set statistics for each ORA Block.
* Also assign default value if there is no statistics for any block.
* @param {array} blockIds - list of blockIdx in current course
* @param {object} payload - get_ora2_responses api response
*/
function oraBlockStatisticsFromPayload(blockIds, payload) {
const blocks = {};
const defaultValues = {
status: RequestStatus.SUCCESSFUL,
total: 0,
training: 0,
peer: 0,
self: 0,
waiting: 0,
staff: 0,
done: 0,
};
blockIds.forEach(blockId => {
blocks[blockId] = { ...defaultValues, ...payload[blockId] };
});
return blocks;
}

/**
* Given current state.oraBlocks object, prepares summary.
* @param {object} data - state.oraBlocks
*/
function prepareStatisticsSummary(data) {
const dataKeys = Object.keys(data);
const dataItemArr = dataKeys.map((key) => data[key]);

const sumByKey = (key) => dataItemArr
.map((item) => (item[key] ? item[key] : 0))
.reduce((result, val) => result + val, 0);

const fields = ['total', 'training', 'peer', 'self', 'waiting', 'staff', 'done'];
const summaryData = {
units: dataKeys.length,
assessments: dataKeys.length,
};

fields.forEach(field => {
summaryData[field] = sumByKey(field);
});

return summaryData;
}

const oraSlice = createSlice({
name: 'ora',
initialState: {
status: RequestStatus.IN_PROGRESS,
blocks: {},
summary: {},
},
reducers: {
fetchOraBlocksRequest: (state) => {
Expand All @@ -57,10 +107,12 @@ const oraSlice = createSlice({
state.blocks[payload.blockId].status = RequestStatus.IN_PROGRESS;
},
fetchOraReportSuccess: (state, { payload }) => {
Object.keys(payload).forEach((blockId) => {
state.blocks[blockId].status = RequestStatus.SUCCESSFUL;
Object.assign(state.blocks[blockId], payload[blockId]);
const statistics = oraBlockStatisticsFromPayload(Object.keys(state.blocks), payload);
Object.keys(state.blocks).forEach(blockId => {
state.blocks[blockId] = { ...state.blocks[blockId], ...statistics[blockId] };
});
// as statistics updated, update summary too.
state.summary = prepareStatisticsSummary(state.blocks);
},
fetchOraReportFailed: (state, { payload }) => {
state.blocks[payload.blockId].status = RequestStatus.FAILED;
Expand Down
37 changes: 37 additions & 0 deletions src/ora-dashboard/embed-ora-modal/EmbedORAModal.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { Button, Modal } from '@edx/paragon';
import { getConfig } from '@edx/frontend-platform';
import React, { useState } from 'react';

import PropTypes from 'prop-types';

const { LMS_BASE_URL } = getConfig();

function EmbedORAModal({ usageKey, title, buttonText }) {
const [open, setOpen] = useState(false);

return (
<div className="embed-ora-modal">
<Modal
open={open}
dialogClassName="modal-lg"
title={title}
renderDefaultCloseButton={false}
onClose={() => setOpen(false)}
body={(
<div className="embed-responsive embed-responsive-16by9">
<iframe title={title} className="embed-responsive-item ora-iframe" src={`${LMS_BASE_URL}/xblock/${usageKey}`} />
</div>
)}
/>
<Button variant="light" onClick={() => setOpen(true)}>{buttonText}</Button>
</div>
);
}

EmbedORAModal.propTypes = {
usageKey: PropTypes.string.isRequired,
title: PropTypes.string.isRequired,
buttonText: PropTypes.oneOfType([PropTypes.string, PropTypes.element]).isRequired,
};

export default EmbedORAModal;
Loading