779 lines
30 KiB
JavaScript
779 lines
30 KiB
JavaScript
/**
|
|
* This program is free software; you can redistribute it and/or
|
|
* modify it under the terms of the GNU General Public License
|
|
* as published by the Free Software Foundation; under version 2
|
|
* of the License (non-upgradable).
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with this program; if not, write to the Free Software
|
|
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
|
*
|
|
* Copyright (c) 2016-2021 (original work) Open Assessment Technologies SA ;
|
|
*/
|
|
define([
|
|
'jquery',
|
|
'lodash',
|
|
'i18n',
|
|
'async',
|
|
'ui/component',
|
|
'core/logger',
|
|
'core/store',
|
|
'core/request',
|
|
'core/dataProvider/request',
|
|
'ui/dialog/alert',
|
|
'ui/feedback',
|
|
'util/url',
|
|
'taoClientDiagnostic/tools/performances/tester',
|
|
'taoClientDiagnostic/tools/bandwidth/tester',
|
|
'taoClientDiagnostic/tools/upload/tester',
|
|
'taoClientDiagnostic/tools/browser/tester',
|
|
'taoClientDiagnostic/tools/getStatus',
|
|
'taoClientDiagnostic/tools/getConfig',
|
|
'tpl!taoClientDiagnostic/tools/diagnostic/tpl/main',
|
|
'tpl!taoClientDiagnostic/tools/diagnostic/tpl/result',
|
|
'tpl!taoClientDiagnostic/tools/diagnostic/tpl/details',
|
|
'tpl!taoClientDiagnostic/tools/diagnostic/tpl/feedback',
|
|
'tpl!taoClientDiagnostic/tools/diagnostic/tpl/quality-bar',
|
|
'css!taoClientDiagnosticCss/diagnostics'
|
|
], function(
|
|
$,
|
|
_,
|
|
__,
|
|
async,
|
|
component,
|
|
loggerFactory,
|
|
store,
|
|
request,
|
|
requestData,
|
|
dialogAlert,
|
|
feedback,
|
|
urlHelper,
|
|
performancesTester,
|
|
bandwidthTester,
|
|
uploadTester,
|
|
browserTester,
|
|
getStatus,
|
|
getConfig,
|
|
mainTpl,
|
|
resultTpl,
|
|
detailsTpl,
|
|
feedbackTpl,
|
|
qualityBarTpl
|
|
) {
|
|
'use strict';
|
|
/**
|
|
* @type {logger}
|
|
* @private
|
|
*/
|
|
const logger = loggerFactory('taoClientDiagnostic/diagnostic');
|
|
|
|
/**
|
|
* Some default values
|
|
* @type {object}
|
|
* @private
|
|
*/
|
|
const _defaults = {
|
|
title: __('System Compatibility'),
|
|
header: __(
|
|
'This tool will run a number of tests in order to establish how well your current environment is suitable to run the TAO platform.'
|
|
),
|
|
info: __('Be aware that these tests will take up to several minutes.'),
|
|
button: __('Test system compatibility'),
|
|
actionStore: 'storeData',
|
|
actionSchool: 'schoolName',
|
|
controller: 'DiagnosticChecker',
|
|
extension: 'taoClientDiagnostic',
|
|
actionDropId: 'deleteId',
|
|
storeAllRuns: false,
|
|
configurableText: {}
|
|
};
|
|
|
|
/**
|
|
* A list of thresholds for summary
|
|
* @type {Array}
|
|
* @private
|
|
*/
|
|
const _thresholds = [
|
|
{
|
|
threshold: 0,
|
|
message: __('Your system requires a compatibility update, please contact your system administrator.'),
|
|
type: 'error'
|
|
},
|
|
{
|
|
threshold: 33,
|
|
message: __('Your system is not optimal, please contact your system administrator.'),
|
|
type: 'warning'
|
|
},
|
|
{
|
|
threshold: 66,
|
|
message: __('Your system is fully compliant.'),
|
|
type: 'success'
|
|
}
|
|
];
|
|
|
|
/**
|
|
* Defines a diagnostic tool
|
|
* @type {object}
|
|
*/
|
|
const diagnostic = {
|
|
/**
|
|
* Updates the displayed status
|
|
* @param {string} status
|
|
* @returns {diagnostic}
|
|
* @private
|
|
*/
|
|
changeStatus(status) {
|
|
if (this.is('rendered')) {
|
|
this.controls.$status.html(status);
|
|
}
|
|
return this;
|
|
},
|
|
|
|
/**
|
|
* Sends the detailed stats to the server
|
|
* @param {string} type The type of stats
|
|
* @param {object} data The stats details
|
|
* @param {Function} done A callback method called once server has responded
|
|
*/
|
|
store(type, data, done) {
|
|
const config = this.config;
|
|
const url = urlHelper.route(config.actionStore, config.controller, config.extension, config.storeParams);
|
|
|
|
data = _.omit(data, 'values');
|
|
data.type = type;
|
|
|
|
request({ url, data, method: 'POST', noToken: true })
|
|
.then(done)
|
|
.catch(err => {
|
|
logger.error(err);
|
|
feedback().error(__('Unable to save the results! Please check your connection.'));
|
|
done();
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Retrieve a custom message from the config
|
|
* @param key
|
|
* @returns {*}
|
|
*/
|
|
getCustomMsg(key) {
|
|
return this.config.configurableText[key];
|
|
},
|
|
|
|
/**
|
|
* Enrich the feedback object with a custom message if the test has failed
|
|
* @param {object} status - the test result
|
|
* @param {string} msg - the custom message
|
|
*/
|
|
addCustomFeedbackMsg(status, msg) {
|
|
if (this.hasFailed(status) && msg) {
|
|
if (_.isFunction(status.customMsgRenderer)) {
|
|
msg = status.customMsgRenderer(msg);
|
|
}
|
|
status.feedback = status.feedback || {};
|
|
status.feedback.customMsg = msg;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Check if a result is considered as failed
|
|
* @param {object} result
|
|
* @returns {boolean}
|
|
*/
|
|
hasFailed(result) {
|
|
return !(result && result.feedback && result.feedback.type === 'success');
|
|
},
|
|
|
|
/**
|
|
* Add a result row
|
|
* @param {object} result
|
|
* @returns {diagnostic}
|
|
*/
|
|
addResult(result) {
|
|
if (this.is('rendered')) {
|
|
// adjust the width of the displayed label, if any, to the text length
|
|
if (result.quality && result.quality.label && result.quality.label.toString().length > 2) {
|
|
result.quality.wide = true;
|
|
}
|
|
|
|
// create and append the result to the displayed list
|
|
const $main = $(resultTpl(result));
|
|
const $result = $main.find('.result');
|
|
if (result.feedback) {
|
|
$result.append($(feedbackTpl(result.feedback)));
|
|
}
|
|
if (result.quality) {
|
|
$result.append($(qualityBarTpl(result.quality)));
|
|
}
|
|
if (result.details) {
|
|
$main.find('.details').append($(detailsTpl(result.details)));
|
|
}
|
|
|
|
const $indicator = $main.find('.quality-indicator');
|
|
this.controls.$results.append($main);
|
|
|
|
// the result is hidden by default, show it with a little animation
|
|
$main.fadeIn(() => {
|
|
if ($indicator.length) {
|
|
$indicator.animate({
|
|
left: (result.percentage * $main.outerWidth()) / 100 - $indicator.outerWidth() / 2
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
return this;
|
|
},
|
|
|
|
/**
|
|
* Removes the last results if any
|
|
* @returns {diagnostic}
|
|
*/
|
|
cleanUp() {
|
|
this.controls.$results.empty();
|
|
return this;
|
|
},
|
|
|
|
/**
|
|
* Enables the start button
|
|
* @returns {diagnostic}
|
|
*/
|
|
enable() {
|
|
this.controls.$start.removeClass('hidden');
|
|
return this;
|
|
},
|
|
|
|
/**
|
|
* Disables the start button
|
|
* @returns {diagnostic}
|
|
*/
|
|
disable() {
|
|
this.controls.$start.addClass('hidden');
|
|
return this;
|
|
},
|
|
|
|
/**
|
|
* Does some preparations before starting the diagnostics
|
|
* @returns {diagnostic}
|
|
* @private
|
|
*/
|
|
prepare() {
|
|
/**
|
|
* Notifies the diagnostic start
|
|
* @event diagnostic#start
|
|
*/
|
|
this.trigger('start');
|
|
this.changeStatus(__('Starting...'));
|
|
this.setState('running', true);
|
|
this.setState('done', false);
|
|
|
|
// first we need a clean space to display the results, so remove the last results if any
|
|
this.cleanUp();
|
|
|
|
// remove the start button during the diagnostic
|
|
this.disable();
|
|
|
|
return this;
|
|
},
|
|
|
|
/**
|
|
* Does some post process after ending the diagnostics
|
|
* @returns {diagnostic}
|
|
* @private
|
|
*/
|
|
finish() {
|
|
const config = this.config;
|
|
|
|
// restore the start button to allow a new diagnostic run
|
|
this.enable();
|
|
|
|
if (config.storeAllRuns) {
|
|
this.deleteIdentifier();
|
|
}
|
|
|
|
/**
|
|
* Notifies the diagnostic end
|
|
* @event diagnostic#end
|
|
*/
|
|
this.trigger('end');
|
|
this.changeStatus(__('Done!'));
|
|
this.setState('running', false);
|
|
this.setState('done', true);
|
|
|
|
return this;
|
|
},
|
|
|
|
/**
|
|
* delete unique id for this test session (next test will generate new one)
|
|
*/
|
|
deleteIdentifier() {
|
|
const url = urlHelper.route(this.config.actionDropId, this.config.controller, this.config.extension);
|
|
return request({ url, method: 'POST', noToken: true });
|
|
},
|
|
|
|
/**
|
|
* Runs the diagnostics
|
|
* @returns {diagnostic}
|
|
*/
|
|
run() {
|
|
const information = [];
|
|
const scores = {};
|
|
const testers = [];
|
|
const customInput = this.getCustomInput();
|
|
|
|
const doRun = () => {
|
|
// common handling for testers
|
|
const doCheck = (testerConfig, cb) => {
|
|
const testerId = testerConfig.id;
|
|
|
|
/**
|
|
* Notifies the start of a tester operation
|
|
* @event diagnostic#starttester
|
|
* @param {string} name - The name of the tester
|
|
*/
|
|
this.trigger('starttester', testerId);
|
|
this.setState(testerId, true);
|
|
|
|
/**
|
|
* Process the diagnostic from the loaded tester
|
|
* @param {Function} testerFactory
|
|
* @private
|
|
*/
|
|
const processTester = testerFactory => {
|
|
const tester = testerFactory(getConfig(testerConfig, this.config), this);
|
|
this.changeStatus(tester.labels.status);
|
|
tester.start((status, details, results) => {
|
|
if (testerConfig.customMsgKey) {
|
|
const customMsg = this.getCustomMsg(testerConfig.customMsgKey);
|
|
this.addCustomFeedbackMsg(status, customMsg);
|
|
}
|
|
|
|
// the returned details must be ingested into the main details list
|
|
_.forEach(details, info => information.push(info));
|
|
scores[status.id] = status;
|
|
|
|
/**
|
|
* Notifies the end of a tester operation
|
|
* @event diagnostic#endtester
|
|
* @param {string} id - The identifier of the tester
|
|
* @param {Array} results - The results of the test
|
|
*/
|
|
this.trigger('endtester', testerId, status);
|
|
this.setState(testerId, false);
|
|
|
|
// results should be filtered in order to encode complex data
|
|
results = _.mapValues(results, value => {
|
|
switch (typeof value) {
|
|
case 'boolean':
|
|
return value ? 1 : 0;
|
|
case 'object':
|
|
return JSON.stringify(value);
|
|
}
|
|
return value;
|
|
});
|
|
|
|
// send the data to store
|
|
this.store(testerId, results, () => {
|
|
this.addResult(status);
|
|
cb();
|
|
});
|
|
});
|
|
};
|
|
|
|
/**
|
|
* React to loading failure
|
|
* @param {Error} err
|
|
* @private
|
|
*/
|
|
const processFailure = err => {
|
|
logger.error(err);
|
|
feedback().error(
|
|
__(
|
|
'Unable to process with the diagnostic tester %s. The tester module is unreachable.',
|
|
testerId
|
|
)
|
|
);
|
|
cb();
|
|
};
|
|
|
|
require([testerConfig.tester], processTester, processFailure);
|
|
};
|
|
|
|
if (this.is('rendered')) {
|
|
// set up the component to a new run
|
|
this.prepare();
|
|
|
|
_.forEach(this.config.testers, (testerConfig, testerId) => {
|
|
testerConfig.id = testerConfig.id || testerId;
|
|
if (testerConfig.enabled) {
|
|
testers.push(cb => doCheck(testerConfig, cb));
|
|
}
|
|
});
|
|
|
|
// launch each testers in series, then display the results
|
|
async.series(testers, () => {
|
|
// pick the lowest percentage as the main score
|
|
const total = _.min(scores, 'globalPercentage');
|
|
|
|
// get a status according to the main score
|
|
const status = getStatus(total.globalPercentage, _thresholds);
|
|
|
|
// display the result
|
|
status.title = __('Total');
|
|
status.id = 'total';
|
|
this.addCustomFeedbackMsg(status, this.config.configurableText.diagTotalCheckResult);
|
|
|
|
status.details = information;
|
|
this.addResult(status);
|
|
|
|
// done !
|
|
this.finish();
|
|
});
|
|
}
|
|
};
|
|
|
|
if (_.size(customInput) > 0) {
|
|
this.store('custom_input', customInput, doRun);
|
|
} else {
|
|
doRun();
|
|
}
|
|
|
|
return this;
|
|
},
|
|
|
|
getCustomInput() {
|
|
const vars = {};
|
|
|
|
window.location.href.replace(location.hash, '').replace(/[?&]+([^=&]+)=?([^&]*)?/gi, (m, key, value) => {
|
|
if (_.has(this.config['customInput'], key)) {
|
|
vars[key] = typeof value !== 'undefined' ? value : '';
|
|
}
|
|
});
|
|
|
|
return vars;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Builds an instance of the diagnostic tool
|
|
* @param {object} container - Container in which the initialisation will render the diagnostic
|
|
* @param {object} config
|
|
* @param {string} [config.title] - The displayed title
|
|
* @param {string} [config.header] - A header text displayed to describe the component
|
|
* @param {string} [config.info] - An information text displayed to warn about run duration
|
|
* @param {string} [config.button] - The caption of the start button
|
|
* @param {string} [config.actionStore] - The name of the action to call to store the results
|
|
* @param {string} [config.actionCheck] - The name of the action to call to check the browser results
|
|
* @param {string} [config.actionSchool] - The name of the action to call to get the school name
|
|
* @param {string} [config.controller] - The name of the controller to call
|
|
* @param {string} [config.extension] - The name of the extension containing the controller
|
|
* @param {object} [config.storeParams] - A list of additional parameters to send with diagnostic results
|
|
* @param {boolean} [config.requireSchoolName] - If `true` require a school name to allow the tests to start
|
|
* @param {boolean} [config.requireSchoolId] - If `true` require a school ID to allow the tests to start
|
|
* @param {boolean} [config.validateSchoolName] - If `true` require a school number and a PIN to get the school name and to allow the tests to start
|
|
*
|
|
* @param {string} [config.browser.action] - The name of the action to call to get the browser checker
|
|
* @param {string} [config.browser.controller] - The name of the controller to call to get the browser checker
|
|
* @param {string} [config.browser.extension] - The name of the extension containing the controller to call to get the browser checker
|
|
*
|
|
* @param {number} [config.bandwidth.unit] - The typical bandwidth needed for a test taker (Mbps)
|
|
* @param {Array} [config.bandwidth.ideal] - The thresholds for optimal bandwidth, one by bar
|
|
* @param {number} [config.bandwidth.max] - Maximum number of test takers to display
|
|
*
|
|
* @param {Array} [config.performances.samples] - A list of samples to render in order to compute the rendering performances
|
|
* @param {number} [config.performances.occurrences] - The number of renderings by samples
|
|
* @param {number} [config.performances.timeout] - Max allowed duration for a sample rendering
|
|
* @param {number} [config.performances.optimal] - The threshold for optimal performances
|
|
* @param {number} [config.performances.threshold] - The threshold for minimal performances
|
|
* @returns {diagnostic}
|
|
*/
|
|
return function diagnosticFactory(container, config) {
|
|
// fix the translations for content loaded from config files
|
|
if (config) {
|
|
_.forEach(['title', 'header', 'footer', 'info', 'button'], name => {
|
|
if (config[name]) {
|
|
config[name] = __(config[name]);
|
|
}
|
|
});
|
|
}
|
|
|
|
const diagComponent = component(diagnostic, _defaults)
|
|
.setTemplate(mainTpl)
|
|
|
|
// uninstalls the component
|
|
.on('destroy', function onDiagnosticDestroy() {
|
|
this.controls = null;
|
|
})
|
|
|
|
// initialise component
|
|
.on('init', function onDiagnosticInit() {
|
|
this.render(container);
|
|
})
|
|
// renders the component
|
|
.on('render', function onDiagnosticRender() {
|
|
/**
|
|
* Starts the tests
|
|
* @param {object} [data]
|
|
* @private
|
|
*/
|
|
const runDiagnostics = data => {
|
|
// append the school name to the queries
|
|
if (data && _.isPlainObject(data)) {
|
|
this.config.storeParams = _.assign(this.config.storeParams || {}, data);
|
|
}
|
|
|
|
this.run();
|
|
};
|
|
|
|
/**
|
|
* Default launcher
|
|
* @private
|
|
*/
|
|
let launch = () => runDiagnostics();
|
|
|
|
/**
|
|
* Gets a control by its registered name
|
|
* @param {string} name - the name registered in the collection of controls
|
|
* @private
|
|
*/
|
|
const getControl = name => this.controls[`\$${name}`];
|
|
|
|
/**
|
|
* Gets the value of an input field
|
|
* @param {string} name - the name registered in the collection of controls
|
|
* @returns {string}
|
|
* @private
|
|
*/
|
|
function getInputValue(name) {
|
|
const $control = getControl(name);
|
|
return (($control && $control.val()) || '').trim();
|
|
}
|
|
|
|
/**
|
|
* Sets the value of an input field
|
|
* @param {string} name - the name registered in the collection of controls
|
|
* @param {string} value
|
|
* @private
|
|
*/
|
|
function setInputValue(name, value) {
|
|
const $control = getControl(name);
|
|
$control && $control.val(value);
|
|
}
|
|
|
|
/**
|
|
* Enable/Disable a control
|
|
* @param {string} name - the name registered in the collection of controls
|
|
* @param {boolean} [state]
|
|
* @private
|
|
*/
|
|
function toggleControl(name, state) {
|
|
const $control = getControl(name);
|
|
if ($control) {
|
|
if (typeof state === 'undefined') {
|
|
state = !$control.is(':enabled');
|
|
}
|
|
if (state) {
|
|
$control.prop('disabled', false);
|
|
} else {
|
|
$control.prop('disabled', true);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Requests the server to get the school name
|
|
* @param {object} values
|
|
* @private
|
|
*/
|
|
const requestSchoolName = values => {
|
|
const componentConfig = this.config;
|
|
return requestData(
|
|
urlHelper.route(
|
|
componentConfig.actionSchool,
|
|
componentConfig.controller,
|
|
componentConfig.extension
|
|
),
|
|
values,
|
|
'POST'
|
|
).then(data => {
|
|
return {
|
|
school_name: data,
|
|
school_number: values.school_number
|
|
};
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Install the school name manager.
|
|
* @todo: improve this by moving it into a plugin, and obviously implement the plugin handling
|
|
* @private
|
|
*/
|
|
const manageSchoolProperties = (fields, validate) => {
|
|
/**
|
|
* Checks if the start button can be enabled
|
|
* @returns {boolean}
|
|
* @private
|
|
*/
|
|
function toggleStart() {
|
|
const allow = _.every(fields, getInputValue);
|
|
toggleControl('start', allow);
|
|
return allow;
|
|
}
|
|
|
|
/**
|
|
* Enables/Disables the fields
|
|
* @param {boolean} state
|
|
* @private
|
|
*/
|
|
function toggleFields(state) {
|
|
_.forEach(fields, function(fieldName) {
|
|
toggleControl(fieldName, state);
|
|
});
|
|
}
|
|
|
|
// ensure the diagnostic cannot start without all fields properly input
|
|
_.forEach(fields, fieldName => {
|
|
this.controls[`\$${fieldName}`] = this.getElement()
|
|
.find(`[data-control="${fieldName}"]`)
|
|
.on('keypress', e => {
|
|
const shouldStart = e.which === 13;
|
|
if (shouldStart) {
|
|
e.preventDefault();
|
|
}
|
|
_.defer(() => {
|
|
if (toggleStart() && shouldStart) {
|
|
this.controls.$start.click();
|
|
}
|
|
});
|
|
});
|
|
});
|
|
|
|
toggleStart();
|
|
|
|
// will store the school name in the browser storage, that will allow to restore it next time
|
|
toggleFields(false);
|
|
store('client-diagnostic')
|
|
.then(storage => {
|
|
// store the school name on test start, to ensure consistency
|
|
this.on('start.school', () => {
|
|
_.forEach(fields, fieldName => {
|
|
storage.setItem(fieldName, getInputValue(fieldName)).catch(error => {
|
|
logger.error(error);
|
|
});
|
|
});
|
|
});
|
|
|
|
// restore the school name on load
|
|
return Promise.all(
|
|
_.map(fields, fieldName => {
|
|
return storage.getItem(fieldName).then(value => {
|
|
setInputValue(fieldName, value);
|
|
});
|
|
})
|
|
);
|
|
})
|
|
.catch(error => {
|
|
logger.error(error);
|
|
})
|
|
.then(() => {
|
|
toggleFields(true);
|
|
toggleStart();
|
|
});
|
|
|
|
// ensure the fields are validated and the school name is properly sent before allowing to launch the test
|
|
launch = () => {
|
|
const values = _.reduce(
|
|
fields,
|
|
(result, fieldName) => {
|
|
result[fieldName] = getInputValue(fieldName);
|
|
return result;
|
|
},
|
|
{}
|
|
);
|
|
|
|
this.changeStatus(__('Getting school name...'))
|
|
.cleanUp()
|
|
.disable();
|
|
|
|
if (_.isFunction(validate)) {
|
|
validate(values)
|
|
.then(runDiagnostics)
|
|
.catch(error => {
|
|
const response = error.response || {};
|
|
const message =
|
|
response.errorMsg ||
|
|
response.errorMessage ||
|
|
__('An error occurred! Please verify your input!');
|
|
dialogAlert(message);
|
|
logger.error(error);
|
|
this.changeStatus(__('Failed to get school name')).enable();
|
|
});
|
|
} else {
|
|
runDiagnostics(values);
|
|
}
|
|
};
|
|
|
|
// ensure the fields are not writable while the test is running
|
|
this.on('start.school', () => {
|
|
toggleFields(false);
|
|
}).on('end.school', () => {
|
|
toggleFields(true);
|
|
});
|
|
};
|
|
|
|
// get access to all needed placeholders
|
|
this.controls = {
|
|
$start: this.$component.find('[data-action="test-launcher"]'),
|
|
$status: this.$component.find('.status h2'),
|
|
$results: this.$component.find('.results')
|
|
};
|
|
|
|
// start the diagnostic
|
|
this.controls.$start.on('click', () => {
|
|
this.controls.$start.is(':enabled') && launch();
|
|
});
|
|
|
|
if (this.config.requireSchoolName) {
|
|
if (this.config.validateSchoolName) {
|
|
manageSchoolProperties(['school_number', 'school_pin'], requestSchoolName);
|
|
} else {
|
|
manageSchoolProperties(['school_name']);
|
|
}
|
|
}
|
|
|
|
if (this.config.requireSchoolId) {
|
|
manageSchoolProperties(['school_id', 'workstation']);
|
|
}
|
|
|
|
// show result details
|
|
this.controls.$results.on('click', 'button[data-action="show-details"]', function onShowDetails() {
|
|
const $btn = $(this).closest('button');
|
|
const $result = $btn.closest('[data-result]');
|
|
const $details = $result.find('.details');
|
|
$details.removeClass('hidden');
|
|
$btn.addClass('hidden');
|
|
$result.find('[data-action="hide-details"]').removeClass('hidden');
|
|
});
|
|
|
|
// hide result details
|
|
this.controls.$results.on('click', 'button[data-action="hide-details"]', function onHideDetails() {
|
|
const $btn = $(this).closest('button');
|
|
const $result = $btn.closest('[data-result]');
|
|
const $details = $result.find('.details');
|
|
$details.addClass('hidden');
|
|
$btn.addClass('hidden');
|
|
$result.find('[data-action="show-details"]').removeClass('hidden');
|
|
});
|
|
});
|
|
|
|
_.defer(() => diagComponent.init(config));
|
|
|
|
return diagComponent;
|
|
};
|
|
});
|