Commit 8ef79dd0 authored by charlie-ablett's avatar charlie-ablett

Slanger event subscriptions

parent a55ce476
...@@ -27,6 +27,7 @@ ...@@ -27,6 +27,7 @@
"lodash": "4.17.2", "lodash": "4.17.2",
"moment": "2.17.0", "moment": "2.17.0",
"object-assign": "4.1.0", "object-assign": "4.1.0",
"pusher-js": "4.0.0",
"react": "15.4.1", "react": "15.4.1",
"react-dom": "15.4.1", "react-dom": "15.4.1",
"react-redux": "4.4.6", "react-redux": "4.4.6",
......
import * as actions from '../constants/ActionTypes';
import { checkStatus, setAlert } from './actions_helper.js';
import { browserHistory } from 'react-router';
import * as AlertTypes from '../constants/AlertTypes';
import _ from 'lodash';
import settings from '../../settings';
export function getAllUsers(appState) {
const { authToken, tokenType, client, expiry, uid } = appState.session;
const signedIn = (authToken != null);
return(getAllUsersWithTokens(signedIn, authToken, tokenType, client, expiry, uid));
}
export function getAllUsersWithTokens(signedIn, authToken, tokenType, client, expiry, uid) {
return function(dispatch) {
let theResponse;
dispatch({type: actions.GET_ALL_USERS_REQUEST});
fetch(`${settings.apiBaseUrl}/api/admin/users`, {
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.users)) {
let error = new Error(theResponse.statusText);
error.response = json.errors;
throw error;
}
else {
dispatch(successGetAllUsers(json));
}
})
.catch(error => {
dispatch(setAlert(error.response, AlertTypes.ERROR));
dispatch(rejectGetAllUsers(error));
});
return null;
};
}
export function successGetAllUsers(json) {
return {
type: actions.GET_ALL_USERSS_SUCCESS,
data: json
};
}
export function rejectGetAllUsers(error) {
return {
type: actions.GET_ALL_USERS_FAILURE,
error: error.toString()
};
}
import * from './authenticationActions.js'; import * from './authenticationActions.js';
import * from './recipeActions.js'; import * from './recipeActions.js';
import * from './fileActions.js'; import * from './fileActions.js';
import * from './adminActions.js';
...@@ -229,9 +229,39 @@ export function deselectRecipe() { ...@@ -229,9 +229,39 @@ export function deselectRecipe() {
export function resetPlaceholders() { export function resetPlaceholders() {
return { return {
type: actions.RESET_PLACEHOLDERS type: actions.RESET_PLACEHOLDERS
} };
} }
///////////////////////////////////////////////////////////////
/////////////////////// mark chain state //////////////////////
///////////////////////////////////////////////////////////////
//
// export function markAsStarted(recipeId, chainId) {
// console.log("marking chain id as started", chainId)
// return {
// type: actions.MARK_CHAIN_AS_STARTED,
// chainId: chainId,
// recipeId: recipeId
// };
// }
//
// export function markAsCompleted(recipeId, chainId) {
// console.log("marking chain id as completed", chainId)
// return {
// type: actions.MARK_CHAIN_AS_COMPLETED,
// chainId: chainId,
// recipeId: recipeId
// };
// }
//
// export function markAsErrored(recipeId, chainId) {
// return {
// type: actions.MARK_CHAIN_AS_ERRORED,
// chainId: chainId,
// recipeId: recipeId
// };
// }
/////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////
///////////////// new recipe - step class list //////////////// ///////////////// new recipe - step class list ////////////////
/////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////
......
...@@ -3,26 +3,68 @@ import { connect } from 'react-redux'; ...@@ -3,26 +3,68 @@ import { connect } from 'react-redux';
import { bindActionCreators } from 'redux'; import { bindActionCreators } from 'redux';
import _ from 'lodash'; import _ from 'lodash';
import * as actions from '../actions/authenticationActions'; import * as actions from '../constants/PusherActions';
import ExecutionDetailStep from './ExecutionDetailStep'; import ExecutionDetailStep from './ExecutionDetailStep';
import ExecutionFileList from './ExecutionFileList'; import ExecutionFileList from './ExecutionFileList';
import TimeAgo from 'react-timeago'; import TimeAgo from 'react-timeago';
import { subscribe, unsubscribe } from 'pusher-redux';
export class ExecutionDetail extends Component { export class ExecutionDetail extends Component {
constructor(props, context) {
super(props, context);
this.subscribe = this.subscribe.bind(this);
this.unsubscribe = this.unsubscribe.bind(this);
}
componentWillMount() {
this.subscribe();
}
componentWillUnmount() {
this.unsubscribe();
}
subscribe() {
// let chainId = this.props.processChain.id;
// let channel = this.processChainChannel(chainId);
let channel = "process_chain_execution";
subscribe(channel, 'processing_started', actions.MARK_CHAIN_AS_STARTED);
subscribe(channel, 'processing_completed', actions.MARK_CHAIN_AS_COMPLETED);
subscribe(channel, 'processing_error', actions.MARK_CHAIN_AS_ERRORED);
subscribe(channel, 'process_step_started', actions.MARK_STEP_AS_STARTED);
subscribe(channel, 'process_step_completed', actions.MARK_STEP_AS_COMPLETED);
}
unsubscribe() {
// let chainId = this.props.processChain.id;
// let channel = this.processChainChannel(chainId);
let channel = "process_chain_execution";
unsubscribe(channel, 'processing_started', actions.MARK_CHAIN_AS_STARTED);
unsubscribe(channel, 'processing_completed', actions.MARK_CHAIN_AS_COMPLETED);
unsubscribe(channel, 'processing_error', actions.MARK_CHAIN_AS_ERRORED);
unsubscribe(channel, 'process_step_started', actions.MARK_STEP_AS_STARTED);
unsubscribe(channel, 'process_step_completed', actions.MARK_STEP_AS_COMPLETED);
}
renderFinishedTitle() { renderFinishedTitle() {
let { processChain } = this.props; let { processChain } = this.props;
// const success_or_failure = processChain.finished_at ? "successfully" : "with errors";
return(<span>Finished processing <TimeAgo date={processChain.finished_at} /> </span>); return(<span>Finished processing <TimeAgo date={processChain.finished_at} /> </span>);
} }
processChainChannel(processChainId) {
return(`process_chain_${processChainId}`);
}
renderInProgressTitle() { renderInProgressTitle() {
let { processChain } = this.props; let { processChain } = this.props;
if(_.isNil(processChain.started_at)) { if(_.isNil(processChain.executed_at)) {
return(<span>Waiting to be processed (submitted <TimeAgo date={processChain.executed_at} />)</span>); return(<span>Waiting to be processed (submitted <TimeAgo date={processChain.created_at} />) {processChain.executed_at}</span>);
} }
return(<span>Started processing (started <TimeAgo date={processChain.started_at} />)</span>); return(<span>Processing (started <TimeAgo date={processChain.executed_at} />)</span>);
} }
renderLinkText(file_name) { renderLinkText(file_name) {
......
import React, { Component, PropTypes } from 'react'; import React, { Component, PropTypes } from 'react';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import _ from "lodash"; import _ from "lodash";
import * as actions from '../actions/authenticationActions'; import * as actions from '../actions/authenticationActions';
...@@ -28,20 +30,23 @@ export class ExecutionDetailStep extends Component { ...@@ -28,20 +30,23 @@ export class ExecutionDetailStep extends Component {
return(""); return("");
} }
renderIcon(started_at, execution_errors, successful) { renderIcon(processStep) {
if(_.isNil(started_at)) { if(processStep.in_progress === true) {
return(<span className="fa fa-gear fa-spin fa-2x fa-fw step-processing-centered" />);
}
else if(_.isNil(processStep.started_at)) {
return(<span className="small-info-text fa fa-question"/>); return(<span className="small-info-text fa fa-question"/>);
} }
else if(successful === true) { else if(processStep.successful === true) {
return(<span className="fa fa-check"/>); return(<span className="fa fa-check"/>);
} }
else if(!_.isNil(execution_errors) && !_.isEmpty(execution_errors)) { else if(!_.isNil(processStep.execution_errors) && !_.isEmpty(processStep.execution_errors)) {
return(<span className="fa fa-ban"/>); return(<span className="fa fa-times"/>);
} }
} }
renderLink(filePath, processStepId, finished_at) { renderLink(filePath, processStepId, finished_at) {
if(!_.isNil(filePath) && !_.isEmpty(filePath) && !_.isUndefined(finished_at)) { if(!_.isNil(filePath)) {
return( return(
<a href="#" onClick={e => this.handleOutputFileDownload(e, filePath, processStepId)}>{filePath} <span className="fa fa-download"/></a> <a href="#" onClick={e => this.handleOutputFileDownload(e, filePath, processStepId)}>{filePath} <span className="fa fa-download"/></a>
); );
...@@ -52,8 +57,8 @@ export class ExecutionDetailStep extends Component { ...@@ -52,8 +57,8 @@ export class ExecutionDetailStep extends Component {
} }
renderZipLink(processStep) { renderZipLink(processStep) {
if(_.isEmpty(processStep.finished_at)) { if(_.isNil(processStep.finished_at)) {
return(<span className="small-info-text">no files available <span className="fa fa-download"/></span>); return(<span className="small-info-text">no files available yet <span className="fa fa-download"/></span>);
} }
return( return(
...@@ -73,7 +78,14 @@ export class ExecutionDetailStep extends Component { ...@@ -73,7 +78,14 @@ export class ExecutionDetailStep extends Component {
} }
renderErrors(processStep) { renderErrors(processStep) {
let errors = processStep.execution_errors || <span className="small-info-text">none</span>; let errors = processStep.execution_errors;
if(_.isNil(processStep.finished_at)) {
errors = <span/>;
}
else if(_.isNil(errors) || _.isEmpty(errors)) {
errors = <span className="small-info-text">none</span>;
}
return( return(
<div>Errors: <span className="fail">{ errors }</span></div> <div>Errors: <span className="fail">{ errors }</span></div>
...@@ -81,7 +93,14 @@ export class ExecutionDetailStep extends Component { ...@@ -81,7 +93,14 @@ export class ExecutionDetailStep extends Component {
} }
renderNotes(processStep) { renderNotes(processStep) {
let notes = processStep.notes || <span className="small-info-text">none</span>; let notes = processStep.notes;
if(_.isNil(processStep.finished_at)) {
notes = <span/>;
}
else if(_.isNil(notes) || _.isEmpty(notes)) {
notes = <span className="small-info-text">none</span>;
}
return( return(
<div>Notes: { notes }</div> <div>Notes: { notes }</div>
...@@ -124,7 +143,12 @@ export class ExecutionDetailStep extends Component { ...@@ -124,7 +143,12 @@ export class ExecutionDetailStep extends Component {
} }
renderStepShorthand(stepName) { renderStepShorthand(stepName) {
return(<div><span>{this.stepShorthand(stepName)}</span></div>); let { processStep } = this.props;
let className = "";
if(processStep.in_progress === true) {
className = "bold";
}
return(<div><span className={className}>{this.stepShorthand(stepName)}</span></div>);
} }
render() { render() {
...@@ -132,7 +156,7 @@ export class ExecutionDetailStep extends Component { ...@@ -132,7 +156,7 @@ export class ExecutionDetailStep extends Component {
return( return(
<tr> <tr>
<td className={`cell centered ${this.successClass(processStep.execution_errors, processStep.successful)}`}>{this.renderIcon(processStep.started_at, processStep.execution_errors, processStep.successful)}</td> <td className={`cell centered ${this.successClass(processStep.execution_errors, processStep.successful)}`}>{this.renderIcon(processStep)}</td>
<td className="cell centered"> <td className="cell centered">
{this.renderStepShorthand(processStep.step_class_name)} {this.renderStepShorthand(processStep.step_class_name)}
{this.renderVersion(processStep.version)} {this.renderVersion(processStep.version)}
...@@ -144,8 +168,23 @@ export class ExecutionDetailStep extends Component { ...@@ -144,8 +168,23 @@ export class ExecutionDetailStep extends Component {
} }
ExecutionDetailStep.propTypes = { ExecutionDetailStep.propTypes = {
dispatch: PropTypes.func,
processStep: PropTypes.object.isRequired, processStep: PropTypes.object.isRequired,
appState: PropTypes.object appState: PropTypes.object
}; };
export default ExecutionDetailStep; function mapStateToProps(state) {
return { appState: state.appState };
}
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators(actions, dispatch),
dispatch: dispatch
};
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(ExecutionDetailStep);
...@@ -38,12 +38,12 @@ export class ExecutionFileList extends Component { ...@@ -38,12 +38,12 @@ export class ExecutionFileList extends Component {
} }
else if(isOutput === true) { else if(isOutput === true) {
return( return(
<a href="#" onClick={e => this.handleChainOutputFileDownload(e, filePath, id)}>{filePath} <span className="fa fa-download"/></a> <a href={`#id=${id}`} onClick={e => this.handleChainOutputFileDownload(e, filePath, id)}>{filePath} <span className="fa fa-download"/></a>
); );
} }
else if(isOutput === false) { else if(isOutput === false) {
return( return(
<a href="#" onClick={e => this.handleInputFileDownload(e, filePath, id)}>{filePath} <span className="fa fa-download"/></a> <a href={`#id=${id}`} onClick={e => this.handleInputFileDownload(e, filePath, id)}>{filePath} <span className="fa fa-download"/></a>
); );
} }
else { else {
...@@ -54,12 +54,12 @@ export class ExecutionFileList extends Component { ...@@ -54,12 +54,12 @@ export class ExecutionFileList extends Component {
renderChainZipLink(isOutput, id) { renderChainZipLink(isOutput, id) {
if(isOutput === true) { if(isOutput === true) {
return( return(
<a href="#" onClick={e => this.handleChainOutputZipDownload(e, id)}>all files (zip) <span className="fa fa-download"/></a> <a href={`#id=${id}`} onClick={e => this.handleChainOutputZipDownload(e, id)}>all files (zip) <span className="fa fa-download"/></a>
); );
} }
else if(isOutput === false) { else if(isOutput === false) {
return( return(
<a href="#" onClick={e => this.handleInputZipDownload(e, id)}>all files (zip) <span className="fa fa-download"/></a> <a href={`#id=${id}`} onClick={e => this.handleInputZipDownload(e, id)}>all files (zip) <span className="fa fa-download"/></a>
); );
} }
else { else {
......
...@@ -12,8 +12,9 @@ const RecipesList = (props) => { ...@@ -12,8 +12,9 @@ const RecipesList = (props) => {
if(props.getRecipesInProgress === true) { if(props.getRecipesInProgress === true) {
return( return(
<div className="recipe-list-container text-center"> <div className="loading-centered">
<span>Retrieving recipes in progress...</span> <div><span className="fa fa-cog fa-spin fa-3x fa-fw" /></div>
<div><span className="small-info">loading...</span></div>
</div> </div>
); );
} }
......
...@@ -6,28 +6,43 @@ import SignOutForm from '../components/SignOutForm'; ...@@ -6,28 +6,43 @@ import SignOutForm from '../components/SignOutForm';
import _ from 'lodash'; import _ from 'lodash';
import { Link } from 'react-router'; import { Link } from 'react-router';
const UserHeader = (props) => { export class UserHeader extends Component {
let { appState } = props; renderAdminPanel(user) {
let { session } = appState; if(user.admin === true) {
return(
<Link to="/admin/dashboard" href="#">Admin panel</Link>
);
}
return;
}
render() {
let { appState } = this.props;
let { session } = appState;
if(_.isNull(session.user)) { if(_.isNull(session.user)) {
return ( return (
<span><span className="italic">Welcome, guest</span> | <Link to="/signin" href="#">Sign In</Link></span> <span><span className="italic">Welcome, guest</span> | <Link to="/signin" href="#">Sign In</Link></span>
); );
} else { } else {
return ( return (
<span className="italic"> <span>
Signed in as: <a href="#">{session.user.email}</a> { this.renderAdminPanel(session.user) }
{' '} <span className="italic">
<SignOutForm /> Signed in as: <a href="#">{session.user.email}</a>
</span> {' '}
); <SignOutForm />
</span>
</span>
);
}
} }
}; }
UserHeader.propTypes = { UserHeader.propTypes = {
appState: PropTypes.object.isRequired appState: PropTypes.object.isRequired,
user: PropTypes.object.isRequired
}; };
export default UserHeader; export default UserHeader;
import React, { Component, PropTypes } from 'react';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import { Link } from 'react-router';
import * as actions from '../../actions/adminActions';
import _ from 'lodash';
export class UserListItem extends Component {
render() {
let { user } = this.props;
return(
<Link to={`/admin/users/${user.id}`}>
<li className="user-item">
<div className="user-item__header user-item__unselected">
{user.name}
</div>
<div className="user-item__body">
{user.email}
</div>
</li>
</Link>
);
}
}
UserListItem.propTypes = {
user: PropTypes.object.isRequired,
isSelected: PropTypes.bool.isRequired
};
export default UserListItem;
import React, { PropTypes } from 'react';
import UserListItem from './UserListItem.js';
import _ from 'lodash';
import { Link } from 'react-router';
const UsersList = (props) => {
if(_.isEmpty(props.session)) {
return(
<p className="help-block disabled">Sign in as an admin to see users</p>
);
}
if(props.getUsersInProgress === true) {
return(
<div className="recipe-list-container text-center">
<span>Retrieving users in progress...</span>
</div>
);
}
return (
<div>
<ul className="user-list-container">
{ props.admin.users.map(user =>
<UserListItem key={user.id}
user={user} />)
}
</ul>
</div>
);
};
UsersList.propTypes = {
session: PropTypes.object,
getUsersInProgress: PropTypes.bool.isRequired,
users: PropTypes.array.isRequired
};
export default UsersList;
...@@ -54,3 +54,9 @@ export const DOWNLOAD_FILE_FAILURE = "DOWNLOAD_FILE_FAILURE"; ...@@ -54,3 +54,9 @@ export const DOWNLOAD_FILE_FAILURE = "DOWNLOAD_FILE_FAILURE";
export const AVAILABLE_STEP_LIST_REQUEST = "AVAILABLE_STEP_LIST_REQUEST"; export const AVAILABLE_STEP_LIST_REQUEST = "AVAILABLE_STEP_LIST_REQUEST";
export const AVAILABLE_STEP_LIST_SUCCESS = "AVAILABLE_STEP_LIST_SUCCESS"; export const AVAILABLE_STEP_LIST_SUCCESS = "AVAILABLE_STEP_LIST_SUCCESS";
export const AVAILABLE_STEP_LIST_FAILURE = "AVAILABLE_STEP_LIST_FAILURE"; export const AVAILABLE_STEP_LIST_FAILURE = "AVAILABLE_STEP_LIST_FAILURE";
/////////////////// admin //////////////////////
export const GET_ALL_USERS_REQUEST = "GET_ALL_USERS_REQUEST";
export const GET_ALL_USERS_SUCCESS = "GET_ALL_USERS_SUCCESS";
export const GET_ALL_USERS_FAILURE = "GET_ALL_USERS_FAILURE";
export const MARK_CHAIN_AS_STARTED = "MARK_CHAIN_AS_STARTED";
export const MARK_CHAIN_AS_COMPLETED = "MARK_CHAIN_AS_COMPLETED";
export const MARK_CHAIN_AS_ERRORED = "MARK_CHAIN_AS_ERRORED";
export const MARK_STEP_AS_STARTED = "MARK_STEP_AS_STARTED";
export const MARK_STEP_AS_COMPLETED = "MARK_STEP_AS_COMPLETED";
import React, { Component, PropTypes } from 'react';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import { browserHistory, Link } from 'react-router';
import _ from 'lodash';
import * as actions from '../../actions/adminActions.js';
import Header from '../../components/Header';
import UsersList from '../../components/admin/UsersList';
import AlertList from '../../components/AlertList';
import { setAlert } from '../../actions/actions_helper';
class AdminDashboardPage extends Component {
static propTypes = {
actions: PropTypes.object.isRequired,
appState: PropTypes.object.isRequired,
dispatch: PropTypes.func.isRequired
};
componentWillMount = () => {
this.checkAuth();
}
componentDidMount = () => {
const { appState, dispatch } = this.props;
this.checkAuth();
dispatch(actions.getAllUsers(appState));
}
checkAuth() {
const { appState, dispatch } = this.props;
const { session } = appState;
if(_.isNull(session.user)) {
dispatch(setAlert("Please sign in"));
browserHistory.push('/signin');
return;
// } else if(session.user.admin != true) {
// dispatch(setAlert("Authorised users only"));
// browserHistory.push('/');
// return;
}
}
pageContent(appState) {
if(_.isNil(appState.admin.users)) {
return(
<div className="loading-centered">
<div><span className="fa fa-cog fa-spin fa-3x fa-fw" /></div>
<div><span className="small-info">loading...</span></div>
</div>
);
}
else {
return(
<div>
<h1>Admin dashboard</h1>
<h4>accounts & users currently registered</h4>
<UsersList
users={appState.admin.users}
getUsersInProgress={appState.getUsersInProgress}
session={appState.session}
/>
</div>
);
}
}
render() {
const { appState } = this.props;
return (
<div>
<Header
appState={appState}
/>
<div className="content-container">
<div className="breadcrumb-container">
<Link to="/" className="breadcrumb">Home</Link>
<span className="breadcrumb-divider">&gt;&gt;</span>
<Link to="/admin/users" className="breadcrumb"><span className="fa fa-users-o"/> Users</Link>