CQS - Loading states
While stats are being loaded: - Adds a spinner to the top of the page - Dims the summary table metrics and replaces them with "N/A" - Reads a "please wait while we're loading" sentence in the question breakdown section - Disables any buttons that are visible Note: the summary percentile chart could use some work but it'll have to be done in a different patch. Closes CNVS-16597 TEST PLAN ---- ---- - go to new quiz stats - try to get the page to load for a bit so you can see the effects, otherwise try reloading to test each item or get in touch and maybe we can figure something out + verify the items above are met while the stats are being loaded + verify everything goes back to normal when loading is complete Change-Id: Ibd9049d3a81d85d376b79102c1567e470f06de9c Reviewed-on: https://gerrit.instructure.com/43997 Reviewed-by: Ryan Taylor <rtaylor@instructure.com> Tested-by: Jenkins <jenkins@instructure.com> QA-Review: Trevor deHaan <tdehaan@instructure.com> Product-Review: Ahmad Amireh <ahmad@instructure.com>
This commit is contained in:
parent
2c76c27ad5
commit
1e0760b716
|
@ -35,6 +35,10 @@
|
|||
}
|
||||
}
|
||||
|
||||
tbody td {
|
||||
transition: opacity 0.5s linear 0s;
|
||||
}
|
||||
|
||||
.chart {
|
||||
margin-top: 60px;
|
||||
width: 100%;
|
||||
|
@ -81,4 +85,10 @@
|
|||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
|
||||
&.loading {
|
||||
tbody td {
|
||||
opacity: 0.25;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
@import "./question_statistics";
|
||||
@import "../../vendor/css/jquery.qtip";
|
||||
@import "./ext/qtip";
|
||||
@import "./components/ic_spinner";
|
||||
|
||||
#canvas-quiz-statistics {
|
||||
border-top: 1px solid $borderColor;
|
||||
|
|
|
@ -0,0 +1,64 @@
|
|||
/**
|
||||
* Courtesy of http://tobiasahlin.com/spinkit/
|
||||
*/
|
||||
.ic-Spinner {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
margin-left: $whitespace;
|
||||
margin-top: -6px;
|
||||
height: 38px;
|
||||
|
||||
& > div {
|
||||
background-color: $highlightColor;
|
||||
height: 100%;
|
||||
width: 10px;
|
||||
margin-right: 1px;
|
||||
display: inline-block;
|
||||
|
||||
-webkit-animation: ic-Spinner__stretchdelay 1.2s infinite ease-in-out;
|
||||
animation: ic-Spinner__stretchdelay 1.2s infinite ease-in-out;
|
||||
}
|
||||
|
||||
.rect2 {
|
||||
-webkit-animation-delay: -1.1s;
|
||||
animation-delay: -1.1s;
|
||||
}
|
||||
|
||||
.rect3 {
|
||||
-webkit-animation-delay: -1.0s;
|
||||
animation-delay: -1.0s;
|
||||
}
|
||||
|
||||
.rect4 {
|
||||
-webkit-animation-delay: -0.9s;
|
||||
animation-delay: -0.9s;
|
||||
}
|
||||
|
||||
.rect5 {
|
||||
-webkit-animation-delay: -0.8s;
|
||||
animation-delay: -0.8s;
|
||||
}
|
||||
}
|
||||
|
||||
@-webkit-keyframes ic-Spinner__stretchdelay {
|
||||
0%, 40%, 100% {
|
||||
-webkit-transform: scaleY(0.4);
|
||||
transform: scaleY(0.4);
|
||||
}
|
||||
20% {
|
||||
-webkit-transform: scaleY(1.0);
|
||||
transform: scaleY(1.0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes ic-Spinner__stretchdelay {
|
||||
0%, 40%, 100% {
|
||||
-webkit-transform: scaleY(0.4);
|
||||
transform: scaleY(0.4);
|
||||
}
|
||||
|
||||
20% {
|
||||
-webkit-transform: scaleY(1.0);
|
||||
transform: scaleY(1.0);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
/** @jsx React.DOM */
|
||||
define(function(require) {
|
||||
var React = require('react');
|
||||
var Spinner = React.createClass({
|
||||
render: function() {
|
||||
return(
|
||||
<div className="ic-Spinner">
|
||||
<div className="rect1"></div>
|
||||
<div className="rect2"></div>
|
||||
<div className="rect3"></div>
|
||||
<div className="rect4"></div>
|
||||
<div className="rect5"></div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
return Spinner;
|
||||
});
|
|
@ -1,5 +1,4 @@
|
|||
define(function(require) {
|
||||
var rawAjax = require('../../util/xhr_request');
|
||||
var $ = require('canvas_packages/jquery');
|
||||
var Root = this;
|
||||
var DEBUG = {
|
||||
|
|
|
@ -19,6 +19,18 @@ define(function(require) {
|
|||
options.data = JSON.stringify(options.data);
|
||||
}
|
||||
|
||||
//>>excludeStart("production", pragmas.production);
|
||||
if (config.fakeXHRDelay) {
|
||||
var svc = RSVP.defer();
|
||||
|
||||
setTimeout(function() {
|
||||
RSVP.Promise.cast(ajax(options)).then(svc.resolve, svc.reject);
|
||||
}, config.fakeXHRDelay);
|
||||
|
||||
return svc.promise;
|
||||
}
|
||||
//>>excludeEnd("production");
|
||||
|
||||
return RSVP.Promise.cast(ajax(options));
|
||||
}
|
||||
};
|
||||
|
|
|
@ -7,6 +7,7 @@ define(function(require) {
|
|||
var onChange = function() {
|
||||
update({
|
||||
quizStatistics: quizStatistics.get(),
|
||||
isLoadingStatistics: quizStatistics.isLoading(),
|
||||
quizReports: quizReports.getAll(),
|
||||
});
|
||||
};
|
||||
|
|
|
@ -62,6 +62,16 @@ define(function(require) {
|
|||
*/
|
||||
__reset__: function() {
|
||||
this._callbacks = [];
|
||||
this.state = this.getInitialState();
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
return {};
|
||||
},
|
||||
|
||||
setState: function(newState) {
|
||||
extend(this.state, newState);
|
||||
this.emitChange();
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -11,6 +11,12 @@ define(function(require) {
|
|||
* Load stats.
|
||||
*/
|
||||
var store = new Store('statistics', {
|
||||
getInitialState: function() {
|
||||
return {
|
||||
loading: false
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* Load quiz statistics.
|
||||
*
|
||||
|
@ -22,13 +28,16 @@ define(function(require) {
|
|||
* Fulfills when the stats have been loaded and injected.
|
||||
*/
|
||||
load: function() {
|
||||
var onLoad = this.populate.bind(this);
|
||||
|
||||
if (!config.quizStatisticsUrl) {
|
||||
return config.onError('Missing configuration parameter "quizStatisticsUrl".');
|
||||
}
|
||||
|
||||
return quizStats.fetch().then(onLoad);
|
||||
this.setState({ loading: true });
|
||||
|
||||
return quizStats.fetch().then(function onLoad(payload) {
|
||||
this.populate(payload);
|
||||
this.setState({ loading: false });
|
||||
}.bind(this));
|
||||
},
|
||||
|
||||
/**
|
||||
|
@ -66,6 +75,10 @@ define(function(require) {
|
|||
return false;
|
||||
},
|
||||
|
||||
isLoading: function() {
|
||||
return this.state.loading;
|
||||
},
|
||||
|
||||
getSubmissionStatistics: function() {
|
||||
var stats = this.get();
|
||||
if (stats) {
|
||||
|
|
|
@ -46,8 +46,6 @@ define(function(require) {
|
|||
var props = this.props;
|
||||
var quizStatistics = this.props.quizStatistics;
|
||||
var submissionStatistics = quizStatistics.submissionStatistics;
|
||||
var questionStatistics = quizStatistics.questionStatistics;
|
||||
var participantCount = submissionStatistics.uniqueCount;
|
||||
|
||||
return(
|
||||
<div id="canvas-quiz-statistics">
|
||||
|
@ -65,6 +63,7 @@ define(function(require) {
|
|||
durationAverage={submissionStatistics.durationAverage}
|
||||
quizReports={this.props.quizReports}
|
||||
scores={submissionStatistics.scores}
|
||||
loading={this.props.isLoadingStatistics}
|
||||
/>
|
||||
</section>
|
||||
|
||||
|
@ -79,17 +78,47 @@ define(function(require) {
|
|||
<ToggleDetailsButton
|
||||
onClick={this.toggleAllDetails}
|
||||
expanded={quizStatistics.expandingAll}
|
||||
disabled={this.props.isLoadingStatistics}
|
||||
controlsAll />
|
||||
</SightedUserContent>
|
||||
</aside>
|
||||
</header>
|
||||
|
||||
{questionStatistics.map(this.renderQuestion.bind(null, participantCount))}
|
||||
{this.renderQuestions()}
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
|
||||
renderQuestions: function() {
|
||||
var isLoadingStatistics = this.props.isLoadingStatistics;
|
||||
var questionStatistics = this.props.quizStatistics.questionStatistics;
|
||||
var participantCount = this.props.quizStatistics.submissionStatistics.uniqueCount;
|
||||
|
||||
if (isLoadingStatistics) {
|
||||
return (
|
||||
<p>
|
||||
{I18n.t('loading_questions',
|
||||
'Question statistics are being loaded. Please wait a while.')}
|
||||
</p>
|
||||
);
|
||||
}
|
||||
else if (questionStatistics.length === 0) {
|
||||
return (
|
||||
<p>
|
||||
{I18n.t('empty_question_breakdown', 'There are no question statistics available.')}
|
||||
</p>
|
||||
);
|
||||
}
|
||||
else {
|
||||
return (
|
||||
<div>
|
||||
{questionStatistics.map(this.renderQuestion.bind(null, participantCount))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
renderQuestion: function(participantCount, question) {
|
||||
var renderer = Renderers[question.questionType] || QuestionRenderer;
|
||||
var stats = this.props.quizStatistics;
|
||||
|
|
|
@ -40,6 +40,7 @@ define(function(require) {
|
|||
<button
|
||||
title={label}
|
||||
onClick={this.props.onClick}
|
||||
disabled={this.props.disabled}
|
||||
className="btn"
|
||||
aria-live="polite">
|
||||
<ScreenReaderContent children={label} />
|
||||
|
|
|
@ -9,6 +9,8 @@ define(function(require) {
|
|||
var formatNumber = require('../util/format_number');
|
||||
var SightedUserContent = require('jsx!../components/sighted_user_content');
|
||||
var ScreenReaderContent = require('jsx!../components/screen_reader_content');
|
||||
var Spinner = require('jsx!../components/spinner');
|
||||
var NA_LABEL = I18n.t('not_available_abbrev', 'N/A');
|
||||
|
||||
var Column = React.createClass({
|
||||
render: function() {
|
||||
|
@ -48,18 +50,23 @@ define(function(require) {
|
|||
},
|
||||
|
||||
render: function() {
|
||||
var isLoading = this.props.loading;
|
||||
|
||||
return(
|
||||
<div id="summary-statistics">
|
||||
<div id="summary-statistics" className={isLoading ? 'loading' : undefined}>
|
||||
<header className="padded">
|
||||
<h2 className="section-title inline">
|
||||
{I18n.t('quiz_summary', 'Quiz Summary')}
|
||||
</h2>
|
||||
|
||||
{isLoading && <Spinner />}
|
||||
|
||||
<div className="pull-right">
|
||||
{this.props.quizReports.map(this.renderReport)}
|
||||
</div>
|
||||
</header>
|
||||
|
||||
|
||||
<table className="text-left">
|
||||
<ScreenReaderContent tagName="caption" forceSentenceDelimiter>
|
||||
{I18n.t('table_description',
|
||||
|
@ -90,24 +97,27 @@ define(function(require) {
|
|||
<tbody>
|
||||
<tr>
|
||||
<td className="emphasized">
|
||||
{this.ratioFor(this.props.scoreAverage)}%
|
||||
</td>
|
||||
<td>{this.ratioFor(this.props.scoreHigh)}%</td>
|
||||
<td>{this.ratioFor(this.props.scoreLow)}%</td>
|
||||
<td>{formatNumber(round(this.props.scoreStdev, 2), 2)}</td>
|
||||
<td>
|
||||
<ScreenReaderContent forceSentenceDelimiter>
|
||||
{secondsToTime.toReadableString(this.props.durationAverage)}
|
||||
</ScreenReaderContent>
|
||||
{/*
|
||||
try to hide the [HH:]MM:SS timestamp from SR users because
|
||||
it's not really useful, however this doesn't work in all
|
||||
modes such as the Speak-All mode (at least on VoiceOver)
|
||||
*/}
|
||||
<SightedUserContent>
|
||||
{secondsToTime(this.props.durationAverage)}
|
||||
</SightedUserContent>
|
||||
{isLoading ? NA_LABEL : (this.ratioFor(this.props.scoreAverage) + '%')}
|
||||
</td>
|
||||
<td>{isLoading ? NA_LABEL : (this.ratioFor(this.props.scoreHigh) + '%')}</td>
|
||||
<td>{isLoading ? NA_LABEL : (this.ratioFor(this.props.scoreLow) + '%')}</td>
|
||||
<td>{isLoading ? NA_LABEL : formatNumber(round(this.props.scoreStdev, 2), 2)}</td>
|
||||
{isLoading ?
|
||||
<td key="duration">{NA_LABEL}</td> :
|
||||
<td key="duration">
|
||||
<ScreenReaderContent forceSentenceDelimiter>
|
||||
{secondsToTime.toReadableString(this.props.durationAverage)}
|
||||
</ScreenReaderContent>
|
||||
{/*
|
||||
try to hide the [HH:]MM:SS timestamp from SR users because
|
||||
it's not really useful, however this doesn't work in all
|
||||
modes such as the Speak-All mode (at least on VoiceOver)
|
||||
*/}
|
||||
<SightedUserContent>
|
||||
{secondsToTime(this.props.durationAverage)}
|
||||
</SightedUserContent>
|
||||
</td>
|
||||
}
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
|
Loading…
Reference in New Issue