From 6c60dd3bccac42e3f6a6d84fe1d7a5a30ef5289f Mon Sep 17 00:00:00 2001 From: Frikky Date: Tue, 26 Aug 2025 16:07:44 +0200 Subject: [PATCH] Merging fixes for 2.1.0 --- frontend/src/components/Billing.jsx | 155 +++- frontend/src/components/BillingStats.jsx | 254 +++--- frontend/src/components/CacheView.jsx | 114 ++- .../src/components/CollectIngestModal.jsx | 185 +++-- frontend/src/components/EditWorkflow.jsx | 258 +++++- frontend/src/components/LicencePopup.jsx | 6 +- frontend/src/components/Navbar.jsx | 16 +- .../src/components/OrgHeaderexpandedNew.jsx | 8 +- frontend/src/components/ParsedAction.jsx | 72 +- frontend/src/components/PartnerDetails.jsx | 33 +- frontend/src/components/PartnerSettings.jsx | 83 +- frontend/src/components/PartnerTab.jsx | 5 +- .../src/components/ShuffleCodeEditor1.jsx | 230 +++++- frontend/src/views/AgentUI.jsx | 18 +- frontend/src/views/AngularWorkflow.jsx | 759 +++++++++++++++--- frontend/src/views/AppExplorer.jsx | 49 +- frontend/src/views/Apps2.jsx | 28 +- frontend/src/views/Docs.jsx | 23 +- frontend/src/views/LoginPage.jsx | 4 +- frontend/src/views/Workflows2.jsx | 413 +++++++++- 20 files changed, 2115 insertions(+), 598 deletions(-) diff --git a/frontend/src/components/Billing.jsx b/frontend/src/components/Billing.jsx index 19698a13..8b14eaf5 100644 --- a/frontend/src/components/Billing.jsx +++ b/frontend/src/components/Billing.jsx @@ -91,10 +91,13 @@ const Billing = memo((props) => { const [currentTab, setCurrentTab] = useState(0) const [allChildOrgs, setAllChildOrgs] = useState([]) const [allChildOrgsStats, setAllChildOrgsStats] = useState([]) + const [statistics, setStatistics] = useState([]) + const [monthlyAppRunsParent, setMonthlyAppRunsParent] = useState(0) + const [monthlyAllSuborgExecutions, setMonthlyAllSuborgExecutions] = useState(0) useEffect(() => { - if (userdata.app_execution_limit !== undefined && userdata.app_execution_usage !== undefined) { - const percentage = ((userdata.app_execution_usage + userdata.app_executions_suborgs) / userdata.app_execution_limit) * 100; + if (monthlyAppRunsParent > 0 || monthlyAllSuborgExecutions > 0) { + const percentage = ((monthlyAppRunsParent + monthlyAllSuborgExecutions) / userdata.app_execution_limit) * 100; setCurrentAppRunsInPercentage(Math.round(percentage)); setCurrentAppRunsInNumber(userdata.app_execution_limit - userdata.app_execution_usage - userdata.app_executions_suborgs); } @@ -102,7 +105,7 @@ const Billing = memo((props) => { if (userdata?.id?.length > 0 && isLoggedIn === false){ setIsLoggedIn(true) } - }, [userdata]); + }, [monthlyAppRunsParent, monthlyAllSuborgExecutions, userdata]); const [BillingEmail, setBillingEmail] = useState(selectedOrganization?.Billing?.Email); @@ -199,6 +202,48 @@ const Billing = memo((props) => { } }, []) + + const getStats = (orgid) => { + + if (orgid === undefined || orgid === null) { + return + } + + fetch(`${globalUrl}/api/v1/orgs/${orgid}/stats`, { + 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!: ", response.status); + return; + } + + return response.json(); + }) + .then((responseJson) => { + if (responseJson["success"] === false) { + return + } + + setStatistics(responseJson); + }) + .catch((error) => { + console.log("error: ", error) + }); + } + + + useEffect(() => { + if (selectedOrganization && selectedOrganization?.id?.length > 0) { + getStats(selectedOrganization.id); + } + }, [selectedOrganization]); + const paperStyle = { padding: 20, // maxWidth: 400, @@ -1973,7 +2018,13 @@ const Billing = memo((props) => { }; const isChildOrg = userdata?.active_org?.creator_org !== "" && userdata?.active_org?.creator_org !== undefined && userdata?.active_org?.creator_org !== null - + + useEffect(() => { + if (isChildOrg && currentTab === 0) { + setCurrentTab(1); + } + }, [isChildOrg, currentTab]); + return (
@@ -2373,8 +2424,8 @@ const Billing = memo((props) => { We offer priority support through consultations and training to help you make the most of our product. If you have any questions, please reach out to us at {supportEmail}. We offer priority support through consultations and training to help you make the most of our product. If you have any questions, please reach out to us at -
- {billingInfo.subscription !== undefined && billingInfo.subscription !== null ? ( +
+ {/* {billingInfo.subscription !== undefined && billingInfo.subscription !== null ? ( isChildOrg ? null : ( { selectedOrganization={selectedOrganization} /> ) - ) : null} + ) : null} */}
@@ -2410,17 +2461,17 @@ const Billing = memo((props) => { }} /> - You have used {currentAppRunsInPercentage}% of total app execution limit or {userdata.app_execution_usage + userdata.app_executions_suborgs} app runs out of {userdata.app_execution_limit} app runs. + You have used {currentAppRunsInPercentage}% of total app execution limit or {Number(monthlyAppRunsParent ?? 0) + Number(monthlyAllSuborgExecutions ?? 0)} app runs out of {userdata.app_execution_limit} app runs this month. {userdata?.active_org?.creator_org?.length > 0 ? null : ( <> - Parent Organization App Executions: {userdata.app_execution_usage} + Parent Organization App Executions: {monthlyAppRunsParent} - Sub-Organization App Executions: {userdata.app_executions_suborgs || "N/A"} + Sub-Organization App Executions: {monthlyAllSuborgExecutions || "N/A"} )} @@ -2602,12 +2653,7 @@ const Billing = memo((props) => { { - setCurrentTab(-1) - - // Force re-render - setTimeout(() => { - setCurrentTab(newValue) - }, 100); + setCurrentTab(newValue) }} style={{ marginTop: 20 }} TabIndicatorProps={{ @@ -2619,52 +2665,66 @@ const Billing = memo((props) => { } }} > - } + - - {isCloud ? - - : null} - + {isChildOrg ? null : + />} + {isCloud ? + + : null}
- {currentTab === 0 ? -
- -
+ { + currentTab === 0 ? + : currentTab === 1 ? -
+
- : + : currentTab === 2 ? { setAllChildOrgs={setAllChildOrgs} allChildOrgsStats={allChildOrgsStats} setAllChildOrgsStats={setAllChildOrgsStats} + currentTab={currentTab} /> + : + }
diff --git a/frontend/src/components/BillingStats.jsx b/frontend/src/components/BillingStats.jsx index 7bd33476..8e011d42 100644 --- a/frontend/src/components/BillingStats.jsx +++ b/frontend/src/components/BillingStats.jsx @@ -45,6 +45,12 @@ const AppStats = (defaultprops) => { inputWorkflows, clickedFromOrgTab, syncStats, + statistics, + monthlyAppRunsParent, + setMonthlyAppRunsParent, + monthlyAllSuborgExecutions, + setMonthlyAllSuborgExecutions, + currentTab } = defaultprops; const [keys, setKeys] = useState([]) @@ -57,7 +63,6 @@ const AppStats = (defaultprops) => { const [endTime, setEndTime] = useState("") const [startTime, setStartTime] = useState("") - const [statistics, setStatistics] = useState(undefined); const [filteredStatistics, setFilteredStatistics] = useState(undefined); const [apprunCost, setApprunCost] = useState(0) @@ -78,6 +83,11 @@ const AppStats = (defaultprops) => { } }, []) + useEffect(() => { + if (statistics && statistics?.org_id?.length > 0) { + handleDataSetting(statistics, "day") + } +}, [statistics]) const getWorkflowStats = async (workflow, startTime, endTime) => { @@ -219,6 +229,7 @@ const AppStats = (defaultprops) => { const statKey = syncStats === true ? "onprem_stats" : "daily_statistics" if (statistics[statKey] === undefined || statistics[statKey] === null) { setFilteredStatistics(statistics) + setMonthlyAppRunsParent(statistics["monthly_app_executions"] ?? 0) return } @@ -265,20 +276,27 @@ const AppStats = (defaultprops) => { } } - // Make a date at the 1st of the current month + // Make a date at the 1st of the current month - only when no start time is selected var foundstarttime = (new Date()) - foundstarttime.setDate(1) if (startTime !== "" && startTime !== undefined && startTime !== null) { - foundstarttime = startTime + foundstarttime = new Date(startTime) + // Set to start of day to include the entire start date + foundstarttime.setHours(0, 0, 0, 0) + } else { + // Default to 1st of current month when no start time is selected + foundstarttime.setDate(1) + foundstarttime.setHours(0, 0, 0, 0) } - // Set to tomorrow by default + // Set end time properly var foundendtime = (new Date()) - foundendtime.setDate(foundendtime.getDate() + 1) - - // Check if endtime is after the daily statistics["date"] string if (endTime !== "" && endTime !== undefined && endTime !== null) { - foundendtime = endTime + foundendtime = new Date(endTime) + // Set to end of day to include the entire end date + foundendtime.setHours(23, 59, 59, 999) + } else { + // Default to current date when no end time is selected + foundendtime.setHours(23, 59, 59, 999) } // Check if start time is before the daily statistics["date"] string @@ -290,8 +308,20 @@ const AppStats = (defaultprops) => { } const date = new Date(item["date"]) - if (date >= foundstarttime) { - if (date <= foundendtime) { + // Normalize the date to start of day for comparison + const normalizedDate = new Date(date) + normalizedDate.setHours(0, 0, 0, 0) + + // Normalize foundstarttime for comparison + const normalizedStartTime = new Date(foundstarttime) + normalizedStartTime.setHours(0, 0, 0, 0) + + // Normalize foundendtime for comparison + const normalizedEndTime = new Date(foundendtime) + normalizedEndTime.setHours(0, 0, 0, 0) + + if (normalizedDate >= normalizedStartTime) { + if (normalizedDate <= normalizedEndTime) { newlist.push(item) } } @@ -326,6 +356,10 @@ const AppStats = (defaultprops) => { workflowexecutions += item["workflow_executions"] appexecutions += item["app_executions"] + if (currentTab === 0) { + appexecutions += (item["child_app_executions"] ?? 0) + } + estimatedcost += (item["app_executions"] * invocationCost) } @@ -343,7 +377,16 @@ const AppStats = (defaultprops) => { } setFilteredStatistics(tmpstats) - handleDataSetting(tmpstats, "day") + handleDataSetting(tmpstats, "day") + // if we have done monthly reset than only show monthly app runs as current month app run + const currentMonth = new Date().getMonth() + 1 + if (!monthlyAppRunsParent && statistics["monthly_app_executions"] > 0 && currentMonth === statistics["last_monthly_reset_month"]) { + setMonthlyAppRunsParent(statistics["monthly_app_executions"]) + } + + if (!monthlyAllSuborgExecutions && statistics["monthly_child_app_executions"]> 0 && currentMonth === statistics["last_monthly_reset_month"]) { + setMonthlyAllSuborgExecutions(statistics["monthly_child_app_executions"]) + } if (workflows !== undefined && workflows !== null && workflows.length > 0) { @@ -429,7 +472,7 @@ const AppStats = (defaultprops) => { if (item["child_app_executions"] !== undefined && item["child_app_executions"] !== null) { childorgappRuns["data"].push({ key: new Date(item["date"]), - data: inputdata["child_app_executions"] + data: item["child_app_executions"] }) } @@ -449,40 +492,45 @@ const AppStats = (defaultprops) => { } } - // Adds data for today - if (inputdata["daily_app_executions"] !== undefined && inputdata["daily_app_executions"] !== null) { - appRuns["data"].push({ - key: new Date(), - data: inputdata["daily_app_executions"] - }) + // Only add today's data if endTime is not set or if today falls within the selected date range + const today = new Date() + const shouldAddTodayData = endTime === "" || endTime === undefined || endTime === null || + (new Date(endTime) >= today.setHours(0, 0, 0, 0)) - appcostRuns["data"].push({ - key: new Date(), - data: (inputdata["daily_app_executions"] * invocationCost).toFixed(2) - }) - } + if (shouldAddTodayData) { + // Adds data for today + if (inputdata["daily_app_executions"] !== undefined && inputdata["daily_app_executions"] !== null) { + appRuns["data"].push({ + key: new Date(), + data: inputdata["daily_app_executions"] + }) - if (inputdata["daily_child_app_executions"] !== undefined && inputdata["daily_app_executions"] !== null) { - childorgappRuns["data"].push({ - key: new Date(), - data: inputdata["daily_child_app_executions"] - }) + appcostRuns["data"].push({ + key: new Date(), + data: (inputdata["daily_app_executions"] * invocationCost).toFixed(2) + }) + } - //setApprunCosts(appcostRuns) - } + if (inputdata["daily_child_app_executions"] !== undefined && inputdata["daily_app_executions"] !== null) { + childorgappRuns["data"].push({ + key: new Date(), + data: inputdata["daily_child_app_executions"] + }) + } - if (inputdata["daily_workflow_executions"] !== undefined && inputdata["daily_workflow_executions"] !== null) { - workflowRuns["data"].push({ - key: new Date(), - data: inputdata["daily_workflow_executions"] - }) - } + if (inputdata["daily_workflow_executions"] !== undefined && inputdata["daily_workflow_executions"] !== null) { + workflowRuns["data"].push({ + key: new Date(), + data: inputdata["daily_workflow_executions"] + }) + } - if (inputdata["daily_subflow_executions"] !== undefined && inputdata["daily_subflow_executions"] !== null) { - subflowRuns["data"].push({ - key: new Date(), - data: inputdata["daily_subflow_executions"] - }) + if (inputdata["daily_subflow_executions"] !== undefined && inputdata["daily_subflow_executions"] !== null) { + subflowRuns["data"].push({ + key: new Date(), + data: inputdata["daily_subflow_executions"] + }) + } } // Only for parent orgs @@ -494,49 +542,8 @@ const AppStats = (defaultprops) => { setWorkflowRuns(workflowRuns) setAppruns(appRuns) setApprunCosts(appcostRuns) - } - - const getStats = (orgid) => { - - if (orgid === undefined || orgid === null) { - return - } - - fetch(`${globalUrl}/api/v1/orgs/${orgid}/stats`, { - 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!: ", response.status); - return; - } - - return response.json(); - }) - .then((responseJson) => { - if (responseJson["success"] === false) { - return - } - - setStatistics(responseJson) - handleDataSetting(responseJson, "day") - }) - .catch((error) => { - console.log("error: ", error) - }); } - useEffect(() => { - if(selectedOrganization?.id?.length > 0) { - getStats(selectedOrganization.id) - } - }, [selectedOrganization]) - const paperStyle = { textAlign: "center", padding: "40px", @@ -656,15 +663,19 @@ const AppStats = (defaultprops) => { ] const data = ( -
+
All shown statistics are gathered from Your Organisation Statistics. - It exists to give you more insight into your workflows, and to understand your utilization of the Shuffle platform. The billing tracker is in Beta, and is always calculated manually before being invoiced. + {currentTab === 0 ? + + All Organization app runs are calculated base on addition of parent org app runs + all child org app runs. + : It exists to give you more insight into your workflows, and to + understand your utilization of the Shuffle platform.{" "}}
{syncStats !== true ? null : "PS: You are currently looking at data from your onprem synced org"} @@ -675,7 +686,7 @@ const AppStats = (defaultprops) => { {filteredStatistics !== undefined ?
- {syncStats == true ? null : + {/* {syncStats == true ? null : The cost of app runs in the selected period based on {filteredStatistics.monthly_app_executions} App Runs. These numbers do not exclude your included 10.000/month or {includedExecutions} App Runs per month. App Run cost: ${invocationCost}. @@ -695,7 +706,7 @@ const AppStats = (defaultprops) => { - } + } */} {syncStats === true ? null : { } - {syncStats === true ? null : + {syncStats === true || currentTab === 0 ? null : Workflow runs in the selected period @@ -731,7 +742,7 @@ const AppStats = (defaultprops) => { } - {syncStats === true ? null : + {/* {syncStats === true ? null : Estimated cost to be billed at the end of the current month. Subtracted contractually included app runs. Actual cost month to date: ${monthToDateCost}. App Run cost: ${invocationCost}. @@ -746,15 +757,9 @@ const AppStats = (defaultprops) => { - } -
- : null} -
- - - {clickedFromOrgTab? ( - -
+ } */} + +
{
- ):( - -
- } - /> - } - />
-
- )} + : null} +
{appRuns === undefined ? null : - + } - {childOrgsAppRuns === undefined ? + {childOrgsAppRuns === undefined || currentTab === 1 ? null : - + } - {workflowRuns === undefined ? + {workflowRuns === undefined || currentTab === 0? null : - + } - {subflowRuns === undefined ? + {subflowRuns === undefined || currentTab === 0 ? null : - + } {/*appRunCosts === undefined ? @@ -944,7 +918,7 @@ const AppStats = (defaultprops) => { */} - {syncStats === true ? null : + {syncStats === true || currentTab === 0 ? null :
{resultLoading ?
@@ -1000,7 +974,7 @@ const AppStats = (defaultprops) => { ) const dataWrapper = ( -
{data}
+
{data}
); return dataWrapper; diff --git a/frontend/src/components/CacheView.jsx b/frontend/src/components/CacheView.jsx index e9670baf..4da13127 100644 --- a/frontend/src/components/CacheView.jsx +++ b/frontend/src/components/CacheView.jsx @@ -35,6 +35,7 @@ import { InputLabel, Pagination, PaginationItem, + Avatar, } from "@mui/material"; import { @@ -134,7 +135,7 @@ const CacheView = memo((props) => { // Direct category migration from ../components/Files.jsx const [selectAllChecked, setSelectAllChecked] = React.useState(false) const [renderTextBox, setRenderTextBox] = React.useState(false); - const [datastoreCategories, setDatastoreCategories] = React.useState(["default"]); + const [datastoreCategories, setDatastoreCategories] = React.useState(["default", "protected"]); const [selectedCategory, setSelectedCategory] = React.useState("default"); const [selectedFileId, setSelectedFileId] = React.useState(""); const [updateToThisCategory, setUpdateToThisCategory] = useState("") @@ -322,7 +323,6 @@ const CacheView = memo((props) => { } var url = `${globalUrl}/api/v1/orgs/${orgId}/list_cache` - if (category !== undefined && category !== null && category !== "default" && category !== "") { url += "?category=" + category.replaceAll(" ", "_") } else { @@ -398,7 +398,7 @@ const CacheView = memo((props) => { } } - if ((category === undefined || category === "default" || category === "") && datastoreCategories.length === 1 && datastoreCategories[0] === "default") { + if ((category === undefined || category === "default" || category === "") && datastoreCategories.length === 2 && datastoreCategories[0] === "default") { var newcategories = ["default"] for (var key in responseJson.keys) { var foundcategory = responseJson.keys[key].category @@ -585,7 +585,7 @@ const CacheView = memo((props) => { }) .then((responseJson) => { setAddCache(responseJson); - toast("New key added Successfully!"); + toast.success("New key added!"); listOrgCache(orgId, selectedCategory, 0, pageSize, page); setModalOpen(false); }) @@ -699,6 +699,7 @@ const CacheView = memo((props) => {
+ { sortable: true, }, { - width: 600, + width: 540, field: 'value', filterable: true, headerName: 'Value', renderCell: (props) => { const data = props.row + + if (data?.category?.toLowerCase() === "protected") { + return ( + + *************** + + ) + } + const validate = validateJson(data.value) return ( @@ -1485,8 +1499,8 @@ const CacheView = memo((props) => { backgroundColor: theme.palette.platformColor, border: theme.palette.defaultBorder, padding: 5, - minWidth: 600, - maxHeight: 600, + minWidth: 500, + maxHeight: 500, overflowY: "auto", }} collapsed={true} @@ -1507,6 +1521,74 @@ const CacheView = memo((props) => { ) } }, + { + field: 'category', + headerName: 'Category', + description: 'Category for this key.', + width: 75, + filterable: false, + sortable: true, + renderCell: (props) => { + // Return avatar with hover for the category + const data = props.row + const clickCategory = (e) => { + setCategoryConfig(undefined) + setCategoryAutomations(defaultAutomation) + + if (selectAllChecked || selectedFiles.length > 0) { + setUpdateToThisCategory(data.category) + return + } + + setSelectedCategory(data.category) + if (data.category === "all" || data.category === "default") { + listOrgCache(orgId, "", 0, pageSize, page) + } else { + listOrgCache(orgId, data.category, 0, pageSize, page) + } + + // Add it to the url as a query + if (window.location.search.includes("category=")) { + const newurl = window.location.href.replace(/category=[^&]+/, `category=${data.category}`) + window.history.pushState({ path: newurl }, "", newurl) + } else { + window.history.pushState({ path: window.location.href }, "", `${window.location.href}&category=${data.category}`) + } + } + + + const iconDetails = GetIconInfo({ + "app_name": data.category, + "name": data.category, + }) + + const avatarLetter = (data.category === "" || data.category === "default" ? " " : data.category.charAt(0).toUpperCase())[0] + return ( + + { + clickCategory(e) + }} + style={{ + color: "white", + backgroundColor: iconDetails?.iconBackgroundColor || theme.palette.primary.secondary, + marginLeft: 15, + height: 30, + width: 30, + cursor: data.category !== "" && data.category !== "default" ? "pointer" : "default", + }} + variant="rounded" + > + {iconDetails?.originalIcon ? + iconDetails?.originalIcon + : + avatarLetter + } + + + ) + } + }, { field: 'actions', headerName: 'Actions', @@ -1520,7 +1602,7 @@ const CacheView = memo((props) => { return ( - {data?.workflow_id === "" || data?.workflow_id === null || data?.workflow_id === undefined ? + {data?.workflow_id === "" || data?.workflow_id === null || data?.workflow_id === undefined || data?.workflow_id?.length !== 36 ? { { title={"Go to workflow"} style={{}} aria-label={"Download"} + placement="left" > { > { > Learn more + + {selectedCategory === "protected" ? +
+ Protected keys are encrypted, only available to admins, and will be masked when used in workflows. This is a basic protection, and is NOT bulletproof. +
+ : null} +
@@ -2244,7 +2334,7 @@ const CacheView = memo((props) => { setSelectedRows(newSelection); }} keepNonExistentRowsSelected={false} - getRowId={(row) => row.key} + getRowId={(row) => `${row?.key}_${row?.category}`} autoHeight={true} sx={{ diff --git a/frontend/src/components/CollectIngestModal.jsx b/frontend/src/components/CollectIngestModal.jsx index 499b2fc3..d00faffd 100644 --- a/frontend/src/components/CollectIngestModal.jsx +++ b/frontend/src/components/CollectIngestModal.jsx @@ -23,8 +23,12 @@ import { import { Rocket as RocketIcon, FilterAlt as FilterAltIcon, + Add as AddIcon, } from '@mui/icons-material'; +import algoliasearch from 'algoliasearch/lite'; +const searchClient = algoliasearch("JNSS5CFDZZ", "c8f882473ff42d41158430be09ec2b4e") + const CollectIngestModal = (props) => { const { globalUrl, open, setOpen, workflows, getWorkflows, apps, } = props; @@ -86,11 +90,13 @@ const CollectIngestModal = (props) => { } const IngestItem = (props) => { - const { type, appCategory, index } = props + const { type, appCategory, index, webhook } = props const [hovering, setHovering] = useState(false); const [selectedApps, setSelectedApps] = useState([]); - //const [isFinished, setIsFinished] = useState(false); + + const [showAppsearch, setShowAppsearch] = useState(false); + const [algoliaOptions, setAlgoliaOptions] = useState([]); const appname = type const ingestedAmount = 20 @@ -167,7 +173,7 @@ const CollectIngestModal = (props) => { // { borderRadius: theme.palette.borderRadius, border: hovering ? `2px solid ${theme.palette.primary.main}` : foundMatchingWorkflow !== null ? `2px solid ${theme.palette.success.main}` : `2px solid ${theme.palette.secondary.main}`, textAlign: "center", - marginBottom: 5, + marginBottom: 10, overflow: "hidden", }} @@ -189,86 +195,116 @@ const CollectIngestModal = (props) => { }} onMouseLeave={() => setHovering(false)} > -
- {iconDetails?.originalIcon && ( - iconDetails?.originalIcon - )} +
- +
- {appname} - -
+
+ {selectedApps.map((app, index) => { + // Show image of each one + return ( +
+ + + +
+ ) + })} -
- {matchingapps.length > 0 ? - + { + setShowAppsearch(!showAppsearch) + }} + > + + + +
- value={selectedApps} - onChange={(event, value) => { - setSelectedApps(value) - }} + {showAppsearch ? + { - const parsedname = option.name.replaceAll("_", " ") + value={selectedApps} + onChange={(event, value) => { + setSelectedApps(value) + }} - return ( -
- {option.name} - - {parsedname} - -
- ) - }} - renderInput={(params) => { - return ( - - ) - }} - /> - : null} + getOptionLabel={(option) => { + const parsedname = option.name.replaceAll("_", " ") + return ( +
+ {option.name} + + {parsedname} + +
+ ) + }} + renderInput={(params) => { + return ( + + ) + }} + /> + : + } +
- startIngestion(appname, newapps, appCategory, index) - //} - }}> - {foundMatchingWorkflow !== null ? - "Re-Create Ingestion" - : - "Start Ingestion" - } - +
+ {iconDetails?.originalIcon && ( + iconDetails?.originalIcon + )} + + + + {appname} + +
{foundMatchingWorkflow !== null ? @@ -319,7 +355,7 @@ const CollectIngestModal = (props) => { sx: { borderRadius: theme?.palette?.DialogStyle?.borderRadius, border: theme?.palette?.DialogStyle?.border, - minWidth: 500, + minWidth: 850, minHeight: 700, fontFamily: theme?.typography?.fontFamily, backgroundColor: theme?.palette?.DialogStyle?.backgroundColor, @@ -350,13 +386,14 @@ const CollectIngestModal = (props) => { - + + + - diff --git a/frontend/src/components/EditWorkflow.jsx b/frontend/src/components/EditWorkflow.jsx index 00b9608d..d227a2bd 100644 --- a/frontend/src/components/EditWorkflow.jsx +++ b/frontend/src/components/EditWorkflow.jsx @@ -3,6 +3,7 @@ import { getTheme } from '../theme.jsx'; import { isMobile } from "react-device-detect" import { MuiChipsInput } from "mui-chips-input"; import { toast } from "react-toastify" +import ReactGA from 'react-ga4'; import UsecaseSearch from "../components/UsecaseSearch.jsx" import WorkflowGrid from "../components/WorkflowGrid.jsx" import dayjs from 'dayjs'; @@ -63,7 +64,10 @@ import { Add as AddIcon, Remove as RemoveIcon, EditNote as EditNoteIcon, - AutoAwesome as AutoAwesomeIcon + AutoAwesome as AutoAwesomeIcon, + CloudUpload as CloudUploadIcon, + CheckCircle as CheckCircleIcon, + Close as CloseIcon } from "@mui/icons-material"; const EditWorkflow = (props) => { @@ -94,6 +98,11 @@ const EditWorkflow = (props) => { const [selectedCleanupActions, setSelectedCleanupActions] = React.useState(workflow?.form_control?.cleanup_actions !== undefined && workflow?.form_control?.cleanup_actions !== null ? JSON.parse(JSON.stringify(workflow?.form_control?.cleanup_actions)) : []) const [formWidth, setFormWidth] = React.useState(boxWidth === undefined || boxWidth === null ? 500 : boxWidth) + + // Flowchart upload states + const [uploadedImage, setUploadedImage] = React.useState(null) + const [imageBase64, setImageBase64] = React.useState("") + const [imageUploading, setImageUploading] = React.useState(false) const classes = useStyles(); @@ -103,6 +112,59 @@ const EditWorkflow = (props) => { } }, [formWidth]) + // Handle file upload and base64 conversion + const handleImageUpload = (file) => { + const allowedTypes = ['image/png', 'image/jpeg', 'image/jpg'] + if (!allowedTypes.includes(file.type)) { + toast.error("Please upload a PNG, JPG, or JPEG image") + return + } + + const maxSize = 5 * 1024 * 1024 // 5MB in bytes + if (file.size > maxSize) { + toast.error("Image must be less than 5MB") + return + } + + setImageUploading(true) + + const reader = new FileReader() + reader.onload = (e) => { + const base64 = e.target.result + setImageBase64(base64) + setUploadedImage({ + name: file.name, + size: file.size, + type: file.type + }) + setImageUploading(false) + + // Disable "Create from scratch" when image is uploaded + if (newWorkflow) { + setWorkflowAsCode(false) + } + } + reader.onerror = () => { + toast.error("Failed to read image file") + setImageUploading(false) + } + reader.readAsDataURL(file) + } + + const removeUploadedImage = () => { + setUploadedImage(null) + setImageBase64("") + setImageUploading(false) + } + + const formatFileSize = (bytes) => { + if (bytes === 0) return '0 Bytes' + const k = 1024 + const sizes = ['Bytes', 'KB', 'MB', 'GB'] + const i = Math.floor(Math.log(bytes) / Math.log(k)) + return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i] + } + if (scrollTo !== undefined && scrollTo !== null && scrollTo.length > 0 && scrollDone === false) { setTimeout(() => { const foundScroll = document.getElementById(scrollTo) @@ -308,7 +370,7 @@ const EditWorkflow = (props) => { variant="contained" style={{}} id="save_workflow_button" - disabled={name.length === 0 || submitLoading === true || aiGenerateLoading === true} + disabled={name.length === 0 || submitLoading === true || aiGenerateLoading === true || uploadedImage !== null} onClick={() => { setSubmitLoading(true) @@ -395,24 +457,46 @@ const EditWorkflow = (props) => { - Generate using the name, description, usecases and tags provided. Required: Name + Description + Generate using the name, description, usecases and tags provided. Required: Name + (Description OR Flowchart Image) } >
+ {/* Flowchart Upload Section - Only for new workflows */} + {newWorkflow === true ? ( +
+ {!uploadedImage ? ( +
{ + const input = document.createElement('input') + input.type = 'file' + input.accept = 'image/png,image/jpeg,image/jpg' + input.onchange = (e) => { + if (e.target.files.length > 0) { + handleImageUpload(e.target.files[0]) + } + } + input.click() + }} + > + {imageUploading ? ( +
+ + + Processing image... + +
+ ) : ( +
+ + + Generate Workflow from Flowchart + + + Click to upload your flowchart - AI will convert it to a workflow + + + PNG, JPG, JPEG β€’ Max 5MB + +
+ )} +
+ ) : ( +
+
+ +
+ + {uploadedImage.name} + + + {formatFileSize(uploadedImage.size)} + +
+
+ + + +
+ )} +
+ ) : null} + {showMoreClicked === true ?
{ - Beta Feature: When a workflow run is done, the data from the selected actions will be removed by replacing it with a default value. This is useful for cleaning up sensitive data, or data that is no longer needed. This is done after a workflow run is finished or aborted, and is not reversible. Data will remain in the workflow run result (last node value) even if the action result itself is cleaned up. + When a workflow run is done, the data from the selected actions will be removed by replacing it with a default value. This is useful for cleaning up sensitive data, or data that is no longer needed. This is done after a workflow run is finished or aborted, and is not reversible. Data will remain in the workflow run result (last node value) even if the action result itself is cleaned up. diff --git a/frontend/src/components/LicencePopup.jsx b/frontend/src/components/LicencePopup.jsx index 0267267d..70e4cc6e 100644 --- a/frontend/src/components/LicencePopup.jsx +++ b/frontend/src/components/LicencePopup.jsx @@ -418,6 +418,10 @@ const LicencePopup = (props) => { showSupport = true } + if (userdata?.app_execution_limit >= 300000) { + top_text = "Enterprise Plan" + } + if (subscription.name.includes("Open Source")) { top_text = "Open Source" showSupport = true @@ -761,7 +765,7 @@ const LicencePopup = (props) => { { isCloud ? userdata?.app_execution_limit && userdata?.app_execution_limit !== 10000 ? - "You have already subscribed to the Scale plan, which includes " + (userdata?.app_execution_limit/1000) + "K app runs/month. You can increase the limit by upgrading current plan. Contact support@shuffler.io for more information." : + `You have already subscribed to the ${top_text}, which includes ${userdata?.app_execution_limit/1000}K app runs/month. You can increase the limit by upgrading current plan. Contact support@shuffler.io for more information.` : `You are using free Starter plan with max ${userdata?.app_execution_limit === 10000 ? "10,000" : "2,000"} runs per month. Upgrade to increase this limit.` : diff --git a/frontend/src/components/Navbar.jsx b/frontend/src/components/Navbar.jsx index 41b80efe..63efe7a9 100644 --- a/frontend/src/components/Navbar.jsx +++ b/frontend/src/components/Navbar.jsx @@ -87,15 +87,15 @@ const menuData = { ], Services: [ { - title: "Professional Services", + title: "Proof of Concept", description: - "Professional Services help you solve problems at your convenience.", + "Register for POC to test the best of Shuffle for free for 30 days.", icon: "/images/ProfessionalServices.svg", - path: "/professional-services", + path: "/poc", gaData: { category: "navbar", action: "services_click", - label: "professional_services_click" + label: "proof_of_concept_click" } }, { @@ -103,7 +103,7 @@ const menuData = { description: "Support to help you build automations with confidence.", icon: "/images/Support.svg", - path: "/contact?category=support", + path: "/support", gaData: { category: "navbar", action: "services_click", @@ -1486,7 +1486,7 @@ const Navbar = (props) => { } }} > - Become a partner + Become a Partner @@ -1661,7 +1661,7 @@ const Navbar = (props) => { const topbarHeight = showTopbar ? 40 : 0 const topbar = !isCloud || !showTopbar ? null : - curpath === "/" || curpath.includes("/docs") || curpath === "/pricing" || curpath === "/contact" || curpath === "/search" || curpath === "/usecases" || curpath === "/training" || curpath === "/professional-services" ? + curpath === "/" || curpath.includes("/docs") || curpath === "/pricing" || curpath === "/contact" || curpath === "/search" || curpath === "/usecases" || curpath === "/training" || curpath === "/poc" || curpath === "/professional-services" ? {/* uncommit this to show topbar for release */} {/*
diff --git a/frontend/src/components/OrgHeaderexpandedNew.jsx b/frontend/src/components/OrgHeaderexpandedNew.jsx index 18d65423..d0cd4952 100644 --- a/frontend/src/components/OrgHeaderexpandedNew.jsx +++ b/frontend/src/components/OrgHeaderexpandedNew.jsx @@ -419,7 +419,7 @@ const OrgHeaderexpandedNew = (props) => {
- Name + Name {
{userdata?.support ? (
-
Status
+
Status