import React, { useEffect} from 'react';
import { useInterval } from 'react-powerhooks';
import Grid from '@material-ui/core/Grid';
import Paper from '@material-ui/core/Paper';
import Tooltip from '@material-ui/core/Tooltip';
import Divider from '@material-ui/core/Divider';
import Button from '@material-ui/core/Button';
import TextField from '@material-ui/core/TextField';
import FormControl from '@material-ui/core/FormControl';
import IconButton from '@material-ui/core/IconButton';
import Menu from '@material-ui/core/Menu';
import MenuItem from '@material-ui/core/MenuItem';
import FormControlLabel from '@material-ui/core/FormControlLabel';
import Switch from '@material-ui/core/Switch';
import CircularProgress from '@material-ui/core/CircularProgress';
import CachedIcon from '@material-ui/icons/Cached';
import EditIcon from '@material-ui/icons/Edit';
import MoreVertIcon from '@material-ui/icons/MoreVert';
import PlayArrowIcon from '@material-ui/icons/PlayArrow';
import AddIcon from '@material-ui/icons/Add';
import PublishIcon from '@material-ui/icons/Publish';
//import JSONPretty from 'react-json-pretty';
//import JSONPrettyMon from 'react-json-pretty/dist/monikai'
import ReactJson from 'react-json-view'
import {Link} from 'react-router-dom';
import { useAlert } from "react-alert";
import Dialog from '@material-ui/core/Dialog';
import DialogTitle from '@material-ui/core/DialogTitle';
import DialogActions from '@material-ui/core/DialogActions';
import DialogContent from '@material-ui/core/DialogContent';
import CloudDownloadIcon from '@material-ui/icons/CloudDownload';
const inputColor = "#383B40"
const surfaceColor = "#27292D"
const Workflows = (props) => {
const { globalUrl, isLoggedIn, isLoaded, } = props;
document.title = "Shuffle - Workflows"
const alert = useAlert()
var upload = ""
const [file, setFile] = React.useState("");
const [workflows, setWorkflows] = React.useState([]);
const [selectedWorkflow, setSelectedWorkflow] = React.useState({});
const [selectedExecution, setSelectedExecution] = React.useState({});
const [workflowExecutions, setWorkflowExecutions] = React.useState([]);
const [firstrequest, setFirstrequest] = React.useState(true)
const [workflowDone, setWorkflowDone] = React.useState(false)
const [, setTrackingId] = React.useState("")
const [collapseJson, setCollapseJson] = React.useState(false)
const [field1, setField1] = React.useState("")
const [field2, setField2] = React.useState("")
const [downloadUrl, setDownloadUrl] = React.useState("https://github.com/frikky/shuffle-workflows")
const [loadWorkflowsModalOpen, setLoadWorkflowsModalOpen] = React.useState(false)
const [modalOpen, setModalOpen] = React.useState(false);
const [newWorkflowName, setNewWorkflowName] = React.useState("");
const [newWorkflowDescription, setNewWorkflowDescription] = React.useState("");
const [editingWorkflow, setEditingWorkflow] = React.useState({})
const { start, stop } = useInterval({
duration: 5000,
startImmediate: false,
callback: () => {
getWorkflowExecution(selectedWorkflow.id)
}
});
const getAvailableWorkflows = () => {
fetch(globalUrl+"/api/v1/workflows", {
method: 'GET',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
},
credentials: "include",
})
.then((response) => {
if (response.status !== 200) {
console.log("Status not 200 for workflows :O!")
return
}
return response.json()
})
.then((responseJson) => {
setSelectedExecution({})
setWorkflowExecutions([])
if (responseJson !== undefined) {
setWorkflows(responseJson)
setWorkflowDone(true)
} else {
if (isLoggedIn) {
alert.error("An error occurred while loading workflows")
} else {
window.location = "/login"
}
return
}
if (responseJson.length > 0){
setSelectedWorkflow(responseJson[0])
getWorkflowExecution(responseJson[0].id)
}
})
.catch(error => {
alert.error(error.toString())
});
}
useEffect(() => {
if (workflows.length <= 0 && firstrequest) {
setFirstrequest(false)
getAvailableWorkflows()
}
})
const viewStyle = {
color: "#ffffff",
width: "100%",
display: "flex",
maxWidth: "1768px",
margin: "auto",
maxHeight: "90vh",
}
const emptyWorkflowStyle = {
paddingTop: "200px",
width: "1024px",
margin: "auto",
}
const boxStyle = {
padding: "20px 20px 20px 20px",
width: "100%",
height: "250px",
color: "white",
backgroundColor: surfaceColor,
display: "flex",
flexDirection: "column",
}
const scrollStyle = {
marginTop: "10px",
overflow: "scroll",
height: "90%",
overflowX: "auto",
overflowY: "auto",
}
const paperAppStyle = {
minHeight: "100px",
maxHeight: "100px",
minWidth: "100%",
maxWidth: "100%",
marginTop: "5px",
color: "white",
backgroundColor: surfaceColor,
cursor: "pointer",
display: "flex",
}
const getWorkflowExecution = (id) => {
fetch(globalUrl+"/api/v1/workflows/"+id+"/executions", {
method: 'GET',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
},
credentials: "include",
})
.then((response) => {
if (response.status !== 200) {
console.log("Status not 200 for WORKFLOW EXECUTION :O!")
alert.error("Failed loading executions for current workflow")
}
return response.json()
})
.then((responseJson) => {
setWorkflowExecutions(responseJson)
if (responseJson.length > 0) {
setSelectedExecution(responseJson[0])
}
})
.catch(error => {
alert.error(error.toString())
});
}
const abortExecution = (workflowid, executionid) => {
alert.success("Aborting execution")
fetch(globalUrl+"/api/v1/workflows/"+workflowid+"/executions/"+executionid+"/abort", {
method: 'GET',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
},
credentials: "include",
})
.then((response) => {
if (response.status !== 200) {
console.log("Status not 200 for WORKFLOW EXECUTION :O!")
}
getWorkflowExecution(workflowid)
return response.json()
})
.catch(error => {
alert.error(error.toString())
});
}
const executeWorkflow = (id) => {
alert.show("Executing workflow "+id)
setTrackingId(id)
fetch(globalUrl+"/api/v1/workflows/"+id+"/execute", {
method: 'GET',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
},
credentials: "include",
})
.then((response) => {
if (response.status !== 200) {
console.log("Status not 200 for WORKFLOW EXECUTION :O!")
}
return response.json()
})
.then((responseJson) => {
if (!responseJson.success) {
alert.error(responseJson.reason)
}
})
.catch(error => {
alert.error(error.toString())
});
if (id === selectedWorkflow.id) {
sleep(2000).then(() => {
stop()
start()
})
}
}
function sleep (time) {
return new Promise((resolve) => setTimeout(resolve, time));
}
const exportWorkflow = (data) => {
console.log("export")
let dataStr = JSON.stringify(data)
let dataUri = 'data:application/json;charset=utf-8,'+ encodeURIComponent(dataStr);
let exportFileDefaultName = data.name+'.json';
let linkElement = document.createElement('a');
linkElement.setAttribute('href', dataUri);
linkElement.setAttribute('download', exportFileDefaultName);
linkElement.click();
}
const copyWorkflow = (data) => {
alert.success("Copying workflow "+data.name)
data.id = ""
data.name = data.name+"_copy"
fetch(globalUrl+"/api/v1/workflows", {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
},
body: JSON.stringify(data),
credentials: "include",
})
.then((response) => {
if (response.status !== 200) {
console.log("Status not 200 for workflows :O!")
return
}
return response.json()
})
.then((responseJson) => {
getAvailableWorkflows()
})
.catch(error => {
alert.error(error.toString())
});
}
const deleteWorkflow = (id) => {
alert.success("Deleted workflow "+id)
fetch(globalUrl+"/api/v1/workflows/"+id, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
},
credentials: "include",
})
.then((response) => {
if (response.status !== 200) {
console.log("Status not 200 for setting workflows :O!")
}
return response.json()
})
.then((responseJson) => {
getAvailableWorkflows()
})
.catch(error => {
alert.error(error.toString())
});
}
// dropdown with copy etc I guess
const WorkflowPaper = (props) => {
const { data } = props;
const [open, setOpen] = React.useState(false);
const [anchorEl, setAnchorEl] = React.useState(null);
var boxWidth = "2px"
if (selectedWorkflow.id === data.id) {
boxWidth = "4px"
}
var boxColor = "orange"
if (data.is_valid) {
boxColor = "green"
}
const menuClick = (event) => {
setOpen(!open)
setAnchorEl(event.currentTarget);
}
return (
{
}}>
{
if (selectedWorkflow.id !== data.id) {
setSelectedWorkflow(data)
getWorkflowExecution(data.id)
}
}}>
{data.name}
{
if (selectedWorkflow.id !== data.id) {
setSelectedWorkflow(data)
getWorkflowExecution(data.id)
}
}}>
{}}>
Edit
executeWorkflow(data.id)}>
)
}
const executionPaper = (data) => {
var boxWidth = "2px"
if (selectedExecution.execution_id === data.execution_id) {
boxWidth = "4px"
}
var boxColor = "orange"
if (data.status === "ABORTED" || data.status === "UNFINISHED" || data.status === "FAILURE"){
boxColor = "red"
} else if (data.status === "FINISHED") {
boxColor = "green"
}
var t = new Date(data.started_at*1000)
if (data.workflow.actions === null || data.workflow.actions === undefined ) {
return null
}
var actions = data.workflow.actions.length
if (data.results !== null) {
var results = data.results.length
}
return (
{
setSelectedExecution(data)
}}>
Status : {data.status}
Actions: {results}/{actions}
abortExecution(data.workflow_id, data.execution_id)}>Abort
Started: {t.toISOString()}
)
}
const dividerColor = "rgb(225, 228, 232)"
const resultPaperAppStyle = {
minHeight: "100px",
minWidth: "100%",
overflow: "hidden",
maxWidth: "100%",
marginTop: "5px",
color: "white",
backgroundColor: surfaceColor,
cursor: "pointer",
display: "flex",
}
function replaceAll(string, search, replace) {
return string.split(search).join(replace);
}
const resultsPaper = (data) => {
var boxWidth = "2px"
var boxColor = "orange"
if (data.status === "ABORTED" || data.status === "UNFINISHED" || data.status === "FAILURE"){
boxColor = "red"
} else if (data.status === "FINISHED" || data.status === "SUCCESS") {
boxColor = "green"
} else if (data.status === "SKIPPED" || data.status === "EXECUTING") {
boxColor = "yellow"
} else {
boxColor = "green"
}
var t = new Date(data.started_at*1000)
var jsonvalid = true
var showResult = data.result.trim()
showResult = replaceAll(showResult, " None", " \"None\"");
try {
const tmp = String(JSON.parse(showResult))
if (!tmp.includes("{") && !tmp.includes("[")) {
jsonvalid = false
}
} catch (e) {
jsonvalid = false
}
//console.log("VALID: ", jsonvalid)
if (jsonvalid) {
showResult =
} else {
// FIXME - have everything parsed as json, either just for frontend
// or in the backend
/*
const newdata = {"result": data.result}
showResult =
*/
}
return (
{}}>
Name : {data.action.label}
App: {data.action.app_name}, Version: {data.action.app_version}
Action: {data.action.name}, Environment: {data.action.environment}, Status: {data.status}
Started: {t.toISOString()}
{showResult}
)
}
const resultsHandler = Object.getOwnPropertyNames(selectedExecution).length > 0 && selectedExecution.results !== null ?
{selectedExecution.results.sort((a, b) => a.started_at - b.started_at).map(data => {
return (
resultsPaper(data)
)
})}
:
No results yet
const resultsLength = Object.getOwnPropertyNames(selectedExecution).length > 0 && selectedExecution.results !== null ? selectedExecution.results.length : 0
const ExecutionDetails = () => {
var starttime = new Date(selectedExecution.started_at*1000)
var endtime = new Date(selectedExecution.started_at*1000)
var parsedArgument = selectedExecution.execution_argument
if (selectedExecution.execution_argument !== undefined && selectedExecution.execution_argument.length > 0) {
parsedArgument = replaceAll(parsedArgument, " None", " \"None\"");
}
var arg = null
if (selectedExecution.execution_argument !== undefined && selectedExecution.execution_argument.length > 0) {
var jsonvalid = true
var showResult = selectedExecution.execution_argument.trim()
showResult = replaceAll(showResult, " None", " \"None\"");
try {
const tmp = String(JSON.parse(showResult))
if (!tmp.includes("{") && !tmp.includes("[")) {
jsonvalid = false
}
} catch (e) {
jsonvalid = false
}
arg = jsonvalid ?
: showResult
}
var lastresult = null
if (selectedExecution.result !== undefined && selectedExecution.result.length > 0) {
var jsonvalid = true
var showResult = selectedExecution.result.trim()
showResult = replaceAll(showResult, " None", " \"None\"");
try {
const tmp = JSON.parse(showResult)
if (!tmp.includes("{") && !tmp.includes("[")) {
jsonvalid = false
}
} catch (e) {
jsonvalid = false
}
lastresult = jsonvalid ?
: showResult
}
/*
ID: {selectedExecution.execution_id}
Last node: {selectedExecution.workflow.actions.find(data => data.id === selectedExecution.last_node).actions[0].label}
*/
if (Object.getOwnPropertyNames(selectedExecution).length > 0 && selectedExecution.workflow.actions !== null) {
return (
Status: {selectedExecution.status}
Started: {starttime.toISOString()}
Finished: {endtime.toISOString()}
{/*
Last Result: {lastresult}
*/}
{arg}
{resultsHandler}
)
}
return (
There are no executiondetails yet. Click "execute" to run your first one.
)
}
const ExecutionsView = () => {
if (workflowExecutions.length > 0) {
const sortedWorkflows = workflowExecutions.sort((a, b) => a.started_at - b.started_at).reverse()
return (
{sortedWorkflows.map(data => {
return (
executionPaper(data)
)
})}
)
}
return (
There are no executions for this workflow yet
)
}
// Can create and set workflows
const setNewWorkflow = (name, description, editingWorkflow, redirect) => {
var method = "POST"
var extraData = ""
var workflowdata = {}
if (editingWorkflow.id !== undefined) {
method = "PUT"
extraData = "/"+editingWorkflow.id
workflowdata = editingWorkflow
}
workflowdata["name"] = name
workflowdata["description"] = description
//console.log(workflowdata)
//return
return fetch(globalUrl+"/api/v1/workflows"+extraData, {
method: method,
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
},
body: JSON.stringify(workflowdata),
credentials: "include",
})
.then((response) => {
if (response.status !== 200) {
console.log("Status not 200 for workflows :O!")
return
}
return response.json()
})
.then((responseJson) => {
if (method === "POST" && redirect) {
window.location.pathname = "/workflows/"+responseJson["id"]
} else if (!redirect) {
// Update :)
getAvailableWorkflows()
} else {
alert.info("Successfully changed basic info for workflow")
}
return responseJson
})
.catch(error => {
alert.error(error.toString())
});
}
const importFiles = (event) => {
const file = event.target.value
if (event.target.files.length > 0) {
for (var key in event.target.files) {
const file = event.target.files[key]
if (file.type !== "application/json") {
if (file.type !== undefined) {
alert.error("File has to contain valid json")
}
continue
}
const reader = new FileReader()
// Waits for the read
reader.addEventListener('load', (event) => {
var data = reader.result
try {
data = JSON.parse(reader.result)
} catch (e) {
alert.error("Invalid JSON: "+e)
return
}
// Initialize the workflow itself
const ret = setNewWorkflow(data.name, data.description, {}, false)
.then((response) => {
if (response !== undefined) {
// SET THE FULL THING
data.id = response.id
// Actually create it
const ret = setNewWorkflow(data.name, data.description, data, false)
.then((response) => {
if (response !== undefined) {
alert.success("Successfully imported "+data.name)
}
})
}
})
.catch(error => {
alert.error("Import error: "+error.toString())
});
})
// Actually reads
reader.readAsText(file)
}
}
setLoadWorkflowsModalOpen(false)
}
const modalView = modalOpen ?
{setModalOpen(false)}}
PaperProps={{
style: {
backgroundColor: surfaceColor,
color: "white",
minWidth: "800px",
},
}}
>
{editingWorkflow.id !== undefined ? "Editing" : "New"} workflow
setNewWorkflowName(event.target.value)}
InputProps={{
style:{
color: "white",
},
}}
color="primary"
placeholder="Name"
margin="dense"
defaultValue={newWorkflowName}
fullWidth
/>
setNewWorkflowDescription(event.target.value)}
InputProps={{
style:{
color: "white",
},
}}
color="primary"
defaultValue={newWorkflowDescription}
placeholder="Description"
margin="dense"
fullWidth
/>
setModalOpen(false)} color="primary">
Cancel
{
if (editingWorkflow.id !== undefined) {
setNewWorkflow(newWorkflowName, newWorkflowDescription, editingWorkflow, false)
setNewWorkflowName("")
setNewWorkflowDescription("")
setEditingWorkflow({})
} else {
setNewWorkflow(newWorkflowName, newWorkflowDescription, {}, true)
}
setModalOpen(false)
}} color="primary">
Submit
: null
const viewSize = {
workflowView: 1,
executionsView: 1,
executionResults: 2,
}
const workflowViewStyle = {
flex: viewSize.workflowView,
marginLeft: "10px",
marginRight: "10px",
}
if (viewSize.workflowView === 0) {
workflowViewStyle.display = "none"
}
const workflowView = workflows.length > 0 ?
Workflows
setModalOpen(true)}>
{/*
upload.click()}>
*/}
setLoadWorkflowsModalOpen(true)}>
upload = ref} onChange={importFiles} />
{workflows.map(data => {
return (
)
})}
Executions: {selectedWorkflow.name}
{
alert.info("Refreshing executions");
getWorkflowExecution(selectedWorkflow.id)
}}>
Execution Timeline
Collapse results
control={
{setCollapseJson(!collapseJson)}} />}
/>
:
Welcome to Shuffle
Shuffle is a flexible, easy to use, automation platform allowing users to integrate their services and devices freely. It's made to significantly reduce the amount of manual labor, and is focused on security applications. Click here to learn more.
If you want to jump straight into it, click here to create your first workflow:
setModalOpen(true)}>New workflow
const importWorkflowsFromUrl = (url) => {
console.log("IMPORT WORKFLOWS FROM ", downloadUrl)
const parsedData = {
"url": url,
}
if (field1.length > 0) {
parsedData["field_1"] = field1
}
if (field2.length > 0) {
parsedData["field_2"] = field2
}
alert.success("Getting specific workflows from your URL.")
var cors = "cors"
fetch(globalUrl+"/api/v1/workflows/download_remote", {
method: "POST",
mode: "cors",
headers: {
'Accept': 'application/json',
},
body: JSON.stringify(parsedData),
credentials: "include",
})
.then((response) => {
if (response.status === 200) {
alert.success("Successfully loaded workflows from "+downloadUrl)
getAvailableWorkflows()
}
return response.json()
})
.then((responseJson) => {
console.log("DATA: ", responseJson)
if (!responseJson.success) {
if (responseJson.reason !== undefined) {
alert.error("Failed loading: "+responseJson.reason)
} else {
alert.error("Failed loading")
}
}
})
.catch(error => {
alert.error(error.toString())
})
}
const handleGithubValidation = () => {
importWorkflowsFromUrl(downloadUrl)
setLoadWorkflowsModalOpen(false)
}
const workflowDownloadModalOpen = loadWorkflowsModalOpen ?
{
}}
PaperProps={{
style: {
backgroundColor: surfaceColor,
color: "white",
minWidth: "800px",
minHeight: "320px",
},
}}
>
Load workflows from github repo
Repository (supported: github, gitlab, bitbucket)
setDownloadUrl(e.target.value)}
placeholder="https://github.com/frikky/shuffle-apps"
fullWidth
/>
Authentication (optional - private repos etc):
setField1(e.target.value)}
type="username"
placeholder="Username / APIkey (optional)"
fullWidth
/>
setField2(e.target.value)}
type="password"
placeholder="Password (optional)"
fullWidth
/>
setLoadWorkflowsModalOpen(false)} color="primary">
Cancel
{
handleGithubValidation()
}} color="primary">
Submit
: null
const loadedCheck = isLoaded && isLoggedIn && workflowDone ?
{workflowView}
{modalView}
{workflowDownloadModalOpen}
:
// Maybe use gridview or something, idk
return (
{loadedCheck}
)
}
export default Workflows