diff --git a/frontend/src/components/AppGrid.jsx b/frontend/src/components/AppGrid.jsx index 11e25f0b..f15456c6 100644 --- a/frontend/src/components/AppGrid.jsx +++ b/frontend/src/components/AppGrid.jsx @@ -79,6 +79,7 @@ const AppGrid = (props) => { const [formMail, setFormMail] = React.useState(""); const [message, setMessage] = React.useState(""); const [formMessage, setFormMessage] = React.useState(""); + const [deactivatedIndexes, setDeactivatedIndexes] = React.useState([]); const buttonStyle = { borderRadius: 30, @@ -352,7 +353,7 @@ const AppGrid = (props) => { if (responseJson.success === false) { toast.error(responseJson.reason); } else { - toast.success(`App ${type}d Successfully!`); + //toast.success(`App ${type}d Successfully!`); if (type === 'activate') { setAllActivatedAppIds(prev => [...prev, data.objectID]); setIsAnyAppActivated(true); @@ -395,7 +396,7 @@ const AppGrid = (props) => { {!isLoading ? (
{hits.length === 0 && searchQuery.length >= 0 && showNoAppFound ? ( - No App Found + No Apps Found ) : (
{ Filter By + + + +
)} @@ -1722,6 +1727,10 @@ const AppGrid = (props) => { ? `/apps/${data.id}` : `https://shuffler.io/apps/${data.id}`; + if (data.name === "" && data.id === "") { + return null + } + return ( { width: 230, textAlign: 'start', marginLeft: 8, - color: "rgba(158, 158, 158, 1)" + color: "rgba(158, 158, 158, 1)", + display: "flex", }} > - {data.tags && - data.tags.map((tag, tagIndex) => ( - - {normalizedString(tag)} - {tagIndex < data.tags.length - 1 ? ", " : ""} - - ))} -
+
+ {data.generated !== true ? +
+ {data.tags && + data.tags.slice(0,2).map((tag, tagIndex) => ( + + {normalizedString(tag)} + {tagIndex < data.tags.length - 1 ? ", " : ""} + + )) + } +
+ : null} +
+ {currTab === 1 && !deactivatedIndexes.includes(index) && mouseHoverIndex === index && data.generated === true ? + + : null} + + {/* )} */} @@ -1946,6 +2009,7 @@ const AppGrid = (props) => { selectedOptionOfCreatedWith={selectedOptionOfCreatedWith} /> )} + { setSelectedTagsForUserAndOrgApps={setSelectedTagsForUserAndOrgApps} setSelectedOptionOfCreatedWith={setSelectedOptionOfCreatedWith} /> + diff --git a/frontend/src/components/Billing.jsx b/frontend/src/components/Billing.jsx index 26b88ca5..f7598852 100644 --- a/frontend/src/components/Billing.jsx +++ b/frontend/src/components/Billing.jsx @@ -26,7 +26,8 @@ import { Tooltip, DialogContentText, DialogActions, - LinearProgress + LinearProgress, + Slider } from "@mui/material"; import { useNavigate, Link, json } from "react-router-dom"; @@ -880,6 +881,305 @@ const Billing = (props) => { ) } + const ConsultationManagement = (props) => { + const { globalUrl, userdata, selectedOrganization, } = props; + + const [inputHour, setInputHour] = React.useState( + selectedOrganization.Billing && + selectedOrganization.Billing.Consultation && + selectedOrganization.Billing.Consultation.hours !== undefined && + selectedOrganization.Billing.Consultation.hours !== "" + ? selectedOrganization.Billing.Consultation.hours + : 0 + ); + + const [inputMinutes, setInputMinutes] = React.useState( + selectedOrganization.Billing && + selectedOrganization.Billing.Consultation && + selectedOrganization.Billing.Consultation.minutes !== undefined && + selectedOrganization.Billing.Consultation.minutes !== "" + ? selectedOrganization.Billing.Consultation.minutes + : 0 + ); + + const [editConsultation, setEditConsultation] = React.useState(false); + const [openUpgradePlan, setOpenUpgradePlan] = React.useState(false); + const [consultationHours, setConsultationHours] = React.useState(5); + const [message, setMessage] = React.useState(""); + const [hovered, setHovered] = React.useState(false) + + const formatedHours = String(inputHour).padStart(2, "0") + const formatedMinutes = String(inputMinutes).padStart(2, "0") + + const handleHourChange = (event) => { + setInputHour(parseInt(event.target.value, 10)); + }; + + const handleMinuteChange = (event) => { + setInputMinutes(parseInt(event.target.value, 10)); + }; + const toggleEditMode = () => { + setEditConsultation(!editConsultation); + }; + + const handleCancel = () => { + setEditConsultation(false); + }; + + const handleSave = () => { + + toast("Saving consultation hours. Please wait.") + + const url = `${globalUrl}/api/v1/orgs/${selectedOrganization.id}`; + const data = { + org_id: selectedOrganization.id, + Billing: { + Consultation: { + hours: String(inputHour), + minutes: String(inputMinutes), + }, + } + }; + + fetch(url, { + body: JSON.stringify(data), + mode: "cors", + method: "POST", + credentials: "include", + crossDomain: true, + withCredentials: true, + headers: { + "Content-Type": "application/json; charset=utf-8", + }, + }) + .then((response) => { + if (response.status !== 200) { + console.log("Error in response"); + } + return response.json(); + }) + .then((responseJson) => { + console.log("Response from consultation save: ", responseJson); + if (responseJson.success === true) { + toast.success("Consultation hours saved successfully"); + setEditConsultation(false); + } else { + toast.error("Failed saving consultation hours."); + } + }) + .catch((error) => { + console.log("Error: ", error); + }); + } + + const handleUpgradeConsultation = () => { + + toast("Sending request for consultation hours. Please wait.") + + const url = `${globalUrl}/api/v1/orgs/${selectedOrganization.id}/consultation`; + const data = { + org_id: selectedOrganization.id, + consultationHours: String(consultationHours), + message: message, + }; + + fetch(url, { + body: JSON.stringify(data), + mode: "cors", + method: "POST", + credentials: "include", + crossDomain: true, + withCredentials: true, + headers: { + "Content-Type": "application/json; charset=utf-8", + }, + }) + .then((response) => { + if (response.status !== 200) { + console.log("Error in response"); + } + return response.json(); + }) + .then((responseJson) => { + console.log("Response from consultation save: ", responseJson); + if (responseJson.success === true) { + toast.success("Thank you for your request. We will get back to you soon."); + setOpenUpgradePlan(false); + setEditConsultation(false); + } else { + toast.error("Failed sending consultation hours request. Please try again later."); + } + }) + .catch((error) => { + console.log("Error: ", error); + }); + } + + var newPaperstyle = JSON.parse(JSON.stringify(paperStyle)) + + return ( + setHovered(true)} + onMouseLeave={() => setHovered(false)}> + + Professional Services + + + + Consultation & Management + +
+ + Current Plan includes total {inputHour} hours and {inputMinutes} minutes of consultation and management by our experts. + +
+ {editConsultation ? + <> + + : + + + : + + {`${formatedHours}h:${formatedMinutes}m`} + } +
+ {userdata.support === true ? +
+ {editConsultation ? ( + + ) : ( + + )} + {editConsultation && } +
+ : null} + + Features + +
    +
  • + + Debug/Create workflows with our experts + +
  • +
  • + + Ask questions and get help with your workflows and integrations by our experts + +
  • +
+
+ + setOpenUpgradePlan(false)} + fullWidth + style={{ display: 'flex', justifyContent: 'center', alignItems: 'center' }} + PaperProps={{ + style: { + width: 500, + margin: 0, + } + }} + > + + Upgrade Consultation Plan + + + + Enter the total hours of consultation you want to include in your plan. + +
+ setConsultationHours(val)} + aria-labelledby="continuous-slider" + step={5} + min={5} + max={50} + style={{ width: '80%', color: theme.palette.primary.main }} // Adding primary color for better visibility + marks + valueLabelDisplay="auto" + /> +
+ + If you have any additional requirements or questions, please leave a message below. + + setMessage(e.target.value)} + /> + +
+
+
+ ) + } const addDealModal = ( { {userdata.support === true ?
For sales: Create  - - New Cloud Contract + + EU contract  or  - - New Onprem Contract + + NOT EU contract   -   @@ -1336,7 +1636,7 @@ const Billing = (props) => {   -   - Sales Process + Sales Process (old)
: @@ -1405,6 +1705,12 @@ const Billing = (props) => { /> : null} + {isCloud && billingInfo.subscription !== undefined && billingInfo.subscription !== null ? isChildOrg ? null : + : null} {isCloud && selectedOrganization.subscriptions !== undefined && diff --git a/frontend/src/components/EditWorkflow.jsx b/frontend/src/components/EditWorkflow.jsx index e2443664..46be4138 100644 --- a/frontend/src/components/EditWorkflow.jsx +++ b/frontend/src/components/EditWorkflow.jsx @@ -6,6 +6,7 @@ import UsecaseSearch from "../components/UsecaseSearch.jsx" import WorkflowGrid from "../components/WorkflowGrid.jsx" import dayjs from 'dayjs'; import WorkflowTemplatePopup from "./WorkflowTemplatePopup.jsx"; +import WorkflowValidationTimeline from "../components/WorkflowValidationTimeline.jsx" import { Badge, @@ -206,6 +207,16 @@ const EditWorkflow = (props) => { Workflows can be built from scratch, or from templates. Usecases can help you discover next steps, and you can search for them directly. Learn more + +
+ +
+ {showUpload === true ?
@@ -957,7 +968,7 @@ const EditWorkflow = (props) => { }} > {showMoreClicked ? : } - {showMoreClicked ? "Collapse": "Expand"} + {showMoreClicked ? "Less Options": "More Options"}
diff --git a/frontend/src/components/LicencePopup.jsx b/frontend/src/components/LicencePopup.jsx index 1c12d7fd..014fa46f 100644 --- a/frontend/src/components/LicencePopup.jsx +++ b/frontend/src/components/LicencePopup.jsx @@ -515,16 +515,16 @@ const LicencePopup = (props) => { } useEffect(() => { - console.log("New variant: ", shuffleVariant) + console.log("New variant: ", shuffleVariant) - if (shuffleVariant === 1) { - setCalculatedCost("$600") - setSelectedValue(8) - } else { - setCalculatedCost("$540") - setSelectedValue(300) - } - }, [shuffleVariant]) + if (shuffleVariant === 1) { + setCalculatedCost("$960") + setSelectedValue(8) + } else { + setCalculatedCost("$960") + setSelectedValue(300) + } + }, [shuffleVariant]) if (typeof window === 'undefined' || window.location === undefined) { return null @@ -680,7 +680,7 @@ const LicencePopup = (props) => { color: "white", } - + console.log("Priceitem: ", shuffleVariant) const isLoggedInHandler = () => { if (calculatedCost === payasyougo) { handlePayasyougo(props.userdata) @@ -690,7 +690,7 @@ const LicencePopup = (props) => { const priceItem = window.location.origin === "https://shuffler.io" ? shuffleVariant === 0 ? "app_executions" : "cores" : - shuffleVariant === 0 ? "price_1MROFrDzMUgUjxHShcSxgHO1" : "price_1NXjQqDzMUgUjxHSg690R4FP" + shuffleVariant === 0 ? "price_1PWI5zDzMUgUjxHSKkz0fGdN" : "price_1NXjQqDzMUgUjxHSg690R4FP" const successUrl = `${window.location.origin}/admin?admin_tab=billing&payment=success` const failUrl = `${window.location.origin}/pricing?admin_tab=billing&payment=failure` diff --git a/frontend/src/components/NewHeader.jsx b/frontend/src/components/NewHeader.jsx index 12bc756a..fa626601 100644 --- a/frontend/src/components/NewHeader.jsx +++ b/frontend/src/components/NewHeader.jsx @@ -517,12 +517,11 @@ const Header = (props) => { window.location.href = responseJson["url"] return }, 2000) - } - + } else { setTimeout(() => { window.location.reload() }, 2000); - + } toast("Successfully changed active organization - refreshing!"); } else { if (responseJson.reason !== undefined && responseJson.reason !== null && responseJson.reason.length > 0) { @@ -1547,7 +1546,7 @@ const Header = (props) => {
{/* Shuffle 1.4.0 is out! Read more about  */} - Shuffle now offers  + Early Success! More  {/* { ReactGA.event({ @@ -1596,10 +1595,11 @@ const Header = (props) => { navigate("/training") - }} style={{ cursor: "pointer", textDecoration: "none", color: "rgba(255,255,255,0.8)" }}> - Public Training! + }} style={{ cursor: "pointer", textDecoration: "none", fontWeight:600, color: "rgba(255,255,255,0.8)" }}> + Public Trainings +  Ahead! { setShowTopbar(false) diff --git a/frontend/src/components/OrgHeaderexpanded.jsx b/frontend/src/components/OrgHeaderexpanded.jsx index 4cfbd690..58d1a244 100644 --- a/frontend/src/components/OrgHeaderexpanded.jsx +++ b/frontend/src/components/OrgHeaderexpanded.jsx @@ -1,516 +1,517 @@ -import React, { useEffect } from "react"; - -import { makeStyles } from "@mui/styles"; -import theme from '../theme.jsx'; -import { toast } from "react-toastify" -import Chip from '@mui/material/Chip'; -import Stack from '@mui/material/Stack'; -import SubflowSuggestions from "../components/SubflowSuggestions.jsx"; - -import { - FormControl, - InputLabel, - Paper, - OutlinedInput, - Checkbox, - Card, - Tooltip, - FormControlLabel, - Typography, - Switch, - Select, - MenuItem, - Divider, - TextField, - Button, - Tabs, - Tab, - Grid, - IconButton, - Autocomplete, - Dialog, - DialogTitle, - DialogActions, - DialogContent, - Box -} from "@mui/material"; - -import { - ExpandLess as ExpandLessIcon, - ExpandMore as ExpandMoreIcon, - Save as SaveIcon, -} from "@mui/icons-material"; - -const useStyles = makeStyles({ - notchedOutline: { - borderColor: "#f85a3e !important", - }, -}) - -const OrgHeaderexpanded = (props) => { - const { - userdata, - selectedOrganization, - setSelectedOrganization, - globalUrl, - isCloud, - adminTab, +import React, { useEffect } from "react"; + +import { makeStyles } from "@mui/styles"; +import theme from '../theme.jsx'; +import { toast } from "react-toastify" +import Chip from '@mui/material/Chip'; +import Stack from '@mui/material/Stack'; +import SubflowSuggestions from "../components/SubflowSuggestions.jsx"; + +import { + FormControl, + InputLabel, + Paper, + OutlinedInput, + Checkbox, + Card, + Tooltip, + FormControlLabel, + Typography, + Switch, + Select, + MenuItem, + Divider, + TextField, + Button, + Tabs, + Tab, + Grid, + IconButton, + Autocomplete, + Dialog, + DialogTitle, + DialogActions, + DialogContent, + Box +} from "@mui/material"; + +import { + ExpandLess as ExpandLessIcon, + ExpandMore as ExpandMoreIcon, + Save as SaveIcon, +} from "@mui/icons-material"; + +const useStyles = makeStyles({ + notchedOutline: { + borderColor: "#f85a3e !important", + }, +}) + +const OrgHeaderexpanded = (props) => { + const { + userdata, + selectedOrganization, + setSelectedOrganization, + globalUrl, + isCloud, + adminTab, selectedStatus, setSelectedStatus, isEditOrgTab - } = props; - - const classes = useStyles(); - const defaultBranch = "master"; - - const [orgName, setOrgName] = React.useState(selectedOrganization.name); - const [orgDescription, setOrgDescription] = React.useState( - selectedOrganization.description - ); - - const [appDownloadUrl, setAppDownloadUrl] = React.useState( - selectedOrganization.defaults === undefined - ? "https://github.com/frikky/shuffle-apps" - : selectedOrganization.defaults.app_download_repo === undefined || - selectedOrganization.defaults.app_download_repo.length === 0 - ? "https://github.com/frikky/shuffle-apps" - : selectedOrganization.defaults.app_download_repo - ); - const [appDownloadBranch, setAppDownloadBranch] = React.useState( - selectedOrganization.defaults === undefined - ? defaultBranch - : selectedOrganization.defaults.app_download_branch === undefined || - selectedOrganization.defaults.app_download_branch.length === 0 - ? defaultBranch - : selectedOrganization.defaults.app_download_branch - ); - const [workflowDownloadUrl, setWorkflowDownloadUrl] = React.useState( - selectedOrganization.defaults === undefined - ? "https://github.com/frikky/shuffle-apps" - : selectedOrganization.defaults.workflow_download_repo === undefined || - selectedOrganization.defaults.workflow_download_repo.length === 0 - ? "https://github.com/frikky/shuffle-workflows" - : selectedOrganization.defaults.workflow_download_repo - ); - const [workflowDownloadBranch, setWorkflowDownloadBranch] = React.useState( - selectedOrganization.defaults === undefined - ? defaultBranch - : selectedOrganization.defaults.workflow_download_branch === undefined || - selectedOrganization.defaults.workflow_download_branch.length === 0 - ? defaultBranch - : selectedOrganization.defaults.workflow_download_branch - ); - const [ssoEntrypoint, setSsoEntrypoint] = React.useState( - selectedOrganization.sso_config === undefined - ? "" - : selectedOrganization.sso_config.sso_entrypoint === undefined || - selectedOrganization.sso_config.sso_entrypoint.length === 0 - ? "" - : selectedOrganization.sso_config.sso_entrypoint - ); - const [ssoCertificate, setSsoCertificate] = React.useState( - selectedOrganization.sso_config === undefined - ? "" - : selectedOrganization.sso_config.sso_certificate === undefined || - selectedOrganization.sso_config.sso_certificate.length === 0 - ? "" - : selectedOrganization.sso_config.sso_certificate - ); - const [SSORequired, setSSORequired] = React.useState(selectedOrganization.sso_config === undefined - ? false - : selectedOrganization.sso_config.SSORequired === undefined - ? false - : selectedOrganization.sso_config.SSORequired); - - const [notificationWorkflow, setNotificationWorkflow] = React.useState( - selectedOrganization.defaults === undefined - ? "" - : selectedOrganization.defaults.notification_workflow === undefined || - selectedOrganization.defaults.notification_workflow.length === 0 - ? "" - : selectedOrganization.defaults.notification_workflow - ); - - const [documentationReference, setDocumentationReference] = React.useState( - selectedOrganization.defaults === undefined - ? "" - : selectedOrganization.defaults.documentation_reference === undefined || - selectedOrganization.defaults.documentation_reference.length === 0 - ? "" - : selectedOrganization.defaults.documentation_reference - ); - const [openidClientId, setOpenidClientId] = React.useState( - selectedOrganization.sso_config === undefined - ? "" - : selectedOrganization.sso_config.client_id === undefined || - selectedOrganization.sso_config.client_id.length === 0 - ? "" - : selectedOrganization.sso_config.client_id - ); - const [openidClientSecret, setOpenidClientSecret] = React.useState( - selectedOrganization.sso_config === undefined - ? "" - : selectedOrganization.sso_config.client_secret === undefined || - selectedOrganization.sso_config.client_secret.length === 0 - ? "" - : selectedOrganization.sso_config.client_secret - ); - const [openidAuthorization, setOpenidAuthorization] = React.useState( - selectedOrganization.sso_config === undefined - ? "" - : selectedOrganization.sso_config.openid_authorization === undefined || - selectedOrganization.sso_config.openid_authorization.length === 0 - ? "" - : selectedOrganization.sso_config.openid_authorization - ); - const [openidToken, setOpenidToken] = React.useState( - selectedOrganization.sso_config === undefined - ? "" - : selectedOrganization.sso_config.openid_token === undefined || - selectedOrganization.sso_config.openid_token.length === 0 - ? "" - : selectedOrganization.sso_config.openid_token - ) + } = props; + + const classes = useStyles(); + const defaultBranch = "master"; + + const [orgName, setOrgName] = React.useState(selectedOrganization.name); + const [orgDescription, setOrgDescription] = React.useState( + selectedOrganization.description + ); + + const [appDownloadUrl, setAppDownloadUrl] = React.useState( + selectedOrganization.defaults === undefined + ? "https://github.com/frikky/shuffle-apps" + : selectedOrganization.defaults.app_download_repo === undefined || + selectedOrganization.defaults.app_download_repo.length === 0 + ? "https://github.com/frikky/shuffle-apps" + : selectedOrganization.defaults.app_download_repo + ); + const [appDownloadBranch, setAppDownloadBranch] = React.useState( + selectedOrganization.defaults === undefined + ? defaultBranch + : selectedOrganization.defaults.app_download_branch === undefined || + selectedOrganization.defaults.app_download_branch.length === 0 + ? defaultBranch + : selectedOrganization.defaults.app_download_branch + ); + const [workflowDownloadUrl, setWorkflowDownloadUrl] = React.useState( + selectedOrganization.defaults === undefined + ? "https://github.com/frikky/shuffle-apps" + : selectedOrganization.defaults.workflow_download_repo === undefined || + selectedOrganization.defaults.workflow_download_repo.length === 0 + ? "https://github.com/frikky/shuffle-workflows" + : selectedOrganization.defaults.workflow_download_repo + ); + const [workflowDownloadBranch, setWorkflowDownloadBranch] = React.useState( + selectedOrganization.defaults === undefined + ? defaultBranch + : selectedOrganization.defaults.workflow_download_branch === undefined || + selectedOrganization.defaults.workflow_download_branch.length === 0 + ? defaultBranch + : selectedOrganization.defaults.workflow_download_branch + ); + const [ssoEntrypoint, setSsoEntrypoint] = React.useState( + selectedOrganization.sso_config === undefined + ? "" + : selectedOrganization.sso_config.sso_entrypoint === undefined || + selectedOrganization.sso_config.sso_entrypoint.length === 0 + ? "" + : selectedOrganization.sso_config.sso_entrypoint + ); + const [ssoCertificate, setSsoCertificate] = React.useState( + selectedOrganization.sso_config === undefined + ? "" + : selectedOrganization.sso_config.sso_certificate === undefined || + selectedOrganization.sso_config.sso_certificate.length === 0 + ? "" + : selectedOrganization.sso_config.sso_certificate + ); + const [SSORequired, setSSORequired] = React.useState(selectedOrganization.sso_config === undefined + ? false + : selectedOrganization.sso_config.SSORequired === undefined + ? false + : selectedOrganization.sso_config.SSORequired); + + const [notificationWorkflow, setNotificationWorkflow] = React.useState( + selectedOrganization.defaults === undefined + ? "" + : selectedOrganization.defaults.notification_workflow === undefined || + selectedOrganization.defaults.notification_workflow.length === 0 + ? "" + : selectedOrganization.defaults.notification_workflow + ); + + const [documentationReference, setDocumentationReference] = React.useState( + selectedOrganization.defaults === undefined + ? "" + : selectedOrganization.defaults.documentation_reference === undefined || + selectedOrganization.defaults.documentation_reference.length === 0 + ? "" + : selectedOrganization.defaults.documentation_reference + ); + const [openidClientId, setOpenidClientId] = React.useState( + selectedOrganization.sso_config === undefined + ? "" + : selectedOrganization.sso_config.client_id === undefined || + selectedOrganization.sso_config.client_id.length === 0 + ? "" + : selectedOrganization.sso_config.client_id + ); + const [openidClientSecret, setOpenidClientSecret] = React.useState( + selectedOrganization.sso_config === undefined + ? "" + : selectedOrganization.sso_config.client_secret === undefined || + selectedOrganization.sso_config.client_secret.length === 0 + ? "" + : selectedOrganization.sso_config.client_secret + ); + const [openidAuthorization, setOpenidAuthorization] = React.useState( + selectedOrganization.sso_config === undefined + ? "" + : selectedOrganization.sso_config.openid_authorization === undefined || + selectedOrganization.sso_config.openid_authorization.length === 0 + ? "" + : selectedOrganization.sso_config.openid_authorization + ); + const [openidToken, setOpenidToken] = React.useState( + selectedOrganization.sso_config === undefined + ? "" + : selectedOrganization.sso_config.openid_token === undefined || + selectedOrganization.sso_config.openid_token.length === 0 + ? "" + : selectedOrganization.sso_config.openid_token + ) const [uploadRepo, setUploadRepo] = React.useState(selectedOrganization.defaults === undefined ? "" : selectedOrganization.defaults.workflow_upload_repo === undefined || selectedOrganization.defaults.workflow_upload_repo.length === 0 ? "" : selectedOrganization.defaults.workflow_upload_repo) const [uploadBranch, setUploadBranch] = React.useState(selectedOrganization.defaults === undefined ? defaultBranch : selectedOrganization.defaults.workflow_upload_branch === undefined || selectedOrganization.defaults.workflow_upload_branch.length === 0 ? defaultBranch : selectedOrganization.defaults.workflow_upload_branch) const [uploadUsername, setUploadUsername] = React.useState(selectedOrganization.defaults === undefined ? "" : selectedOrganization.defaults.workflow_upload_username === undefined || selectedOrganization.defaults.workflow_upload_username.length === 0 ? "" : selectedOrganization.defaults.workflow_upload_username) const [uploadToken, setUploadToken] = React.useState(selectedOrganization.defaults === undefined ? "" : selectedOrganization.defaults.workflow_upload_token === undefined || selectedOrganization.defaults.workflow_upload_token.length === 0 ? "" : selectedOrganization.defaults.workflow_upload_token) - - const [workflows, setWorkflows] = React.useState([]) - const [workflow, setWorkflow] = React.useState({}) - - const getAvailableWorkflows = (trigger_index) => { - 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) => { - if (responseJson !== undefined) { - setWorkflows(responseJson) - - if (selectedOrganization.defaults !== undefined && selectedOrganization.defaults.notification_workflow !== undefined) { - - const workflow = responseJson.find((workflow) => workflow.id === selectedOrganization.defaults.notification_workflow) - if (workflow !== undefined && workflow !== null) { - setWorkflow(workflow) - } - } - } - }) - .catch((error) => { - console.log("Error getting workflows: " + error); - }) - } - - useEffect(() => { - getAvailableWorkflows() - }, []) - - const handleEditOrg = ( - name, - description, - orgId, - image, - defaults, - sso_config - ) => { - - const data = { - name: name, - description: description, - org_id: orgId, - image: image, - defaults: defaults, - sso_config: sso_config, - }; - - const url = globalUrl + `/api/v1/orgs/${selectedOrganization.id}`; - 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 updating org: ", responseJson.reason); - } else { - toast("Successfully edited org!"); - } - }) - ) - .catch((error) => { - toast("Err: " + error.toString()); - }); - }; - - - const handleWorkflowSelectionUpdate = (e, isUserinput) => { - if (e.target.value === undefined || e.target.value === null || e.target.value.id === undefined) { - console.log("Returning as there's no id") - return null - } - - setWorkflow(e.target.value) - setNotificationWorkflow(e.target.value.id) - toast("Updated notification workflow. Don't forget to save!") - } - - const orgSaveButton = ( - -
- -
-
- ); - - const toggleBetweenRequiredOrOptional = (event) => { - setSSORequired(event.target.checked); - }; - - return ( -
- - - - Notification Workflow - - {/* - - */} - - -
- {workflows !== undefined && workflows !== null && workflows.length > 0 ? - { - if ( - option === undefined || - option === null || - option.name === undefined || - option.name === null - ) { - return "No Workflow Selected"; - } - - const newname = ( - option.name.charAt(0).toUpperCase() + option.name.substring(1) - ).replaceAll("_", " "); - return newname; - }} - options={workflows} - fullWidth - style={{ - backgroundColor: theme.palette.inputColor, - height: 50, - borderRadius: theme.palette.borderRadius, - }} - onChange={(event, newValue) => { - console.log("Found value: ", newValue) - - var parsedinput = { target: { value: newValue } } - - // For variables - if (typeof newValue === 'string' && newValue.startsWith("$")) { - parsedinput = { - target: { - value: { - "name": newValue, - "id": newValue, - "actions": [], - "triggers": [], - } - } - } - } - - handleWorkflowSelectionUpdate(parsedinput) - }} - renderOption={(props, data, state) => { - if (data.id === workflow.id) { - data = workflow; - } - - return ( - - {data.image !== undefined && data.image !== null && data.image.length > 0 ? - {data.name} - : null} - - Choose {data.name} - - - } placement="bottom"> - { - var parsedinput = { target: { value: data } } - handleWorkflowSelectionUpdate(parsedinput) - }} - > - {data.name} - - - ) - }} - renderInput={(params) => { - return ( - - ); - }} - /> - : - { - setNotificationWorkflow(e.target.value); - }} - InputProps={{ - classes: { - notchedOutline: classes.notchedOutline, - }, - style: { - color: "white", - }, - }} - /> - } -
- {orgSaveButton} -
-
-
-
- - - Org Documentation reference - { - setDocumentationReference(e.target.value); - }} - InputProps={{ - classes: { - notchedOutline: classes.notchedOutline, - }, - style: { - color: "white", - }, - }} - /> - - + + const [workflows, setWorkflows] = React.useState([]) + const [workflow, setWorkflow] = React.useState({}) + + const getAvailableWorkflows = (trigger_index) => { + 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) => { + if (responseJson !== undefined) { + setWorkflows(responseJson) + + if (selectedOrganization.defaults !== undefined && selectedOrganization.defaults.notification_workflow !== undefined) { + + const workflow = responseJson.find((workflow) => workflow.id === selectedOrganization.defaults.notification_workflow) + if (workflow !== undefined && workflow !== null) { + setWorkflow(workflow) + } + } + } + }) + .catch((error) => { + console.log("Error getting workflows: " + error); + }) + } + + useEffect(() => { + getAvailableWorkflows() + }, []) + + const handleEditOrg = ( + name, + description, + orgId, + image, + defaults, + sso_config + ) => { + + const data = { + name: name, + description: description, + org_id: orgId, + image: image, + defaults: defaults, + sso_config: sso_config, + }; + + const url = globalUrl + `/api/v1/orgs/${selectedOrganization.id}`; + 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 updating org: ", responseJson.reason); + } else { + toast("Successfully edited org!"); + } + }) + ) + .catch((error) => { + toast("Err: " + error.toString()); + }); + }; + + + const handleWorkflowSelectionUpdate = (e, isUserinput) => { + if (e.target.value === undefined || e.target.value === null || e.target.value.id === undefined) { + console.log("Returning as there's no id") + return null + } + + setWorkflow(e.target.value) + setNotificationWorkflow(e.target.value.id) + toast("Updated notification workflow. Don't forget to save!") + } + + const orgSaveButton = ( + +
+ +
+
+ ); + + const toggleBetweenRequiredOrOptional = (event) => { + setSSORequired(event.target.checked); + }; + + return ( +
+ + + + Notification Workflow + + {/* + + */} + + +
+ {workflows !== undefined && workflows !== null && workflows.length > 0 ? + { + if ( + option === undefined || + option === null || + option.name === undefined || + option.name === null + ) { + return "No Workflow Selected"; + } + + const newname = ( + option.name.charAt(0).toUpperCase() + option.name.substring(1) + ).replaceAll("_", " "); + return newname; + }} + options={workflows} + fullWidth + style={{ + backgroundColor: theme.palette.inputColor, + height: 50, + borderRadius: theme.palette.borderRadius, + }} + onChange={(event, newValue) => { + console.log("Found value: ", newValue) + + var parsedinput = { target: { value: newValue } } + + // For variables + if (typeof newValue === 'string' && newValue.startsWith("$")) { + parsedinput = { + target: { + value: { + "name": newValue, + "id": newValue, + "actions": [], + "triggers": [], + } + } + } + } + + handleWorkflowSelectionUpdate(parsedinput) + }} + renderOption={(props, data, state) => { + if (data.id === workflow.id) { + data = workflow; + } + + return ( + + {data.image !== undefined && data.image !== null && data.image.length > 0 ? + {data.name} + : null} + + Choose {data.name} + + + } placement="bottom"> + { + var parsedinput = { target: { value: data } } + handleWorkflowSelectionUpdate(parsedinput) + }} + > + {data.name} + + + ) + }} + renderInput={(params) => { + return ( + + ); + }} + /> + : + { + setNotificationWorkflow(e.target.value); + }} + InputProps={{ + classes: { + notchedOutline: classes.notchedOutline, + }, + style: { + color: "white", + }, + }} + /> + } +
+ {orgSaveButton} +
+
+
+
+ + + Org Documentation reference + { + setDocumentationReference(e.target.value); + }} + InputProps={{ + classes: { + notchedOutline: classes.notchedOutline, + }, + style: { + color: "white", + }, + }} + /> + + Workflow Backup Repository @@ -663,409 +664,409 @@ const OrgHeaderexpanded = (props) => { SSO Configuration -
- Make SAML SSO or OpenID Authentication Required or Optional for Your Organization. -
- - {SSORequired ? 'Required' : 'Optional'} -
-
- - OpenID connect - - - - Client ID - 0 - } - id="outlined-with-placeholder" - margin="normal" - variant="outlined" - placeholder="The OpenID client ID from the identity provider" - value={openidClientId} - onChange={(e) => { - setOpenidClientId(e.target.value); - }} - InputProps={{ - classes: { - notchedOutline: classes.notchedOutline, - }, - style: { - color: "white", - }, - }} - /> - - - - - Client Secret (optional) - 0 - } - id="outlined-with-placeholder" - margin="normal" - variant="outlined" - placeholder="The OpenID client secret - DONT use this if dealing with implicit auth / PKCE" - value={openidClientSecret} - onChange={(e) => { - setOpenidClientSecret(e.target.value); - }} - InputProps={{ - classes: { - notchedOutline: classes.notchedOutline, - }, - style: { - color: "white", - }, - }} - /> - - - - - - - Authorization URL - { - setOpenidAuthorization(e.target.value) - }} - InputProps={{ - classes: { - notchedOutline: classes.notchedOutline, - }, - style: { - color: "white", - }, - }} - /> - - - - - Token URL - { - setOpenidToken(e.target.value) - }} - InputProps={{ - classes: { - notchedOutline: classes.notchedOutline, - }, - style: { - color: "white", - }, - }} - /> - - - - - {/* } */} - {/*isCloud ? null : */} - - SAML SSO (v1.1) - - - - SSO Entrypoint (IdP) - 0 - } - id="outlined-with-placeholder" - margin="normal" - variant="outlined" - placeholder="The entrypoint URL from your provider" - value={ssoEntrypoint} - onChange={(e) => { - setSsoEntrypoint(e.target.value); - }} - InputProps={{ - classes: { - notchedOutline: classes.notchedOutline, - }, - style: { - color: "white", - }, - }} - /> - - - - - SSO Certificate (X509) - { - setSsoCertificate(e.target.value); - }} - InputProps={{ - classes: { - notchedOutline: classes.notchedOutline, - }, - style: { - color: "white", - }, - }} - /> - - - - {isCloud ? - - IdP URL for Shuffle: https://shuffler.io/api/v1/login_sso - - : null} - - {isCloud ? null : ( - - - App Download URL - { - setAppDownloadUrl(e.target.value); - }} - InputProps={{ - classes: { - notchedOutline: classes.notchedOutline, - }, - style: { - color: "white", - }, - }} - /> - - - )} - {isCloud ? null : ( - - - App Download Branch - { - setAppDownloadBranch(e.target.value); - }} - InputProps={{ - classes: { - notchedOutline: classes.notchedOutline, - }, - style: { - color: "white", - }, - }} - /> - - - )} - {isCloud ? null : ( - - - Workflow Download URL - { - setWorkflowDownloadUrl(e.target.value); - }} - InputProps={{ - classes: { - notchedOutline: classes.notchedOutline, - }, - style: { - color: "white", - }, - }} - /> - - - )} - {isCloud ? null : ( - - - Workflow Download Branch - { - setWorkflowDownloadBranch(e.target.value); - }} - InputProps={{ - classes: { - notchedOutline: classes.notchedOutline, - }, - style: { - color: "white", - }, - }} - /> - - - )} - -
- {orgSaveButton} -
- {/* - - {expanded ? - - : - - } - - */} -
-
- ) -} - +
+ Make SAML SSO or OpenID Authentication Required or Optional for Your Organization. +
+ + {SSORequired ? 'Required' : 'Optional'} +
+
+ + OpenID connect + + + + Client ID + 0 + } + id="outlined-with-placeholder" + margin="normal" + variant="outlined" + placeholder="The OpenID client ID from the identity provider" + value={openidClientId} + onChange={(e) => { + setOpenidClientId(e.target.value); + }} + InputProps={{ + classes: { + notchedOutline: classes.notchedOutline, + }, + style: { + color: "white", + }, + }} + /> + + + + + Client Secret (optional) + 0 + } + id="outlined-with-placeholder" + margin="normal" + variant="outlined" + placeholder="The OpenID client secret - DONT use this if dealing with implicit auth / PKCE" + value={openidClientSecret} + onChange={(e) => { + setOpenidClientSecret(e.target.value); + }} + InputProps={{ + classes: { + notchedOutline: classes.notchedOutline, + }, + style: { + color: "white", + }, + }} + /> + + + + + + + Authorization URL + { + setOpenidAuthorization(e.target.value) + }} + InputProps={{ + classes: { + notchedOutline: classes.notchedOutline, + }, + style: { + color: "white", + }, + }} + /> + + + + + Token URL + { + setOpenidToken(e.target.value) + }} + InputProps={{ + classes: { + notchedOutline: classes.notchedOutline, + }, + style: { + color: "white", + }, + }} + /> + + + + + {/* } */} + {/*isCloud ? null : */} + + SAML SSO (v1.1) + + + + SSO Entrypoint (IdP) + 0 + } + id="outlined-with-placeholder" + margin="normal" + variant="outlined" + placeholder="The entrypoint URL from your provider" + value={ssoEntrypoint} + onChange={(e) => { + setSsoEntrypoint(e.target.value); + }} + InputProps={{ + classes: { + notchedOutline: classes.notchedOutline, + }, + style: { + color: "white", + }, + }} + /> + + + + + SSO Certificate (X509) + { + setSsoCertificate(e.target.value); + }} + InputProps={{ + classes: { + notchedOutline: classes.notchedOutline, + }, + style: { + color: "white", + }, + }} + /> + + + + {isCloud ? + + IdP URL for Shuffle: https://shuffler.io/api/v1/login_sso + + : null} + + {isCloud ? null : ( + + + App Download URL + { + setAppDownloadUrl(e.target.value); + }} + InputProps={{ + classes: { + notchedOutline: classes.notchedOutline, + }, + style: { + color: "white", + }, + }} + /> + + + )} + {isCloud ? null : ( + + + App Download Branch + { + setAppDownloadBranch(e.target.value); + }} + InputProps={{ + classes: { + notchedOutline: classes.notchedOutline, + }, + style: { + color: "white", + }, + }} + /> + + + )} + {isCloud ? null : ( + + + Workflow Download URL + { + setWorkflowDownloadUrl(e.target.value); + }} + InputProps={{ + classes: { + notchedOutline: classes.notchedOutline, + }, + style: { + color: "white", + }, + }} + /> + + + )} + {isCloud ? null : ( + + + Workflow Download Branch + { + setWorkflowDownloadBranch(e.target.value); + }} + InputProps={{ + classes: { + notchedOutline: classes.notchedOutline, + }, + style: { + color: "white", + }, + }} + /> + + + )} + +
+ {orgSaveButton} +
+ {/* + + {expanded ? + + : + + } + + */} +
+
+ ) +} + export default OrgHeaderexpanded; diff --git a/frontend/src/components/ParsedAction.jsx b/frontend/src/components/ParsedAction.jsx index 4d73ebdd..034859e0 100755 --- a/frontend/src/components/ParsedAction.jsx +++ b/frontend/src/components/ParsedAction.jsx @@ -166,7 +166,7 @@ const ParsedAction = (props) => { setExpansionModalOpen, listCache, - + setActiveDialog, authGroups, apps, setEditorData, @@ -179,7 +179,7 @@ const ParsedAction = (props) => { const [activateHidingBodyButton, setActivateHidingBodyButton] = React.useState(false) const [appActionName, setAppActionName] = React.useState(selectedAction.label); const [delay, setDelay] = React.useState(selectedAction?.execution_delay || 0); - + const [prevActionName, setPrevActionName] = React.useState(selectedAction.label); const [fieldCount, setFieldCount] = React.useState(0); const [hiddenDescription, setHiddenDescription] = React.useState(true); const [autoCompleting, setAutocompleting] = React.useState(false); @@ -206,16 +206,6 @@ const ParsedAction = (props) => { setLastSaved(false) } }, [expansionModalOpen]) - useEffect(() => { - setParamValues(selectedAction.parameters.map((param) => { - return { - name: param.name, - value: param.value, - } - })) - },[ - selectedAction, selectedApp,setNewSelectedAction, workflow, - ]) useEffect(() => { setParamValues(selectedAction.parameters.map((param) => { @@ -317,7 +307,6 @@ const ParsedAction = (props) => { (action) => action.name.toLowerCase() === selectedAction.name.toLowerCase() ); - console.log("FOUNDACTION: ", foundAction); if (foundAction !== null && foundAction !== undefined) { var foundparams = []; for (let [paramkey,paramkeyval] in Object.entries(foundAction.parameters)) { @@ -416,6 +405,10 @@ const ParsedAction = (props) => { if (selectedAction.label !== appActionName) { setAppActionName(selectedAction.label); } + + if(selectedAction.label !== prevActionName){ + setPrevActionName(selectedAction.label) + } // Only set delay if it has changed const newDelay = selectedAction?.execution_delay || 0; @@ -555,6 +548,11 @@ const ParsedAction = (props) => { } } } + + if (parentNode.label === undefined) { + parentNode.label = "" + } + newActionList.push({ type: "action", id: parentNode.id, @@ -1302,7 +1300,6 @@ const ParsedAction = (props) => { const selectedAppIcon = selectedAction.large_image - var baselabel = selectedAction.label return (
@@ -1621,8 +1618,8 @@ const ParsedAction = (props) => { } onBlur={(e) => { // Copy the name value - const name = appActionName - const parsedBaseLabel = "$"+baselabel.toLowerCase().replaceAll(" ", "_") + const name = e.target.value + const parsedBaseLabel = "$"+prevActionName.toLowerCase().replaceAll(" ", "_") const newname = "$"+name.toLowerCase().replaceAll(" ", "_") // Check if it's the same as the current name in use @@ -1751,7 +1748,6 @@ const ParsedAction = (props) => { } const params = workflow.actions[key].parameters - console.log(params) if (params === null || params === undefined) { continue } @@ -1775,7 +1771,6 @@ const ParsedAction = (props) => { // Need to make sure e.g. changing the first here doesn't change the 2nd // $change_me // $change_me_2 - const foundindex = param.value.toLowerCase().indexOf(parsedBaseLabel, previous) if (foundindex === previous && foundindex !== 0) { break @@ -1818,7 +1813,7 @@ const ParsedAction = (props) => { setWorkflow(workflow); setUpdate(Math.random()); - baselabel = name + setPrevActionName(name) }} />
@@ -3088,7 +3083,7 @@ const ParsedAction = (props) => { event.preventDefault() setFieldCount(count) setExpansionModalOpen(true) - + setActiveDialog("codeeditor") //setcodedata(data.value) var parsedvalue = data.value if (parsedvalue === undefined || parsedvalue === null) { diff --git a/frontend/src/components/ShuffleCodeEditor1.jsx b/frontend/src/components/ShuffleCodeEditor1.jsx index 1dc0ecaa..05830da8 100644 --- a/frontend/src/components/ShuffleCodeEditor1.jsx +++ b/frontend/src/components/ShuffleCodeEditor1.jsx @@ -105,7 +105,8 @@ const CodeEditor = (props) => { selectedAction , workflowExecutions, getParents, - + activeDialog, + setActiveDialog, fieldname, contentLoading, } = props @@ -958,10 +959,10 @@ const CodeEditor = (props) => { return ( { @@ -976,8 +977,10 @@ const CodeEditor = (props) => { }} PaperComponent={PaperComponent} PaperProps={{ + onClick: () => setActiveDialog("codeeditor"), style: { - zIndex: 12501, + // zIndex: 12501, + pointerEvents: "auto", color: "white", minWidth: isMobile ? "100%" : isFileEditor ? 650 : "80%", maxWidth: isMobile ? "100%" : isFileEditor ? 650 : 1100, @@ -1525,6 +1528,7 @@ const CodeEditor = (props) => { whiteSpace: "pre-wrap", wordWrap: "break-word", backgroundColor: "rgba(40,40,40,1)", + zIndex: activeDialog === "codeeditor" ? 1200 : 1100, }} onLoad={(editor) => { highlight_variables(localcodedata) @@ -1577,6 +1581,7 @@ const CodeEditor = (props) => { paddingLeft: 10, paddingTop: 0, display: "flex", + cursor: "move" }} >
@@ -1629,6 +1634,7 @@ const CodeEditor = (props) => { overflow: "auto", minWidth: 450, maxWidth: "100%", + zIndex: activeDialog === "codeeditor" ? 1200 : 1100, }} collapsed={false} enableClipboard={(copy) => { @@ -1661,6 +1667,7 @@ const CodeEditor = (props) => { minHeight: 450, overflow: "auto", wordWrap: "anywhere", + zIndex: activeDialog === "codeeditor" ? 1200 : 1100, }} > {expOutput} diff --git a/frontend/src/components/WorkflowValidationTimeline.jsx b/frontend/src/components/WorkflowValidationTimeline.jsx new file mode 100644 index 00000000..cbbb7bd6 --- /dev/null +++ b/frontend/src/components/WorkflowValidationTimeline.jsx @@ -0,0 +1,443 @@ +import React, { useState, } from "react"; +import { makeStyles, createStyles } from "@mui/styles"; +import { toast } from "react-toastify" + +import { + Tooltip, + Typography, + + Avatar, + AvatarGroup, +} from "@mui/material" + +import { + green, + yellow, + red, +} from "../views/AngularWorkflow.jsx" + +import { validateJson, GetIconInfo } from "../views/Workflows.jsx"; +import theme from "../theme.jsx"; +const itemHeight = 40 + +export const getParentNodes = (workflow, action) => { + if (action === undefined || action === null) { + return [] + } + + if (workflow.actions === undefined || workflow.actions === null) { + workflow.actions = [] + } + + if (workflow.triggers === undefined || workflow.triggers === null) { + workflow.triggers = [] + } + + if (workflow.branches === undefined || workflow.branches === null) { + workflow.branches = [] + } + + var allkeys = [action.id]; + var handled = []; + var results = []; + + // maxiter = max amount of parent nodes to loop + // also handles breaks if there are issues + var iterations = 0; + var maxiter = 10; + while (true) { + for (let parentkey in allkeys) { + if (allkeys[parentkey] === undefined) { + continue + } + + var currentnode = workflow.actions.find((element) => element.id === allkeys[parentkey]) + if (currentnode === undefined) { + currentnode = workflow.triggers.find((element) => element.id === allkeys[parentkey]) + + if (currentnode === undefined) { + console.log("Could not find parent node for: ", allkeys[parentkey]) + continue + } + } + + if (handled.includes(currentnode.id)) { + continue + } else { + handled.push(currentnode.id); + results.push(currentnode); + } + + // Get the name / label here too? + if (currentnode.length === 0) { + continue; + } + + // FIXME: This part is only handling first level, + // but needs to recurse + var incomingEdges = [] + for (var branchkey in workflow.branches) { + const branch = workflow.branches[branchkey] + if (branch.destination_id !== currentnode.id) { + continue + } + + // Go up in the levels + const parents = getParentNodes(workflow, { + id: branch.source_id, + }) + if (parents.length > 0) { + incomingEdges = incomingEdges.concat(parents) + } + + incomingEdges.push(branch) + } + + for (let i = 0; i < incomingEdges.length; i++) { + var tmp = incomingEdges[i]; + if (tmp.decorator === true) { + continue + } + + if (!allkeys.includes(tmp.source_id)) { + allkeys.push(tmp.source_id) + } + } + } + + if (results.length === allkeys.length || iterations === maxiter) { + break + } + + iterations += 1 + } + + // Remove on the end as we don't want to remove everything + results = results.filter((data) => data.id !== action.id) + results = results.filter((data) => data.type === "ACTION" || data.app_name === "Shuffle Workflow" || data.app_name === "User Input" || data.app_name === "shuffle-subflow") + results.push({ label: "Execution Argument", type: "INTERNAL" }) + + return results +} + +const WorkflowValidationTimeline = (props) => { + const { workflow, originalWorkflow, apps, getParents, execution} = props + + + if (workflow === undefined || workflow === null) { + return null + } + + if (workflow.validation === undefined || workflow.validation === null) { + return null + } + + if (workflow.actions === undefined || workflow.actions === null) { + workflow.actions = [] + } + + if (workflow.triggers === undefined || workflow.triggers === null) { + workflow.triggers = [] + + } + + if (workflow.branches === undefined || workflow.branches === null) { + workflow.branches = [] + + } + + var results = [] + if (execution !== undefined) { + results = execution.results + } + + // 1. Find startnode + // 2. Map childnodes from it + const startnodeId = workflow.start + + // Find parent of startnodeId and if it's a webhook + var relevantactions = [] + for (var key in workflow.branches) { + const branch = workflow.branches[key] + if (branch.destination_id !== startnodeId) { + continue + } + + for (var triggerkey in workflow.triggers) { + const trigger = workflow.triggers[triggerkey] + if (trigger.trigger_type !== "WEBHOOK") { + continue + } + + if (trigger.id === branch.source_id) { + trigger.order = -1 + relevantactions.push(trigger) + break + } + } + } + + + if (getParents !== undefined) { + for (var key in workflow.actions) { + const action = workflow.actions[key] + if (action.id === startnodeId) { + action.order = 0 + relevantactions.push(action) + continue + } + + const parents = getParents(action) + //const parents = getParentNodes(workflow, action) + //console.log("PARENTS", key, parents) + if (parents !== undefined && parents !== null) { + const parentfound = parents.find((element) => element.id === startnodeId) + if (parentfound !== undefined) { + + // FIXME: add order here based on how many steps away from the startnode + // This just has the parent count + action.order = parents.length + + relevantactions.push(action) + } + } + } + } else { + for (var key in workflow.triggers) { + const trigger = workflow.triggers[key] + if (trigger.trigger_type !== "SUBFLOW" && trigger.trigger_type !== "USERINPUT") { + continue + } + + if (workflow.actions.find((element) => element.id === trigger.id) === undefined) { + workflow.actions.push(trigger) + } + } + + relevantactions = workflow.actions + } + + // Sort according to how many parents a node has. MAY be wrong~ + relevantactions.sort((a, b) => { + if (a.order === undefined) { + return 1 + } + + if (b.order === undefined) { + return -1 + } + + return a.order - b.order + }) + + // FIXME: Add other relevant items as well from subflows (?) + var nodecolor = "grey" + var branchcolor = "grey" + var skipped = false + + var previousTools = false + + return ( +
+
+ {relevantactions.map((action, index) => { + action.result = {} + if (results !== undefined) { + const foundResult = results.find((element) => element.action.id === action.id) + if (foundResult !== undefined) { + action.result = foundResult + + action.status = foundResult.status + } + } + + const lastitem = index === relevantactions.length - 1 + if (!lastitem) { + if (action.app_name === "Shuffle Tools") { + if (action.status === "SUCCESS") { + branchcolor = red + + // Check action.result for the actual status + const validate = validateJson(action.result.result) + if (validate.valid) { + if (validate.result.success === true) { + branchcolor = green + } else { + branchcolor = "grey" + } + } + + + } else if (action.status === "SKIPPED") { + branchcolor = "grey" + } else { + if (action.status === undefined) { + branchcolor = green + } else { + branchcolor = red + } + } + + previousTools = true + return null + } else { + if (action.status === "SUCCESS") { + nodecolor = green + } else if (action.status === "SKIPPED") { + nodecolor = "grey" + } else { + if (action.status === undefined) { + nodecolor = green + } else { + nodecolor = red + } + } + } + } else { + nodecolor = "grey" + } + + if (action.status === "SKIPPED") { + skipped = true + } + + var image = "" + if (action.large_image !== undefined && action.large_image !== null && action.large_image !== "") { + image = action.large_image + } else { + if (originalWorkflow !== undefined) { + for (var key in originalWorkflow.actions) { + if (originalWorkflow.actions[key].id === action.id) { + image = originalWorkflow.actions[key].large_image + break + } + } + + if (image === "") { + for (var key in originalWorkflow.triggers) { + if (originalWorkflow.triggers[key].id === action.id) { + image = originalWorkflow.triggers[key].large_image + break + } + } + + } + } + } + + var founderror = "" + if (workflow.validation !== undefined && workflow.validation !== null && workflow.validation.errors !== undefined && workflow.validation.errors !== null) { + const foundError = workflow.validation.errors.find((element) => element.action_id === action.id) + if (foundError !== undefined) { + founderror = foundError.error + nodecolor = yellow + branchcolor = yellow + } + } + + if (skipped && !lastitem) { + nodecolor = "grey" + branchcolor = "grey" + } + + if (previousTools) { + previousTools = false + } else { + branchcolor = nodecolor + } + + var appgroup = [] + if (action.trigger_type === "WEBHOOK") { + nodecolor = green + branchcolor = green + } else if (action.app_name === "shuffle-subflow") { + if (action.status === "SUCCESS") { + nodecolor = green + branchcolor = green + } + + for (var subflowkey in workflow.validation.subflow_apps) { + const subflowApp = workflow.validation.subflow_apps[subflowkey] + if (subflowApp.error === action.id) { + appgroup.push(subflowApp) + } + } + + } + + var flex = index !== 0 && index !== relevantactions.length - 1 ? 1 : 3 + const branchTooltip = branchcolor === yellow ? "Check nodes for errors" : "" + + return ( +
+ {lastitem ? + +
+ + : null} + + {appgroup.length > 0 ? + + {appgroup.map((subflowApp, subflowIndex) => { + var appimage = "" + if (apps !== undefined && apps !== null && apps.length > 0) { + for (var key in apps) { + const app = apps[key] + if (app.name === subflowApp.app_name) { + appimage = apps[key].large_image + break + } + } + } + + return ( + + + + ) + })} + + : + + {founderror.length > 0 ? founderror : ``} + + } placement="top"> + + + {image !== "" ? + + : null} + + + } + + {lastitem ? null : + +
+ + } +
+ ) + })} +
+ +
+ ) +} + +export default WorkflowValidationTimeline diff --git a/frontend/src/views/Admin.jsx b/frontend/src/views/Admin.jsx index 15382695..d7ee12b6 100755 --- a/frontend/src/views/Admin.jsx +++ b/frontend/src/views/Admin.jsx @@ -5385,13 +5385,17 @@ If you're interested, please let me know a time that works for you, or set up a + {validIcon} style={{ minWidth: 65, maxWidth: 65, }} onClick={() => { + if (data.validation === null || data.validation === undefined) { + return + } + if (data.validation.workflow_id === undefined || data.validation.workflow_id === null || data.validation.workflow_id.length === 0) { toast.warn("No workflow runs found for this auth yet. Check back later.") return diff --git a/frontend/src/views/AngularWorkflow.jsx b/frontend/src/views/AngularWorkflow.jsx index 889a407b..e562221f 100755 --- a/frontend/src/views/AngularWorkflow.jsx +++ b/frontend/src/views/AngularWorkflow.jsx @@ -139,6 +139,7 @@ import Draggable from "react-draggable"; import cytoscapestyle from "../defaultCytoscapeStyle.jsx"; import ShuffleCodeEditor from "../components/ShuffleCodeEditor1.jsx"; +import WorkflowValidationTimeline from "../components/WorkflowValidationTimeline.jsx" import { validateJson, GetIconInfo } from "../views/Workflows.jsx"; import { GetParsedPaths, internalIds, } from "../views/Apps.jsx"; import ConfigureWorkflow from "../components/ConfigureWorkflow.jsx"; @@ -319,6 +320,7 @@ export function SetJsonDotnotation(jsonInput, inputKey) { export const green = "#86c142"; export const yellow = "#FECC00"; +export const red = "red"; export function removeParam(key, sourceURL) { if (sourceURL === undefined) { @@ -483,7 +485,7 @@ const AngularWorkflow = (defaultprops) => { const [selectedTriggerIndex, setSelectedTriggerIndex] = React.useState({}); const [selectedEdge, setSelectedEdge] = React.useState({}); const [selectedEdgeIndex, setSelectedEdgeIndex] = React.useState({}); - + const [activeDialog, setActiveDialog] = React.useState(""); const [visited, setVisited] = React.useState([]); const [allRevisions, setAllRevisions] = useState([]) @@ -1641,7 +1643,6 @@ const releaseToConnectLabel = "Release to Connect" setExecutionData(responseJson) } else { if (responseJson.status === "ABORTED" || responseJson.status === "STOPPED" || responseJson.status === "FAILURE" || responseJson.status === "WAITING" || responseJson.status === "FINISHED") { - console.log("DONE!") stop() } @@ -6731,7 +6732,7 @@ const releaseToConnectLabel = "Release to Connect" const parentlabel = parentNode.data("label").toLowerCase().replace(" ", "_") const parentname = parentNode.data("app_name").toLowerCase().replace(" ", "_") - if (!parentlabel.startsWith(parentname)) { + if (!parentlabel.startsWith(parentname)+"_") { return } @@ -6755,13 +6756,14 @@ const releaseToConnectLabel = "Release to Connect" } if (curapp.actions[startIndex].name !== parentActionname) { + console.log("Return 2") return } break } - //const parentAction = parentNode.data("name") + console.log("CONTINUE EVEN WHEN FIELDS ARE FILLED") const iconInfo = { icon: "M16 1H4c-1.1 0-2 .9-2 2v14h2V3h12V1zm-1 4l6 6v10c0 1.1-.9 2-2 2H7.99C6.89 23 6 22.1 6 21l.01-14c0-1.1.89-2 1.99-2h7zm-1 7h5.5L14 6.5V12z", @@ -7133,7 +7135,6 @@ const releaseToConnectLabel = "Release to Connect" // Find how many executions it has var executions = 0 const matchingExecutions = workflowExecutions.filter((execution => execution.execution_source === nodedata.app_name.toLowerCase())) - console.log("Matches: ", matchingExecutions.length) const color = matchingExecutions.length > 0 ? "#34a853" : "#ea4436" const decoratorNode = { position: { @@ -10202,6 +10203,7 @@ const releaseToConnectLabel = "Release to Connect" // Starts on current node and climbs UP the tree to the root object. // Sends back everything in it's path + // FIXME: Use the GetParentNodes in WorkflowValidationTimeline.jsx instead const getParents = (action) => { if (action === undefined || action === null) { return [] @@ -12491,15 +12493,10 @@ const releaseToConnectLabel = "Release to Connect" let appIdsInWorkflow = []; Object.entries(workflowApps).forEach(([key, value]) => { - console.log("VALUE: ", value) appIdsInWorkflow.push(value.app_id); }) appIdsInWorkflow = [...new Set(appIdsInWorkflow)]; - - console.log("appIdsInWorkflow: ", appIdsInWorkflow) - - console.log("authData: ", authData, "workflowApps: ", workflowApps) // loop through the authData and create transformedData which looks like: // appId: [auth1, auth2, ...] @@ -12518,8 +12515,6 @@ const releaseToConnectLabel = "Release to Connect" }); - console.log("transformedData: ", transformedData) - return transformedData; }; @@ -12535,7 +12530,6 @@ const releaseToConnectLabel = "Release to Connect" const handleShowingValue = (appName) => { let mappingWithName = {} let listWithValues = workflow.triggers[selectedTriggerIndex].parameters[5]?.value.split(";").filter(e => e).map(e => e.split("=")) - console.log("LIST WITH VALUES: ", listWithValues) if (listWithValues === undefined || listWithValues === null || listWithValues.length === 0) { return "no-overrides"; } @@ -12614,10 +12608,15 @@ const releaseToConnectLabel = "Release to Connect" return (
- {Object.entries(transformedAuthData).map(([appId, authList]) => ( + {Object.entries(transformedAuthData).map(([appId, authList]) => { + if (authList === undefined || authList === null || authList.length < 2) { + return null; + } + + return (