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

CP3108 Staff Dashboard - Avengers Leaderboard #1069

Merged
merged 13 commits into from
May 19, 2020
25 changes: 25 additions & 0 deletions src/actions/__tests__/dashboard.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { IGroupOverview } from '../../components/dashboard/groupShape';
import * as actionTypes from '../actionTypes';
import { fetchGroupOverviews, updateGroupOverviews } from '../dashboard';

test('fetchGroupOverviews generates correct action object', () => {
const action = fetchGroupOverviews();
expect(action).toEqual({
type: actionTypes.FETCH_GROUP_OVERVIEWS
});
});

test('updateGroupOverviews generates correct action object', () => {
const overviews: IGroupOverview[] = [
{
id: 1,
avengerName: 'Billy',
groupName: 'Test Group 1'
}
];
const action = updateGroupOverviews(overviews);
expect(action).toEqual({
type: actionTypes.UPDATE_GROUP_OVERVIEWS,
payload: overviews
});
});
5 changes: 5 additions & 0 deletions src/actions/actionTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,3 +121,8 @@ export const FETCH_NOTIFICATIONS = 'FETCH_NOTIFICATIONS';
export const ACKNOWLEDGE_NOTIFICATIONS = 'ACKNOWLEDGE_NOTIFICATIONS';
export const UPDATE_NOTIFICATIONS = 'UPDATE_NOTIFICATIONS';
export const NOTIFY_CHATKIT_USERS = 'NOTIFY_CHATKIT_USERS';

/** Dashboard */

export const FETCH_GROUP_OVERVIEWS = 'FETCH_GROUP_OVERVIEWS';
export const UPDATE_GROUP_OVERVIEWS = 'UPDATE_GROUP_OVERVIEWS';
10 changes: 10 additions & 0 deletions src/actions/dashboard.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { action } from 'typesafe-actions';

import * as actionTypes from './actionTypes';

import { IGroupOverview } from '../components/dashboard/groupShape';

export const fetchGroupOverviews = () => action(actionTypes.FETCH_GROUP_OVERVIEWS);

export const updateGroupOverviews = (groupOverviews: IGroupOverview[]) =>
action(actionTypes.UPDATE_GROUP_OVERVIEWS, groupOverviews);
1 change: 1 addition & 0 deletions src/actions/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export * from './collabEditing';
export * from './commons';
export * from './dashboard';
export * from './game';
export * from './interpreter';
export * from './material';
Expand Down
9 changes: 9 additions & 0 deletions src/components/academy/NavigationBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,15 @@ const NavigationBar: React.SFC<OwnProps> = props => (
</NavbarGroup>
{props.role === Role.Admin || props.role === Role.Staff ? (
<NavbarGroup align={Alignment.RIGHT}>
<NavLink
to={'/academy/dashboard'}
activeClassName={Classes.ACTIVE}
className={classNames('NavigationBar__link', Classes.BUTTON, Classes.MINIMAL)}
>
<Icon icon="globe" />
<div className="navbar-button-text hidden-xs">Dashboard</div>
</NavLink>

<NavLink
to={'/academy/sourcereel'}
activeClassName={Classes.ACTIVE}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,12 @@ exports[`Grading NavLink renders for Role.Admin 1`] = `
</NavLink>
</Blueprint3.NavbarGroup>
<Blueprint3.NavbarGroup align=\\"right\\">
<NavLink to=\\"/academy/dashboard\\" activeClassName=\\"bp3-active\\" className=\\"NavigationBar__link bp3-button bp3-minimal\\" aria-current=\\"page\\">
<Blueprint3.Icon icon=\\"globe\\" />
<div className=\\"navbar-button-text hidden-xs\\">
Dashboard
</div>
</NavLink>
<NavLink to=\\"/academy/sourcereel\\" activeClassName=\\"bp3-active\\" className=\\"NavigationBar__link bp3-button bp3-minimal\\" aria-current=\\"page\\">
<Blueprint3.Icon icon=\\"mobile-video\\" />
<div className=\\"navbar-button-text hidden-xs\\">
Expand Down Expand Up @@ -142,6 +148,12 @@ exports[`Grading NavLink renders for Role.Staff 1`] = `
</NavLink>
</Blueprint3.NavbarGroup>
<Blueprint3.NavbarGroup align=\\"right\\">
<NavLink to=\\"/academy/dashboard\\" activeClassName=\\"bp3-active\\" className=\\"NavigationBar__link bp3-button bp3-minimal\\" aria-current=\\"page\\">
<Blueprint3.Icon icon=\\"globe\\" />
<div className=\\"navbar-button-text hidden-xs\\">
Dashboard
</div>
</NavLink>
<NavLink to=\\"/academy/sourcereel\\" activeClassName=\\"bp3-active\\" className=\\"NavigationBar__link bp3-button bp3-minimal\\" aria-current=\\"page\\">
<Blueprint3.Icon icon=\\"mobile-video\\" />
<div className=\\"navbar-button-text hidden-xs\\">
Expand Down
2 changes: 2 additions & 0 deletions src/components/academy/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { Redirect, Route, RouteComponentProps, Switch } from 'react-router';

import Grading from '../../containers/academy/grading';
import AssessmentContainer from '../../containers/assessment';
import Dashboard from '../../containers/dashboard/DashboardContainer';
import Game from '../../containers/GameContainer';
import MaterialUpload from '../../containers/material/MaterialUploadContainer';
import Sourcereel from '../../containers/sourcecast/SourcereelContainer';
Expand Down Expand Up @@ -77,6 +78,7 @@ class Academy extends React.Component<IAcademyProps> {
render={assessmentRenderFactory(AssessmentCategories.Practical)}
/>

<Route path="/academy/dashboard" component={Dashboard} />
<Route path={`/academy/grading/${gradingRegExp}`} component={Grading} />
<Route path={'/academy/material'} component={MaterialUpload} />
<Route path="/academy/sourcereel" component={Sourcereel} />
Expand Down
182 changes: 182 additions & 0 deletions src/components/dashboard/Dashboard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
import { ColDef, GridApi, GridReadyEvent } from 'ag-grid';
import { AgGridReact } from 'ag-grid-react';
import 'ag-grid/dist/styles/ag-grid.css';
import 'ag-grid/dist/styles/ag-theme-balham.css';
import * as React from 'react';

import { GradingOverview } from '../academy/grading/gradingShape';
import ContentDisplay from '../commons/ContentDisplay';
import { IGroupOverview } from './groupShape';

type State = {
filterValue: string;
groupFilterEnabled: boolean;
currPage: number;
maxPages: number;
rowCountString: string;
isBackDisabled: boolean;
isForwardDisabled: boolean;
};

interface IDashboardProps extends IDispatchProps, IStateProps {}

export interface IDispatchProps {
handleFetchGradingOverviews: (filterToGroup?: boolean) => void;
handleFetchGroupOverviews: () => void;
}

export interface IStateProps {
gradingOverviews: GradingOverview[];
groupOverviews: IGroupOverview[];
}

export type LeaderBoardInfo = {
avengerName: string;
numOfUngradedMissions: number;
totalNumOfMissions: number;
numOfUngradedQuests: number;
totalNumOfQuests: number;
};

class Dashboard extends React.Component<IDashboardProps, State> {
private columnDefs: ColDef[];
private gridApi?: GridApi;

public constructor(props: IDashboardProps) {
super(props);
this.columnDefs = [
{
headerName: 'Avenger',
field: 'avengerName',
width: 100
},
{
headerName: 'Number of Ungraded Missions',
field: 'numOfUngradedMissions'
},
{
headerName: 'Number of Submitted Missions',
field: 'totalNumOfMissions'
},
{
headerName: 'Number of Ungraded Quests',
field: 'numOfUngradedQuests'
},
{
headerName: 'Number of Submitted Quests',
field: 'totalNumOfQuests'
}
];
}

public componentDidMount() {
this.props.handleFetchGroupOverviews();
}

public componentDidUpdate(prevProps: IDashboardProps, prevState: State) {
if (this.gridApi && this.props.gradingOverviews.length !== prevProps.gradingOverviews.length) {
this.gridApi.setRowData(this.updateLeaderBoard());
}
}

public handleFetchGradingOverviews = () => {
this.props.handleFetchGradingOverviews(false);
};

public render() {
const data = this.updateLeaderBoard();
const grid = (
<div className="GradingContainer">
<div className="Grading ag-grid-parent ag-theme-balham">
<AgGridReact
gridAutoHeight={true}
enableColResize={true}
enableSorting={true}
enableFilter={true}
columnDefs={this.columnDefs}
onGridReady={this.onGridReady}
onGridSizeChanged={this.resizeGrid}
rowData={data}
rowHeight={30}
pagination={true}
paginationPageSize={25}
suppressMovableColumns={true}
suppressPaginationPanel={true}
/>
</div>
</div>
);

return (
<div>
<ContentDisplay display={grid} loadContentDispatch={this.handleFetchGradingOverviews} />
</div>
);
}

private updateLeaderBoard = () => {
if (this.props.groupOverviews.length === 0) {
return [];
}
const gradingOverview: GradingOverview[] = this.filterSubmissionsByCategory();
const filteredData: LeaderBoardInfo[] = [];
for (const current of gradingOverview) {
if (current.submissionStatus !== 'submitted') {
continue;
}
const groupName = current.groupName;
const groupOverviews = this.props.groupOverviews;
const index = groupOverviews.findIndex(x => x.groupName === groupName);

if (index !== -1) {
if (filteredData[index] === undefined) {
filteredData[index] = {
avengerName: groupOverviews[index].avengerName,
numOfUngradedMissions: 0,
totalNumOfMissions: 0,
numOfUngradedQuests: 0,
totalNumOfQuests: 0
};
}

const currentEntry = filteredData[index];
const gradingStatus = current.gradingStatus;

if (current.assessmentCategory === 'Mission') {
if (gradingStatus === 'none' || gradingStatus === 'grading') {
currentEntry.numOfUngradedMissions++;
}
currentEntry.totalNumOfMissions++;
} else {
if (gradingStatus === 'none' || gradingStatus === 'grading') {
currentEntry.numOfUngradedQuests++;
}
currentEntry.totalNumOfQuests++;
}
}
}
return filteredData;
};

private filterSubmissionsByCategory = () => {
if (!this.props.gradingOverviews) {
return [];
}
return (this.props.gradingOverviews as GradingOverview[]).filter(
sub => sub.assessmentCategory === 'Sidequest' || sub.assessmentCategory === 'Mission'
);
};

private onGridReady = (params: GridReadyEvent) => {
this.gridApi = params.api;
this.gridApi.sizeColumnsToFit();
};

private resizeGrid = () => {
if (this.gridApi) {
this.gridApi.sizeColumnsToFit();
}
};
}

export default Dashboard;
5 changes: 5 additions & 0 deletions src/components/dashboard/groupShape.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export interface IGroupOverview {
id: number;
groupName: string;
avengerName: string;
}
27 changes: 27 additions & 0 deletions src/containers/dashboard/DashboardContainer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { connect, MapDispatchToProps, MapStateToProps } from 'react-redux';
import { bindActionCreators, Dispatch } from 'redux';

import { fetchGroupOverviews } from '../../actions/dashboard';
import { fetchGradingOverviews } from '../../actions/session';
import { IDispatchProps, IStateProps } from '../../components/dashboard/Dashboard';
import Dashboard from '../../components/dashboard/Dashboard';
import { IState } from '../../reducers/states';

const mapStateToProps: MapStateToProps<IStateProps, {}, IState> = state => ({
gradingOverviews: state.session.gradingOverviews ? state.session.gradingOverviews : [],
groupOverviews: state.dashboard.groupOverviews
});

const mapDispatchToProps: MapDispatchToProps<IDispatchProps, {}> = (dispatch: Dispatch<any>) =>
bindActionCreators(
{
handleFetchGradingOverviews: fetchGradingOverviews,
handleFetchGroupOverviews: fetchGroupOverviews
},
dispatch
);

export default connect(
mapStateToProps,
mapDispatchToProps
)(Dashboard);
5 changes: 5 additions & 0 deletions src/mocks/backend.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { history } from '../utils/history';
import { showSuccessMessage, showWarningMessage } from '../utils/notification';
import { mockAssessmentOverviews, mockAssessments } from './assessmentAPI';
import { mockFetchGrading, mockFetchGradingOverview } from './gradingAPI';
import { mockGroupOverviews } from './groupAPI';
import { mockNotifications } from './userAPI';

export function* mockBackendSaga(): SagaIterator {
Expand Down Expand Up @@ -208,4 +209,8 @@ export function* mockBackendSaga(): SagaIterator {
) {
yield put(actions.updateNotifications(mockNotifications));
});

yield takeEvery(actionTypes.FETCH_GROUP_OVERVIEWS, function*() {
yield put(actions.updateGroupOverviews([...mockGroupOverviews]));
});
}
19 changes: 19 additions & 0 deletions src/mocks/groupAPI.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { IGroupOverview } from '../components/dashboard/groupShape';

export const mockGroupOverviews: IGroupOverview[] = [
{
id: 1,
avengerName: 'John',
groupName: 'Mock Group 1'
},
{
id: 2,
avengerName: 'Billy',
groupName: 'Mock Group 2'
},
{
id: 3,
avengerName: 'Harry',
groupName: 'Mock Group 3'
}
];
2 changes: 2 additions & 0 deletions src/mocks/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import mockStore from 'redux-mock-store';
import {
defaultAcademy,
defaultApplication,
defaultDashBoard,
defaultPlayground,
defaultSession,
defaultWorkspaceManager,
Expand All @@ -15,6 +16,7 @@ export function mockInitialStore<P>(): Store<IState> {
const state: IState = {
academy: defaultAcademy,
application: defaultApplication,
dashboard: defaultDashBoard,
playground: defaultPlayground,
workspaces: defaultWorkspaceManager,
session: defaultSession
Expand Down
Loading