import React, { useEffect} from 'react';
import { useInterval } from 'react-powerhooks';
import { makeStyles } from '@material-ui/core/styles';
import {Avatar, Grid, Paper, Tooltip, Divider, Button, TextField, FormControl, IconButton, Menu, MenuItem, FormControlLabel, Chip, Switch, Typography, Zoom, CircularProgress, Dialog, DialogTitle, DialogActions, DialogContent} from '@material-ui/core';
import {FileCopy as FileCopyIcon, Delete as DeleteIcon, BubbleChart as BubbleChartIcon, Restore as RestoreIcon, Cached as CachedIcon, GetApp as GetAppIcon, Apps as AppsIcon, Edit as EditIcon, MoreVert as MoreVertIcon, PlayArrow as PlayArrowIcon, Add as AddIcon, Publish as PublishIcon, CloudUpload as CloudUploadIcon, CloudDownload as CloudDownloadIcon} from '@material-ui/icons';
//import {Search as SearchIcon, ArrowUpward as ArrowUpwardIcon, Visibility as VisibilityIcon, Done as DoneIcon, Close as CloseIcon, Error as ErrorIcon, FindReplace as FindreplaceIcon, ArrowLeft as ArrowLeftIcon, Cached as CachedIcon, DirectionsRun as DirectionsRunIcon, Add as AddIcon, Polymer as PolymerIcon, FormatListNumbered as FormatListNumberedIcon, Create as CreateIcon, PlayArrow as PlayArrowIcon, AspectRatio as AspectRatioIcon, MoreVert as MoreVertIcon, Apps as AppsIcon, Schedule as ScheduleIcon, FavoriteBorder as FavoriteBorderIcon, Pause as PauseIcon, Delete as DeleteIcon, AddCircleOutline as AddCircleOutlineIcon, Save as SaveIcon, KeyboardArrowLeft as KeyboardArrowLeftIcon, KeyboardArrowRight as KeyboardArrowRightIcon, ArrowBack as ArrowBackIcon, Settings as SettingsIcon, LockOpen as LockOpenIcon, ExpandMore as ExpandMoreIcon, VpnKey as VpnKeyIcon} from '@material-ui/icons';
import {DataGrid, GridToolbarContainer, GridDensitySelector, GridToolbar} from '@material-ui/data-grid';
//import JSONPretty from 'react-json-pretty';
//import JSONPrettyMon from 'react-json-pretty/dist/monikai'
import ReactJson from 'react-json-view'
import Dropzone from '../components/Dropzone';
import {Link} from 'react-router-dom';
import { useAlert } from "react-alert";
import ChipInput from 'material-ui-chip-input'
import uuid from "uuid"
import CytoscapeWrapper from '../components/RenderCytoscape'
//import mobileImage from '../assets/img/mobile.svg';
//import bagImage from '../assets/img/bag.svg';
//import bookImage from '../assets/img/book.svg';
const inputColor = "#383B40"
const surfaceColor = "#27292D"
const flexContainerStyle = {
display: "flex",
flexDirection: "row",
justifyContent: "left",
alignContent: "space-between",
}
const flexBoxStyle = {
height: 125,
borderRadius: 4,
boxSizing: "border-box",
letterSpacing: "0.4px",
color: "#D6791E",
margin: 10,
flex: 1,
}
const useStyles = makeStyles((theme) => ({
root: {
border: 0,
'& .MuiDataGrid-columnsContainer': {
backgroundColor: theme.palette.type === 'light' ? '#fafafa' : '#1d1d1d',
},
'& .MuiDataGrid-iconSeparator': {
display: 'none',
},
'& .MuiDataGrid-colCell, .MuiDataGrid-cell': {
borderRight: `1px solid ${
theme.palette.type === 'light' ? 'white' : '#303030'
}`,
},
'& .MuiDataGrid-columnsContainer, .MuiDataGrid-cell': {
borderBottom: `1px solid ${
theme.palette.type === 'light' ? '#f0f0f0' : '#303030'
}`,
},
'& .MuiDataGrid-cell': {
color:
theme.palette.type === 'light'
? 'white'
: 'rgba(255,255,255,0.65)',
},
'& .MuiPaginationItem-root, .MuiTablePagination-actions, .MuiTablePagination-caption': {
borderRadius: 0,
color: "white",
},
},
}));
//const activeWorkflowStyle = {backgroundColor: "#FFF5EE"}
//const notificationStyle = {backgroundColor: "#E5F9FF"}
//const activeWorkflowStyle = {backgroundColor: "#3d3f43"}
const availableWorkflowStyle = {backgroundColor: "#3d3f43"}
const notificationStyle = {backgroundColor: "#3d3f43"}
const activeWorkflowStyle = {backgroundColor: "#3d3f43"}
const fontSize_16 = {fontSize: "16px",}
const counterStyle = {fontSize: "36px",fontWeight:"bold"}
const blockRightStyle = {textAlign: "right",padding: "20px 20px 0px 0px",width:"100%"}
const chipStyle = {
backgroundColor: "#3d3f43", height: 30, marginRight: 5, paddingLeft: 5, paddingRight: 5, height: 28, cursor: "pointer", borderColor: "#3d3f43", color: "white",
}
const flexContentStyle = {
display: "flex",
flexDirection: "row"
}
const iconStyle = {
width: "75px",
height: "75px",
padding: "20px"
}
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
}
}
const result = jsonvalid ? JSON.parse(showResult) : showResult
//console.log("VALID: ", jsonvalid, result)
return {
"valid": jsonvalid,
"result": result,
}
}
const Workflows = (props) => {
const { globalUrl, isLoggedIn, isLoaded, removeCookie, cookies, userdata} = props;
document.title = "Shuffle - Workflows"
const referenceUrl = globalUrl+"/api/v1/hooks/"
const alert = useAlert()
const classes = useStyles();
var upload = ""
const [file, setFile] = React.useState("");
const [workflows, setWorkflows] = React.useState([]);
const [filteredWorkflows, setFilteredWorkflows] = 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 [publishModalOpen, setPublishModalOpen] = React.useState(false);
const [editingWorkflow, setEditingWorkflow] = React.useState({})
const [executionLoading, setExecutionLoading] = React.useState(false)
const [importLoading, setImportLoading] = React.useState(false)
const [isDropzone, setIsDropzone] = React.useState(false);
const [view, setView] = React.useState("grid")
const [filters, setFilters] = React.useState([])
const isCloud = window.location.host === "localhost:3002" || window.location.host === "shuffler.io"
const findWorkflow = (filters) => {
if (filters.length === 0) {
setFilteredWorkflows(workflows)
return
}
var newWorkflows = []
for (var workflowKey in workflows) {
const curWorkflow = workflows[workflowKey]
var found = [false]
if (curWorkflow.tags === undefined || curWorkflow.tags === null) {
found = filters.map(filter => curWorkflow.name.toLowerCase().includes(filter))
} else {
found = filters.map(filter => curWorkflow.name.toLowerCase().includes(filter.toLowerCase()) || curWorkflow.tags.includes(filter))
}
//console.log("FOUND: ", found)
//if (found) {
if (found.every(v => v === true)) {
newWorkflows.push(curWorkflow)
continue
}
}
if (newWorkflows.length !== workflows.length) {
setFilteredWorkflows(newWorkflows)
}
}
const addFilter = (data) => {
if (data === null || data === undefined) {
return
}
if (data.includes("<") && data.includes(">")) {
return
}
if (filters.includes(data)) {
return
}
filters.push(data.toLowerCase())
setFilters(filters)
findWorkflow(filters)
}
const removeFilter = (index) => {
var newfilters = filters
if (index < 0) {
console.log("Can't handle index: ", index)
return
}
//console.log("Removing filter index", index)
newfilters.splice(index, 1)
//console.log("FILTER LENGTH: ", filters.length)
if (newfilters.length === 0) {
newfilters = []
setFilters(newfilters)
} else {
setFilters(newfilters)
}
//console.log("FILTERS: ", newfilters)
findWorkflow(newfilters)
}
const publishModal = publishModalOpen ?
: null
const deleteModal = deleteModalOpen ?
: null
const uploadFile = (e) => {
const isDropzone = e.dataTransfer === undefined ? false : e.dataTransfer.files.length > 0;
const files = isDropzone ? e.dataTransfer.files : e.target.files;
const reader = new FileReader();
alert.info("Starting upload. Please wait while we validate the workflows")
try {
reader.addEventListener('load', (e) => {
var data = e.target.result;
setIsDropzone(false)
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())
});
})
} catch (e) {
console.log("Error in dropzone: ", e)
}
reader.readAsText(files[0]);
}
useEffect(() => {
if (isDropzone) {
//redirectOpenApi();
setIsDropzone(false);
}
}, [isDropzone]);
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!: ", response.status)
if (isCloud) {
window.location.pathname = "/login"
}
alert.info("Failed getting workflows.")
setWorkflowDone(true)
return
}
return response.json()
})
.then((responseJson) => {
setSelectedExecution({})
setWorkflowExecutions([])
//console.log(responseJson)
if (responseJson !== undefined) {
setWorkflows(responseJson)
setFilteredWorkflows(responseJson)
setWorkflowDone(true)
} else {
if (isLoggedIn) {
alert.error("An error occurred while loading workflows")
}
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: 1024,
maxWidth: 1024,
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 paperAppContainer = {
display: "flex",
flexWrap: 'wrap',
alignContent: "space-between",
}
const paperAppStyle = {
minHeight: 130,
width: "100%",
color: "white",
backgroundColor: surfaceColor,
padding: "12px 12px 0px 15px",
borderRadius: 5,
display: "flex",
boxSizing: "border-box",
position: "relative",
}
const gridContainer = {
height: "auto",
color: "white",
margin: "10px",
backgroundColor: surfaceColor,
}
const workflowActionStyle = {
display: "flex",
width: 160,
height: 44,
justifyContent: "space-between",
}
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())
});
}
function sleep (time) {
return new Promise((resolve) => setTimeout(resolve, time));
}
const exportAllWorkflows = () => {
for (var key in workflows) {
exportWorkflow(workflows[key])
}
}
const deduplicateIds = (data) => {
if (data.triggers !== null && data.triggers !== undefined) {
for (var key in data.triggers) {
const trigger = data.triggers[key]
if (trigger.app_name === "Shuffle Workflow") {
if (trigger.parameters.length > 2) {
trigger.parameters[2].value = ""
}
}
if (trigger.status == "running") {
trigger.status = "stopped"
}
const newId = uuid.v4()
if (trigger.trigger_type === "WEBHOOK") {
const hookname = "webhook_"+newId
if (trigger.parameters.length === 2) {
trigger.parameters[0].value = referenceUrl+"webhook_"+trigger.id
trigger.parameters[1].value = "webhook_"+trigger.id
} else {
alert.info("Something is wrong with the webhook in the copy")
}
}
for (var branchkey in data.branches) {
const branch = data.branches[branchkey]
if (branch.source_id === trigger.id) {
branch.source_id = newId
}
if (branch.destination_id === trigger.id) {
branch.destination_id = newId
}
}
trigger.environment = isCloud ? "cloud" : "Shuffle"
trigger.id = newId
}
}
if (data.actions !== null && data.actions !== undefined) {
for (var key in data.actions) {
data.actions[key].authentication_id = ""
for (var subkey in data.actions[key].parameters) {
const param = data.actions[key].parameters[subkey]
if (param.name.includes("key") || param.name.includes("user") || param.name.includes("pass") || param.name.includes("api") || param.name.includes("auth") || param.name.includes("secret") || param.name.includes("domain") || param.name.includes("url") || param.name.includes("mail")) {
// FIXME: This may be a vuln if api-keys are generated that start with $
if (param.value.startsWith("$")) {
console.log("Skipping field, as it's referencing a variable")
} else {
param.value = ""
param.is_valid = false
}
}
}
const newId = uuid.v4()
for (var branchkey in data.branches) {
const branch = data.branches[branchkey]
if (branch.source_id === data.actions[key].id) {
branch.source_id = newId
}
if (branch.destination_id === data.actions[key].id) {
branch.destination_id = newId
}
}
if (data.actions[key].id === data.start) {
data.start = newId
}
//data.actions[key].environment = isCloud ? "cloud" : "Shuffle"
data.actions[key].environment = ""
data.actions[key].id = newId
}
}
if (data.workflow_variables !== null && data.workflow_variables !== undefined) {
for (var key in data.workflow_variables) {
const param = data.workflow_variables[key]
if (param.name.includes("key") || param.name.includes("user") || param.name.includes("pass") || param.name.includes("api") || param.name.includes("auth") || param.name.includes("secret") || param.name.includes("email")) {
param.value = ""
param.is_valid = false
}
}
}
return data
}
const sanitizeWorkflow = (data) => {
data["owner"] = ""
console.log("Sanitize start: ", data)
data = deduplicateIds(data)
data["org"] = []
data["org_id"] = ""
data["execution_org"] = {}
// These are backwards.. True = saved before. Very confuse.
data["previously_saved"] = false
data["first_save"] = false
console.log("Sanitize end: ", data)
return data
}
const exportWorkflow = (data) => {
let exportFileDefaultName = data.name+'.json';
data = sanitizeWorkflow(data)
return
// Add correct ID's for triggers
// Add mag
let dataStr = JSON.stringify(data)
let dataUri = 'data:application/json;charset=utf-8,'+ encodeURIComponent(dataStr);
let linkElement = document.createElement('a');
linkElement.setAttribute('href', dataUri);
linkElement.setAttribute('download', exportFileDefaultName);
linkElement.click();
}
const publishWorkflow = (data) => {
data = JSON.parse(JSON.stringify(data))
data = sanitizeWorkflow(data)
alert.info("Sanitizing and publishing "+data.name)
// This ALWAYS talks to Shuffle cloud
fetch(globalUrl+"/api/v1/workflows/"+data.id+"/publish", {
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 workflow publish :O!")
} else {
if (isCloud) {
alert.success("Successfully published workflow")
} else {
alert.success("Successfully published workflow to https://shuffler.io")
}
}
return response.json()
})
.then((responseJson) => {
if (responseJson.reason !== undefined) {
alert.error("Failed publishing: ", responseJson.reason)
}
getAvailableWorkflows()
})
.catch(error => {
alert.error(error.toString())
})
}
const copyWorkflow = (data) => {
data = JSON.parse(JSON.stringify(data))
alert.success("Copying workflow "+data.name)
data.id = ""
data.name = data.name+"_copy"
data = deduplicateIds(data)
//console.log("COPIED DATA: ", data)
//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) => {
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!")
alert.error("Failed deleting workflow")
} else {
alert.success("Deleted workflow "+id)
}
return response.json()
})
.then((responseJson) => {
getAvailableWorkflows()
})
.catch(error => {
alert.error(error.toString())
});
}
const handleChipClick = (e) => {
addFilter(e.target.innerHTML)
}
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 = "#FECC00"
if (data.is_valid) {
boxColor = "#86c142"
}
if (!data.previously_saved) {
boxColor = "#f85a3e"
}
const menuClick = (event) => {
setOpen(!open)
setAnchorEl(event.currentTarget);
}
var parsedName = data.name
if (parsedName !== undefined && parsedName !== null && parsedName.length > 25) {
parsedName = parsedName.slice(0,26)+".."
}
const actions = data.actions !== null ? data.actions.length : 0
const [triggers, schedules, webhooks, subflows] = getWorkflowMeta(data)
return (
Name: {data.action.label}
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.