Commit e572c282 authored by charlie-ablett's avatar charlie-ablett

Updates to INK 2.0

parent c2bc3ec5
......@@ -19,7 +19,7 @@ function apiUrl() {
if(process.env.NODE_ENV == 'development') {
return('http://localhost:8080');
} else if(stage == 'production') {
return('http://ink-api.coko.foundation');
return('http://inkdemo-api.coko.foundation');
} else if(stage == 'staging') {
return('http://ink-api-staging.coko.foundation');
} else if(stage == 'demo') {
......
......@@ -14,11 +14,12 @@ export function executeRecipe(recipeId, formData, signedIn, authToken, tokenType
if(signedIn === false || signedIn === null || signedIn === undefined) {
return { type: actions.EXECUTE_RECIPE_REQUEST };
}
console.log("execute recipe: 1")
return function(dispatch) {
let theResponse;
dispatch({type: actions.EXECUTE_RECIPE_REQUEST});
console.log("execute recipe: 2")
fetch(`${settings.apiBaseUrl}/api/recipes/${recipeId}/execute`, {
method: 'POST',
headers: {
......@@ -33,24 +34,29 @@ export function executeRecipe(recipeId, formData, signedIn, authToken, tokenType
})
.then(response => {
theResponse = response;
console.log("execute recipe: 3")
return response.json();
})
.then(json => {
checkStatus(theResponse, dispatch, signedIn);
if(_.isUndefined(json.process_chain)) {
console.log("execute recipe: 4a")
let error = new Error(theResponse.statusText);
error.response = json.errors;
throw error;
}
else {
console.log("execute recipe: 4b")
dispatch(setAlert(["Processing started successfully"], AlertTypes.SUCCESS));
console.log("execute recipe: 5")
dispatch(successExecuteRecipe(json));
dispatch(getRecipe(recipeId, signedIn, authToken, tokenType, client, expiry, uid));
console.log("execute recipe: 6")
// dispatch(getRecipe(recipeId, signedIn, authToken, tokenType, client, expiry, uid));
}
})
.catch(error => {
// console.log("error", error)
dispatch(setAlert(error.response, AlertTypes.ERROR));
console.log("execute recipe: 7", error)
dispatch(setAlert(error.response || "Error executing recipe", AlertTypes.ERROR));
dispatch(rejectExecuteRecipe(error));
});
return null;
......@@ -75,11 +81,14 @@ export function rejectExecuteRecipe(error) {
////////////////// get single recipe //////////////////////////
///////////////////////////////////////////////////////////////
export function getRecipe(recipeId, appState) {
const { authToken, tokenType, client, expiry, uid } = appState;
export function getRecipeWithTokens(recipeId, appState) {
const { authToken, tokenType, client, expiry, uid } = appState.session;
const signedIn = (authToken != null);
return(getRecipe(recipeId, signedIn, authToken, tokenType, client, expiry, uid));
}
export function getRecipe(recipeId, signedIn, authToken, tokenType, client, expiry, uid) {
if(signedIn === false || signedIn === null || signedIn === undefined) {
return { type: actions.GET_RECIPE_REQUEST };
}
......@@ -105,7 +114,6 @@ export function getRecipe(recipeId, appState) {
return response.json();
})
.then(json => {
let signedIn = appState.session ? appState.session.signedIn : false;
checkStatus(theResponse, dispatch, signedIn);
if(_.isUndefined(json.recipe)) {
let error = new Error(theResponse.statusText);
......@@ -117,7 +125,7 @@ export function getRecipe(recipeId, appState) {
}
})
.catch(error => {
dispatch(setAlert(error.response, AlertTypes.ERROR));
dispatch(setAlert("Cannot retrieve this recipe", AlertTypes.ERROR));
dispatch(rejectGetRecipe(error));
});
return null;
......@@ -205,6 +213,79 @@ export function rejectGetAllRecipes(error) {
};
}
export function getPresetsForRecipeStep(recipeStep, appState) {
const { authToken, tokenType, client, expiry, uid } = appState.session;
const signedIn = (authToken != null);
return function(dispatch) {
let theResponse;
dispatch({
type: actions.GET_PRESETS_FOR_RECIPE_STEP_REQUEST,
recipeStepId: recipeStep.id,
recipeId: recipeStep.recipe_id
});
fetch(`${settings.apiBaseUrl}/api/recipe_steps/${recipeStep.id}/recipe_step_presets`, {
method: 'GET',
headers: {
'Accept': settings.apiVersionHeader,
'Content-Type': 'application/json',
'Access-Token': authToken,
'Client': client,
'Token-Type': tokenType,
'Expiry': expiry,
'uid': uid
}
})
.then(response => {
theResponse = response;
return response.json();
})
.then(json => {
checkStatus(theResponse, dispatch, signedIn);
if(_.isUndefined(json)) {
let error = new Error(theResponse.statusText);
error.response = json.errors;
throw error;
}
else {
dispatch(successGetPresetsForRecipeStep(json, recipeStep));
}
})
.catch(error => {
dispatch(rejectGetPresetsForRecipeStep(error, recipeStep));
});
return null;
};
}
export function successGetPresetsForRecipeStep(json, recipeStep) {
return {
type: actions.GET_PRESETS_FOR_RECIPE_STEP_SUCCESS,
data: json,
recipeStepId: recipeStep.id,
recipeId: recipeStep.recipe_id
};
}
export function rejectGetPresetsForRecipeStep(error, recipeStep) {
return {
type: actions.GET_PRESETS_FOR_RECIPE_STEP_FAILURE,
error: error.toString(),
recipeStepId: recipeStep.id,
recipeId: recipeStep.recipe_id
};
}
export function setPresetForRecipeStepExecution(preset, recipeStep) {
return{
type: actions.SET_PRESET_FOR_RECIPE_STEP_EXECUTION,
preset: preset,
recipeStepId: recipeStep.id,
recipeId: recipeStep.recipe_id
}
}
///////////////////////////////////////////////////////////////
//////////////////// select recipe ////////////////////////////
///////////////////////////////////////////////////////////////
......@@ -232,6 +313,35 @@ export function resetPlaceholders() {
};
}
///////////////////////////////////////////////////////////////
///////////// recipe execution - parameter list ///////////////
///////////////////////////////////////////////////////////////
export function resetParameterList(recipeId, recipeStepId) {
return {
type: actions.RESET_PARAMETER_LIST,
recipeId: recipeId,
recipeStepId: recipeStepId
};
}
export function addToParameterList(recipeId, recipeStepId) {
return {
type: actions.ADD_TO_PARAMETER_LIST,
recipeId: recipeId,
recipeStepId: recipeStepId
};
}
export function removeFromParameterList(recipeId, recipeStepId, index) {
return {
type: actions.REMOVE_FROM_PARAMETER_LIST,
index: index,
recipeId: recipeId,
recipeStepId: recipeStepId
};
}
///////////////////////////////////////////////////////////////
//////////////// expand/collapse file lists ///////////////////
///////////////////////////////////////////////////////////////
......@@ -389,7 +499,6 @@ export function unfavouriteRecipe(recipeId, signedIn, authToken, tokenType, clie
throw error;
}
else {
console.log("unf recipeId", recipeId)
dispatch(setRecipeFavourite(json.favourite, recipeId));
}
})
......@@ -406,6 +515,83 @@ export function rejectUnfavouriteRecipe(error) {
};
}
///////////////////////////////////////////////////////////////
///////////////////// create new preset ///////////////////////
///////////////////////////////////////////////////////////////
export function createPreset(recipeId, processStep, formData, signedIn, authToken, tokenType, client, expiry, uid) {
if(signedIn === false || signedIn === null || signedIn === undefined) {
return { type: actions.CREATE_PRESET_REQUEST };
}
formData.process_step_id = processStep.id;
return function(dispatch) {
let theResponse;
dispatch(
{
type: actions.CREATE_PRESET_REQUEST,
processStep: processStep,
recipeId: recipeId
}
);
let data = JSON.stringify(formData);
fetch(`${settings.apiBaseUrl}/api/recipe_step_presets/create_from_process_step`, {
method: 'POST',
headers: {
'Accept': settings.apiVersionHeader,
'Content-Type': 'application/json',
'Access-Token': authToken,
'Client': client,
'Token-Type': tokenType,
'Expiry': expiry,
'uid': uid
},
body: data
})
.then(response => {
theResponse = response;
return response.json();
})
.then(json => {
checkStatus(theResponse, dispatch, signedIn);
if(_.isEmpty(json)) {
let error = new Error(theResponse.statusText);
error.response = json.errors;
throw error;
}
else {
dispatch(successCreatePreset(json, processStep, recipeId));
}
})
.catch(error => {
dispatch(rejectCreatePreset(error, processStep, recipeId));
});
return null;
};
}
export function successCreatePreset(preset, processStep, recipeId) {
return {
type: actions.CREATE_PRESET_SUCCESS,
preset: preset,
processStep: processStep,
recipeId: recipeId
};
}
export function rejectCreatePreset(error, processStep, recipeId) {
return {
type: actions.CREATE_PRESET_FAILURE,
error: error.toString(),
processStep: processStep,
recipeId: recipeId
};
}
///////////////////////////////////////////////////////////////
///////////////// new recipe - step class list ////////////////
///////////////////////////////////////////////////////////////
......
import * as actions from '../constants/ActionTypes';
import * as viewConstants from '../constants/Views';
////////////////////////// show recipe ///////////////////////////
export function setView(viewName) {
return {
type: actions.SET_VIEW,
viewName: viewName
};
}
export function resetView() {
return {
type: actions.SET_VIEW,
viewName: viewConstants.SHOW_RECIPE_STEP_VIEW
};
}
//////////////////////// show param tab ///////////////////////////
export function setParameterTabView(viewName, recipeStep) {
return {
type: actions.SET_PARAMETER_TAB_VIEW,
viewName: viewName,
recipeStep: recipeStep
};
}
......@@ -31,7 +31,6 @@ export class EditRecipeForm extends Component {
render() {
let { recipe } = this.props;
console.log("recipe to edit:", recipe)
return(
<form ref="editRecipeForm">
......
......@@ -51,19 +51,7 @@ export class ExecutionDetail extends Component {
processChain={processChain}
inputFileManifest={processChain.input_file_manifest}
/>
<table className="table">
<thead>
<tr>
<th>status</th>
<th>Step info</th>
<th>Output</th>
</tr>
</thead>
<tbody>
{processChain.process_steps.map(step => <ExecutionDetailStep key={step.id} recipeId={processChain.recipe_id} processStep={step} dispatch={dispatch} appState={appState} />)}
</tbody>
</table>
{processChain.process_steps.map(step => <ExecutionDetailStep key={step.id} recipeId={processChain.recipe_id} processStep={step} dispatch={dispatch} appState={appState} />)}
</div>
);
}
......
......@@ -5,8 +5,10 @@ import _ from "lodash";
import SmoothCollapse from 'react-smooth-collapse';
import * as actions from '../actions/authenticationActions';
import * as recipeActions from '../actions/recipeActions';
import { downloadLogFile, downloadOutputFile, downloadStepOutputZip } from '../businessLogic/fileLogic';
import { expandStepFileList, collapseStepFileList } from '../actions/recipeActions';
import SavePresetForm from './recipe-steps/SavePresetForm';
export class ExecutionDetailStep extends Component {
......@@ -38,6 +40,14 @@ export class ExecutionDetailStep extends Component {
}
}
handleSavePreset = (values) => {
const { appState, dispatch, processStep, recipeId } = this.props;
const { authToken, tokenType, client, expiry, uid } = appState.session;
const signedIn = (authToken != null);
dispatch(recipeActions.createPreset(recipeId, processStep, values, signedIn, authToken, tokenType, client, expiry, uid));
}
listExpandCollapseToggle(processStep) {
return(
<a href="#" onClick={e => this.handleToggle(e, processStep.file_list_collapse, processStep.id, processStep.process_chain_id, this.props.recipeId)}>
......@@ -57,10 +67,10 @@ export class ExecutionDetailStep extends Component {
successClass(execution_errors, successful) {
if(successful === true) {
return("success");
return("success success-background");
}
else if(!_.isNil(execution_errors) && !_.isEmpty(execution_errors)) {
return("fail");
return("fail fail-background");
}
return("");
}
......@@ -201,9 +211,9 @@ export class ExecutionDetailStep extends Component {
outputRowClasses(processStep) {
if(_.isNil(processStep.finished_at) && _.isNil(processStep.output_file_manifest)) {
return("cell centered");
return("step-execution-result-item centered");
}
return("cell");
return("step-execution-result-item");
}
renderVersion(version) {
......@@ -229,16 +239,12 @@ export class ExecutionDetailStep extends Component {
);
}
return(
<table className="table">
<div>
<thead><tr><th>key</th><th>value</th></tr></thead>
<tbody>
{executionParameters && Object.keys(executionParameters).map(function(key, idx) {
return <tr key={idx}><td>{key}</td><td>{executionParameters[key]}</td></tr>;
}.bind(this))}
</tbody>
</div>
</table>
<div className="step-execution-result-parameters">
{executionParameters && Object.keys(executionParameters).map(function(key, idx) {
return <div className="param-row" key={idx}><div className="param-key"><span className="fa fa-key"/> {key}</div><div className="param-value">{executionParameters[key]}</div></div>;
}.bind(this))}
<SavePresetForm onSubmit={this.handleSavePreset} processStep={this.props.processStep}/>
</div>
);
}
......@@ -260,17 +266,17 @@ export class ExecutionDetailStep extends Component {
const { processStep } = this.props;
return(
<tr>
<td className={`cell centered ${this.successClass(processStep.execution_errors, processStep.successful)}`}>{this.renderIcon(processStep)}</td>
<td className="cell centered">
<div className="step-execution-result-container">
<div className={`step-execution-result-item result-icon centered ${this.successClass(processStep.execution_errors, processStep.successful)}`}>{this.renderIcon(processStep)}</div>
<div className="step-execution-result-item">
{this.renderStepShorthand(processStep.step_class_name)}
{this.renderVersion(processStep.version)}
{this.renderExecutionParameters(processStep.execution_parameters)}
</td>
<td className={this.outputRowClasses(processStep)}>
</div>
<div className={this.outputRowClasses(processStep)}>
{ this.renderOutput(processStep) }
</td>
</tr>
</div>
</div>
);
}
}
......
import React, { Component, PropTypes } from 'react';
import ReactDOM from 'react-dom';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
// import { Field, reduxForm } from 'redux-form'
import { bindActionCreators, compose } from 'redux';
import { reduxForm, Field, Form, formValueSelector } from 'redux-form';
import Dropzone from 'react-dropzone';
import * as actions from '../actions/recipeActions';
import _ from 'lodash';
import ParameterSummary from './execution/ParameterSummary.js';
import * as viewConstants from '../constants/Views';
export class ExecutionFileForm extends Component {
const FILE_FIELD_NAME = 'input_files';
handleExecution = (e) => {
e.preventDefault();
const renderUploadFileList = (files) => {
if(files && Array.isArray(files)) {
return(
<ul className="dropzone-file-list">
{ files.map((file, i) =>
<li key={i} className="dropzone-file-list-item">
<span className="fa fa-file-o"/> {file.name}
</li>
)}
</ul>
);
} else {
return(<div className="left-indent small-info"><span className="fa fa-hand-o-left"/> Please select some</div>);
}
}
const renderDropzoneInput = (field) => {
const files = field.input.value;
return (
<div className="dropzone-container">
<div className="dropzone-widget">
<Dropzone
name={field.name}
onDrop={( filesToUpload, e ) => field.input.onChange(filesToUpload)}
>
<div className="dropzone">Drop files here, or click to select files.</div>
<div className="dropzone-icon-container">
<span className="dropzone-icon fa fa-files-o"/>
</div>
</Dropzone>
{field.meta.touched &&
field.meta.error &&
<span className="error">{field.meta.error}</span>}
</div>
<div className="dropzone-file-list">
<div className="dropzone-file-list-title left-indent"><span className="fa fa-files-o"/> Files to upload ({files.length})</div>
{ renderUploadFileList(files) }
</div>
</div>
);
};
class ExecutionFileForm extends Component {
onSubmit(data) {
const { dispatch, appState } = this.props;
const input_files = ReactDOM.findDOMNode(this.refs.input_files).files;
const form_data = new FormData();
for (let file of input_files) {
form_data.append('input_files[]', file);
let formData = new FormData();
formData.append('execution_parameters', JSON.stringify(this.compileResults()));
_.map(data.input_files, function(file) {
formData.append('input_files[]', file);
});
const { signedIn, authToken, tokenType, client, expiry, uid } = appState.session;
dispatch(actions.executeRecipe(this.props.recipe.id, formData, signedIn, authToken, tokenType, client, expiry, uid));
}
compileResults() {
let { state, recipe } = this.props;
let compiledResults = {};
let thisContext = this;
_.map(recipe.recipe_steps, function(step) {
let params = {};
params.data = thisContext.assembleParameters(step, state);
compiledResults[step.position] = params;
});
console.log("Parameters:", compiledResults)
return compiledResults;
// execution_parameters: { "1" => {data: { "animal" => "honey badger", "abc" => "2.5" }} },
}
assembleParameters(step, state) {
let position = step.position;
let newSet = {};
if(step.parameterView == viewConstants.SHOW_PARAMETER_FORM) {
// if param view is showing
const selector = formValueSelector(`executionParameterForm_${position}`);
let params = selector(state, 'parameters');
let whatever = this;
_.forEach(params, function(paramSet) {
newSet[paramSet.key] = paramSet.value;
});
} else if(step.parameterView == viewConstants.SHOW_PRESET_VIEW) {
// if preset view is showingPresetView
if(!_.isEmpty(step.selectedPreset)) {
return step.selectedPreset.execution_parameters;
}
}
return newSet;
}
const { signedIn, authToken, tokenType, client, expiry, uid } = appState.session;
dispatch(actions.executeRecipe(this.props.recipe.id, form_data, signedIn, authToken, tokenType, client, expiry, uid));
ReactDOM.findDOMNode(this.refs.fileUploadForm).reset();
executionButton(inProgress) {
if(inProgress) {
return(
<button type="submit" className="file-button" disabled>
Starting... <span className="fa fa-gear fa-spin fa-fw" />
</button>
);
}
else {
return(
<button type="submit" className="file-button">
GO!
</button>
);
}
}
render() {
let { recipe } = this.props;
return(
<form ref="fileUploadForm">
<div className="choose-file-container">
<input className="file-picker" encType="multipart/form-data" multiple type="file" name="input_files[]" label="Input File" ref="input_files" id="input_files" />
<br/>
<button className="file-button" onClick={this.handleExecution}>GO!</button>
const {
handleSubmit,
reset,
} = this.props;
return (
<Form onSubmit={handleSubmit(this.onSubmit.bind(this))}>
<div className="execution-action-container">
<div>
<Field
name={FILE_FIELD_NAME}
component={renderDropzoneInput}
/>
</div>
<div className="parameter-summary-container">
<div className="parameter-summary-title">
<span className="fa fa-key"/> Execution Parameters
</div>
<ParameterSummary recipe={this.props.recipe} />
</div>
<div className="padding-top-1rem">
{ this.executionButton(this.props.appState.executionPlaceholderCount >= 1) }
</div>
</div>
</form>
</Form>
);
}
}
ExecutionFileForm.propTypes = {
handleSubmit: PropTypes.func.isRequired,
reset: PropTypes.func.isRequired,
recipe: PropTypes.object.isRequired,
dispatch: PropTypes.func.isRequired,
appState: PropTypes.object.isRequired
......@@ -46,7 +158,8 @@ ExecutionFileForm.propTypes = {
function mapStateToProps(state) {
return {
appState: state.appState
appState: state.appState,
state: state
};
}
......@@ -57,7 +170,7 @@ function mapDispatchToProps(dispatch) {
};
}
export default connect(
mapStateToProps,
mapDispatchToProps
export default compose(
connect(mapStateToProps, mapDispatchToProps),
reduxForm({form: "executeRecipe"})
)(ExecutionFileForm);
import React, {PropTypes} from 'react';
import _ from 'lodash';
function isRecent(startedAt) {
let time = _.now() - new Date(startedAt).valueOf()
return(time < 86400000);
}
export function processChainsInProgress(recipe) {
let isProcessing = false;
_.forEach(recipe.process_chains, function(chain) {
if(_.isNil(chain.finished_at) && isRecent(chain.created_at)) {
isProcessing = true;
}
});
return(isProcessing);
}
const ExecutionInProgressWidget = (props) => {
let inProgress = processChainsInProgress(props.recipe);
if (inProgress) {
return(
<span className="fa fa-gear fa-spin fa-fw"/>
);
}
else {
return(