diff --git a/src/components/forms/EditPipelineEnvironmentsForm/EditPipelineEnvironmentsForm.js b/src/components/forms/EditPipelineEnvironmentsForm/EditPipelineEnvironmentsForm.js
new file mode 100644
index 000000000..8a5d8d350
--- /dev/null
+++ b/src/components/forms/EditPipelineEnvironmentsForm/EditPipelineEnvironmentsForm.js
@@ -0,0 +1,70 @@
+import React, { Component } from 'react';
+import PropTypes from 'prop-types';
+import { reduxForm } from 'redux-form';
+import { FormattedMessage } from 'react-intl';
+
+import EditEnvironmentList from '../EditEnvironmentSimpleForm/EditEnvironmentList';
+import { SaveIcon } from '../../icons';
+import Callout from '../../widgets/Callout';
+import FormBox from '../../widgets/FormBox';
+import SubmitButton from '../SubmitButton';
+
+class EditPipelineEnvironmentsForm extends Component {
+ render() {
+ const { runtimeEnvironments, dirty, submitting, handleSubmit, submitFailed, submitSucceeded, invalid } = this.props;
+ return (
+
+ }
+ succeeded={submitSucceeded}
+ dirty={dirty}
+ footer={
+
+ }
+ messages={{
+ submit: ,
+ submitting: ,
+ success: ,
+ }}
+ />
+
+ }>
+ {submitFailed && (
+
+
+
+ )}
+
+
+
+ );
+ }
+}
+
+EditPipelineEnvironmentsForm.propTypes = {
+ runtimeEnvironments: PropTypes.array.isRequired,
+ handleSubmit: PropTypes.func.isRequired,
+ dirty: PropTypes.bool,
+ submitting: PropTypes.bool,
+ submitFailed: PropTypes.bool,
+ submitSucceeded: PropTypes.bool,
+ invalid: PropTypes.bool,
+};
+
+export default reduxForm({
+ form: 'editPipelineEnvironments',
+ enableReinitialize: true,
+ keepDirtyOnReinitialize: false,
+})(EditPipelineEnvironmentsForm);
diff --git a/src/components/forms/EditPipelineEnvironmentsForm/index.js b/src/components/forms/EditPipelineEnvironmentsForm/index.js
new file mode 100644
index 000000000..3b5af4e6d
--- /dev/null
+++ b/src/components/forms/EditPipelineEnvironmentsForm/index.js
@@ -0,0 +1,2 @@
+import EditPipelineEnvironmentsForm from './EditPipelineEnvironmentsForm';
+export default EditPipelineEnvironmentsForm;
diff --git a/src/components/forms/EditPipelineForm/EditPipelineForm.js b/src/components/forms/EditPipelineForm/EditPipelineForm.js
index 228af7fa3..99887d1a0 100644
--- a/src/components/forms/EditPipelineForm/EditPipelineForm.js
+++ b/src/components/forms/EditPipelineForm/EditPipelineForm.js
@@ -1,72 +1,46 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
-import ImmutablePropTypes from 'react-immutable-proptypes';
-import { connect } from 'react-redux';
-import { reduxForm, Field, touch, formValueSelector } from 'redux-form';
+import { reduxForm, Field } from 'redux-form';
import { FormattedMessage } from 'react-intl';
import { Container, Row, Col } from 'react-bootstrap';
-import { TextField, MarkdownTextAreaField, PipelineField, PipelineVariablesField } from '../Fields';
-
+import { TextField, MarkdownTextAreaField, CheckboxField } from '../Fields';
+import { SaveIcon } from '../../icons';
import Callout from '../../widgets/Callout';
import FormBox from '../../widgets/FormBox';
import SubmitButton from '../SubmitButton';
-import { validatePipeline } from '../../../redux/modules/pipelines';
-import { extractVariables } from '../../../helpers/boxes';
-import { fetchSupplementaryFilesForPipeline } from '../../../redux/modules/pipelineFiles';
-import { createGetPipelineFiles } from '../../../redux/selectors/pipelineFiles';
class EditPipelineForm extends Component {
- componentDidMount = () => this.props.loadAsync();
-
- componentDidUpdate(prevProps) {
- if (this.props.pipeline.id !== prevProps.pipeline.id) {
- this.props.loadAsync();
- }
- }
-
render() {
const {
- initialValues: pipeline,
- anyTouched,
+ isSuperadmin = false,
+ dirty,
submitting,
handleSubmit,
submitFailed,
submitSucceeded,
- variables = [],
invalid,
- asyncValidating,
- supplementaryFiles,
} = this.props;
return (
- }
+ title={}
succeeded={submitSucceeded}
- dirty={anyTouched}
+ dirty={dirty}
footer={
}
messages={{
- submit: ,
- submitting: (
-
- ),
- success: ,
- validating: ,
+ submit: ,
+ submitting: ,
+ success: ,
}}
/>
@@ -93,37 +67,110 @@ class EditPipelineForm extends Component {
label={
}
/>
-
-
- }
- />
-
-
-
-
+
+
+
+
+
+ }
/>
- }
- supplementaryFiles={supplementaryFiles}
- />
-
-
+
+
+
+
+
+
+
+
+ }
+ />
+
+
+
+ }
+ />
+
+
+ }
+ />
+
+
+
+ }
+ />
+
+
+
+ }
+ />
+
+
+
+ }
+ />
+
+
+
+ }
+ />
+
+
+ >
+ )}
);
@@ -131,19 +178,15 @@ class EditPipelineForm extends Component {
}
EditPipelineForm.propTypes = {
- initialValues: PropTypes.object.isRequired,
+ isSuperadmin: PropTypes.bool,
values: PropTypes.object,
handleSubmit: PropTypes.func.isRequired,
- anyTouched: PropTypes.bool,
+ dirty: PropTypes.bool,
submitting: PropTypes.bool,
submitFailed: PropTypes.bool,
submitSucceeded: PropTypes.bool,
invalid: PropTypes.bool,
- variables: PropTypes.array,
asyncValidating: PropTypes.oneOfType([PropTypes.bool, PropTypes.string]),
- supplementaryFiles: ImmutablePropTypes.map,
- loadAsync: PropTypes.func.isRequired,
- pipeline: PropTypes.object,
};
const validate = ({ name, description }) => {
@@ -170,42 +213,9 @@ const validate = ({ name, description }) => {
return errors;
};
-const asyncValidate = (values, dispatch, { initialValues: { id, version } }) =>
- new Promise((resolve, reject) =>
- dispatch(validatePipeline(id, version))
- .then(res => res.value)
- .then(({ versionIsUpToDate }) => {
- const errors = {};
- if (versionIsUpToDate === false) {
- errors.name = (
-
- );
- dispatch(touch('editPipeline', 'name'));
- }
-
- if (Object.keys(errors).length > 0) {
- throw errors;
- }
- })
- .then(resolve())
- .catch(errors => reject(errors))
- );
-
-export default connect(
- (state, { pipeline }) => ({
- variables: extractVariables(formValueSelector('editPipeline')(state, 'pipeline.boxes')),
- supplementaryFiles: createGetPipelineFiles(pipeline.supplementaryFilesIds)(state),
- }),
- (dispatch, { pipeline }) => ({
- loadAsync: () => dispatch(fetchSupplementaryFilesForPipeline(pipeline.id)),
- })
-)(
- reduxForm({
- form: 'editPipeline',
- validate,
- asyncValidate,
- })(EditPipelineForm)
-);
+export default reduxForm({
+ form: 'editPipeline',
+ enableReinitialize: true,
+ keepDirtyOnReinitialize: false,
+ validate,
+})(EditPipelineForm);
diff --git a/src/locales/cs.json b/src/locales/cs.json
index a7a95b0c6..677a36228 100644
--- a/src/locales/cs.json
+++ b/src/locales/cs.json
@@ -480,14 +480,18 @@
"app.editPipeline.disclaimer": "Upozornění",
"app.editPipeline.disclaimerWarning": "Upravení pipeline může rozbít všechny úlohy, které pipeline používají!",
"app.editPipeline.title": "Změnit nastavení a obsah pipeline",
- "app.editPipelineFields.pipeline": "Pipeline:",
- "app.editPipelineFields.pipelineVariables": "Pipeline proměnné:",
- "app.editPipelineForm.description": "Popis pro vedoucího skupiny:",
+ "app.editPipelineEnvironmentsForm.title": "Běhová prostředí pipeline",
+ "app.editPipelineForm.description": "Podrobnější popis (pro autory úloh):",
+ "app.editPipelineForm.global": "Globalní pipeline spojená s konkrétními běhovými prostředími",
+ "app.editPipelineForm.hasEntryPoint": "Obsahuje vstupní bod",
+ "app.editPipelineForm.hasExtraFiles": "Obsahuje extra soubory",
+ "app.editPipelineForm.isCompilationPipeline": "Kompilační",
+ "app.editPipelineForm.isExecutionPipeline": "Spouštěcí",
+ "app.editPipelineForm.judgeOnlyPipeline": "Pouze sudí",
"app.editPipelineForm.name": "Jméno pipeline:",
- "app.editPipelineForm.submit": "Uložit změny",
- "app.editPipelineForm.submitting": "Ukládání...",
- "app.editPipelineForm.success": "Změny byly uloženy.",
- "app.editPipelineForm.title": "Změnit pipeline {name}",
+ "app.editPipelineForm.producesFiles": "Produkuje výstupní soubory",
+ "app.editPipelineForm.producesStdout": "Produkuje std. výstup",
+ "app.editPipelineForm.title": "Metadata pipeline",
"app.editPipelineForm.validation.description": "Prosíme vyplňte popis této pipeline.",
"app.editPipelineForm.validation.emptyName": "Prosíme vyplňte název této pipeline.",
"app.editPipelineForm.validation.versionDiffers": "Někdo změnil nastavení této pipeline v průběhu její editace. Prosíme obnovte tuto stránku a proveďte své změny znovu.",
diff --git a/src/locales/en.json b/src/locales/en.json
index 4ecd821d8..0323726c4 100644
--- a/src/locales/en.json
+++ b/src/locales/en.json
@@ -480,14 +480,18 @@
"app.editPipeline.disclaimer": "Disclaimer",
"app.editPipeline.disclaimerWarning": "Modifying the pipeline might break all exercises using the pipeline!",
"app.editPipeline.title": "Change Pipeline Settings and Contents",
- "app.editPipelineFields.pipeline": "The pipeline:",
- "app.editPipelineFields.pipelineVariables": "Pipeline variables:",
- "app.editPipelineForm.description": "Description for supervisors:",
+ "app.editPipelineEnvironmentsForm.title": "Pipeline Runtime Environments",
+ "app.editPipelineForm.description": "Detailed description (for exercise authors):",
+ "app.editPipelineForm.global": "Global pipeline associated with particular runtime environments",
+ "app.editPipelineForm.hasEntryPoint": "Has entry-point",
+ "app.editPipelineForm.hasExtraFiles": "Has extra files",
+ "app.editPipelineForm.isCompilationPipeline": "Compilation",
+ "app.editPipelineForm.isExecutionPipeline": "Execution",
+ "app.editPipelineForm.judgeOnlyPipeline": "Judge-Only",
"app.editPipelineForm.name": "Pipeline name:",
- "app.editPipelineForm.submit": "Save changes",
- "app.editPipelineForm.submitting": "Saving changes...",
- "app.editPipelineForm.success": "Settings were saved.",
- "app.editPipelineForm.title": "Edit pipeline {name}",
+ "app.editPipelineForm.producesFiles": "Produces output files",
+ "app.editPipelineForm.producesStdout": "Produces std. out",
+ "app.editPipelineForm.title": "Pipeline Metadata",
"app.editPipelineForm.validation.description": "Please fill the description of the pipeline.",
"app.editPipelineForm.validation.emptyName": "Please fill the name of the pipeline.",
"app.editPipelineForm.validation.versionDiffers": "Somebody has changed the pipeline while you have been editing it. Please reload the page and apply your changes once more.",
diff --git a/src/pages/EditPipeline/EditPipeline.js b/src/pages/EditPipeline/EditPipeline.js
index 11fcb9ab9..224a9990e 100644
--- a/src/pages/EditPipeline/EditPipeline.js
+++ b/src/pages/EditPipeline/EditPipeline.js
@@ -4,23 +4,45 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
import { FormattedMessage } from 'react-intl';
import { Row, Col } from 'react-bootstrap';
import { connect } from 'react-redux';
-import { reset, formValueSelector } from 'redux-form';
+import { reset } from 'redux-form';
+import { defaultMemoize } from 'reselect';
import Page from '../../components/layout/Page';
import Box from '../../components/widgets/Box';
import Callout from '../../components/widgets/Callout';
import EditPipelineForm from '../../components/forms/EditPipelineForm';
+import EditPipelineEnvironmentsForm from '../../components/forms/EditPipelineEnvironmentsForm';
import { EditIcon } from '../../components/icons';
import PipelineFilesTableContainer from '../../containers/PipelineFilesTableContainer';
import DeletePipelineButtonContainer from '../../containers/DeletePipelineButtonContainer';
+import ResourceRenderer from '../../components/helpers/ResourceRenderer';
-import { fetchPipelineIfNeeded, editPipeline } from '../../redux/modules/pipelines';
+import { fetchPipelineIfNeeded, editPipeline, setPipelineRuntimeEnvironments } from '../../redux/modules/pipelines';
+import { fetchRuntimeEnvironments } from '../../redux/modules/runtimeEnvironments';
import { getPipeline } from '../../redux/selectors/pipelines';
-import { loggedInUserIdSelector } from '../../redux/selectors/auth';
-import { getBoxTypes } from '../../redux/selectors/boxes';
+import { runtimeEnvironmentsSelector } from '../../redux/selectors/runtimeEnvironments';
+import { isLoggedAsSuperAdmin } from '../../redux/selectors/users';
import withLinks from '../../helpers/withLinks';
-import { transformPipelineDataForApi, extractVariables } from '../../helpers/boxes';
+import { arrayToObject } from '../../helpers/common';
+
+// convert pipeline data into initial structure for pipeline edit metadata form
+const perpareInitialPipelineData = ({ name, description, version, parameters, author }) => ({
+ name,
+ description,
+ version,
+ parameters,
+ global: author === null,
+});
+
+// get selected runtimes and all runtimes and prepare object for environment selection form
+const perpareInitialEnvironmentsData = defaultMemoize((selectedIds, runtimeEnvironments) =>
+ arrayToObject(
+ runtimeEnvironments.map(rte => rte.id),
+ id => id,
+ id => selectedIds.includes(id)
+ )
+);
class EditPipeline extends Component {
componentDidMount = () => this.props.loadAsync();
@@ -32,16 +54,52 @@ class EditPipeline extends Component {
}
}
- static loadAsync = ({ pipelineId }, dispatch) => Promise.all([dispatch(fetchPipelineIfNeeded(pipelineId))]);
+ static loadAsync = ({ pipelineId }, dispatch) =>
+ Promise.all([dispatch(fetchPipelineIfNeeded(pipelineId)), dispatch(fetchRuntimeEnvironments())]);
+
+ // save pipeline metadata (not the structure)
+ savePipeline = ({
+ name,
+ description,
+ version,
+ parameters: {
+ isCompilationPipeline = false,
+ isExecutionPipeline = false,
+ judgeOnlyPipeline = false,
+ producesStdout = false,
+ producesFiles = false,
+ hasEntryPoint = false,
+ hasExtraFiles = false,
+ },
+ global,
+ }) => {
+ const dataForApi = { name, description, version };
+ if (this.props.isSuperadmin) {
+ dataForApi.parameters = {
+ isCompilationPipeline,
+ isExecutionPipeline,
+ judgeOnlyPipeline,
+ producesStdout,
+ producesFiles,
+ hasEntryPoint,
+ hasExtraFiles,
+ };
+ dataForApi.global = global;
+ }
+ return this.props.editPipeline(dataForApi);
+ };
+
+ // save associations between pipeline and runtime environments
+ saveEnvironments = formData =>
+ this.props.setPipelineRuntimeEnvironments(Object.keys(formData).filter(id => formData[id]));
render() {
const {
links: { PIPELINES_URI },
history: { replace },
pipeline,
- boxTypes,
- editPipeline,
- variables: extractedVariables,
+ runtimeEnvironments,
+ isSuperadmin,
} = this.props;
return (
@@ -66,31 +124,15 @@ class EditPipeline extends Component {
+
({
- ...acc,
- [btoa(variable.name)]: variable.value,
- }),
- {}
- ),
- },
- }}
- onSubmit={({ pipeline, ...formData }) =>
- editPipeline(data.version, {
- pipeline: transformPipelineDataForApi(boxTypes, pipeline, extractedVariables),
- ...formData,
- })
- }
- pipeline={data}
+ initialValues={perpareInitialPipelineData(data)}
+ onSubmit={this.savePipeline}
+ isSuperadmin={isSuperadmin}
/>
@@ -99,11 +141,26 @@ class EditPipeline extends Component {
+
+
+ {isSuperadmin && (
+
+ {environments => (
+
+ )}
+
+ )}
+
+
{
return {
pipeline: getPipeline(pipelineId)(state),
- boxTypes: getBoxTypes(state),
- userId: loggedInUserIdSelector(state),
- variables: extractVariables(formValueSelector('editPipeline')(state, 'pipeline.boxes')),
+ runtimeEnvironments: runtimeEnvironmentsSelector(state),
+ isSuperadmin: isLoggedAsSuperAdmin(state),
};
},
(
@@ -176,7 +233,9 @@ export default withLinks(
) => ({
reset: () => dispatch(reset('editPipeline')),
loadAsync: () => EditPipeline.loadAsync({ pipelineId }, dispatch),
- editPipeline: (version, data) => dispatch(editPipeline(pipelineId, { ...data, version })),
+ editPipeline: data => dispatch(editPipeline(pipelineId, data)),
+ setPipelineRuntimeEnvironments: environments =>
+ dispatch(setPipelineRuntimeEnvironments(pipelineId, environments)),
})
)(EditPipeline)
);
diff --git a/src/redux/helpers/resourceManager/actionTypesFactory.js b/src/redux/helpers/resourceManager/actionTypesFactory.js
index 7b95b2ab8..7298099bb 100644
--- a/src/redux/helpers/resourceManager/actionTypesFactory.js
+++ b/src/redux/helpers/resourceManager/actionTypesFactory.js
@@ -2,14 +2,26 @@
* @module actionTypesFactory
*/
+import { arrayToObject } from '../../../helpers/common';
+
const defaultPrefix = resourceName => `recodex/resource/${resourceName}`;
const twoPhaseActions = ['ADD', 'UPDATE', 'REMOVE', 'FETCH', 'FETCH_MANY'];
const onePhaseActions = ['INVALIDATE'];
+export const defaultActionPostfixes = ['', '_PENDING', '_FULFILLED', '_REJECTED'];
+export const createActionsWithPostfixes = (baseName, prefix, postfixes = defaultActionPostfixes) =>
+ arrayToObject(
+ postfixes,
+ postfix => `${baseName}${postfix}`,
+ postfix => `${prefix}/${baseName}${postfix}`
+ );
+
export const getActionTypes = (prefix, actions, postfixes = ['']) =>
actions.reduce(
(acc, action) => ({
...acc,
+ ...createActionsWithPostfixes(action, prefix, postfixes),
+ /*
...postfixes.reduce(
(acc, postfix) => ({
...acc,
@@ -17,6 +29,7 @@ export const getActionTypes = (prefix, actions, postfixes = ['']) =>
}),
{}
),
+ */
}),
{}
);
@@ -26,7 +39,7 @@ export const getActionTypes = (prefix, actions, postfixes = ['']) =>
* @param {string} prefix Unique prefix for the actions
*/
const actionTypesFactory = (resourceName, prefix = defaultPrefix(resourceName)) => ({
- ...getActionTypes(prefix, twoPhaseActions, ['', '_PENDING', '_FULFILLED', '_REJECTED']),
+ ...getActionTypes(prefix, twoPhaseActions, defaultActionPostfixes),
...getActionTypes(prefix, onePhaseActions),
});
diff --git a/src/redux/helpers/resourceManager/index.js b/src/redux/helpers/resourceManager/index.js
index f74a9000d..b91ffc771 100644
--- a/src/redux/helpers/resourceManager/index.js
+++ b/src/redux/helpers/resourceManager/index.js
@@ -1,7 +1,7 @@
import { createAction } from 'redux-actions';
import { createApiAction } from '../../middleware/apiMiddleware';
-import actionTypesFactory from './actionTypesFactory';
+import actionTypesFactory, { createActionsWithPostfixes } from './actionTypesFactory';
import actionCreatorsFactory from './actionCreatorsFactory';
import reducerFactory, { initialState } from './reducerFactory';
import createRecord, { getData, getJsData, getId } from './recordFactory';
@@ -43,6 +43,7 @@ export {
getId,
defaultNeedsRefetching,
createRecord,
+ createActionsWithPostfixes,
};
/**
diff --git a/src/redux/modules/pipelines.js b/src/redux/modules/pipelines.js
index 876c150a2..59c1ece15 100644
--- a/src/redux/modules/pipelines.js
+++ b/src/redux/modules/pipelines.js
@@ -1,6 +1,11 @@
import { handleActions } from 'redux-actions';
import { Map, List } from 'immutable';
-import factory, { initialState, createRecord, resourceStatus } from '../helpers/resourceManager';
+import factory, {
+ initialState,
+ createRecord,
+ resourceStatus,
+ createActionsWithPostfixes,
+} from '../helpers/resourceManager';
import { createApiAction } from '../middleware/apiMiddleware';
import { actionTypes as pipelineFilesActionTypes } from './pipelineFiles';
@@ -13,10 +18,8 @@ const { actions, reduceActions } = factory({ resourceName });
export const additionalActionTypes = {
VALIDATE_PIPELINE: 'recodex/pipelines/VALIDATE_PIPELINE',
- FORK_PIPELINE: 'recodex/pipelines/FORK_PIPELINE',
- FORK_PIPELINE_PENDING: 'recodex/pipelines/FORK_PIPELINE_PENDING',
- FORK_PIPELINE_REJECTED: 'recodex/pipelines/FORK_PIPELINE_REJECTED',
- FORK_PIPELINE_FULFILLED: 'recodex/pipelines/FORK_PIPELINE_FULFILLED',
+ ...createActionsWithPostfixes('FORK_PIPELINE', 'recodex/pipelines'),
+ ...createActionsWithPostfixes('SET_ENVIRONMENTS', 'recodex/pipelines'),
};
export const fetchPipeline = actions.fetchResource;
@@ -70,6 +73,14 @@ export const validatePipeline = (id, version) =>
body: { version },
});
+export const setPipelineRuntimeEnvironments = (id, environments) =>
+ createApiAction({
+ type: additionalActionTypes.SET_ENVIRONMENTS,
+ endpoint: `/pipelines/${id}/runtime-environments`,
+ method: 'POST',
+ body: { environments },
+ });
+
const reducer = handleActions(
Object.assign({}, reduceActions, {
[pipelineFilesActionTypes.ADD_FILES_FULFILLED]: (state, { payload: files, meta: { pipelineId } }) =>
@@ -95,6 +106,17 @@ const reducer = handleActions(
pipelineId,
}),
+ [additionalActionTypes.SET_ENVIRONMENTS_FULFILLED]: (state, { payload }) =>
+ state.setIn(
+ ['resources', payload.id],
+ createRecord({
+ data: payload,
+ state: resourceStatus.FULFILLED,
+ didInvalidate: false,
+ lastUpdate: Date.now(),
+ })
+ ),
+
// Pagination result needs to store entity data here whilst indices are stored in pagination module
[paginationActionTypes.FETCH_PAGINATED_FULFILLED]: (state, { payload: { items }, meta: { endpoint } }) =>
endpoint === 'pipelines'