From 66513a177dc9960d8eabbbce8a0d66d872428b7d Mon Sep 17 00:00:00 2001 From: amitk Date: Mon, 15 Mar 2021 14:43:55 +0530 Subject: [PATCH 1/2] Myview dashboard - new page, If we want to replace it with a workfolow page need to currect route file --- frontend/src/App.jsx | 3 + frontend/src/assets/img/bag.svg | 9 + frontend/src/assets/img/book.svg | 9 + frontend/src/assets/img/mobile.svg | 9 + frontend/src/views/MyView.jsx | 1495 ++++++++++++++++++++++++++++ 5 files changed, 1525 insertions(+) create mode 100644 frontend/src/assets/img/bag.svg create mode 100644 frontend/src/assets/img/book.svg create mode 100644 frontend/src/assets/img/mobile.svg create mode 100644 frontend/src/views/MyView.jsx diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 0864b984..e3a70266 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 AlertTemplate from "./components/AlertTemplate"; @@ -148,6 +150,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..7b9c56bb --- /dev/null +++ b/frontend/src/views/MyView.jsx @@ -0,0 +1,1495 @@ +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'; + + +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 { 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 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()) + }); + } + + // 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) + } + }}/> + + + +
{ + 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 = + + {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

+

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
+
+
+
+
+ +
+ {workflows.map((data, index) => { + return ( + + ) + })} +
+
+
+ ) + } + + 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 From f67992a6d2ddb72ee0fd1db83ae0b81c6fd6ec58 Mon Sep 17 00:00:00 2001 From: amitk Date: Thu, 25 Mar 2021 09:16:56 +0530 Subject: [PATCH 2/2] added new view which prints grid and table view --- frontend/package.json | 1 + frontend/src/views/MyView.jsx | 212 ++++++++++++++++++++++++++++++---- 2 files changed, 189 insertions(+), 24 deletions(-) diff --git a/frontend/package.json b/frontend/package.json index e3b88353..d63bcd75 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/views/MyView.jsx b/frontend/src/views/MyView.jsx index 7b9c56bb..6ca5d0b0 100644 --- a/frontend/src/views/MyView.jsx +++ b/frontend/src/views/MyView.jsx @@ -46,6 +46,16 @@ 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" @@ -152,6 +162,7 @@ const MyView = (props) => { 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, @@ -307,6 +318,22 @@ const MyView = (props) => { 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", { @@ -508,6 +535,25 @@ const MyView = (props) => { }); } + 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; @@ -530,21 +576,8 @@ const MyView = (props) => { } 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 [schedules, webhooks] = getWorkFlowMeta(data); const imgSize = 25 return ( @@ -570,7 +603,7 @@ const MyView = (props) => {
- + @@ -1187,6 +1220,16 @@ borderRadius: "4px", color: "black", height: "30px", "width": "30px", fontSize: const workflowButtons = + {view === "grid" && ( + + + + )} + {view === "list" && ( + + + + )} {workflows.length > 0 ? @@ -1214,6 +1257,120 @@ borderRadius: "4px", color: "black", height: "30px", "width": "30px", fontSize: + 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 ( @@ -1287,18 +1444,25 @@ borderRadius: "4px", color: "black", height: "30px", "width": "30px", fontSize:
-
- {workflows.map((data, index) => { - return ( - - ) - })} -
+ {view === "grid" && ( +
+ {workflows.map((data, index) => { + return ( + + ) + })} +
+ )} + + {view === "list" && ( + + )} + ) } - + const importWorkflowsFromUrl = (url) => { console.log("IMPORT WORKFLOWS FROM ", downloadUrl)