diff --git a/backend/app_sdk/build.sh b/backend/app_sdk/build.sh index 6023b389..02732783 100644 --- a/backend/app_sdk/build.sh +++ b/backend/app_sdk/build.sh @@ -3,7 +3,7 @@ ### DEFAULT NAME=shuffle-app_sdk -VERSION=0.9.34 +VERSION=0.9.35 docker rmi docker.pkg.github.com/frikky/shuffle/$NAME:$VERSION --force docker build . -f Dockerfile -t frikky/shuffle:app_sdk -t frikky/$NAME:$VERSION -t docker.pkg.github.com/frikky/shuffle/$NAME:$VERSION -t ghcr.io/frikky/$NAME:$VERSION -t ghcr.io/frikky/$NAME:nightly diff --git a/backend/go-app/go.mod b/backend/go-app/go.mod index 6e8698d7..5395b0f1 100644 --- a/backend/go-app/go.mod +++ b/backend/go-app/go.mod @@ -22,7 +22,7 @@ require ( github.com/gorilla/mux v1.8.0 github.com/h2non/filetype v1.1.1 github.com/satori/go.uuid v1.2.0 - github.com/shuffle/shuffle-shared v0.1.35 + github.com/shuffle/shuffle-shared v0.1.36 github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e // indirect go4.org v0.0.0-20201209231011-d4a079459e60 // indirect golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 diff --git a/backend/go-app/main.go b/backend/go-app/main.go index ec8039c9..29839575 100644 --- a/backend/go-app/main.go +++ b/backend/go-app/main.go @@ -3809,6 +3809,7 @@ func runInitEs(ctx context.Context) { log.Printf("[DEBUG] Getting organizations") activeOrgs, err := shuffle.GetAllOrgs(ctx) + setUsers := false //log.Printf("ORGS: %d", len(activeOrgs)) if err != nil { @@ -3896,6 +3897,7 @@ func runInitEs(ctx context.Context) { } } + _ = setUsers schedules, err := shuffle.GetAllSchedules(ctx, "ALL") if err != nil { log.Printf("[WARNING] Failed getting schedules during service init: %s", err) @@ -3937,6 +3939,7 @@ func runInitEs(ctx context.Context) { } } + parsedApikey := "" users, err := shuffle.GetAllUsers(ctx) if len(users) == 0 { log.Printf("[INFO] Trying to set up user based on environments SHUFFLE_DEFAULT_USERNAME & SHUFFLE_DEFAULT_PASSWORD") @@ -3948,6 +3951,10 @@ func runInitEs(ctx context.Context) { } else { apikey := os.Getenv("SHUFFLE_DEFAULT_APIKEY") + if len(parsedApikey) == 0 { + parsedApikey = apikey + } + log.Printf("[DEBUG] Creating org for default user %s", username) orgId := uuid.NewV4().String() orgSetupName := "default" @@ -3996,8 +4003,15 @@ func runInitEs(ctx context.Context) { } } } + } else { + for _, user := range users { + if user.Role == "admin" && len(user.ApiKey) > 0 { + parsedApikey = user.ApiKey + log.Printf("[DEBUG] Using apikey of %s (%s) for cleanup", user.Username, user.Id) + break + } + } } - _ = setUsers log.Printf("[INFO] Starting cloud schedules for orgs if enabled!") type requestStruct struct { @@ -4041,6 +4055,57 @@ func runInitEs(ctx context.Context) { forceUpdate = true } + // FIXME: Have this for all envs in all orgs (loop and find). + if len(parsedApikey) > 0 { + cleanupSchedule := 3600 + environments := []string{"Shuffle"} + log.Printf("[DEBUG] Starting schedule setup for execution cleanup every %d seconds. Running first immediately.", cleanupSchedule) + cleanupJob := func() func() { + return func() { + log.Printf("[INFO] Running schedule for cleaning up or re-running unfinished workflows in %d environments.", len(environments)) + + for _, environment := range environments { + url := fmt.Sprintf("http://localhost:5001/api/v1/environments/%s/stop", environment) + httpClient := &http.Client{} + req, err := http.NewRequest( + "GET", + url, + nil, + ) + + req.Header.Add("Authorization", fmt.Sprintf(`Bearer %s`, parsedApikey)) + if err != nil { + log.Printf("[ERROR] Failed CREATING environment request for %s: %s", environment, err) + continue + + } + + newresp, err := httpClient.Do(req) + if err != nil { + log.Printf("[ERROR] Failed running environment request %s: %s", environment, err) + continue + } + + respBody, err := ioutil.ReadAll(newresp.Body) + if err != nil { + log.Printf("[ERROR] Failed setting respbody %s", err) + continue + } + log.Printf("[DEBUG] Successfully ran workflow cleanup request for %s. Body: %s", environment, string(respBody)) + } + } + } + + jobret, err := newscheduler.Every(cleanupSchedule).Seconds().Run(cleanupJob()) + if err != nil { + log.Printf("[ERROR] Failed to schedule Cleanup: %s", err) + } else { + _ = jobret + } + } else { + log.Printf("[DEBUG] Couldn't find a valid API-key, hence couldn't run cleanup") + } + // Getting apps to see if we should initialize a test // FIXME: Isn't this a little backwards? workflowapps, err := shuffle.GetAllWorkflowApps(ctx, 1000) diff --git a/docker-compose.yml b/docker-compose.yml index b03ecd3f..1a54e2ff 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,7 +1,7 @@ version: '3' services: frontend: - #build: ./frontend + build: ./frontend image: ghcr.io/frikky/shuffle-frontend:nightly container_name: shuffle-frontend hostname: shuffle-frontend @@ -16,7 +16,7 @@ services: depends_on: - backend backend: - build: ./backend + #build: ./backend image: ghcr.io/frikky/shuffle-backend:nightly container_name: shuffle-backend hostname: ${BACKEND_HOSTNAME} diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index f1d6cecd..b5fbda46 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -35,7 +35,7 @@ import { createMuiTheme, MuiThemeProvider } from "@material-ui/core/styles"; import ScrollToTop from "./components/ScrollToTop"; import AlertTemplate from "./components/AlertTemplate"; -import { positions, Provider } from "react-alert"; +import { useAlert, positions, Provider } from "react-alert"; import { isMobile } from "react-device-detect"; import detectEthereumProvider from "@metamask/detect-provider"; @@ -51,16 +51,17 @@ if (window.location.port === "3000") { const App = (message, props) => { const [userdata, setUserData] = useState({}); - const [notifications, setNotifications] = useState([]); - const [cookies, setCookie, removeCookie] = useCookies([]); - const [isLoggedIn, setIsLoggedIn] = useState(false); - const [dataset, setDataset] = useState(false); - const [isLoaded, setIsLoaded] = useState(false); + const [notifications, setNotifications] = useState([]) + const [cookies, setCookie, removeCookie] = useCookies([]) + const [isLoggedIn, setIsLoggedIn] = useState(false) + const [dataset, setDataset] = useState(false) + const [isLoaded, setIsLoaded] = useState(false) const [curpath, setCurpath] = useState( typeof window === "undefined" || window.location === undefined ? "" : window.location.pathname - ); + ) + useEffect(() => { if (dataset === false) { @@ -468,6 +469,7 @@ const App = (message, props) => { path="/workflows/:key" render={(props) => ( { type: "Shuffle DB", name: "Shuffle DB", value: "$shuffle_cache", - highlight: "shuffle", - autocomplete: "shuffle", + highlight: "shuffle_cache", + autocomplete: "shuffle_cache", example: "", }); if ( diff --git a/frontend/src/defaultCytoscapeStyle.js b/frontend/src/defaultCytoscapeStyle.js index 491d9476..3c3122ee 100644 --- a/frontend/src/defaultCytoscapeStyle.js +++ b/frontend/src/defaultCytoscapeStyle.js @@ -53,17 +53,18 @@ const data = [ selector: `node[type="COMMENT"]`, css: { shape: "roundrectangle", - "background-color": "data(backgroundcolor)", - "border-color": "#ffffff", color: "data(color)", width: "data(width)", height: "data(height)", - "border-radius": "5px", - "background-opacity": "0.5", padding: "0px", margin: "0px", + "background-color": "data(backgroundcolor)", + "border-color": "#ffffff", "text-margin-x": "0px", "z-index": 4999, + "border-radius": "5px", + "background-opacity": "0.5", + "text-wrap": "wrap", }, }, { diff --git a/frontend/src/theme.js b/frontend/src/theme.js index c6b39a39..6f5b7f40 100644 --- a/frontend/src/theme.js +++ b/frontend/src/theme.js @@ -16,6 +16,12 @@ const theme = createMuiTheme({ surfaceColor: "#27292d", inputColor: "#383B40", borderRadius: 5, + jsonTheme: "brewer", + reactJsonStyle: { + borderRadius: 5, + border: "1px solid rgba(255,255,255,0.7)", + padding: 5, + }, textFieldStyle: { backgroundColor: "#383B40", borderRadius: 5, diff --git a/frontend/src/views/AngularWorkflow.jsx b/frontend/src/views/AngularWorkflow.jsx index 333329fe..8ab716aa 100644 --- a/frontend/src/views/AngularWorkflow.jsx +++ b/frontend/src/views/AngularWorkflow.jsx @@ -9,8 +9,10 @@ import { useBeforeunload } from "react-beforeunload"; import ReactJson from "react-json-view"; import NestedMenuItem from "material-ui-nested-menu-item"; import ReactMarkdown from "react-markdown"; +import { useAlert } from "react-alert"; import { + Popover, TextField, Drawer, Button, @@ -85,7 +87,6 @@ import Draggable from "react-draggable"; import cytoscapestyle from "../defaultCytoscapeStyle"; import cxtmenu from "cytoscape-cxtmenu"; -import { useAlert } from "react-alert"; import { validateJson, GetIconInfo } from "./Workflows.jsx"; import { GetParsedPaths } from "./Apps.jsx"; import ConfigureWorkflow from "../components/ConfigureWorkflow.jsx"; @@ -182,7 +183,7 @@ const svgSize = 24; const AngularWorkflow = (props) => { const { globalUrl, isLoggedIn, isLoaded, userdata } = props; const referenceUrl = globalUrl + "/api/v1/hooks/"; - const alert = useAlert(); + const alert = useAlert() const theme = useTheme(); const green = "#86c142"; const yellow = "#FECC00"; @@ -190,6 +191,7 @@ const AngularWorkflow = (props) => { const [bodyWidth, bodyHeight] = useWindowSize(); var to_be_copied = ""; + const [firstrequest, setFirstrequest] = React.useState(true); const [cystyle] = useState(cytoscapestyle); const [cy, setCy] = React.useState(); @@ -274,7 +276,6 @@ const AngularWorkflow = (props) => { const [apps, setApps] = React.useState([]); const [filteredApps, setFilteredApps] = React.useState([]); const [prioritizedApps, setPrioritizedApps] = React.useState([]); - const [firstrequest, setFirstrequest] = React.useState(true); const [environments, setEnvironments] = React.useState([]); const [established, setEstablished] = React.useState(false); @@ -992,12 +993,28 @@ const AngularWorkflow = (props) => { var curworkflowComment = useworkflow.comments.find( (a) => a.id === cyelements[key].data()["id"] - ); + ) + if (curworkflowComment === undefined) { curworkflowComment = cyelements[key].data(); } + const parsedHeight = parseInt(curworkflowComment["height"]) + if (!isNaN(parsedHeight)) { + curworkflowComment.height = parsedHeight + } else { + curworkflowComment.width = 150 + } + + const parsedWidth = parseInt(curworkflowComment["width"]) + if (!isNaN(parsedWidth)) { + curworkflowComment.width = parsedWidth + } else { + curworkflowComment.width = 200 + } + curworkflowComment.position = cyelements[key].position(); + console.log(curworkflowComment) newComments.push(curworkflowComment); } else { @@ -1935,6 +1952,7 @@ const AngularWorkflow = (props) => { // Nodeselectbatching: // https://stackoverflow.com/questions/16677856/cy-onselect-callback-only-once + // onNodeClick const onNodeSelect = (event, newAppAuth) => { const data = event.target.data(); if (data.isButton) { @@ -2076,9 +2094,13 @@ const AngularWorkflow = (props) => { } if (data.type === "ACTION") { + //var curaction = JSON.parse(JSON.stringify(data)) + // FIXME: Trust it to just work? + //event.target.data() var curaction = workflow.actions.find((a) => a.id === data.id); if (!curaction || curaction === undefined) { alert.error("Action not found. Please remake it."); + event.target.remove(); return; } @@ -2632,10 +2654,12 @@ const AngularWorkflow = (props) => { setLastSaved(false); const node = event.target; const nodedata = event.target.data(); + console.log("Node added: ", nodedata) if ( nodedata.finished === false || (nodedata.id !== undefined && nodedata.is_valid === undefined) ) { + console.log("Returning because node is not valid: ", nodedata) return; } @@ -2645,6 +2669,7 @@ const AngularWorkflow = (props) => { nodedata.isStartNode = true; } else { if (workflow.actions === null) { + console.log("Returning because node has no value") return; } @@ -2658,6 +2683,13 @@ const AngularWorkflow = (props) => { } if (nodedata.type === "ACTION") { + /* + var curaction = workflow.actions.find((a) => a.id === nodedata.id); + if (curaction === null || curaction === undefined) { + alert.error("Node not found. Please remake it.") + event.target.remove(); + } + */ if ( workflow.actions.length === 1 && workflow.actions[0].id === workflow.start @@ -2894,7 +2926,6 @@ const AngularWorkflow = (props) => { break; case 67: - console.log(event); if (event.ctrlKey && !event.shiftKey) { if ( event.path !== undefined && @@ -3121,6 +3152,7 @@ const AngularWorkflow = (props) => { // App length necessary cus of cy initialization if ( + // First load - gets the workflow elements.length === 0 && workflow.actions !== undefined && !graphSetup && @@ -3129,6 +3161,8 @@ const AngularWorkflow = (props) => { setGraphSetup(true); setupGraph(); } else if ( + // 2nd load - configures cytoscape + // !established && cy !== undefined && apps !== null && @@ -3143,11 +3177,16 @@ const AngularWorkflow = (props) => { setEstablished(true); // Validate if the node is just a node lol cy.edgehandles({ - handleNodes: (el) => - el.isNode() && - !el.data("isButton") && - !el.data("isDescriptor") && - !el.data("type") === "COMMENT", + handleNodes: (el) => { + if (el.isNode() && + !el.data("isButton") && + !el.data("isDescriptor") && + el.data("type") !== "COMMENT") { + return true + } + + return false + }, preview: false, toggleOffOnLeave: false, loopAllowed: function (node) { @@ -3175,7 +3214,7 @@ const AngularWorkflow = (props) => { }); cy.on("select", "node", (e) => { - onNodeSelect(e, appAuthentication); + onNodeSelect(e, appAuthentication); }); cy.on("select", "edge", (e) => onEdgeSelect(e)); @@ -3734,8 +3773,8 @@ const AngularWorkflow = (props) => { } } - if (selectedNode.data().decorator === true) { - alert.info("This edge can't be deleted."); + if (selectedNode.data().decorator === true && selectedNode.data("type") !== "COMMENT") { + alert.info("This node can't be deleted."); } else { selectedNode.remove(); } @@ -5307,8 +5346,16 @@ const AngularWorkflow = (props) => { value: "$exec", highlight: "exec", autocomplete: "exec", - example: "hello", - }); + example: "tmp", + }) + actionlist.push({ + type: "Shuffle DB", + name: "Shuffle DB", + value: "shuffle_cache", + highlight: "shuffle_cache", + autocomplete: "shuffle_cache", + example: "tmp", + }) if ( workflow.workflow_variables !== null && @@ -6761,7 +6808,7 @@ const AngularWorkflow = (props) => { highlight: "exec", autocomplete: "exec", example: "hello", - }); + }) if ( workflow.workflow_variables !== null && workflow.workflow_variables !== undefined && @@ -7757,6 +7804,7 @@ const AngularWorkflow = (props) => { }} fullWidth color="primary" + placeholder={"150"} defaultValue={selectedComment.height} onChange={(event) => { selectedComment.height = event.target.value; @@ -7764,7 +7812,7 @@ const AngularWorkflow = (props) => { }} /> -
+
Width
{ }} fullWidth color="primary" + placeholder={"200"} defaultValue={selectedComment.width} onChange={(event) => { selectedComment.width = event.target.value; @@ -7809,6 +7858,7 @@ const AngularWorkflow = (props) => { }} fullWidth color="primary" + placeholder={"#1f2023"} defaultValue={selectedComment["backgroundcolor"]} onChange={(event) => { selectedComment.backgroundcolor = event.target.value; @@ -7816,7 +7866,7 @@ const AngularWorkflow = (props) => { }} />
-
+
Text Color
{ }} fullWidth color="primary" + placeholder={"#ffffff"} defaultValue={selectedComment.color} onChange={(event) => { selectedComment.color = event.target.value; @@ -9550,7 +9601,6 @@ const AngularWorkflow = (props) => {
); } else if (Object.getOwnPropertyNames(selectedComment).length > 0) { - console.log("Returning comment field"); return (
@@ -9675,21 +9725,57 @@ const AngularWorkflow = (props) => { } return ( - { - handleReactJsonClipboard(copy); - }} - onSelect={(select) => { - HandleJsonCopy(showResult, select, "exec"); - console.log("SELECTED!: ", select); - }} - name={"Execution Argument"} - /> - ); +
+ { + setSelectedResult({ + "action": { + "label": "Execution Argument", + "name": "Execution Argument", + "large_image": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAJYAAACOCAMAAADkWgEmAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAAWlBMVEX4Wj69TDgmKCvkVTwlJyskJiokJikkJSkjJSn4Ykf+6+f5h3L////8xLr5alH/9fT7nYz4Wz/919H5cVn/+vr8qpv4XUL94d35e2X//v38t6v4YUbkVDy8SzcVIzHLAAAAAWJLR0QMgbNRYwAAAAlwSFlzAAARsAAAEbAByCf1VAAAAAd0SU1FB+QGGgsvBZ/GkmwAAAFKSURBVHja7dlrTgMxDEXhFgpTiukL2vLc/zbZQH5N7MmReu4KPmlGN4m9WgGzfhgtaOZxM1rQztNoQDvPowHtTKMB7WxHA2TJkiVLlixIZMmSRYgsWbIIkSVLFiGyZMkiRNZirBcma/eKZEW87ZGsOBxPRFbE+R3Jio/LlciKuH0iWfH1/UNkRSR3RRYruSvyWKldkcjK7IpUVl5X5LLSuiKbldQV6aycrihgZXRFCau/K2pY3V1RxersijJWX1cUsnq6opLV0RW1rNldUc2a2RXlrHldsQBrTlfcLwv5EZm/PLIgkHXKPHyQRzXzYoO8BjIvzcgnBvJBxny+Ih/7zNEIcpDEHLshh5TIkS5zAI5cFzCXK8hVFHNxh1xzQpfC0BV6XWTJkkWILFmyCJElSxYhsmTJIkSWLFmEyJIlixBZsmQB8stk/U3/Yb49pVcDMg4AAAAldEVYdGRhdGU6Y3JlYXRlADIwMjAtMDYtMjZUMTE6NDc6MDUrMDI6MDD8QCPmAAAAJXRFWHRkYXRlOm1vZGlmeQAyMDIwLTA2LTI2VDExOjQ3OjA1KzAyOjAwjR2bWgAAAABJRU5ErkJggg==", + }, + "result": validate.valid ? JSON.stringify(validate.result) : validate.result, + "status": "SUCCESS" + }) + setCodeModalOpen(true); + }} + > + + + + + {/* + + */} + + { + handleReactJsonClipboard(copy); + }} + displayDataTypes={false} + onSelect={(select) => { + HandleJsonCopy(validate.result, select, "exec"); + }} + name={"Execution Argument"} + /> +
+ ) } return ( @@ -9879,10 +9965,124 @@ const AngularWorkflow = (props) => { /* Copy the text inside the text field */ document.execCommand("copy"); console.log("COPYING!"); + alert.info("Copied value to clipboard.") } else { console.log("Couldn't find element ", elementName); } - }; + } + + // Not used because of issue with state updates. + const ShowReactJsonField = (props) => { + const { validate, jsonValue, collapsed, label, autocomplete } = props + + const [parsedCollapse, setParsedCollapse] = React.useState(collapsed) + const [open, setOpen] = React.useState(false); + const [anchorPosition, setAnchorPosition] = React.useState({ + top: 750, + left: 16, + }); + + const isFirstRender = React.useRef(true) + useEffect(() => { + console.log("IN useeffectt "+autocomplete) + + if (isFirstRender.current) { + isFirstRender.current = false; + console.log("IN useeffectt (2)"+collapsed) + return; + } + }) + /* + componentWillUpdate = (nextProps, nextState) => { + console.log(nextProps, nextState) + //nextState.value = nextProps.a + nextProps.b; + } + */ + + const jsonRef = React.useRef() + + return ( + + { + console.log("FIELD: ", field) + }} + enableClipboard={(copy) => { + handleReactJsonClipboard(copy); + }} + displayDataTypes={false} + onClick={(event) => { + const pos = { + top: event.screenX, + left: event.screenY, + } + + console.log("POS CLICK: ", pos) + + setAnchorPosition(pos) + }} + onSelect={(select) => { + setOpen(true) + + setTimeout(() => { + setOpen(false) + }, 2500) + + //setAnchorPosition({ + // top: 300, + // right: 300, + //}) + //setAnchorEl(jsonRef.current) + HandleJsonCopy(jsonValue, select, autocomplete); + console.log("SELECTED!: ", select); + }} + name={label} + /> + {anchorPosition !== null ? + { + setAnchorPosition({ + top: 750, + left: 16, + }) + }} + disableRestoreFocus + > + + Copying + + + : null} + + ) + } + + const ShowCopyingTooltip = () => { + const [showCopying, setShowCopying] = React.useState(true) + + if (!showCopying) { + return false + } + + return ( + +
+ + ) + } const executionModal = ( { color="primary" title="Rerun workflow" placement="top" - style={{ zIndex: 10011 }} + style={{ zIndex: 50000}} >
{validate.valid ? ( - { - handleReactJsonClipboard(copy); - }} - displayDataTypes={false} - onSelect={(select) => { - HandleJsonCopy(showResult, select, data.action.label); - console.log("SELECTED!: ", select); - }} - name={"Results for " + data.action.label} - /> + {/* + + */} + + { + handleReactJsonClipboard(copy); + }} + displayDataTypes={false} + onSelect={(select) => { + HandleJsonCopy(showResult, select, data.action.label); + console.log("SELECTED!: ", select); + }} + name={"Results for " + data.action.label} + /> + ) : (
{ console.log(event.srcElement); if (!dragging) { console.log("START"); - setDragging(true); + //setDragging(true); } }} disabled={draggingDisabled} @@ -10701,7 +10907,7 @@ const AngularWorkflow = (props) => { { { Status {selectedResult.status}
{validate.valid ? ( - { - handleReactJsonClipboard(copy); - }} - onSelect={(select) => { - HandleJsonCopy( - JSON.stringify(validate.result), - select, - selectedResult.action.label - ); - }} - name={"Results for " + selectedResult.action.label} - /> + { + handleReactJsonClipboard(copy); + }} + displayDataTypes={false} + onSelect={(select) => { + HandleJsonCopy(validate.result, select, selectedResult.action.label); + }} + name={"Results for " + selectedResult.action.label} + /> ) : (
Result  diff --git a/frontend/src/views/LoginPage.jsx b/frontend/src/views/LoginPage.jsx index 2a51187e..2c1bd14f 100644 --- a/frontend/src/views/LoginPage.jsx +++ b/frontend/src/views/LoginPage.jsx @@ -407,7 +407,7 @@ const LoginDialog = (props) => {
{MFAField === true ? (
- 5-factor code + 2-factor code