Skip to content
Merged
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 @@ -209,4 +210,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