Skip to content
This repository has been archived by the owner on Feb 26, 2023. It is now read-only.

Commit

Permalink
allow for editing a full job in JSON (raw)
Browse files Browse the repository at this point in the history
  • Loading branch information
jippi committed Dec 17, 2016
1 parent 7a60cec commit 852c5db
Show file tree
Hide file tree
Showing 9 changed files with 285 additions and 14 deletions.
1 change: 1 addition & 0 deletions backend/actions.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ const (
unwatchClusterStatistics = "UNWATCH_CLUSTER_STATISTICS"

changeTaskGroupCount = "CHANGE_TASK_GROUP_COUNT"
submitJob = "SUBMIT_JOB"

errorNotification = "ERROR_NOTIFICATION"
successNotification = "SUCCESS_NOTIFICATION"
Expand Down
32 changes: 32 additions & 0 deletions backend/connection.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package main

import (
"encoding/json"
"fmt"
"io"
"math/rand"
Expand Down Expand Up @@ -240,6 +241,10 @@ func (c *Connection) process(action Action) {
// Change task group count
case changeTaskGroupCount:
go c.changeTaskGroupCount(action)

// Submit (create or update) a job
case submitJob:
go c.submitJob(action)
}
}

Expand Down Expand Up @@ -836,3 +841,30 @@ func (c *Connection) changeTaskGroupCount(action Action) {
logger.Info(updateAction.Payload)
c.send <- updateAction
}

func (c *Connection) submitJob(action Action) {
r := rand.New(rand.NewSource(time.Now().UnixNano()))
index := uint64(r.Int())

if *flagReadOnly == true {
logger.Errorf("Unable to submit job: READONLY is set to true")
c.send <- &Action{Type: errorNotification, Payload: "The backend server is in read-only mode", Index: index}
return
}

jobjson := action.Payload.(string)
runjob := api.Job{}
json.Unmarshal([]byte(jobjson), &runjob)

logger.Infof("Started submission of job with id: %s", runjob.ID)

_, _, err := c.hub.nomad.Client.Jobs().Register(&runjob, nil)
if err != nil {
logger.Errorf("connection: unable to submit job '%s' : %s", runjob.ID, err)
c.send <- &Action{Type: errorNotification, Payload: fmt.Sprintf("Unable to submit job : %s", err), Index: index}
return
}

logger.Infof("connection: successfully submit job '%s'", runjob.ID)
c.send <- &Action{Type: successNotification, Payload: "The job has been successfully updated.", Index: index}
}
1 change: 1 addition & 0 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@
"moment-duration-format": "^1.3.0",
"node-uuid": "^1.4.7",
"react": "^15.4.1",
"react-ace": "^4.1.0",
"react-addons-css-transition-group": "^15.4.1",
"react-addons-perf": "^15.4.1",
"react-addons-transition-group": "^15.4.1",
Expand Down
181 changes: 181 additions & 0 deletions frontend/src/components/JobEditRawJSON/JobEditRawJSON.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
import React from 'react'
import Dialog from 'material-ui/Dialog';
import FlatButton from 'material-ui/FlatButton';
import FontIcon from 'material-ui/FontIcon'
import RaisedButton from 'material-ui/RaisedButton';
import { connect } from 'react-redux'
import AceEditor from 'react-ace';
import 'brace/mode/json'
import 'brace/theme/github'
import { SUBMIT_JOB } from '../../sagas/event'

class JobEditRawJSON extends React.Component {

modifiedJob = undefined

constructor(props) {
super(props);

this.state = { open: false }
}

handleOpen = () => {
this.modifiedJob = JSON.stringify(this.props.job, null, 2)

this.setState({
open: true,
job: this.props.job,
submittingJob: false,
jobOutOfSync: false,
readOnlyEditor: false,
});
}

/**
* On every key stroke, we save the changed output
*
* @param {string} value
* @return {void}
*/
onEditorChange = (value) => {
this.modifiedJob = value
}

/**
* On Cancel, we simply just hide the dialog
*
* All state will be reset in `handleOpen`
*
* @return {void}
*/
handleCancel = () => {
this.setState({
...this.state,
open: false
});
};

/**
* When submitting the job by clicking 'submit'
*
* @return {void}
*/
handleSubmit = () => {
this.setState({
...this.state,
submittingJob: true,
readOnlyEditor: true
})

this.props.dispatch({
type: SUBMIT_JOB,
payload: this.modifiedJob
})
};

componentWillReceiveProps = (nextProps) => {
// if we got no job prop, ignore the props
if (!nextProps.job.ID) {
return;
}

// if we get props while submitting a job
if (this.state.submittingJob) {
// on success, close the dialog
if (nextProps.successNotification.index) {
this.setState({ open: false })
return;
}

// on error, make the form editable again
if (nextProps.errorNotification.index) {
this.setState({
...this.state,
submittingJob: false,
readOnlyEditor: false,
})
return;
}
}

// if we got no job state, don't bother with JobModifyIndex check
if (!this.state.job) {
return;
}

// the current job state and the new job prop JobModifyIndex is different, our editor is stale
if (this.state.job.JobModifyIndex != nextProps.job.JobModifyIndex) {
this.setState({
...this.state,
jobOutOfSync: true,
readOnlyEditor: true,
})
}
}

render() {
const actions = [
<FlatButton
label='Cancel'
primary
onTouchTap={ this.handleCancel }
/>,
<FlatButton
label='Submit job'
primary
disabled={ this.state.jobOutOfSync || this.state.submittingJob }
onTouchTap={ this.handleSubmit }
/>,
];

let title = `Edit job: ${this.props.job.ID}`
let titleStyle = {};
if (this.state.jobOutOfSync && !this.state.submittingJob) {
title = title + ' - JOB WAS CHANGED SINCE YOU LOADED IT';
titleStyle = { color: 'red' }
}

return (
<div>
<RaisedButton
label='Edit Job'
onTouchTap={ this.handleOpen }
icon={ <FontIcon className='material-icons'>edit</FontIcon> }
/>
<Dialog
title={ title }
titleStyle={ titleStyle }
actions={ actions }
modal
open={ this.state.open }
bodyStyle={{ padding: 0 }}
>
<AceEditor
mode='json'
theme='github'
name='edit-job-json'
value={ this.modifiedJob }
readOnly={ this.state.readOnlyEditor }
width='100%'
height={ 380 }
tabSize={ 2 }
onChange={ this.onEditorChange }
wrapEnabled
focus
/>
</Dialog>
</div>
);
}
}

function mapStateToProps ({ job, errorNotification, successNotification }) {
return { job, errorNotification, successNotification }
}

JobEditRawJSON.propTypes = {
dispatch: React.PropTypes.func.isRequired,
job: React.PropTypes.object.isRequired,
}

export default connect(mapStateToProps)(JobEditRawJSON)
15 changes: 11 additions & 4 deletions frontend/src/components/NotificationsBar/NotificationsBar.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import React, { Component, PropTypes } from 'react'
import { connect } from 'react-redux'
import Snackbar from 'material-ui/Snackbar'
import { green500, red500 } from 'material-ui/styles/colors'
import { CLEAR_ERROR_NOTIFICATION, CLEAR_SUCCESS_NOTIFICATION } from '../../sagas/event'

class NotificationsBar extends Component {

Expand Down Expand Up @@ -36,6 +37,8 @@ class NotificationsBar extends Component {
}

resetSuccessMessage() {
this.props.dispatch({ type: CLEAR_SUCCESS_NOTIFICATION });

this.setState({
...this.state,
showSuccessMessage: false,
Expand All @@ -44,6 +47,8 @@ class NotificationsBar extends Component {
}

resetErrorMessage() {
this.props.dispatch({ type: CLEAR_ERROR_NOTIFICATION });

this.setState({
...this.state,
showErrorMessage: false,
Expand All @@ -57,16 +62,18 @@ class NotificationsBar extends Component {
<Snackbar
open={ this.state.showErrorMessage }
message={ this.state.errorMessage }
autoHideDuration={ 3000 }
bodyStyle={{ backgroundColor: red500 }}
autoHideDuration={ 500000 }
style={{ width: '100%', textAlign: 'center' }}
bodyStyle={{ backgroundColor: red500, width: '100%', maxWidth: 'none' }}
onRequestClose={ () => { this.resetErrorMessage() } }
/>

<Snackbar
open={ this.state.showSuccessMessage }
message={ this.state.successMessage }
autoHideDuration={ 3000 }
bodyStyle={{ backgroundColor: green500 }}
autoHideDuration={ 500000 }
style={{ width: '100%', textAlign: 'center' }}
bodyStyle={{ backgroundColor: green500, width: '100%', maxWidth: 'none' }}
onRequestClose={ () => { this.resetSuccessMessage() } }
/>
</div>
Expand Down
10 changes: 9 additions & 1 deletion frontend/src/containers/job.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React, { Component, PropTypes } from 'react'
import { connect } from 'react-redux'
import JobTopbar from '../components/JobTopbar/JobTopbar'
import JobEditRawJSON from '../components/JobEditRawJSON/JobEditRawJSON'
import { WATCH_JOB, UNWATCH_JOB } from '../sagas/event'

class Job extends Component {
Expand All @@ -23,8 +24,15 @@ class Job extends Component {
<JobTopbar { ...this.props } />

<div style={{ padding: 10, paddingBottom: 0 }}>
<h2>Job: { this.props.job.Name }</h2>
<div style={{ float: 'left' }}>
<h2>Job: { this.props.job.Name }</h2>
</div>

<div style={{ float: 'right' }}>
<JobEditRawJSON { ...this.props } />
</div>

<br />
<br />

{ this.props.children }
Expand Down
25 changes: 22 additions & 3 deletions frontend/src/reducers/app.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
import { APP_ERROR, ERROR_NOTIFICATION, SUCCESS_NOTIFICATION, FETCHED_CLUSTER_STATISTICS } from '../sagas/event'
import {
APP_ERROR,
ERROR_NOTIFICATION, CLEAR_ERROR_NOTIFICATION,
SUCCESS_NOTIFICATION, CLEAR_SUCCESS_NOTIFICATION,
FETCHED_CLUSTER_STATISTICS
} from '../sagas/event'

export function ClusterStatisticsReducer (state = {}, action) {
switch (action.type) {
Expand All @@ -11,16 +16,30 @@ export function ClusterStatisticsReducer (state = {}, action) {

export function ErrorNotificationReducer (state = {}, action) {
switch (action.type) {
case CLEAR_ERROR_NOTIFICATION:
return {

}
case ERROR_NOTIFICATION:
return { message: action.payload, index: action.index }
return {
message: action.payload,
index: action.index
}
}
return state
}

export function SuccessNotificationReducer (state = {}, action) {
switch (action.type) {
case CLEAR_SUCCESS_NOTIFICATION:
return {

}
case SUCCESS_NOTIFICATION:
return { message: action.payload, index: action.index }
return {
message: action.payload,
index: action.index
}
}
return state
}
Expand Down
Loading

0 comments on commit 852c5db

Please sign in to comment.