From a406c8eaa76fd925a42f6ceda7ea836606802788 Mon Sep 17 00:00:00 2001 From: Frikky Date: Wed, 26 Feb 2025 00:34:54 +0100 Subject: [PATCH] Updated minor multi tenant issues --- frontend/src/components/CacheView.jsx | 350 +++++++++++++++--- frontend/src/components/EditWorkflow.jsx | 16 +- frontend/src/components/Files.jsx | 2 +- .../components/WorkflowValidationTimeline.jsx | 26 +- frontend/src/views/AngularWorkflow.jsx | 92 +++-- 5 files changed, 394 insertions(+), 92 deletions(-) diff --git a/frontend/src/components/CacheView.jsx b/frontend/src/components/CacheView.jsx index 9c3e1a51..5ff867d6 100644 --- a/frontend/src/components/CacheView.jsx +++ b/frontend/src/components/CacheView.jsx @@ -24,11 +24,14 @@ import { Checkbox, MenuItem, DialogContent, + FormControl, + Select, } from "@mui/material"; import { Link as LinkIcon, AutoFixHigh as AutoFixHighIcon, + AutoFixNormal as AutoFixNormalIcon, Edit as EditIcon, FileCopy as FileCopyIcon, SelectAll as SelectAllIcon, @@ -50,8 +53,8 @@ import { Business as BusinessIcon, Visibility as VisibilityIcon, VisibilityOff as VisibilityOffIcon, - CheckBox, - Key, + Clear as ClearIcon, + Add as AddIcon, } from "@mui/icons-material"; import { validateJson, } from "../views/Workflows.jsx"; import { Context } from "../context/ContextApi.jsx"; @@ -91,15 +94,40 @@ const CacheView = memo((props) => { const [showDistributionPopup, setShowDistributionPopup] = useState(false); const [selectedSubOrg, setSelectedSubOrg] = useState([]); const [selectedCacheKey, setSelectedCacheKey] = useState(""); + + // Direct category migration from ../components/Files.jsx + const [selectAllChecked, setSelectAllChecked] = React.useState(false) + const [renderTextBox, setRenderTextBox] = React.useState(false); + const [fileCategories, setFileCategories] = React.useState(["default"]); + const [selectedCategory, setSelectedCategory] = React.useState("default"); + const [selectedFileId, setSelectedFileId] = React.useState(""); + const [updateToThisCategory, setUpdateToThisCategory] = useState("") + const [showFileCategoryPopup, setShowFileCategoryPopup] = React.useState(false); + const [selectedFiles, setSelectedFiles] = useState([]); + useEffect(() => { - if(orgId?.length >0){ - listOrgCache(orgId); + if (orgId?.length > 0) { + listOrgCache(orgId, selectedCategory) } - }, [orgId]); + }, [orgId]) + + const handleKeyDown = (event) => { + if (event.key === 'Enter') { + fileCategories.push(event.target.value); + setSelectedCategory(event.target.value); + setRenderTextBox(false); + } + + if (event.key === 'Escape'){ // not working for some reasons + console.log('escape pressed') + setRenderTextBox(false); + } + } - const listOrgCache = (orgId) => { - fetch(globalUrl + `/api/v1/orgs/${orgId}/list_cache`, { + const listOrgCache = (orgId, category) => { + const url = `${globalUrl}/api/v1/orgs/${orgId}/list_cache${category !== undefined ? `?category=${category.replaceAll(" ", "_")}` : ""}` + fetch(url, { method: "GET", headers: { "Content-Type": "application/json", @@ -119,6 +147,19 @@ const CacheView = memo((props) => { if (responseJson.success === true) { setListCache(responseJson.keys); setCachedLoaded(true); + + if (fileCategories.length === 1 && fileCategories[0] === "default") { + var newcategories = ["default"] + for (var key in responseJson.keys) { + if (responseJson.keys[key].category !== undefined && responseJson.keys[key].category !== null && responseJson.keys[key].category !== "" && !fileCategories.includes(responseJson.keys[key].category)) { + newcategories.push(responseJson.keys[key].category); + } + } + + console.log("CATEGORIES: ", newcategories) + + setFileCategories(newcategories) + } } if (responseJson.cursor !== undefined && responseJson.cursor !== null && responseJson.cursor !== "") { @@ -132,15 +173,12 @@ const CacheView = memo((props) => { const deleteCache = (orgId, key) => { - //toast("Attempting to delete Cache"); - - // method: "DELETE", const method = "POST" - //const url = `${globalUrl}/api/v1/orgs/${orgId}/cache/${key}` const url = `${globalUrl}/api/v1/orgs/${orgId}/delete_cache` const parsed = { "org_id": orgId, "key": key, + "category": selectedCategory === "" || selectedCategory === "default" ? "" : selectedCategory, } fetch(url, { @@ -155,7 +193,7 @@ const CacheView = memo((props) => { if (response.status === 200) { toast("Successfully deleted Cache"); setTimeout(() => { - listOrgCache(orgId); + listOrgCache(orgId, selectedCategory) }, 1000); } else { toast("Failed deleting Cache. Does it still exist?"); @@ -167,7 +205,12 @@ const CacheView = memo((props) => { }; const editOrgCache = (orgId) => { - const cache = { key: dataValue.key , value: value }; + const cache = { + key: dataValue.key, + value: value, + category: selectedCategory, + } + setCacheInput([cache]); fetch(globalUrl + `/api/v1/orgs/${orgId}/set_cache`, { @@ -191,7 +234,7 @@ const CacheView = memo((props) => { .then((responseJson) => { setAddCache(responseJson); toast("Cache Edited Successfully!"); - listOrgCache(orgId); + listOrgCache(orgId, selectedCategory); setModalOpen(false); }) .catch((error) => { @@ -200,9 +243,13 @@ const CacheView = memo((props) => { }; const addOrgCache = (orgId) => { - const cache = { key: key, value: value }; + const cache = { + key: key, + value: value, + category: selectedCategory, + } + setCacheInput([cache]); - console.log("cache input:", cacheInput) fetch(globalUrl + `/api/v1/orgs/${orgId}/set_cache`, { @@ -224,8 +271,8 @@ const CacheView = memo((props) => { }) .then((responseJson) => { setAddCache(responseJson); - toast("New key Added Successfully!"); - listOrgCache(orgId); + toast("New key added Successfully!"); + listOrgCache(orgId, selectedCategory); setModalOpen(false); }) .catch((error) => { @@ -243,7 +290,9 @@ const CacheView = memo((props) => { setValue(JSON.stringify(parsedjson, null, 2)) } catch (e) { console.log("Error parsing JSON: ", e) - //return JSON.stringify(inputvalue); + toast.info("Invalid JSON.", { + autoClose: 1500, + }) } } @@ -306,13 +355,14 @@ const CacheView = memo((props) => { > - { editCache ? "Edit Key" : "Add Key" } + { editCache ? "Edit Key" : "Add Key"}{selectedCategory === "" || selectedCategory === "default" ? "" : ` in category '${selectedCategory}'`}
Key { autoFixJson(value) }} > - +
@@ -436,46 +486,49 @@ const CacheView = memo((props) => { const changeDistribution = (id, selectedSubOrg) => { - editFileConfig(id, [...new Set(selectedSubOrg)]) + editFileConfig(id, [...new Set(selectedSubOrg)], selectedCategory) } - const editFileConfig = (id, selectedSubOrg, cacheKey) => { - const data = { - Key: id, - action: "suborg_distribute", - selected_suborgs: selectedSubOrg, - } - console.log("data: ", data); - - const url = `${globalUrl}/api/v1/orgs/${orgId}/cache/config`; - - fetch(url, { - mode: "cors", - method: "POST", - body: JSON.stringify(data), - credentials: "include", - crossDomain: true, - withCredentials: true, - headers: { - "Content-Type": "application/json; charset=utf-8", - }, - }) - .then((response) => - response.json().then((responseJson) => { - if (responseJson["success"] === false) { - toast("Failed overwriting datastore"); - } else { - toast("Successfully updated datastore!"); - setTimeout(() => { - listOrgCache(orgId); - setShowDistributionPopup(false); - }, 1000); - } - }) - ) - .catch((error) => { - toast("Err: " + error.toString()); - }); + + const editFileConfig = (id, selectedSubOrg, category) => { + const data = { + Key: id, + action: "suborg_distribute", + selected_suborgs: selectedSubOrg, + category: category === undefined || category === "" || category === "default" ? "" : category, + } + + console.log("data: ", data); + + const url = `${globalUrl}/api/v1/orgs/${orgId}/cache/config`; + + fetch(url, { + mode: "cors", + method: "POST", + body: JSON.stringify(data), + credentials: "include", + crossDomain: true, + withCredentials: true, + headers: { + "Content-Type": "application/json; charset=utf-8", + }, + }) + .then((response) => + response.json().then((responseJson) => { + if (responseJson["success"] === false) { + toast("Failed overwriting datastore"); + } else { + toast("Successfully updated datastore!"); + setTimeout(() => { + listOrgCache(orgId, selectedCategory); + setShowDistributionPopup(false); + }, 1000); + } + }) + ) + .catch((error) => { + toast("Err: " + error.toString()); + }); }; @@ -579,7 +632,7 @@ const CacheView = memo((props) => {
-

Shuffle Datastore

+

Shuffle Datastore {selectedCategory === "" || selectedCategory === "default" ? "" : `- Category '${selectedCategory}'`}

Datastore is a permanent key-value database for storing data that can be used cross-workflow.
You can store anything from lists of IPs to complex configurations.  { style={{ marginLeft: 16, marginRight: 15, backgroundColor: isSelectedDataStore?"#2F2F2F":null, boxShadow: isSelectedDataStore ? "none":null,textTransform: isSelectedDataStore ? 'capitalize':null,borderRadius:isSelectedDataStore?4:null, width:isSelectedDataStore?81:null, height:isSelectedDataStore?40:null, }} variant="contained" color="primary" - onClick={() => listOrgCache(orgId)} + onClick={() => listOrgCache(orgId,selectedCategory)} > + + {fileCategories !== undefined && + fileCategories !== null && + fileCategories.length > 1 ? ( + + + { + setShowFileCategoryPopup(false) + }} + > + File Categories + + Please note that your selected files ({selectedFileId?.length}) will be moved to the {updateToThisCategory} category. + + + + + + + + ) : null} + +
+ {renderTextBox ? + + + + : + + + + } + + {renderTextBox && { + handleKeyDown(event); + if(event.key === 'Enter' && selectedFileId.length > 0){ + //setShowFileCategoryPopup(true) + setUpdateToThisCategory(event.target.value) + } + + }} + style={{ + height: 35, + width: 200, + marginTop: 0, + }} + InputProps={{ + style: { + color: "white", + height: 35, + fontSize: 16, + borderRadius: 4, + paddingTop: 0, + }, + }} + color="primary" + placeholder="Category name" + required + margin="dense" + defaultValue={""} + autoFocus + />}
{isSelectedDataStore? null : { ): listCache?.map((data, index) => { + var category = selectedCategory + if (selectedCategory === "default") { + category = "" + } + + if (data?.category === undefined && category === "") { + } else if (data?.category !== category) { + return null + } + var bgColor = isSelectedDataStore? "#212121":"#27292d"; if (index % 2 === 0) { bgColor = isSelectedDataStore? "#1A1A1A":"#1f2023"; diff --git a/frontend/src/components/EditWorkflow.jsx b/frontend/src/components/EditWorkflow.jsx index d961c6fc..a1687fc1 100644 --- a/frontend/src/components/EditWorkflow.jsx +++ b/frontend/src/components/EditWorkflow.jsx @@ -1144,14 +1144,26 @@ const EditWorkflow = (props) => {
+ - + + Publishing + + - Publishing is related to making the workflow itself public. When publishing a workflow, all the details (except sensitive info) become available to everyone. The details below will help a user understand this better. When a workflow is published, you keep the original, and a copy enters the workflow search, and is associated with your creator account, if you have one. You can always unpublish the workflow after. When ready to publish, click the three dots next to a workflow on the main workflow screen. After publishing, you can find it in the Shuffle search engine. + Publishing is related to making this workflow itself public. When publishing a workflow, all the details (except sensitive info) become available to anyone. The fields below will help a user and Shuffle's system understand your workflow better. When a workflow is published, you keep the original, and a copy enters the Shuffle workflow search, and is associated with your creator or partner account, if you have one. You can always unpublish the workflow after. When ready to publish, click the three dots next to a workflow on the main workflow page. + + You can always unpublish a workflow after. + { console.log('escape pressed') setRenderTextBox(false); } - } const changeDistribution = (id, selectedSubOrg) => { @@ -1051,6 +1050,7 @@ const Files = memo((props) => { ) : null} +
{renderTextBox ? diff --git a/frontend/src/components/WorkflowValidationTimeline.jsx b/frontend/src/components/WorkflowValidationTimeline.jsx index 14661ebf..b02b295d 100644 --- a/frontend/src/components/WorkflowValidationTimeline.jsx +++ b/frontend/src/components/WorkflowValidationTimeline.jsx @@ -28,7 +28,16 @@ import { validateJson, GetIconInfo } from "../views/Workflows.jsx"; import theme from "../theme.jsx"; const itemHeight = 24 -export const getParentNodes = (workflow, action) => { +export const getParentNodes = (workflow, action, count) => { + if (count === undefined) { + count = 0 + } + + // 50 levels of parent nodes + if (count > 50) { + return [] + } + if (action === undefined || action === null) { return [] } @@ -81,8 +90,7 @@ export const getParentNodes = (workflow, action) => { continue; } - // FIXME: This part is only handling first level, - // but needs to recurse + // FIXME: recursion var incomingEdges = [] for (var branchkey in workflow.branches) { const branch = workflow.branches[branchkey] @@ -90,13 +98,19 @@ export const getParentNodes = (workflow, action) => { continue } - // Go up in the levels + // FIXME: Go up in the levels + // This somehow creates infinite recursion for now, so + // we are skipping it. + // This function is also not used for cytoscape recursion, + // so it doesn't matter much (yet) + /* const parents = getParentNodes(workflow, { id: branch.source_id, - }) + }, count+1) if (parents.length > 0) { incomingEdges = incomingEdges.concat(parents) } + */ incomingEdges.push(branch) } @@ -215,8 +229,6 @@ const WorkflowValidationTimeline = (props) => { } } - //const parents = getParentNodes(workflow, action) - //console.log("PARENTS", key, parents) if (parents !== undefined && parents !== null && parents.length > 0) { const parentfound = parents.find((element) => element.id === startnodeId) if (parentfound !== undefined) { diff --git a/frontend/src/views/AngularWorkflow.jsx b/frontend/src/views/AngularWorkflow.jsx index bb46004d..ce607d45 100755 --- a/frontend/src/views/AngularWorkflow.jsx +++ b/frontend/src/views/AngularWorkflow.jsx @@ -574,7 +574,7 @@ const AngularWorkflow = (defaultprops) => { }, [editWorkflowModalOpen]) useEffect(() => { - if (selectedTrigger !== undefined && selectedTrigger.parameters !== undefined && selectedTrigger.parameters !== undefined && selectedTrigger.parameters.length > 1) { + if (selectedTrigger !== undefined && selectedTrigger?.parameters !== undefined && selectedTrigger?.parameters !== null && selectedTrigger?.parameters?.length > 1) { // Right now just setting for the subflow setSelectedTriggerValue(selectedTrigger?.parameters[1]?.value) } @@ -4916,7 +4916,7 @@ const AngularWorkflow = (defaultprops) => { if (!branchFound) { var relevantNodes = [] - const minDistance = 225 + const minDistance = 185 const draggedNode = event.target const allnodes = cy.nodes().jsons() for (var nodekey in allnodes) { @@ -5481,6 +5481,7 @@ const AngularWorkflow = (defaultprops) => { // https://stackoverflow.com/questions/16677856/cy-onselect-callback-only-once // onNodeClick const onNodeSelect = (event, newAppAuth) => { + // Forces all states to update at the same time, // Otherwise everything is SUPER slow @@ -5654,7 +5655,14 @@ const AngularWorkflow = (defaultprops) => { } ReactDOM.unstable_batchedUpdates(() => { + const selectedNodes = cy.$(':selected') if (data.isButton) { + if (selectedNodes?.length > 1) { + event.target.unselect() + //console.log(": ", selectedNodes.length) + return + } + if (data.buttonType === "suggestion") { if (cy === undefined) { console.log("Cy not defined yet") @@ -5894,6 +5902,14 @@ const AngularWorkflow = (defaultprops) => { } if (data.type === "ACTION") { + if (selectedNodes?.length > 1) { + console.log("Unselecting ACTION due to multiple nodes selected") + setSelectedAction({}) + setSelectedApp({}) + setSelectedComment({}) + return + } + setSelectedComment({}) // FIXME: is this what is mapping it an actual action in the workflow? wtf? @@ -6241,6 +6257,14 @@ const AngularWorkflow = (defaultprops) => { setSelectedActionEnvironment(env); } } else if (data.type === "TRIGGER") { + if (selectedNodes?.length > 1) { + console.log("Unselecting ACTION due to multiple nodes selected") + setSelectedAction({}) + setSelectedApp({}) + setSelectedComment({}) + return + } + setSelectedComment({}) if (workflow.triggers === null) { workflow.triggers = [] @@ -6442,6 +6466,14 @@ const AngularWorkflow = (defaultprops) => { //setSelectedActionEnvironment(data.env) }, 25) } else if (data.type === "COMMENT") { + if (selectedNodes?.length > 1) { + console.log("Unselecting ACTION due to multiple nodes selected") + setSelectedAction({}) + setSelectedApp({}) + setSelectedComment({}) + return + } + setSelectedComment(data); } else { toast("Can't handle node type " + data.type); @@ -7450,26 +7482,21 @@ const AngularWorkflow = (defaultprops) => { }; const handlePaste = (event) => { - //console.log("EV: ", event) if ( event.path !== undefined && event.path !== null && event.path.length > 0 ) { - //console.log("PATH: ", event.path[0]) if (event.path[0].localName !== "body") { - //console.log("Skipping because body is not targeted") return; } } - //console.log("PATH2: ", event.target) if ( event.target !== undefined && event.target !== null ) { if (event.target.localName !== "body") { - //console.log("Skipping because body is not targeted") return; } } @@ -7479,12 +7506,10 @@ const AngularWorkflow = (defaultprops) => { const clipboard = (event.originalEvent || event).clipboardData.getData( "text/plain" ); - //console.log("Text: ", clipboard) - //window.document.execCommand('insertText', false, text); - // + try { + const allnodes = cy.nodes().jsons() var parsedjson = JSON.parse(clipboard); - // Check if array if (!Array.isArray(parsedjson)) { console.log("Not array! Adding to array.") parsedjson = [parsedjson] @@ -7492,7 +7517,6 @@ const AngularWorkflow = (defaultprops) => { for (let jsonkey in parsedjson) { var item = parsedjson[jsonkey]; - console.log("Adding: ", item); if (item.data === undefined || item.data === null) { console.log("Appending from here") @@ -7512,13 +7536,31 @@ const AngularWorkflow = (defaultprops) => { item.data.isStartNode = false } + // Find a cy.data() label with the same name + const foundnodes = allnodes.filter((data) => { + //console.log("COMP: ", data.data.label, item.data.label) + if (data.data.label === undefined || data.data.label === null) { + return false + } + + return data.data.label === item.data.label + }) + + if (foundnodes !== undefined && foundnodes !== null && foundnodes.length > 0) { + // Weird naming copy lol + item.data.label = item.data.label + "_copy_" + allnodes.length + } + item.data.id = uuidv4() cy.add({ group: item.group, data: item.data, - position: item.position, - }); + position: { + x: item.position.x+20, + y: item.position.y+20, + }, + }) } } catch (e) { console.log("Error pasting: ", e); @@ -9591,7 +9633,12 @@ const AngularWorkflow = (defaultprops) => { console.log("Error fitting cytoscape (4): ", error) } + cy.on("boxselect", "node", (e) => { + e.preventDefault() + e.stopPropagation() + + console.log("BOXSELECT: ", e.boxSelectElements) if (e.target.data("isButton") || e.target.data("isDescriptor") || e.target.data("isSuggestion")) { e.target.unselect(); } @@ -9601,10 +9648,15 @@ const AngularWorkflow = (defaultprops) => { cy.on("boxstart", (e) => { console.log("START"); + e.preventDefault() + e.stopPropagation() }); cy.on("boxend", (e) => { - console.log("END: ", cy) + e.preventDefault() + e.stopPropagation() + + console.log("END: ", e.target, cy) var cydata = cy.$(":selected").jsons(); if (cydata !== undefined && cydata !== null && cydata.length > 0) { // Unselect all nodes @@ -17404,11 +17456,11 @@ const AngularWorkflow = (defaultprops) => { selectedTrigger.status === "running" } defaultValue={ - selectedTrigger?.parameters === undefined || selectedTrigger?.parameters === null || selectedTrigger?.parameters?.length === 0 ? isCloud || selectedTrigger?.environment === "cloud" ? "*/25 * * * *" : "60" : selectedTrigger.parameters[0]?.value + selectedTrigger?.parameters === undefined || selectedTrigger?.parameters === null || selectedTrigger?.parameters?.length === 0 ? isCloud || selectedTrigger?.environment === "cloud" ? "*/25 * * * *" : "60" : selectedTrigger?.parameters[0]?.value } color="primary" placeholder={ - selectedTrigger.parameters === undefined ? isCloud || selectedTrigger?.environment === "cloud" ? "*/25 * * * *" : "60" : selectedTrigger.parameters[0]?.value + selectedTrigger.parameters === undefined || selectedTrigger?.parameters === null || selectedTrigger?.parameters?.length === 0 ? isCloud || selectedTrigger?.environment === "cloud" ? "*/25 * * * *" : "60" : selectedTrigger?.parameters[0]?.value } onBlur={(e) => { setTriggerCronWrapper(e.target.value); @@ -21602,10 +21654,8 @@ const AngularWorkflow = (defaultprops) => { const checked = validateJson(data.value.trim()) if (data.name === "shuffle_action_logs" && data.value !== undefined && data.value !== null && data.value.length > 0 && data.value.includes("add env SHUFFLE_LOGS_DISABLED")) { - data.value = `Logs for this action are not available without an onprem environment with the SHUFFLE_LOGS_DISABLED environment variable set to false: SHUFFLE_LOGS_DISABLED=false. Logs are enabled by default, except in scale mode.` - /* return ( -
+
{ Action Logs + More log details for this action are not available without an onprem environment with the SHUFFLE_LOGS_DISABLED environment variable set to false: SHUFFLE_LOGS_DISABLED=false. Logs are enabled by default, except in scale mode.
) - */ } var showlink = false