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 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 MoreVertIcon from '@material-ui/icons/MoreVert'; import FormControlLabel from '@material-ui/core/FormControlLabel'; import Switch from '@material-ui/core/Switch'; //import JSONPretty from 'react-json-pretty'; //import JSONPrettyMon from 'react-json-pretty/dist/monikai' import ReactJson from 'react-json-view' 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'; const surfaceColor = "#27292D" const Workflows = (props) => { const { globalUrl, isLoggedIn, isLoaded, } = props; document.title = "Shuffle - Workflows" const alert = useAlert() 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 [modalOpen, setModalOpen] = React.useState(false); const [newWorkflowName, setNewWorkflowname] = React.useState(""); const [newWorkflowDescription, setNewWorkflowDescription] = 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) => { console.log(responseJson) setSelectedExecution({}) setWorkflowExecutions([]) if (responseJson !== undefined) { setWorkflows(responseJson) setWorkflowDone(true) } else { if (isLoggedIn) { alert.error("An error occurred while loading workflows") } else { window.location.pathname = "/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!") } return response.json() }) .then((responseJson) => { setWorkflowExecutions(responseJson) if (responseJson.length > 0 && Object.getOwnPropertyNames(selectedExecution).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}

{ setOpen(false) setAnchorEl(null) }} > { copyWorkflow(data) setOpen(false) }} key={"copy"}>{"Copy"} { exportWorkflow(data) setOpen(false) }} key={"export"}>{"Export"} { deleteWorkflow(data.id) setOpen(false) }} key={"delete"}>{"Delete"}
) } 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}
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 showResult = data.result.trim() if ((showResult.startsWith("{") && showResult.endsWith("}")) || (showResult.startsWith("[{") && showResult.endsWith("}]"))) { //showResult = showResult = replaceAll(showResult, " None", " \"None\""); console.log(showResult) showResult = } else { // FIXME - have everything parsed as json, either just for frontend // or in the backend /* const newdata = {"result": data.result} showResult = */ } console.log(data) return ( {}}>

Status: {data.status}

App: {data.action.app_name}, Version: {data.action.app_version} Action: {data.action.name}, Environment: {data.action.environment}
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) console.log(selectedExecution) const arg = selectedExecution.execution_argument !== undefined && selectedExecution.execution_argument.length > 0 ?
Argument: {selectedExecution.execution_argument}
: null /*
ID: {selectedExecution.execution_id}
*/ if (Object.getOwnPropertyNames(selectedExecution).length > 0 && selectedExecution.workflow.actions !== null) { return (
Actions: {selectedExecution.workflow.actions.length}
Results: {resultsLength}
Status: {selectedExecution.status}
Starttime: {starttime.toISOString()}
Finished: {endtime.toISOString()}
Result: {selectedExecution.result}
Last node: {selectedExecution.last_node}
{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

) } const setNewWorkflow = () => { if (newWorkflowName.length === 0) { return } var workflowdata = { "name": newWorkflowName, "description": newWorkflowDescription, } fetch(globalUrl+"/api/v1/workflows", { method: 'POST', 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) => { window.location.pathname = "/workflows/"+responseJson["id"] }) .catch(error => { alert.error(error.toString()) }); } const modalView = modalOpen ? {setModalOpen(false)}} PaperProps={{ style: { backgroundColor: surfaceColor, color: "white", minWidth: "800px", }, }} >
New workflow
setNewWorkflowname(event.target.value)} InputProps={{ style:{ color: "white", }, }} color="primary" placeholder="Name" margin="dense" fullWidth /> setNewWorkflowDescription(event.target.value)} InputProps={{ style:{ color: "white", }, }} color="primary" placeholder="Description" margin="dense" fullWidth />
: 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

{workflows.map(data => { return ( ) })}

Executions

Execution Timeline

Collapse results
control={ {setCollapseJson(!collapseJson)}} />} />
:

Welcome to Shuffle!

Shuffle is a flexible, easy to use, automation framework allowing users to integrate their services and devices to reduce the amount of manual labor required for those tasks. Click here for more information.

If you want to jump straight into it, click the following button to create your first workflow:
const loadedCheck = isLoaded && isLoggedIn && workflowDone ?
{workflowView} {modalView}
:
// Maybe use gridview or something, idk return (
{loadedCheck}
) } export default Workflows