From 0a74f94fc87da96ea48695a21738545deb3f0393 Mon Sep 17 00:00:00 2001 From: Martin Krulis Date: Thu, 18 Jan 2018 17:58:21 +0100 Subject: [PATCH 1/5] WiP: Modifying simple limits form to set both cpu time and wall time. --- .../EditSimpleLimitsForm.js | 48 +++++++++++++++---- .../forms/EditSimpleLimitsForm/styles.less | 4 ++ .../forms/Fields/EditSimpleLimitsField.js | 15 +++--- src/components/forms/Fields/LimitsField.js | 2 +- src/redux/modules/simpleLimits.js | 8 ++-- 5 files changed, 55 insertions(+), 22 deletions(-) diff --git a/src/components/forms/EditSimpleLimitsForm/EditSimpleLimitsForm.js b/src/components/forms/EditSimpleLimitsForm/EditSimpleLimitsForm.js index 4cb229df7..039a6bc1e 100644 --- a/src/components/forms/EditSimpleLimitsForm/EditSimpleLimitsForm.js +++ b/src/components/forms/EditSimpleLimitsForm/EditSimpleLimitsForm.js @@ -1,15 +1,15 @@ import React from 'react'; -import { Alert, Table } from 'react-bootstrap'; +import { Alert, Table, OverlayTrigger, Tooltip } from 'react-bootstrap'; import PropTypes from 'prop-types'; import { FormattedMessage, injectIntl } from 'react-intl'; -import { reduxForm } from 'redux-form'; +import { reduxForm, Field } from 'redux-form'; +import classNames from 'classnames'; -import { EditSimpleLimitsField } from '../Fields'; +import { EditSimpleLimitsField, CheckboxField } from '../Fields'; import SubmitButton from '../SubmitButton'; import FormBox from '../../widgets/FormBox'; import Button from '../../widgets/FlatButton'; import { RefreshIcon } from '../../icons'; - import { encodeTestId, encodeEnvironmentId @@ -107,7 +107,39 @@ const EditSimpleLimitsForm = ({ defaultMessage="Cannot save the exercise limits. Please try again later." /> } - + + + + } + > +
+ + } + /> +
+
@@ -194,8 +226,8 @@ const validate = ({ limits }) => { let sums = {}; Object.keys(limits).forEach(test => Object.keys(limits[test]).forEach(env => { - if (limits[test][env]['wall-time']) { - const val = Number(limits[test][env]['wall-time']); + if (limits[test][env]['time']) { + const val = Number(limits[test][env]['time']); if (!Number.isNaN(val) && val > 0) { sums[env] = (sums[env] || 0) + val; } @@ -210,7 +242,7 @@ const validate = ({ limits }) => { Object.keys(sums).forEach(env => { if (sums[env] > maxSumTime) { testsErrors[env] = { - 'wall-time': ( + time: ( } > - + } {environmentsCount > 1 && 1 && environmentsCount > 1 && { const testEnc = testId ? encodeTestId(testId) : null; const envEnc = environmentId ? encodeEnvironmentId(environmentId) : null; @@ -101,7 +101,7 @@ export const cloneVertically = ( formName, // form identifier testId, // test identifier runtimeEnvironmentId, // environment identifier - field // field identifier (memory or wall-time) + field // field identifier (memory or time) ) => (dispatch, getState) => { const state = getState(); const value = getSimpleLimitsOf( @@ -127,7 +127,7 @@ export const cloneHorizontally = ( formName, // form identifier testId, // test identifier runtimeEnvironmentId, // environment identifier - field // field identifier (memory or wall-time) + field // field identifier (memory or time) ) => (dispatch, getState) => { const state = getState(); const value = getSimpleLimitsOf( @@ -153,7 +153,7 @@ export const cloneAll = ( formName, // form identifier testId, // test identifier runtimeEnvironmentId, // environment identifier - field // field identifier (memory or wall-time) + field // field identifier (memory or time) ) => (dispatch, getState) => { const state = getState(); const value = getSimpleLimitsOf( From 1c3d14fdd2887b96a5828f535751885b2a381eed Mon Sep 17 00:00:00 2001 From: Martin Krulis Date: Mon, 22 Jan 2018 01:38:14 +0100 Subject: [PATCH 2/5] Exercise simple edit page performance optimizations. Terminator font removed from loading page and an Easter prank was set. --- public/public/term_cyr.ttf | Bin 57048 -> 0 bytes .../EditExerciseConfigForm.js | 7 +- .../EditExerciseSimpleConfigForm.js | 286 ++++++------- .../EditSimpleLimitsForm.js | 392 +++++++++--------- .../ResourceRenderer/ResourceRenderer.js | 98 +++-- .../SupplementaryFilesTableContainer.js | 7 +- src/helpers/debugTools.js | 42 ++ src/helpers/exerciseSimpleForm.js | 138 +++--- .../EditExerciseSimpleConfig.js | 390 +++++++++-------- src/redux/selectors/simpleLimits.js | 30 -- src/redux/selectors/supplementaryFiles.js | 19 +- views/index.ejs | 14 +- 12 files changed, 749 insertions(+), 674 deletions(-) delete mode 100644 public/public/term_cyr.ttf create mode 100644 src/helpers/debugTools.js diff --git a/public/public/term_cyr.ttf b/public/public/term_cyr.ttf deleted file mode 100644 index 8b9f399dc2ed332ff258f00fa2a48b52aaf6e07d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 57048 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 { const ConnectedEditExerciseConfigForm = connect( (state, { exercise }) => { - const getSupplementaryFilesForExercise = createGetSupplementaryFiles( - exercise.supplementaryFilesIds - ); return { - supplementaryFiles: getSupplementaryFilesForExercise(state), + supplementaryFiles: getSupplementaryFilesForExercise(exercise.id)(state), formValues: getFormValues('editExerciseConfig')(state) }; }, diff --git a/src/components/forms/EditExerciseSimpleConfigForm/EditExerciseSimpleConfigForm.js b/src/components/forms/EditExerciseSimpleConfigForm/EditExerciseSimpleConfigForm.js index 2bfa740ad..7d64449d0 100644 --- a/src/components/forms/EditExerciseSimpleConfigForm/EditExerciseSimpleConfigForm.js +++ b/src/components/forms/EditExerciseSimpleConfigForm/EditExerciseSimpleConfigForm.js @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { Component } from 'react'; import PropTypes from 'prop-types'; import ImmutablePropTypes from 'react-immutable-proptypes'; import { reduxForm, getFormValues } from 'redux-form'; @@ -13,122 +13,129 @@ import SubmitButton from '../SubmitButton'; import ResourceRenderer from '../../helpers/ResourceRenderer'; import EditExerciseSimpleConfigTest from './EditExerciseSimpleConfigTest'; -import { createGetSupplementaryFiles } from '../../../redux/selectors/supplementaryFiles'; +import { getSupplementaryFilesForExercise } from '../../../redux/selectors/supplementaryFiles'; import { encodeTestId } from '../../../redux/modules/simpleLimits'; import { smartFillExerciseConfigForm } from '../../../redux/modules/exerciseConfigs'; import { exerciseConfigFormErrors } from '../../../redux/selectors/exerciseConfigs'; -const EditExerciseSimpleConfigForm = ({ - reset, - handleSubmit, - submitting, - submitFailed, - submitSucceeded, - invalid, - dirty, - formValues, - formErrors, - supplementaryFiles, - exerciseTests, - smartFill, - intl: { locale } -}) => - - } - unlimitedHeight - noPadding - success={submitSucceeded} - dirty={dirty} - footer={ -
- {dirty && - - {' '} - } +class EditExerciseSimpleConfigForm extends Component { + render() { + const { + reset, + handleSubmit, + submitting, + submitFailed, + submitSucceeded, + invalid, + dirty, + formValues, + formErrors, + supplementaryFiles, + exerciseTests, + smartFill, + intl: { locale } + } = this.props; - - ), - submitting: ( - - ), - success: ( - - ), - validating: ( - - ) - }} - /> -
- } - > - {submitFailed && - - - } - - {(...files) => -
- {exerciseTests - .sort((a, b) => a.name.localeCompare(b.name, locale)) - .map((test, idx) => - - )} -
} -
-
; + return ( + + } + unlimitedHeight + noPadding + success={submitSucceeded} + dirty={dirty} + footer={ +
+ {dirty && + + {' '} + } + + + ), + submitting: ( + + ), + success: ( + + ), + validating: ( + + ) + }} + /> +
+ } + > + {submitFailed && + + + } + + {(...files) => +
+ {exerciseTests + .sort((a, b) => a.name.localeCompare(b.name, locale)) + .map((test, idx) => + + )} +
} +
+
+ ); + } +} EditExerciseSimpleConfigForm.propTypes = { initialValues: PropTypes.object, @@ -197,34 +204,29 @@ const validate = formData => { : undefined; }; -export default injectIntl( - connect( - (state, { exercise }) => { - const getSupplementaryFilesForExercise = createGetSupplementaryFiles( - exercise.supplementaryFilesIds - ); - return { - supplementaryFiles: getSupplementaryFilesForExercise(state), - formValues: getFormValues(FORM_NAME)(state), - formErrors: exerciseConfigFormErrors(state, FORM_NAME) - }; - }, - dispatch => ({ - smartFill: (testId, tests, files) => () => - dispatch(smartFillExerciseConfigForm(FORM_NAME, testId, tests, files)) - }) - )( - reduxForm({ - form: FORM_NAME, - enableReinitialize: true, - keepDirtyOnReinitialize: false, - immutableProps: [ - 'formValues', - 'supplementaryFiles', - 'exerciseTests', - 'handleSubmit' - ], - validate - })(EditExerciseSimpleConfigForm) - ) +export default connect( + (state, { exercise }) => { + return { + supplementaryFiles: getSupplementaryFilesForExercise(exercise.id)(state), + formValues: getFormValues(FORM_NAME)(state), + formErrors: exerciseConfigFormErrors(state, FORM_NAME) + }; + }, + dispatch => ({ + smartFill: (testId, tests, files) => () => + dispatch(smartFillExerciseConfigForm(FORM_NAME, testId, tests, files)) + }) +)( + reduxForm({ + form: FORM_NAME, + enableReinitialize: true, + keepDirtyOnReinitialize: false, + immutableProps: [ + 'formValues', + 'supplementaryFiles', + 'exerciseTests', + 'handleSubmit' + ], + validate + })(injectIntl(EditExerciseSimpleConfigForm)) ); diff --git a/src/components/forms/EditSimpleLimitsForm/EditSimpleLimitsForm.js b/src/components/forms/EditSimpleLimitsForm/EditSimpleLimitsForm.js index 039a6bc1e..8e754171a 100644 --- a/src/components/forms/EditSimpleLimitsForm/EditSimpleLimitsForm.js +++ b/src/components/forms/EditSimpleLimitsForm/EditSimpleLimitsForm.js @@ -1,9 +1,8 @@ -import React from 'react'; +import React, { Component } from 'react'; import { Alert, Table, OverlayTrigger, Tooltip } from 'react-bootstrap'; import PropTypes from 'prop-types'; import { FormattedMessage, injectIntl } from 'react-intl'; import { reduxForm, Field } from 'redux-form'; -import classNames from 'classnames'; import { EditSimpleLimitsField, CheckboxField } from '../Fields'; import SubmitButton from '../SubmitButton'; @@ -18,188 +17,197 @@ import prettyMs from 'pretty-ms'; import styles from './styles.less'; -const EditSimpleLimitsForm = ({ - environments, - tests, - cloneHorizontally, - cloneVertically, - cloneAll, - reset, - handleSubmit, - anyTouched, - dirty, - submitting, - submitFailed, - submitSucceeded, - invalid, - intl: { locale } -}) => - - } - unlimitedHeight - noPadding - success={submitSucceeded} - dirty={dirty} - footer={ -
- {dirty && - - {' '} - } - - ), - submitting: ( - - ), - success: ( - - ), - validating: ( - - ) - }} - /> -
- } - > - {submitFailed && - - - } - +class EditSimpleLimitsForm extends Component { + render() { + const { + environments, + tests, + cloneHorizontally, + cloneVertically, + cloneAll, + reset, + handleSubmit, + dirty, + submitting, + submitFailed, + submitSucceeded, + invalid, + intl: { locale } + } = this.props; + return ( + - - } - > -
+ {dirty && + + {' '} + } + + ), + submitting: ( + + ), + success: ( + + ), + validating: ( + + ) + }} + /> +
+ } > - + } + + + } - /> - - -
- - - - )} - - - - {tests.sort((a, b) => a.name.localeCompare(b.name, locale)).map(test => - - - - {environments.map(environment => { - const id = - encodeTestId(test.id) + - '.' + - encodeEnvironmentId(environment.id); - return ( - + + + {tests + .sort((a, b) => a.name.localeCompare(b.name, locale)) + .map(test => + + + + {environments.map(environment => { + const id = + encodeTestId(test.id) + + '.' + + encodeEnvironmentId(environment.id); + return ( + + ); + })} + + )} + +
- {environments.map(environment => - - {environment.name} -
- {test.name} - 1 ? styles.colSeparator : ''} + > +
+ + } + /> +
+ + + + + - )} - -
+ {environments.map(environment => + - - - ); - })} -
- ; + {environment.name} + + )} +
+ {test.name} + 1 ? styles.colSeparator : '' + } + > + +
+ + ); + } +} EditSimpleLimitsForm.propTypes = { tests: PropTypes.array.isRequired, @@ -266,19 +274,17 @@ const validate = ({ limits }) => { return errors; }; -export default injectIntl( - reduxForm({ - form: 'editSimpleLimits', - enableReinitialize: true, - keepDirtyOnReinitialize: false, - immutableProps: [ - 'environments', - 'tests', - 'cloneHorizontally', - 'cloneVertically', - 'cloneAll', - 'handleSubmit' - ], - validate - })(EditSimpleLimitsForm) -); +export default reduxForm({ + form: 'editSimpleLimits', + enableReinitialize: true, + keepDirtyOnReinitialize: false, + immutableProps: [ + 'environments', + 'tests', + 'cloneHorizontally', + 'cloneVertically', + 'cloneAll', + 'handleSubmit' + ], + validate +})(injectIntl(EditSimpleLimitsForm)); diff --git a/src/components/helpers/ResourceRenderer/ResourceRenderer.js b/src/components/helpers/ResourceRenderer/ResourceRenderer.js index ae72f48d6..a601a8bc0 100644 --- a/src/components/helpers/ResourceRenderer/ResourceRenderer.js +++ b/src/components/helpers/ResourceRenderer/ResourceRenderer.js @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { Component } from 'react'; import PropTypes from 'prop-types'; import ImmutablePropTypes from 'react-immutable-proptypes'; import { List } from 'immutable'; @@ -31,6 +31,20 @@ const defaultFailed = noIcons => /> ; +const shallowResourcesEqual = (oldResources, newResources) => { + if (oldResources.length !== newResources.length) { + return false; + } + + // Ah, finally, some old-school for-loops... + for (let i = 0; i < oldResources.length; ++i) { + if (oldResources[i] !== newResources[i]) { + return false; + } + } + return true; +}; + /** * ResourceRenderer component is given a resource managed by the resourceManager * as a prop and displays different content based on the state of the given @@ -46,34 +60,57 @@ const defaultFailed = noIcons => * When all the files are fully loaded then the component displays the content * for the loaded state. */ -const ResourceRenderer = ({ - noIcons = false, - loading = defaultLoading(noIcons), - failed = defaultFailed(noIcons), - children: ready, - resource, - hiddenUntilReady = false, - forceLoading = false -}) => { - const resources = - List.isList(resource) || Array.isArray(resource) ? resource : [resource]; - const stillLoading = - !resource || - resources.find(res => !res) || - resources.some(isLoading) || - forceLoading; - return stillLoading - ? hiddenUntilReady ? null : loading - : resources.some(hasFailed) - ? hiddenUntilReady ? null : failed - : ready( - ...resources - .filter(res => !isDeleting(res)) - .filter(res => !isDeleted(res)) - .filter(res => !isPosting(res)) - .map(getJsData) - ); // display all ready items -}; +class ResourceRenderer extends Component { + componentWillMount = () => { + // Reset caching variables ... + this.oldResources = null; + this.oldData = null; + }; + + // Perform rendering of the childs whilst keeping resource data cached ... + renderReady = resources => { + const { children: ready, returnAsArray = false } = this.props; + if ( + this.oldResources === null || + !shallowResourcesEqual(this.oldResources, resources) + ) { + this.oldResources = resources; + this.oldData = resources + .filter(res => !isDeleting(res)) + .filter(res => !isDeleted(res)) + .filter(res => !isPosting(res)) + .map(getJsData); + } + return returnAsArray ? ready(this.oldData) : ready(...this.oldData); + }; + + render() { + const { + noIcons = false, + loading = defaultLoading(noIcons), + failed = defaultFailed(noIcons), + // children: ready, + resource, + hiddenUntilReady = false, + forceLoading = false + } = this.props; + + const resources = Array.isArray(resource) + ? resource + : List.isList(resource) ? resource.toArray() : [resource]; + const stillLoading = + !resource || + resources.find(res => !res) || + resources.some(isLoading) || + forceLoading; + + return stillLoading + ? hiddenUntilReady ? null : loading + : resources.some(hasFailed) + ? hiddenUntilReady ? null : failed + : this.renderReady(resources); + } +} ResourceRenderer.propTypes = { loading: PropTypes.element, @@ -86,7 +123,8 @@ ResourceRenderer.propTypes = { ]), hiddenUntilReady: PropTypes.bool, forceLoading: PropTypes.bool, - noIcons: PropTypes.bool + noIcons: PropTypes.bool, + returnAsArray: PropTypes.bool }; export default ResourceRenderer; diff --git a/src/containers/SupplementaryFilesTableContainer/SupplementaryFilesTableContainer.js b/src/containers/SupplementaryFilesTableContainer/SupplementaryFilesTableContainer.js index 99ef2cdcf..56820c8a7 100644 --- a/src/containers/SupplementaryFilesTableContainer/SupplementaryFilesTableContainer.js +++ b/src/containers/SupplementaryFilesTableContainer/SupplementaryFilesTableContainer.js @@ -17,7 +17,7 @@ import { } from '../../redux/modules/supplementaryFiles'; import { downloadSupplementaryFile } from '../../redux/modules/files'; -import { createGetSupplementaryFiles } from '../../redux/selectors/supplementaryFiles'; +import { getSupplementaryFilesForExercise } from '../../redux/selectors/supplementaryFiles'; const SupplementaryFilesTableContainer = ({ exercise, @@ -64,11 +64,8 @@ SupplementaryFilesTableContainer.propTypes = { export default connect( (state, { exercise }) => { - const getSupplementaryFilesForExercise = createGetSupplementaryFiles( - exercise.supplementaryFilesIds - ); return { - supplementaryFiles: getSupplementaryFilesForExercise(state) + supplementaryFiles: getSupplementaryFilesForExercise(exercise.id)(state) }; }, (dispatch, { exercise }) => ({ diff --git a/src/helpers/debugTools.js b/src/helpers/debugTools.js new file mode 100644 index 000000000..3822e33ca --- /dev/null +++ b/src/helpers/debugTools.js @@ -0,0 +1,42 @@ +/* eslint eqeqeq: "off", no-console: ["error", { allow: ["log", "error", "debug"] }] */ +const modificationWatchdogValues = {}; + +export const modificationWatchdog = (name, value, verbose = false) => { + if (modificationWatchdogValues[name] === undefined) { + // First time the value is being registered ... + console.log(`Value '${name}' was created.`); + if (verbose) { + console.log(value); + } + } else if (modificationWatchdogValues[name] !== value) { + console.log(`Value '${name}' was changed.`); + if (verbose) { + console.log(modificationWatchdogValues[name]); + console.log(value); + } + } + modificationWatchdogValues[name] = value; + return value; +}; + +// This should be called from componentWillReceiveProps(nextProps), for instance. +export const logPropsChanges = (className, props, nextProps) => { + let changed = false; + for (const p in props) { + if (props[p] !== nextProps[p]) { + if (!changed) { + changed = true; + console.log(`${className} props changed.`); + } + + if (props[p] != nextProps[p]) { + console.log(`Property '${p}' changed.`); + console.log(props[p]); + console.log(nextProps[p]); + } else { + console.log(`Property '${p}' was recreated with the same value.`); + } + } + } + if (changed) console.log('----------'); +}; diff --git a/src/helpers/exerciseSimpleForm.js b/src/helpers/exerciseSimpleForm.js index 6d7228ed0..b8a5de5dd 100644 --- a/src/helpers/exerciseSimpleForm.js +++ b/src/helpers/exerciseSimpleForm.js @@ -1,4 +1,6 @@ import yaml from 'js-yaml'; +import { defaultMemoize } from 'reselect'; + import { endpointDisguisedAsIdFactory, encodeTestId, @@ -37,23 +39,19 @@ export const getTestsInitValues = (exerciseTests, scoreConfig, locale) => { }; }; -export const transformAndSendTestsValues = ( - formData, - editExerciseTests, - editExerciseScoreConfig -) => { +export const transformTestsValues = formData => { const uniformScore = formData.isUniform === true || formData.isUniform === 'true'; let scoreConfigData = { testWeights: {} }; - let testsData = []; + let tests = []; for (const test of formData.tests) { const testWeight = uniformScore ? 100 : Number(test.weight); scoreConfigData.testWeights[test.name] = testWeight; - testsData.push( + tests.push( test.id ? { id: test.id, @@ -65,14 +63,10 @@ export const transformAndSendTestsValues = ( ); } - return Promise.all([ - editExerciseTests({ - tests: testsData - }), - editExerciseScoreConfig({ - scoreConfig: yaml.safeDump(scoreConfigData) - }) - ]); + return { + tests, + scoreConfig: yaml.safeDump(scoreConfigData) + }; }; /* @@ -201,7 +195,7 @@ const getSimpleConfigSimpleVariable = (variables, testObj, variableName) => { }; // Prepare the initial form data for configuration form ... -export const getSimpleConfigInitValues = (config, tests) => { +export const getSimpleConfigInitValues = defaultMemoize((config, tests) => { const confTests = tests && config[0] && config[0].tests ? config[0].tests : []; @@ -247,7 +241,7 @@ export const getSimpleConfigInitValues = (config, tests) => { return { config: res }; -}; +}); // Prepare one variable to be sent in to the API const transformConfigSimpleVariable = (variables, name, value) => { @@ -321,13 +315,12 @@ const _safeGet = (obj, path) => { return obj; }; -export const transformAndSendConfigValues = ( +export const transformConfigValues = ( formData, pipelines, environments, tests, - originalConfig, - setConfig + originalConfig ) => { let envs = []; for (const environment of environments) { @@ -340,6 +333,7 @@ export const transformAndSendConfigValues = ( const compilationPipeline = envPipelines.find( pipeline => pipeline.parameters.isCompilationPipeline ); + const executionPipelineStdout = envPipelines.find( pipeline => pipeline.parameters.isExecutionPipeline && @@ -403,51 +397,46 @@ export const transformAndSendConfigValues = ( }); } - return setConfig({ - config: envs - }); + return { config: envs }; }; /* * Memory and Time limits */ -export const getLimitsInitValues = ( - limits, - tests, - environments, - exerciseId -) => { - let res = {}; - - tests.forEach(test => { - const testEnc = encodeTestId(test.id); - res[testEnc] = {}; - environments.forEach(environment => { - const envId = encodeEnvironmentId(environment.id); - let lim = limits.getIn([ - endpointDisguisedAsIdFactory({ - exerciseId, - runtimeEnvironmentId: environment.id - }), - 'data', - String(test.id) - ]); - if (lim) { - lim = lim.toJS(); - } - - res[testEnc][envId] = { - memory: lim ? String(lim.memory) : '0', - time: lim ? String(lim['wall-time']) : '0' - }; +export const getLimitsInitValues = defaultMemoize( + (limits, tests, environments, exerciseId) => { + let res = {}; + + tests.forEach(test => { + const testEnc = encodeTestId(test.id); + res[testEnc] = {}; + environments.forEach(environment => { + const envId = encodeEnvironmentId(environment.id); + let lim = limits.getIn([ + endpointDisguisedAsIdFactory({ + exerciseId, + runtimeEnvironmentId: environment.id + }), + 'data', + String(test.id) + ]); + if (lim) { + lim = lim.toJS(); + } + + res[testEnc][envId] = { + memory: lim ? String(lim.memory) : '0', + time: lim ? String(lim['wall-time']) : '0' + }; + }); }); - }); - return { - limits: res, - preciseTime: true - }; -}; + return { + limits: res, + preciseTime: true + }; + } +); const transformLimitsObject = ({ memory, time }, timeField = 'wall-time') => { let res = { @@ -462,23 +451,16 @@ const transformLimitsObject = ({ memory, time }, timeField = 'wall-time') => { * The data have to be re-assembled, since they use different format and keys are encoded. * The dispatching function is invoked for every environment and all promise is returned. */ -export const transformAndSendLimitsValues = ( - formData, - tests, - runtimeEnvironments, - editEnvironmentSimpleLimits -) => - Promise.all( - runtimeEnvironments.map(environment => { - const envId = encodeEnvironmentId(environment.id); - const data = { - limits: tests.reduce((acc, test) => { - acc[test.id] = transformLimitsObject( - formData.limits[encodeTestId(test.id)][envId] - ); - return acc; - }, {}) - }; - return editEnvironmentSimpleLimits(environment.id, data); - }) - ); +export const transformLimitsValues = (formData, tests, runtimeEnvironments) => + runtimeEnvironments.map(environment => { + const envId = encodeEnvironmentId(environment.id); + const data = { + limits: tests.reduce((acc, test) => { + acc[test.id] = transformLimitsObject( + formData.limits[encodeTestId(test.id)][envId] + ); + return acc; + }, {}) + }; + return { id: environment.id, data }; + }); diff --git a/src/pages/EditExerciseSimpleConfig/EditExerciseSimpleConfig.js b/src/pages/EditExerciseSimpleConfig/EditExerciseSimpleConfig.js index b92da1500..b1e3a8de7 100644 --- a/src/pages/EditExerciseSimpleConfig/EditExerciseSimpleConfig.js +++ b/src/pages/EditExerciseSimpleConfig/EditExerciseSimpleConfig.js @@ -4,6 +4,7 @@ import ImmutablePropTypes from 'react-immutable-proptypes'; import { FormattedMessage, injectIntl } from 'react-intl'; import { Row, Col } from 'react-bootstrap'; import { connect } from 'react-redux'; +import { defaultMemoize } from 'reselect'; import Page from '../../components/layout/Page'; import Box from '../../components/widgets/Box'; @@ -64,18 +65,19 @@ import { getEnvInitValues, transformEnvValues, getTestsInitValues, - transformAndSendTestsValues, + transformTestsValues, getSimpleConfigInitValues, - transformAndSendConfigValues, + transformConfigValues, getLimitsInitValues, - transformAndSendLimitsValues + transformLimitsValues } from '../../helpers/exerciseSimpleForm'; class EditExerciseSimpleConfig extends Component { componentWillMount = () => this.props.loadAsync(); - componentWillReceiveProps = props => { - if (this.props.params.exerciseId !== props.params.exerciseId) { - props.loadAsync(); + + componentWillReceiveProps = nextProps => { + if (this.props.params.exerciseId !== nextProps.params.exerciseId) { + nextProps.loadAsync(); } }; @@ -101,6 +103,73 @@ class EditExerciseSimpleConfig extends Component { dispatch(fetchPipelines()) ]); + transformAndSendTestsValues = data => { + const { + editTests, + editScoreConfig, + fetchConfig, + fetchEnvironmentSimpleLimits + } = this.props; + + const { tests, scoreConfig } = transformTestsValues(data); + + return Promise.all([ + editTests({ tests }), + editScoreConfig({ scoreConfig }) + ]).then(() => Promise.all([fetchConfig(), fetchEnvironmentSimpleLimits()])); + }; + + transformAndSendConfigValuesCreator = defaultMemoize( + (pipelines, environments, tests, config) => { + const { setConfig } = this.props; + return data => + setConfig( + transformConfigValues(data, pipelines, environments, tests, config) + ); + } + ); + + transformAndSendEnvValues = defaultMemoize( + (exerciseId, pipelines, environments, tests, config) => { + const { + editEnvironmentConfigs, + reloadConfigAndLimits, + setConfig + } = this.props; + + return data => { + const newEnvironments = transformEnvValues(data, environments); + const configData = transformConfigValues( + getSimpleConfigInitValues(config, tests), + pipelines, + newEnvironments, + tests, + config + ); + return editEnvironmentConfigs({ + environmentConfigs: newEnvironments + }) + .then(() => setConfig(configData)) + .then(reloadConfigAndLimits(exerciseId)); + }; + } + ); + + transformAndSendLimitsValues = defaultMemoize( + (tests, exerciseRuntimeEnvironments) => { + const { editEnvironmentSimpleLimits } = this.props; + return formData => + Promise.all( + transformLimitsValues( + formData, + tests, + exerciseRuntimeEnvironments, + editEnvironmentSimpleLimits + ).map(({ id, data }) => editEnvironmentSimpleLimits(id, data)) + ); + } + ); + render() { const { links: { EXERCISE_URI_FACTORY }, @@ -108,28 +177,20 @@ class EditExerciseSimpleConfig extends Component { exercise, runtimeEnvironments, exerciseConfig, - fetchEnvironmentSimpleLimits, - editEnvironmentSimpleLimits, exerciseEnvironmentConfig, - editEnvironmentConfigs, exerciseScoreConfig, exerciseTests, - editScoreConfig, - editTests, - fetchConfig, - setConfig, limits, pipelines, cloneHorizontally, cloneVertically, cloneAll, - reloadConfigAndLimits, intl: { locale } } = this.props; return ( } description={ - {exercise => + {(exercise, tests) =>
- - - - } - unlimitedHeight - > - - {(scoreConfig, tests) => - - transformAndSendTestsValues( - data, - editTests, - editScoreConfig - ).then(() => - Promise.all([ - fetchConfig(), - fetchEnvironmentSimpleLimits() - ]) - )} - />} - - - - } - unlimitedHeight - > - - {(config, tests, environmentConfigs, ...pipelines) => - + {( + pipelines // pipelines are returned as a whole array (so they can be cached properly) + ) => +
+ + + + } + unlimitedHeight + > + + {scoreConfig => + } + + + + } + unlimitedHeight > - {(...environments) => - { - const newEnvironments = transformEnvValues( - data, - environments - ); - return editEnvironmentConfigs({ - environmentConfigs: newEnvironments - }) - .then(() => - transformAndSendConfigValues( - getSimpleConfigInitValues(config, tests), + + {(config, environmentConfigs) => + + {environments => + } - } - - - - - - - -
+ config + )} + />} + } + + + + + + + +
- - - - {(config, tests, environments, ...pipelines) => - tests.length > 0 - ? - transformAndSendConfigValues( - data, - pipelines, - environments, - tests, - config, - setConfig - )} - /> - :
-

- {' '} - -

- -
} -
- -
-
+ + + + {(config, environments) => + tests.length > 0 + ? + :
+

+ {' '} + +

+ +
} +
+ +
+
+
} +
- {(tests, envConfig) => + {envConfig => tests.length > 0 && exercise.runtimeEnvironments.length > 0 ? - transformAndSendLimitsValues( - data, - tests, - exercise.runtimeEnvironments, - editEnvironmentSimpleLimits - )} + onSubmit={this.transformAndSendLimitsValues( + tests, + exercise.runtimeEnvironments + )} environments={exercise.runtimeEnvironments} tests={tests} initialValues={getLimitsInitValues( @@ -386,6 +415,21 @@ EditExerciseSimpleConfig.propTypes = { intl: PropTypes.shape({ locale: PropTypes.string.isRequired }).isRequired }; +const cloneVerticallyWrapper = defaultMemoize( + dispatch => (formName, testName, runtimeEnvironmentId) => field => () => + dispatch(cloneVertically(formName, testName, runtimeEnvironmentId, field)) +); + +const cloneHorizontallyWrapper = defaultMemoize( + dispatch => (formName, testName, runtimeEnvironmentId) => field => () => + dispatch(cloneHorizontally(formName, testName, runtimeEnvironmentId, field)) +); + +const cloneAllWrapper = defaultMemoize( + dispatch => (formName, testName, runtimeEnvironmentId) => field => () => + dispatch(cloneAll(formName, testName, runtimeEnvironmentId, field)) +); + export default injectIntl( withLinks( connect( @@ -432,25 +476,9 @@ export default injectIntl( editTests: data => dispatch(setExerciseTests(exerciseId, data)), fetchConfig: () => dispatch(fetchExerciseConfig(exerciseId)), setConfig: data => dispatch(setExerciseConfig(exerciseId, data)), - cloneVertically: ( - formName, - testName, - runtimeEnvironmentId - ) => field => () => - dispatch( - cloneVertically(formName, testName, runtimeEnvironmentId, field) - ), - cloneHorizontally: ( - formName, - testName, - runtimeEnvironmentId - ) => field => () => - dispatch( - cloneHorizontally(formName, testName, runtimeEnvironmentId, field) - ), - cloneAll: (formName, testName, runtimeEnvironmentId) => field => () => - dispatch(cloneAll(formName, testName, runtimeEnvironmentId, field)), - + cloneVertically: cloneVerticallyWrapper(dispatch), + cloneHorizontally: cloneHorizontallyWrapper(dispatch), + cloneAll: cloneAllWrapper(dispatch), reloadConfigAndLimits: exerciseId => () => dispatch(fetchExercise(exerciseId)).then(({ value: exercise }) => Promise.all([ diff --git a/src/redux/selectors/simpleLimits.js b/src/redux/selectors/simpleLimits.js index f3f35a964..b2aca995c 100644 --- a/src/redux/selectors/simpleLimits.js +++ b/src/redux/selectors/simpleLimits.js @@ -1,37 +1,7 @@ import { createSelector } from 'reselect'; -import { getExerciseRuntimeEnvironments } from './exercises'; -import { endpointDisguisedAsIdFactory } from '../modules/simpleLimits'; const getLimits = state => state.simpleLimits; -const EMPTY_OBJ = {}; export const simpleLimitsSelector = createSelector(getLimits, limits => limits.get('resources') ); - -export const simpleLimitsAllSelector = exerciseId => - createSelector( - [getLimits, getExerciseRuntimeEnvironments(exerciseId)], - (limits, runtimeEnvironments) => { - if (!limits || !runtimeEnvironments) { - return EMPTY_OBJ; - } - - return runtimeEnvironments.reduce((acc, runtimeEnvironment) => { - const runtimeEnvironmentId = runtimeEnvironment.get('id'); - let testLimits = limits.getIn([ - 'resources', - endpointDisguisedAsIdFactory({ - exerciseId, - runtimeEnvironmentId - }), - 'data' - ]); - if (testLimits) { - testLimits = testLimits.toJS(); - } - acc[runtimeEnvironmentId] = testLimits; - return acc; - }, {}); - } - ); diff --git a/src/redux/selectors/supplementaryFiles.js b/src/redux/selectors/supplementaryFiles.js index ea6d6c260..7d84b8ba1 100644 --- a/src/redux/selectors/supplementaryFiles.js +++ b/src/redux/selectors/supplementaryFiles.js @@ -1,5 +1,6 @@ -import { createSelector } from 'reselect'; +import { createSelector, defaultMemoize } from 'reselect'; import { isReady } from '../helpers/resourceManager'; +import { getExercise } from './exercises'; const getSupplementaryFiles = state => state.supplementaryFiles; export const getSupplementaryFile = id => @@ -16,3 +17,19 @@ export const createGetSupplementaryFiles = ids => .filter(isReady) .filter(file => ids.indexOf(file.getIn(['data', 'id'])) >= 0) ); + +export const getSupplementaryFilesForExercise = defaultMemoize(exerciseId => + createSelector( + [getExercise(exerciseId), supplementaryFilesSelector], + (exercise, supplementaryFiles) => { + const ids = exercise && exercise.getIn(['data', 'supplementaryFilesIds']); + return ( + ids && + supplementaryFiles && + supplementaryFiles + .filter(isReady) + .filter(file => ids.indexOf(file.getIn(['data', 'id'])) >= 0) + ); + } + ) +); diff --git a/views/index.ejs b/views/index.ejs index e8ec9523a..c1e430a85 100644 --- a/views/index.ejs +++ b/views/index.ejs @@ -9,15 +9,9 @@