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 Chip from '@material-ui/core/Chip'; import Switch from '@material-ui/core/Switch'; import Typography from '@material-ui/core/Typography'; import Zoom from '@material-ui/core/Zoom'; import CircularProgress from '@material-ui/core/CircularProgress'; import CachedIcon from '@material-ui/icons/Cached'; import GetAppIcon from '@material-ui/icons/GetApp'; import AppsIcon from '@material-ui/icons/Apps'; 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 ChipInput from 'material-ui-chip-input' 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" export const validateJson = (showResult) => { showResult = showResult.split(" None").join(" \"None\"") showResult = showResult.split(" False").join(" false") showResult = showResult.split(" True").join(" true") var jsonvalid = true try { const tmp = String(JSON.parse(showResult)) if (!showResult.includes("{") && !showResult.includes("[")) { jsonvalid = false } } catch (e) { showResult = showResult.split("\'").join("\"") try { const tmp = String(JSON.parse(showResult)) if (!showResult.includes("{") && !showResult.includes("[")) { jsonvalid = false } } catch (e) { jsonvalid = false } } return { "valid": jsonvalid, "result": jsonvalid ? JSON.parse(showResult) : showResult, } } const Workflows = (props) => { const { globalUrl, isLoggedIn, isLoaded, removeCookie, cookies, userdata} = 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 [selectedWorkflowId, setSelectedWorkflowId] = 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 [downloadBranch, setDownloadBranch] = React.useState("master") const [loadWorkflowsModalOpen, setLoadWorkflowsModalOpen] = React.useState(false) const [modalOpen, setModalOpen] = React.useState(false); const [newWorkflowName, setNewWorkflowName] = React.useState(""); const [newWorkflowDescription, setNewWorkflowDescription] = React.useState(""); const [newWorkflowTags, setNewWorkflowTags] = React.useState([]); const [update, setUpdate] = React.useState("test"); const [deleteModalOpen, setDeleteModalOpen] = React.useState(false); const [editingWorkflow, setEditingWorkflow] = React.useState({}) const [executionLoading, setExecutionLoading] = React.useState(false) const { start, stop } = useInterval({ duration: 5000, startImmediate: false, callback: () => { getWorkflowExecution(selectedWorkflow.id) } }) // DEBUG HERE const handleClickLogout = () => { //console.log("Cookies: ", cookies) //console.log("SHOULD LOG OUT") //console.log(isLoggedIn) // Don't really care about the logout //fetch(globalUrl+"/api/v1/logout", { // credentials: "include", // method: 'POST', // headers: { // 'Content-Type': 'application/json', // }, //}) //.then(() => { // // Log out anyway // removeCookie("session_token", {path: "/"}) // //window.location = "/login" //}) //.catch(error => { // console.log(error) // removeCookie("session_token", {path: "/"}) //}); } const deleteModal = deleteModalOpen ? { setDeleteModalOpen(false) setSelectedWorkflowId("") }} PaperProps={{ style: { backgroundColor: surfaceColor, color: "white", minWidth: 500, }, }} >
Are you sure?
Other workflows relying on this one may stop working
: null 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 { handleClickLogout() } 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", minWidth: 1366, maxWidth: 1766, margin: "auto", maxHeight: "90vh", } const emptyWorkflowStyle = { paddingTop: "200px", width: 1024, 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: "hidden", overflowY: "auto", } const paperAppStyle = { minHeight: "100px", minWidth: "100%", maxWidth: "100%", marginTop: "5px", color: "white", backgroundColor: surfaceColor, borderRadius: 5, padding: 10, cursor: "pointer", display: "flex", } const getWorkflowExecution = (id) => { setExecutionLoading(true) fetch(globalUrl+"/api/v1/workflows/"+id+"/executions", { method: 'GET', headers: { 'Content-Type': 'application/json', 'Accept': 'application/json', }, credentials: "include", }) .then((response) => { setExecutionLoading(false) if (response.status !== 200) { console.log("Status not 200 for WORKFLOW EXECUTION :O!") } return response.json() }) .then((responseJson) => { if (responseJson.success === false) { alert.error("Failed getting executions") } else { if (responseJson.length > 0) { setSelectedExecution(responseJson[0]) setWorkflowExecutions(responseJson) } else { alert.info("Couldn't find executions for the workflow") setSelectedExecution({}) setWorkflowExecutions([]) } } }) .catch(error => { setExecutionLoading(false) 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 exportAllWorkflows = () => { for (var key in workflows) { exportWorkflow(workflows[key]) } } 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'; data["owner"] = "" for (var key in data.triggers) { if (data.triggers[key].status == "running") { data.triggers[key].status = "stopped" } } data["org"] = [] data["org_id"] = "" data.execution_org = {"id": ""} console.log(data) let linkElement = document.createElement('a'); linkElement.setAttribute('href', dataUri); linkElement.setAttribute('download', exportFileDefaultName); linkElement.click(); } const copyWorkflow = (data) => { data = JSON.parse(JSON.stringify(data)) alert.success("Copying workflow "+data.name) console.log("data: ", data) data.id = "" data.name = data.name+"_copy" //return 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); } const actions = data.actions !== null ? data.actions.length : 0 var schedules = 0 var webhooks = 0 var webhookImg = "" var scheduleImg = "" if (data.triggers !== undefined && data.triggers !== null && data.triggers.length > 0) { for (var key in data.triggers) { if (data.triggers[key].app_name === "Webhook") { webhooks += 1 webhookImg = data.triggers[key].large_image } else if (data.triggers[key].app_name === "Schedule") { schedules += 1 scheduleImg = data.triggers[key].large_image } } } const imgSize = 25 return ( { }}>
{ if (selectedWorkflow.id !== data.id) { setSelectedWorkflow(data) getWorkflowExecution(data.id) } }}> {data.name}
{ setOpen(false) setAnchorEl(null) }} > { setModalOpen(true) setEditingWorkflow(data) setNewWorkflowName(data.name) setNewWorkflowDescription(data.description) if (data.tags !== undefined && data.tags !== null) { setNewWorkflowTags(JSON.parse(JSON.stringify(data.tags))) } }} key={"change"}>{"Change details"} { copyWorkflow(data) setOpen(false) }} key={"copy"}>{"Copy"} { exportWorkflow(data) setOpen(false) }} key={"export"}>{"Export"} { setDeleteModalOpen(true) setSelectedWorkflowId(data.id) setOpen(false) }} key={"delete"}>{"Delete"}
{ if (selectedWorkflow.id !== data.id) { setSelectedWorkflow(data) getWorkflowExecution(data.id) } }}> {data.tags !== undefined ? data.tags.map((tag, index) => { if (index >= 3) { return null } return ( ) }) : null}
{data.actions !== undefined && data.actions !== null ? {webhooks > 0 ? {data.title} : null} {schedules > 0 ? {data.title} : null} : null} ) } 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 } 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() const validate = validateJson(showResult) if (validate.valid) { 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, index) => { 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 showResult = selectedExecution.execution_argument.trim() const validate = validateJson(showResult) arg = validate.valid ? : showResult } var lastresult = null if (selectedExecution.result !== undefined && selectedExecution.result.length > 0) { var showResult = selectedExecution.result.trim() const validate = validateJson(showResult) lastresult = validate.valid ? : 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 ( executionLoading ?
:

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 ( executionLoading ?
:

There are no executions for this workflow yet

) } // Can create and set workflows const setNewWorkflow = (name, description, tags, editingWorkflow, redirect) => { var method = "POST" var extraData = "" var workflowdata = {} if (editingWorkflow.id !== undefined) { console.log("Building original workflow") method = "PUT" extraData = "/"+editingWorkflow.id workflowdata = editingWorkflow console.log("REMOVING OWNER") workflowdata["owner"] = "" // FIXME: Loop triggers and turn them off? } workflowdata["name"] = name workflowdata["description"] = description if (tags !== undefined) { workflowdata["tags"] = tags } //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) => { console.log("Importing!") 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, data.tags, {}, 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.tags, 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 /> { newWorkflowTags.push(chip) setNewWorkflowTags(newWorkflowTags) }} onDelete={(chip, index) => { newWorkflowTags.splice(index, 1) setNewWorkflowTags(newWorkflowTags) setUpdate("delete "+chip) }} />
: null const viewSize = { workflowView: 4, executionsView: 3, executionResults: 4, } const workflowViewStyle = { flex: viewSize.workflowView, marginLeft: "10px", marginRight: "10px", } if (viewSize.workflowView === 0) { workflowViewStyle.display = "none" } const workflowButtons = {workflows.length > 0 ? : null} upload = ref} onChange={importFiles} /> {workflows.length > 0 ? : null} const WorkflowView = () => { if (workflows.length === 0) { return (

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:
..OR {workflowButtons}
) } return (

Workflows ({workflows.length})

{workflowButtons}
{workflows.map((data, index) => { return ( ) })}

Executions: {selectedWorkflow.name}

Execution Timeline

Collapse results
} control={ {setCollapseJson(!collapseJson)}} />} />
) } const importWorkflowsFromUrl = (url) => { console.log("IMPORT WORKFLOWS FROM ", downloadUrl) const parsedData = { "url": url, "field_3": downloadBranch || 'master' } 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) 0 ? userdata.active_org.defaults.workflow_download_repo : downloadUrl} InputProps={{ style:{ color: "white", height: "50px", fontSize: "1em", }, }} onChange={e => setDownloadUrl(e.target.value)} placeholder="https://github.com/frikky/shuffle-apps" fullWidth /> Branch (default value is "master"):
0 ? userdata.active_org.defaults.workflow_download_branch : downloadBranch} InputProps={{ style:{ color: "white", height: "50px", fontSize: "1em", }, }} onChange={e => setDownloadBranch(e.target.value)} placeholder="master" 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 />
: null const loadedCheck = isLoaded && isLoggedIn && workflowDone ?
{modalView} {deleteModal} {workflowDownloadModalOpen}
:
Loading Workflows
// Maybe use gridview or something, idk return (
{loadedCheck}
) } export default Workflows