diff --git a/frontend/package.json b/frontend/package.json index bf3c6702..d2b2ef26 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -5,6 +5,7 @@ "private": true, "dependencies": { "@material-ui/core": "^4.5.2", + "@material-ui/data-grid": "^4.0.0-alpha.22", "@material-ui/icons": "^4.5.1", "@material-ui/styles": "^4.5.2", "@use-it/interval": "^1.0.0", diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index ceee6dbf..4d4212de 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -28,6 +28,8 @@ import LandingPageNew from "./views/LandingpageNew"; import LoginPage from "./views/LoginPage"; import SettingsPage from "./views/SettingsPage"; +import MyView from "./views/MyView"; + import { createMuiTheme, MuiThemeProvider } from '@material-ui/core/styles'; import ScrollToTop from "./components/ScrollToTop"; @@ -152,6 +154,7 @@ const App = (message, props) => { { window.location.pathname = "/docs/about" }} /> } /> } /> + } /> } /> diff --git a/frontend/src/assets/img/bag.svg b/frontend/src/assets/img/bag.svg new file mode 100644 index 00000000..c63d8f9c --- /dev/null +++ b/frontend/src/assets/img/bag.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/frontend/src/assets/img/book.svg b/frontend/src/assets/img/book.svg new file mode 100644 index 00000000..c64ae378 --- /dev/null +++ b/frontend/src/assets/img/book.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/frontend/src/assets/img/mobile.svg b/frontend/src/assets/img/mobile.svg new file mode 100644 index 00000000..0608e2c8 --- /dev/null +++ b/frontend/src/assets/img/mobile.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/frontend/src/views/MyView.jsx b/frontend/src/views/MyView.jsx new file mode 100644 index 00000000..6ca5d0b0 --- /dev/null +++ b/frontend/src/views/MyView.jsx @@ -0,0 +1,1659 @@ +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'; +import BubbleChartIcon from '@material-ui/icons/BubbleChart'; +import RestoreIcon from '@material-ui/icons/Restore'; + +import mobileImage from '../assets/img/mobile.svg'; +import bagImage from '../assets/img/bag.svg'; +import bookImage from '../assets/img/book.svg'; + +import { + DataGrid, + GridToolbarContainer, + GridDensitySelector, + GridToolbar, + } from '@material-ui/data-grid'; +import { makeStyles } from '@material-ui/core/styles'; + +import ListIcon from '@material-ui/icons/List'; +import GridOnIcon from '@material-ui/icons/GridOn'; + +const inputColor = "#383B40" +const surfaceColor = "#27292D" + +const flexContainerStyle = { + display: "flex", + flexDirection: "row", + justifyContent: "left", + alignContent: "space-between", +} + +const flexBoxStyle = { + width: "333px", + height: "125px", + margin: "10px", + borderRadius: "4px", + boxShadow: "0px 1px 2px rgba(0, 0, 0, 0.16), 0px 2px 4px rgba(0, 0, 0, 0.12), 0px 1px 8px rgba(0, 0, 0, 0.1)", + boxSizing: "border-box", + letterSpacing: "0.4px", + color: "#D6791E", +} + +const activeWorkflowStyle = {backgroundColor: "#FFF5EE"} +const availableWorkflowStyle = {backgroundColor: "#F0F3FF"} +const notificationStyle = {backgroundColor: "#E5F9FF"} + +const flexContentStyle = { + display: "flex", + flexDirection: "row" +} + +const iconStyle = { + width: "75px", + height: "75px", + padding: "20px" +} + +const fontSize_16 = {fontSize: "16px",} +const counterStyle = {fontSize: "36px",fontWeight:"bold"} +const blockRightStyle = {textAlign: "right",padding: "20px 20px 0px 0px",width:"100%"} + +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 MyView = (props) => { + const { globalUrl, isLoggedIn, isLoaded, 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 [view, setView] = React.useState("grid") + const { start, stop } = useInterval({ + duration: 5000, + startImmediate: false, + callback: () => { + //getWorkflowExecution(selectedWorkflow.id) + } + }) + + // DEBUG HERE + const handleClickLogout = () => {} + + 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: 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 = { + cursor: "pointer", + display: "flex", + flexWrap: 'wrap', + alignContent: "space-between", + } + + const paperAppStyle = { + minHeight: "148px", + width: "333px", + margin: "10px", + color: "white", + backgroundColor: surfaceColor, + borderRadius: 5, + padding: "10px", + cursor: "pointer", + display: "flex", + boxSizing: "border-box", + } + + const gridContainer = { + cursor: "pointer", + height: "auto", + color: "white", + margin: "10px", + backgroundColor: surfaceColor, + } + + const workFlowActionStyle = { + flex: "1", + display: "flex", + width: "150px", + justifyContent: "space-between", + overflow: "hidden" + } + + 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) { + 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" + } + } + + for (var key in data.actions) { + data.actions[key].authentication_id = "" + } + + //return + + 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()) + }); + } + + const getWorkFlowMeta = (data) => { + let schedules = 0 + let webhooks = 0 + let webhookImg = "" + let scheduleImg = "" + if (data.triggers !== undefined && data.triggers !== null && data.triggers.length > 0) { + for (let 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 + } + } + } + return [schedules, webhooks, webhookImg, scheduleImg]; + } + + // 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 + + const [schedules, webhooks] = getWorkFlowMeta(data); + + const imgSize = 25 + return ( + { + }}> +
{ + if (selectedWorkflow.id !== data.id) { + setSelectedWorkflow(data) + //getWorkflowExecution(data.id) + } + }}/> + + + +
{ + if (selectedWorkflow.id !== data.id) { + setSelectedWorkflow(data) + //getWorkflowExecution(data.id) + } + }}> + + {data.name} + +
+
+ + + + + + executeWorkflow(data.id)} /> + + + + + {webhooks > 0 ? + + + + : null} + {schedules > 0 ? + + + + : null} + + + {data.tags !== undefined ? + data.tags.map((tag, index) => { + if (index >= 3) { + return null + } + + return ( + + ) + }) + : null} + +
+
+ {data.actions !== undefined && data.actions !== null ? + + + + + + { + 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"} + + + + + + + + + + + + : 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 ? +
+ +
+ : +

+ Executions have been moved to the Workflow itself.
Click here to see them +

+ ) + } + + // 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 = + + {view === "grid" && ( + + + + )} + {view === "list" && ( + + + + )} + {workflows.length > 0 ? + + + + : null} + + + + upload = ref} onChange={importFiles} /> + {workflows.length > 0 ? + + + + : null} + + + + + + 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 classes = useStyles(); + + const WorkflowGridView = () => { + let workflowData = ""; + if (workflows.length > 0) { + const columns = [ + { field: 'id', headerName: 'ID', width: 70, sortable: false, }, + { field: 'title', headerName: 'Title', width: 330, }, + { field: 'actions', headerName: 'Actions', width: 200, sortable: false, + disableClickEventBubbling: true, + renderCell: (params) => { + const data = params.row.record; + let [schedules, webhooks] = getWorkFlowMeta(data); + + return + + + + + + + + executeWorkflow(data.id)} /> + + + + + {webhooks > 0 ? + + + + : null} + {schedules > 0 ? + + + + : null} + + } + }, + { field: 'tags', headerName: 'Tags', width: 390, sortable: false, + disableClickEventBubbling: true, + renderCell: (params) => { + const data = params.row.record; + return + {data.tags !== undefined ? + data.tags.map((tag, index) => { + if (index >= 3) { + return null + } + return ( + + ) + }) + : null} + + } + }, + ]; + let rows = []; + rows = workflows.map((data, index) => { + let obj = {"id":index+1, "title":data.name, "record":data,}; + return obj; + }); + workflowData = + } + return ( +
+ {workflowData} +
+ ); + } + + 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

+

THIS IS SOME WORKFLOW INFORMATION WHY ISN’T IT GROWING THE CORRECT WAY.

+
+
+ {workflowButtons} +
+
+ +
+
+
+
+
+
{workflows.length}
+
ACTIVE WORKFLOWS
+
+
+
+
+
+
+
+
{workflows.length}
+
AVAILABE WORKFLOWS
+
+
+
+
+
+
+
+
{workflows.length}
+
NOTIFICATIONS
+
+
+
+
+ + {view === "grid" && ( +
+ {workflows.map((data, index) => { + return ( + + ) + })} +
+ )} + + {view === "list" && ( + + )} + +
+
+ ) + } + + 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 MyView