diff --git a/frontend/src/components/AdminNavBar.jsx b/frontend/src/components/AdminNavBar.jsx index 2a480c95..fbbc391d 100644 --- a/frontend/src/components/AdminNavBar.jsx +++ b/frontend/src/components/AdminNavBar.jsx @@ -92,9 +92,14 @@ const AdminNavBar = (props) => { const HandleVisibleTabs = () => { if (userdata?.id?.length > 0) { if (userdata?.active_org?.role === "admin" || userdata?.support) { - setVisibleItems(items); + if (isChildOrg) { + const filteredItems = items.filter(item => item.text !== "Partner"); + setVisibleItems(filteredItems); + }else { + setVisibleItems(items); + } }else { - const filteredItems = items.filter(item => item.text !== "Users" && item.text !== "Files" && item.text !== "Datastore" && item.text !== "Triggers" && item.text !== "Locations"); + const filteredItems = items.filter(item => item.text !== "Users" && item.text !== "Files" && item.text !== "Datastore" && item.text !== "Triggers" && item.text !== "Locations" && item.text !== "Partner"); setVisibleItems(filteredItems); } } @@ -105,12 +110,12 @@ const AdminNavBar = (props) => { // Filter out Users and Tenants tabs if (userdata?.active_org?.role === "admin" || userdata?.support) { const filteredItems = items.filter(item => - item.text !== "Users" && item.text !== "Tenants" + item.text !== "Users" && item.text !== "Tenants" && item.text !== "Partner" ); setVisibleItems(filteredItems); }else { const filteredItems = items.filter(item => - item.text !== "Users" && item.text !== "Tenants" && item.text !== "Files" && item.text !== "Datastore" && item.text !== "Triggers" && item.text !== "Locations" + item.text !== "Users" && item.text !== "Partner" && item.text !== "Tenants" && item.text !== "Files" && item.text !== "Datastore" && item.text !== "Triggers" && item.text !== "Locations" ); setVisibleItems(filteredItems); } @@ -191,7 +196,7 @@ const AdminNavBar = (props) => { const params = new URLSearchParams(location.search); const tab = params?.get('tab')?.toLowerCase(); - if (tab === "users" || tab === "tenants") { + if (tab === "users" || tab === "tenants" || tab === "partner") { toast.info("You are not allowed to access this tab. Please contact your admin for more information. Redirecting to Organization Configuration tab."); setTimeout(() => { setSelectedItem("Organization"); @@ -200,6 +205,7 @@ const AdminNavBar = (props) => { } , 3000); } + } else if (userdata && isOrgLoaded && isUserDataLoaded && userdata?.active_org?.role !== "admin" && !userdata?.support) { const queryParams = new URLSearchParams(location.search); const tabName = queryParams?.get('admin_tab')?.toLowerCase(); diff --git a/frontend/src/components/AnalyticsTab.jsx b/frontend/src/components/AnalyticsTab.jsx index c97560ed..c64dee56 100644 --- a/frontend/src/components/AnalyticsTab.jsx +++ b/frontend/src/components/AnalyticsTab.jsx @@ -2,7 +2,6 @@ // import Switch from '@mui/material/Switch'; // import { Typography, Button } from '@mui/material'; // import { useNavigate, Link, useParams } from "react-router-dom"; -// import { Bar } from 'react-chartjs-2'; // import Grid from '@mui/material/Grid'; // import SearchIcon from '@mui/icons-material/Search'; // import NewReleasesIcon from '@mui/icons-material/NewReleases'; diff --git a/frontend/src/components/Billing.jsx b/frontend/src/components/Billing.jsx index 0b9daa6f..19698a13 100644 --- a/frontend/src/components/Billing.jsx +++ b/frontend/src/components/Billing.jsx @@ -94,9 +94,9 @@ const Billing = memo((props) => { useEffect(() => { if (userdata.app_execution_limit !== undefined && userdata.app_execution_usage !== undefined) { - const percentage = (userdata.app_execution_usage / userdata.app_execution_limit) * 100; + const percentage = ((userdata.app_execution_usage + userdata.app_executions_suborgs) / userdata.app_execution_limit) * 100; setCurrentAppRunsInPercentage(Math.round(percentage)); - setCurrentAppRunsInNumber(userdata.app_execution_limit - userdata.app_execution_usage); + setCurrentAppRunsInNumber(userdata.app_execution_limit - userdata.app_execution_usage - userdata.app_executions_suborgs); } if (userdata?.id?.length > 0 && isLoggedIn === false){ @@ -1860,19 +1860,6 @@ const Billing = memo((props) => { const updateAlertThreshold = (index, field, value) => { - if (field === 'percentage') { - if (value > 100 || value < 0) { - value = 0 - toast("The percentage value should be between 0 and 100") - } - } else if (field === 'count') { - if (value < 0 || value >= userdata.app_execution_limit) { - value = 0 - toast("The count value should be greater than 0 and less than the total app execution limit") - } - } - - const totalValue = userdata.app_execution_limit; const newAlertThresholds = alertThresholds.map((threshold, i) => { if (i === index) { @@ -2423,9 +2410,21 @@ const Billing = memo((props) => { }} /> - You have used {currentAppRunsInPercentage}% of total app execution limit or {userdata.app_execution_usage} app runs out of {userdata.app_execution_limit} app runs. + 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. - + + {userdata?.active_org?.creator_org?.length > 0 ? null : + ( + <> + + Parent Organization App Executions: {userdata.app_execution_usage} + + + Sub-Organization App Executions: {userdata.app_executions_suborgs || "N/A"} + + + )} +
Set email alert thresholds for app runs @@ -2442,8 +2441,8 @@ const Billing = memo((props) => { : " " + 0 + " "} app runs. - - Please note: Once your app runs reach the set alert threshold, all admins in the organization will receive an email notification. + + Please note: Once your app runs reach the set alert threshold, all admins in the organization will receive an email notification. For Parent organizations, the alert will be sent base on the total app runs from both parent and sub-organizations. For Sub-organizations, the alert will be sent based on the app runs of the sub-organization only.
{alertThresholds.map((threshold, index) => ( @@ -2706,6 +2705,7 @@ const BillingStatsChildOrg = memo(({ userdata, globalUrl, selectedOrganization, const { themeMode, brandColor, supportEmail } = useContext(Context); const theme = getTheme(themeMode, brandColor); + // Handle page change const handleChangePage = (event, newPage) => { setPage(newPage); @@ -2830,6 +2830,7 @@ const BillingStatsChildOrg = memo(({ userdata, globalUrl, selectedOrganization, usage: stat?.monthly_app_executions || "N/A", workflows_usage: stat?.total_workflow_executions || "N/A", workflow_usage_limit: subOrg?.sync_features?.workflow_executions?.limit || "N/A", + app_runs_hard_limit: subOrg?.Billing?.app_runs_hard_limit || 0, } }) @@ -2890,6 +2891,33 @@ const BillingStatsChildOrg = memo(({ userdata, globalUrl, selectedOrganization, )} }, + { + field: "app_runs_hard_limit", headerName: "App Executions Hard Limit", width: 200, renderCell: (params) => { + console.log("params.row: ", params.row) + return ( + <> + + {params.row.app_runs_hard_limit} + + { + setOpen(true) + setEditingOrgId(params.row.orgId) + setEditing("app_executions_hard_limit") + if (params.row.app_runs_hard_limit === "N/A") { + setLimit("") + } else { + setLimit(params.row.app_runs_hard_limit) + } + }} + > + + + + ) + } + } ] setSubOrgStatsColumns(columns) @@ -2913,7 +2941,7 @@ const BillingStatsChildOrg = memo(({ userdata, globalUrl, selectedOrganization, return } - if (selectedOrganization.sync_features.app_executions.limit <= 10000) { + if (selectedOrganization.sync_features.app_executions.limit <= 10000 && editing === "app_executions") { toast.error("Insufficient app execution limit to increase child org limit") return } @@ -2939,7 +2967,10 @@ const BillingStatsChildOrg = memo(({ userdata, globalUrl, selectedOrganization, const org = subOrgs[orgIndex] - org.sync_features[editing].limit = limit + if (editing !== "app_executions_hard_limit") { + org.sync_features[editing].limit = limit + } + org.sync_features.editing = true const sync_features = org.sync_features const data = { @@ -2947,6 +2978,13 @@ const BillingStatsChildOrg = memo(({ userdata, globalUrl, selectedOrganization, sync_features: sync_features, } + if (editing === "app_executions_hard_limit") { + data.editing = "app_runs_hard_limit"; + data.billing = { + app_runs_hard_limit: limit || 0 + }; + } + const url = `${globalUrl}/api/v1/orgs/${orgId}`; fetch(url, { method: "POST", @@ -2974,6 +3012,12 @@ const BillingStatsChildOrg = memo(({ userdata, globalUrl, selectedOrganization, newRows[orgIndex].workflow_usage_limit = limit; return newRows; }); + }else if (editing === "app_executions_hard_limit") { + setSubOrgStatsRows((prevRows) => { + const newRows = [...prevRows]; + newRows[orgIndex].app_runs_hard_limit = limit; + return newRows; + }); } } }) @@ -3087,10 +3131,21 @@ const IncreaseLimitPopUp = memo(({ open, onClose, limit, setLimit, HandleEditLim } }} > - + {editing === "app_executions_hard_limit" ? ( + + Add {editing.replaceAll("_", " ").replace(/\b\w/g, c => c.toUpperCase())} + + ) : ( + Increase {editing.replaceAll("_", " ").replace(/\b\w/g, c => c.toUpperCase())} Limit + )} + { editing === "app_executions_hard_limit" ? ( + + Please note that once you set a hard limit for app runs workflows will not be able to run if the limit is reached. You will be notified by email when you reach the limit. + + ) : null} setCurrentLimit(e.target.value)} diff --git a/frontend/src/components/BillingStats.jsx b/frontend/src/components/BillingStats.jsx index 5bb09ddb..7bd33476 100644 --- a/frontend/src/components/BillingStats.jsx +++ b/frontend/src/components/BillingStats.jsx @@ -31,57 +31,10 @@ import { Box, } from "@mui/material"; -import { - BarChart, - BarSeries, - Bar, - BarLabel, - - GridlineSeries, - Gridline, - TooltipArea, - ChartTooltip, - TooltipTemplate, -} from 'reaviz'; - import { typecost, typecost_single, } from "../views/HandlePaymentNew.jsx"; +import LineChartWrapper from '../components/LineChartWrapper.jsx'; import { Context } from '../context/ContextApi.jsx'; -const LineChartWrapper = ({keys, inputname, height, width}) => { - const [hovered, setHovered] = useState(""); - const inputdata = keys.data === undefined ? keys : keys.data - const {themeMode} = useContext(Context) - const theme = getTheme(themeMode) - - - return ( -
- - {inputname} - - - - } - /> - } - gridlines={ - } /> - } - /> - -
- ) -} - const AppStats = (defaultprops) => { const { diff --git a/frontend/src/components/CacheView.jsx b/frontend/src/components/CacheView.jsx index 1e94d131..e9670baf 100644 --- a/frontend/src/components/CacheView.jsx +++ b/frontend/src/components/CacheView.jsx @@ -4,7 +4,8 @@ import { getTheme } from "../theme.jsx"; import { toast } from 'react-toastify'; import ReactJson from "react-json-view-ssr"; -import { GetIconInfo } from "../views/Workflows2.jsx"; +import { GetIconInfo, } from "../views/Workflows2.jsx"; +import { validateJson, handleReactJsonClipboard, } from "../views/Workflows.jsx"; import { red } from "../views/AngularWorkflow.jsx"; import CollectIngestModal from "../components/CollectIngestModal.jsx"; import { @@ -76,8 +77,8 @@ import { SmartToy as SmartToyIcon, Settings as SettingsIcon, FilterAlt as FilterAltIcon, + CompareArrows as CompareArrowsIcon, } from "@mui/icons-material"; -import { validateJson, } from "../views/Workflows.jsx"; import { Context } from "../context/ContextApi.jsx"; const scrollStyle1 = { @@ -130,7 +131,6 @@ const CacheView = memo((props) => { }) const [_, setUpdate] = useState(Math.random()) const [selectedRows, setSelectedRows] = useState([]); - // Direct category migration from ../components/Files.jsx const [selectAllChecked, setSelectAllChecked] = React.useState(false) const [renderTextBox, setRenderTextBox] = React.useState(false); @@ -139,12 +139,14 @@ const CacheView = memo((props) => { const [selectedFileId, setSelectedFileId] = React.useState(""); const [updateToThisCategory, setUpdateToThisCategory] = useState("") const [workflows, setWorkflows] = useState([]); + const [apps, setApps] = useState([]); const [selectedFiles, setSelectedFiles] = useState([]); const [showAutomationMenu, setShowAutomationMenu] = useState(false); const [showSettingsMenu, setShowSettingsMenu] = useState(false); const [showCollectIngestMenu, setShowCollectIngestMenu] = useState(false); + var to_be_copied = ""; const defaultAutomation = [ { "name": "Run workflow", @@ -156,6 +158,42 @@ const CacheView = memo((props) => { "icon": , "enabled": false, }, + { + "name": "Correlate Categories", + "description": "", + "type": "singul", + "options": [{ + "key": "datastore_categories", + "value": "", + }], + "icon": , + "enabled": false, + "disabled": false, + }, + { + "name": "Run AI Agent", + "description": "", + "options": [{ + "key": "", + "value": "", + }], + "icon": , + "enabled": false, + "disabled": true, + }, + { + "name": "Send webhook", + "description": "Sends the updated value to a specified webhook URL.", + "options": [{ + "key": "webhook_url", + "value": "", + }], + "icon": , + "enabled": false, + }, + + + { "name": "Send message", "description": "", @@ -180,27 +218,6 @@ const CacheView = memo((props) => { "enabled": false, "disabled": true, }, - { - "name": "Run AI Agent", - "description": "", - "options": [{ - "key": "", - "value": "", - }], - "icon": , - "enabled": false, - "disabled": true, - }, - { - "name": "Send webhook", - "description": "Sends the updated value to a specified webhook URL.", - "options": [{ - "key": "webhook_url", - "value": "", - }], - "icon": , - "enabled": false, - }, ] const [categoryAutomations, setCategoryAutomations] = useState(defaultAutomation) @@ -210,6 +227,35 @@ const CacheView = memo((props) => { const theme = getTheme(themeMode, brandColor); const classes = useStyles(); + const getApps = () => { + const url = `${globalUrl}/api/v1/apps` + fetch(url, { + 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?.success === false) { + toast.warn("Failed to load apps. Please try again or contact support@shuffler if this persists.") + } else { + setApps(responseJson) + } + }) + .catch((error) => { + toast(error.toString()); + }); + } const getWorkflows = () => { const url = `${globalUrl}/api/v1/workflows` @@ -251,6 +297,7 @@ const CacheView = memo((props) => { useEffect(() => { getWorkflows() + getApps() listOrgCache(orgId, selectedCategory, 0, pageSize, page) }, []) @@ -322,7 +369,7 @@ const CacheView = memo((props) => { .then((responseJson) => { setCachedLoaded(true); if (responseJson?.success === true) { - setListCache(responseJson.keys) + setListCache(responseJson.keys); if (responseJson.total_amount !== undefined && responseJson.total_amount !== null && responseJson.total_amount > 0) { setTotalAmount(responseJson.total_amount) @@ -563,34 +610,6 @@ const CacheView = memo((props) => { } } - const handleReactJsonClipboard = (copy) => { - const elementName = "copy_element_shuffle"; - let copyText = document.getElementById(elementName); - - if (copyText) { - if (copy.namespace && copy.name && copy.src) { - copy = copy.src; - } - - const clipboard = navigator.clipboard; - if (!clipboard) { - toast("Can only copy over HTTPS (port 3443)"); - return; - } - - let stringified = JSON.stringify(copy); - if (stringified.startsWith('"') && stringified.endsWith('"')) { - stringified = stringified.slice(1, -1); - } - - navigator.clipboard.writeText(stringified); - toast("Copied value to clipboard, NOT json path."); - } else { - console.log("Failed to copy from " + elementName + ": ", copyText); - } - }; - - const timestamp = (timestamp) => { if (timestamp === undefined || timestamp === null || timestamp === "") { return null @@ -1096,7 +1115,153 @@ const CacheView = memo((props) => { {showOptions && ( updatedAutomation.options.map((option, optionIndex) => { - if (option?.key === "workflow_id") { + if (option?.key === "datastore_categories") { + if (datastoreCategories === undefined || datastoreCategories === null || datastoreCategories.length <= 1) { + return ( + + No categories available. Please add categories in the settings. + + ) + } + + return ( + option?.value.includes(c)) || []} + classes={{ inputRoot: classes.inputRoot }} + ListboxProps={{ + style: { + backgroundColor: theme.palette.surfaceColor, + color: theme.palette.text.primary, + borderRadius: theme.palette.borderRadius, + }, + }} + onChange={(event, newValue) => { + console.log("New Value: ", newValue) + + option.value = "" + for (var i = 0; i < newValue.length; i++) { + option.value += newValue[i] + "," + } + + if (newValue.length > 0) { + updatedAutomation.enabled = true + } else { + updatedAutomation.enabled = false + } + + updatedAutomation.options[optionIndex] = option + setUpdatedAutomation(updatedAutomation) + setUpdated(true) + + setUpdate(Math.random()) // Force re-render + }} + + getOptionLabel={(option) => { + if (option === undefined || option === null) { + return "No Categories Selected"; + } + + return option + }} + options={datastoreCategories} + fullWidth + style={{ + backgroundColor: theme.palette.textFieldStyle.backgroundColor, + borderRadius: theme.palette.textFieldStyle.borderRadius, + color: theme.palette.textFieldStyle.color, + height: 35, + marginBottom: 40, + }} + renderOption={(props, data, state) => { + const fixedname = data?.charAt(0)?.toUpperCase() + data?.slice(1)?.replaceAll("_", " ") + const iconDetails = GetIconInfo({ + "app_name": fixedname, + "name": fixedname, + }) + + const keyfound = option?.value.includes(data) + + return ( + + {data.image !== undefined && data.image !== null && data.image.length > 0 ? + {data.name} + : null} + + Choose {data} + + + } > + + +
+ {iconDetails?.originalIcon && ( + iconDetails?.originalIcon + )} +
+ + {fixedname} +
+
+
+ ) + }} + renderInput={(params) => { + return ( + + ) + }} + /> + ) + } else if (option?.key === "workflow_id") { return ( { {...props} style={{ backgroundColor: theme.palette.surfaceColor, - color: data.id === option?.value ? "red" : theme.palette.text.primary, - borderBottom: data.id === "parent" ? "2px solid rgba(255,255,255,0.5)" : null + color: data.id === option?.value ? red : theme.palette.text.primary, }} value={data} > @@ -1327,16 +1491,11 @@ const CacheView = memo((props) => { }} collapsed={true} enableClipboard={(copy) => { - // handleReactJsonClipboard(copy); + handleReactJsonClipboard(copy) }} collapseStringsAfterLength={theme.palette.jsonCollapseStringsAfterLength} iconStyle={theme.palette.jsonIconStyle} displayDataTypes={false} - onSelect={(select) => { - - // HandleJsonCopy(showResult, select, data.action.label); - console.log("SELECTED!: ", select); - }} name={null} /> : @@ -1419,7 +1578,9 @@ const CacheView = memo((props) => { { + onClick={(e) => { + e.preventDefault() + e.stopPropagation() // Try to make the value JSON indented const valid = validateJson(data.value) var newvalue = data.value @@ -1469,7 +1630,9 @@ const CacheView = memo((props) => { { + onClick={(e) => { + e.preventDefault() + e.stopPropagation() window.open(`${globalUrl}/api/v1/orgs/${orgId}/cache/${data.key}?type=text&authorization=${data.public_authorization}`, "_blank"); }} > @@ -1486,7 +1649,9 @@ const CacheView = memo((props) => { { + onClick={(e) => { + e.preventDefault() + e.stopPropagation() deleteEntry(orgId, data.key, data.category) }} > @@ -1609,6 +1774,8 @@ const CacheView = memo((props) => { workflows={workflows} getWorkflows={getWorkflows} + + apps={apps} /> {cacheDistributionModal} @@ -2067,12 +2234,14 @@ const CacheView = memo((props) => { { + setSelectedRows(newSelection); + }} onRowSelectionModelChange={(newSelection) => { - setSelectedRows(newSelection) + setSelectedRows(newSelection); }} keepNonExistentRowsSelected={false} getRowId={(row) => row.key} @@ -2245,6 +2414,11 @@ const CacheView = memo((props) => {
+ ); }) diff --git a/frontend/src/components/CollectIngestModal.jsx b/frontend/src/components/CollectIngestModal.jsx index afba4afa..499b2fc3 100644 --- a/frontend/src/components/CollectIngestModal.jsx +++ b/frontend/src/components/CollectIngestModal.jsx @@ -10,10 +10,14 @@ import { DialogTitle, DialogContent, Typography, + IconButton, Paper, LinearProgress, Grid, Button, + Tooltip, + Autocomplete, + TextField, } from '@mui/material'; import { @@ -22,7 +26,7 @@ import { } from '@mui/icons-material'; const CollectIngestModal = (props) => { - const { globalUrl, open, setOpen } = props; + const { globalUrl, open, setOpen, workflows, getWorkflows, apps, } = props; const { themeMode, brandColor } = useContext(Context); const theme = getTheme(themeMode, brandColor); @@ -37,12 +41,17 @@ const CollectIngestModal = (props) => { return null } - const startIngestion = (appname, index) => { - console.log("APPNAME:", appname, "INDEX:", index) + const startIngestion = (bundleName, appnames, category, index) => { - const body = { - "app_name": appname, - "label": appname, + var body = { + "label": bundleName, + "app_name": appnames, + + "category": "", + } + + if (category !== undefined && category !== null) { + body.category = category } const url = `${globalUrl}/api/v2/workflows/generate` @@ -61,21 +70,27 @@ const CollectIngestModal = (props) => { return response.json(); }) .then((data) => { + + if (getWorkflows !== undefined) { + getWorkflows() + } + console.log("Ingestion started successfully:", data); - toast.success(`Ingestion for ${appname} started successfully!`); + toast.success(`Ingestion for ${bundleName} started successfully!`); }) .catch((error) => { console.error("Error starting ingestion:", error); - toast.error(`Failed to start ingestion for ${appname}. Please try again.`); + toast.error(`Failed to start ingestion for ${bundleName}. Please try again or contact support@shuffler.io if this persists.`); }); } const IngestItem = (props) => { - const { type, index } = props + const { type, appCategory, index } = props const [hovering, setHovering] = useState(false); - const [isFinished, setIsFinished] = useState(false); + const [selectedApps, setSelectedApps] = useState([]); + //const [isFinished, setIsFinished] = useState(false); const appname = type const ingestedAmount = 20 @@ -85,24 +100,93 @@ const CollectIngestModal = (props) => { "name": appname, }) + var foundMatchingWorkflow = null + if (workflows !== undefined && workflows !== null && workflows.length > 0) { + const parsedName = type.toLowerCase().replaceAll(" ", "_"); + const foundWorkflow = workflows.find((workflow) => { + return workflow?.name?.toLowerCase().replaceAll(" ", "_") === parsedName + }) + + if (foundWorkflow !== undefined && foundWorkflow !== null) { + foundMatchingWorkflow = foundWorkflow + + + // Find relevant apps and maps them + if (apps.length > 0 && selectedApps.length === 0 && foundWorkflow?.actions !== undefined && foundWorkflow?.actions !== null && foundWorkflow?.actions.length > 0) { + var newSelectedApps = [] + for (var actionkey in foundWorkflow.actions) { + const action = foundWorkflow.actions[actionkey] + + if (action?.app_name !== "Singul" && action?.app_id !== "integration") { + continue + } + + for (var paramkey in action.parameters) { + const param = action.parameters[paramkey] + if (!((param.name === "app_name" || param.name === "appName") && param.value !== undefined && param.value !== null && param.value.length > 0)) { + continue + } + + // Find the app in available apps + const parsedname = param.value.replaceAll(" ", "_").toLowerCase() + for (var appkey in apps) { + const appname = apps[appkey].name.replaceAll(" ", "_").toLowerCase() + if (appname === parsedname) { + newSelectedApps.push(apps[appkey]) + break + } + } + + break + } + } + + if (newSelectedApps.length > 0) { + setSelectedApps(newSelectedApps) + } + } + + } + } + + var matchingapps = [] + if (appCategory !== undefined && appCategory !== null && appCategory.length > 0 && apps !== undefined && apps !== null && apps.length > 0) { + for (var appkey in apps) { + const app = apps[appkey] + + for (var categorykey in app.categories) { + const category = app.categories[categorykey] + if (category.toLowerCase() === appCategory.toLowerCase()) { + matchingapps.push(app) + } + } + } + } + return ( // setHovering(true)} + onMouseEnter={() => { + + //if (foundMatchingWorkflow !== null) { + //} else { + setHovering(true) + //} + }} onMouseLeave={() => setHovering(false)} >
@@ -116,18 +200,92 @@ const CollectIngestModal = (props) => {
- +
+ {matchingapps.length > 0 ? + { + setSelectedApps(value) + }} + + getOptionLabel={(option) => { + const parsedname = option.name.replaceAll("_", " ") + + return ( +
+ {option.name} + + {parsedname} + +
+ ) + }} + renderInput={(params) => { + return ( + + ) + }} + /> + : null} + + + +
+ + {foundMatchingWorkflow !== null ? + + + + + + + + : null} {hovering ?
: null} - {isFinished ? + {foundMatchingWorkflow !== null ?
{ }}> {ingestedAmount} / X + + {/* { variant="determinate" fullWidth value={{ingestedAmount}} /> + */}
: null}
@@ -189,13 +350,13 @@ const CollectIngestModal = (props) => { - + - + diff --git a/frontend/src/components/DashboardBarchart.jsx b/frontend/src/components/DashboardBarchart.jsx index 74970549..fbeab07e 100644 --- a/frontend/src/components/DashboardBarchart.jsx +++ b/frontend/src/components/DashboardBarchart.jsx @@ -1,7 +1,8 @@ import React from 'react'; -import { Bar } from 'react-chartjs-2'; import { toast } from "react-toastify"; +import LineChartWrapper from '../components/LineChartWrapper.jsx'; + export const LoadStats = (globalUrl, cachekey) => { if (globalUrl === undefined) { console.log("Error: Global URL is undefined") @@ -64,84 +65,13 @@ export const LoadStats = (globalUrl, cachekey) => { }) } +// This wrapper is a waste lol const DashboardBarchart = (props) => { - // this is clearly unfinished and not worth my time. - // refer to health page to see how i made it work there w/o using chartjs + const { timelineData, title, height, } = props; - // const { timelineData, title, height, } = props; - // var inputHeight = 15 - // if (height !== undefined && height !== null) { - // inputHeight = height - // } - - // const barOptions = { - // plugins: { - // tooltip: { - // enabled: true, // Ensure tooltips are enabled - // }, - // }, - // tooltips: { - // mode: 'index', - // intersect: false, - // }, - // legend: { - // display: false - // }, - // layout: { - // padding: { - // top: 0, // Adjust the top padding as needed - // bottom: -10, // Adjust the bottom padding as needed - // left: 0, // Adjust the left padding as needed - // right: 0, // Adjust the right padding as needed - // }, - // }, - // scales: { - // y: { - // beginAtZero: false, - // }, - // yAxes: [{ - // ticks: { - // display: false - // }, - // beginAtZero: false, - // }], - // xAxes: [{ - // ticks: { - // display: false - // }, - // beginAtZero: false, - // }] - // }, - // tooltips: { - // callbacks: { - // label: function (tooltipItem, data) { - // const label = data.labels[tooltipItem.index] - // return label.split('\n')[0] - // }, - // afterLabel: function (tooltipItem, data) { - // const amount = tooltipItem.value === undefined || tooltipItem.value === null ? 0 : tooltipItem.value - // return `Amount: ${amount}` - // }, - // title: function () { - // return title === undefined ? '' : title - // } - // } - // } - // } - - // return ( - // { - // if (elements && elements.length > 0) { - // //toast("Click event") - // console.log("Clicked: ", elements) - // } - // }} - // /> - // ) + return ( + + ) } export default DashboardBarchart; diff --git a/frontend/src/components/EditOrgTab.jsx b/frontend/src/components/EditOrgTab.jsx index 0e2b5176..b1646e68 100644 --- a/frontend/src/components/EditOrgTab.jsx +++ b/frontend/src/components/EditOrgTab.jsx @@ -25,6 +25,7 @@ const EditOrgTab = (props) => { handleGetOrg, selectedStatus, setSelectedStatus, handleEditOrg, + handleStatusChange } = props; const [organizationFeatures, setOrganizationFeatures] = React.useState({}); const [users, setUsers] = React.useState([]); @@ -39,43 +40,6 @@ const EditOrgTab = (props) => { const { themeMode, brandColor } = useContext(Context); const theme = getTheme(themeMode, brandColor); - const handleStatusChange = (event) => { - const { value } = event.target; - setSelectedStatus(value); - - handleEditOrg( - selectedOrganization?.name, - selectedOrganization?.description, - selectedOrganization.id, - selectedOrganization.image, - { - app_download_repo: selectedOrganization?.defaults?.app_download_repo, - app_download_branch: selectedOrganization?.defaults?.app_download_branch, - workflow_download_repo: selectedOrganization?.defaults?.workflow_download_repo, - workflow_download_branch: selectedOrganization?.defaults?.workflow_download_branch, - notification_workflow: selectedOrganization?.defaults?.notification_workflow, - documentation_reference: selectedOrganization?.defaults?.documentation_reference, - workflow_upload_repo: selectedOrganization?.defaults?.workflow_upload_repo, - workflow_upload_branch: selectedOrganization?.defaults?.workflow_upload_branch, - workflow_upload_username: selectedOrganization?.defaults?.workflow_upload_username, - workflow_upload_token: selectedOrganization?.defaults?.workflow_upload_token, - newsletter: selectedOrganization?.defaults?.newsletter, - weekly_recommendations: selectedOrganization?.defaults?.weekly_recommendations, - }, - { - sso_entrypoint: selectedOrganization?.sso_config?.sso_entrypoint, - sso_certificate: selectedOrganization?.sso_config?.sso_certificate, - client_id: selectedOrganization?.sso_config?.client_id, - client_secret: selectedOrganization?.sso_config?.client_secret, - openid_authorization: selectedOrganization?.sso_config?.openid_authorization, - openid_token: selectedOrganization?.sso_config?.openid_token, - SSORequired: selectedOrganization?.sso_config?.SSORequired, - auto_provision: selectedOrganization?.sso_config?.auto_provision, - }, - value.length === 0 ? ["none"] : value, - ); - }; - const getUsers = () => { fetch(globalUrl + "/api/v1/getusers", { @@ -443,6 +407,7 @@ If you're interested, please let me know a time that works for you, or set up a isEditOrgTab={true} handleGetOrg={handleGetOrg} serverside={serverside} + handleStatusChange={handleStatusChange} /> diff --git a/frontend/src/components/Files.jsx b/frontend/src/components/Files.jsx index 71c3af28..50872ae9 100644 --- a/frontend/src/components/Files.jsx +++ b/frontend/src/components/Files.jsx @@ -25,8 +25,12 @@ import { Checkbox, Chip, Menu, + Pagination, + PaginationItem, } from "@mui/material"; +import { DataGrid } from "@mui/x-data-grid"; + import { Link as LinkIcon, OpenInNew as OpenInNewIcon, @@ -45,6 +49,7 @@ import Dropzone from "../components/Dropzone.jsx"; import ShuffleCodeEditor from "../components/ShuffleCodeEditor1.jsx"; import {getTheme} from "../theme.jsx"; import { Context } from "../context/ContextApi.jsx"; +import { red } from "../views/AngularWorkflow.jsx"; const Files = memo((props) => { const { globalUrl, userdata, serverside, selectedOrganization, isCloud,isSelectedFiles } = props; @@ -75,9 +80,370 @@ const Files = memo((props) => { const [showDistributionPopup, setShowDistributionPopup] = useState(false) const [selectedSubOrg, setSelectedSubOrg] = useState([]) const [fileIdSelectedForDistribution, setFileIdSelectedForDistribution] = useState("") + const [totalAmount, setTotalAmount] = useState(0); +const [page, setPage] = useState(0); +const [pageSize, setPageSize] = useState(50) +const [selectedRows, setSelectedRows] = useState([]); +const [filesLoaded, setFilesLoaded] = useState(false); //const alert = useAlert(); const allowedFileTypes = ["txt", "py", "yaml", "yml","json", "html", "js", "csv", "log", "eml", "msg", "md", "xml", "sh", "bat", "ps1", "psm1", "psd1", "ps1xml", "pssc", "psc1", "response"] var upload = ""; + const paginatedRows = files.slice(page * pageSize, (page + 1) * pageSize); + + const columns = [ + { + field : 'filename', + headerName: 'Name', + filterable: true, + sortable: true, + width: 250, + renderCell: (params) => { + if (params.row.filename === undefined || params.row.filename === null || params.row.filename.length < 1) { + return ( + + No name + + ) + } + + return ( + + + {params.row.filename} + + + ); + } + }, + { + field: 'Workflow', + headerName: 'Workflow', + renderCell: (params) => { + const file = params.row; + return ( + file.workflow_id === "global" || !file.workflow_id ? ( + + + + ) : ( + + + + + + + + + + ) + ); + }, +}, +{ + field: 'md5_sum', + headerName: 'MD5', + width: 100, + }, + { + field: "Status", + headerName: "Status", + renderCell: (params) => { + const file = params.row; + return ( + + {file.status.charAt(0).toUpperCase() + file.status.slice(1)} + + ); + } + }, + { + field: "filesize", + headerName: "Filesize", + + }, + { + field: "actions", + headerName: "Actions", + width: 200, + renderCell: (params) => { + const file = params.row; + const filenamesplit = file.filename.split(".") + const iseditable = file.filesize < 2000000 && file.status === "active" && (allowedFileTypes.includes(filenamesplit[filenamesplit.length-1]) || !file?.filename.includes(".")) + return ( + + + + { + e.stopPropagation(); + e.preventDefault(); + setOpenEditor(true) + setOpenFileId(file.id) + readFileData(file) + }} + > + + + + + + + {/* + + + { + // Open the file, without downloading it + window.open(`${globalUrl}/api/v1/files/${file.id}/content?type=text&authorization=${file.public_authorization}`, "_blank noreferrer noopener") + }} + > + + + + + */} + + + { + e.stopPropagation(); + e.preventDefault(); + downloadFile(file); + }} + > + + + + + + + + + + + + { + e.stopPropagation(); + e.preventDefault(); + navigator.clipboard.writeText(file.id); + document.execCommand("copy"); + + toast(file.id + " copied to clipboard"); + }} + > + + + + + + + + + + { + e.stopPropagation(); + e.preventDefault(); + deleteFile(file.id, true); + }} + > + + + + + + + + + ) + } + }, + { + field: "distribution", + headerName: "Distribution", + renderCell: (params) => { + const file = params.row; + const isDistributed = file?.suborg_distribution?.length > 0 ? true : false; + + return ( + <> + {selectedOrganization.id !== undefined && file?.org_id !== selectedOrganization.id ? + + + + : + + { + e.stopPropagation(); + e.preventDefault(); + setShowDistributionPopup(true) + if(file?.suborg_distribution?.length > 0){ + setSelectedSubOrg(file.suborg_distribution) + }else{ + setSelectedSubOrg([]) + } + setFileIdSelectedForDistribution(file.id) + }} + /> + + } + + ) + } + } + ] + const handleKeyDown = (event) => { if (event.key === 'Enter') { @@ -97,7 +463,7 @@ const Files = memo((props) => { editFileConfig(id, "suborg_distribute", [...new Set(selectedSubOrg)]) } - + const editFileConfig = (id, parentAction, selectedSubOrg) => { const data = { id: id, @@ -121,9 +487,9 @@ const Files = memo((props) => { .then((response) => response.json().then((responseJson) => { if (responseJson["success"] === false) { - toast("Failed overwriting files"); + toast.error("Failed overwriting files"); } else { - toast("Successfully updated file!"); + toast.success("File updated!"); setTimeout(() => { getFiles(); }, 1000); @@ -170,6 +536,7 @@ const Files = memo((props) => { } const getFiles = (namespace) => { + setFilesLoaded(false) var parsedurl = `${globalUrl}/api/v1/files` if (namespace === undefined || namespace === null || namespace === "default") { @@ -199,6 +566,11 @@ const Files = memo((props) => { .then((responseJson) => { if (responseJson.files !== undefined && responseJson.files !== null) { setFiles(responseJson.files); + if (responseJson.total_amount !== undefined && responseJson.total_amount !== null && responseJson.total_amount > 0) { + setTotalAmount(responseJson.total_amount) + } else { + setTotalAmount(responseJson.files.length) + } setShowLoader(false) setShowDistributionPopup(false) } else if (responseJson.list !== undefined && responseJson.list !== null) { @@ -227,7 +599,9 @@ const Files = memo((props) => { }) .catch((error) => { toast(error.toString()); - }); + }).finally(() => { + setFilesLoaded(true) + }); }; useEffect(() => { @@ -585,8 +959,12 @@ const Files = memo((props) => { ): null - const deleteFile = (file) => { - fetch(globalUrl + "/api/v1/files/" + file.id, { + const deleteFile = (fileId, showSinglDeleteToast) => { + + console.log("Deleting file with ID: ", fileId) + console.log("showSinglDeleteToast: ", showSinglDeleteToast) + + fetch(globalUrl + "/api/v1/files/" + fileId, { method: "DELETE", headers: { "Content-Type": "application/json", @@ -602,18 +980,20 @@ const Files = memo((props) => { return response.json(); }) .then((responseJson) => { - if (responseJson.success) { - toast("Successfully deleted file") + if (responseJson.success && showSinglDeleteToast === true) { + toast.success("Deleted file") } else if ( responseJson.reason !== undefined && responseJson.reason !== null ) { - toast("Failed to delete file: " + responseJson.reason); + toast.error("Failed to delete file: " + responseJson.reason); } - setTimeout(() => { - getFiles(selectedCategory) - }, 1500); + if (showSinglDeleteToast === true) { + setTimeout(() => { + getFiles(selectedCategory) + }, 1500); + } }) .catch((error) => { toast(error.toString()); @@ -909,7 +1289,7 @@ const Files = memo((props) => { {fileDistributionModal}
-
+
@@ -1131,529 +1511,136 @@ const Files = memo((props) => { backgroundColor: theme.palette.textFieldStyle.backgroundColor, }} />} -
- - + { + setSelectedRows(newSelection); + }} + sx={{ + marginTop: 1, + height: files.length*52, + width: "100%", + '.MuiTablePagination-selectLabel, .MuiTablePagination-select, .MuiTablePagination-selectIcon': { + display: 'none', + }, + marginBottom: 20, + }} + hideFooterSelectedRowCount={true} + hideFooter={true} + pagination + autoHeight={true} + getRowId={(row) => row.id} + keepNonExistentRowsSelected={false} + loading={filesLoaded === false} + /> +
- {[ - - { - setSelectAllChecked((prev) => !prev); - setSelectedFiles((prev) => { - if (prev.length === files.length) { - return [] - } else { - return files.map((_, index) => !prev.includes(index)) - } - }) - if (selectAllChecked) { - setSelectedFileId([]) - } else { - setSelectedFileId( - files - .filter((file) => file.namespace === selectedCategory) - .map((file) => file.id) - ); - } - }} - /> - , - "Name", - "Workflow", - "Md5", - "Status", - "Filesize", - "Actions", - "Distribution" - ] - .filter(Boolean) - .map((header, index) => ( - - ))} - - {showLoader ? - [...Array(6)].map((_, rowIndex) => ( - - {Array(8) - .fill() - .map((_, colIndex) => ( - - - - ))} - - )): - files.length === 0 ? ( -
- - No files found - -
- ):( - files?.map((file, index) => { - if (file.namespace === "") { - file.namespace = "default"; - } - - if (file.namespace !== selectedCategory) { - return null; - } - - var bgColor = themeMode === "dark" ? "#212121" : "#FFFFFF"; - if (index % 2 === 0) { - bgColor = themeMode === "dark" ? "#1A1A1A" : "#EAEAEA"; - } - const isDistributed = file?.suborg_distribution?.length > 0 ? true : false; - const filenamesplit = file.filename.split(".") - const iseditable = file.filesize < 2000000 && file.status === "active" && (allowedFileTypes.includes(filenamesplit[filenamesplit.length-1]) || !file?.filename.includes(".")) - return ( - - {/* - - */} - - {handleFileCheckboxChange(index); setSelectedFileId(prev => { - if (prev.includes(file.id)) { - return prev.filter((item) => item !== file.id) - } else { - return [...prev, file.id] - } - })}} - /> - - - - - - : ( - - - - - - - - - - ) - } - style={{ - display: 'table-cell', - overflow: "hidden", - }} - /> - - {file.md5_sum} - - )} - primaryTypographyProps={{ - style:{ - display: 'table-cell', - marginLeft:isSelectedFiles? 15:null, - overflow: "hidden", - whiteSpace: 'nowrap', - textOverflow: 'ellipsis', - maxWidth: 200, - } - }} - /> - - - - - - { - setOpenEditor(true) - setOpenFileId(file.id) - readFileData(file) - }} - > - - - - - - - {/* - - - { - // Open the file, without downloading it - window.open(`${globalUrl}/api/v1/files/${file.id}/content?type=text&authorization=${file.public_authorization}`, "_blank noreferrer noopener") - }} - > - - - - - */} - - - { - downloadFile(file); - }} - > - - - - - - +
- - - - { - navigator.clipboard.writeText(file.id); - document.execCommand("copy"); - - toast(file.id + " copied to clipboard"); - }} - > - - - - - - - - - - { - deleteFile(file); - }} - > - - - - - - - - + display: "flex", + textAlign: "center", + }}> + + {page * pageSize + 1} - {Math.min((page + 1) * pageSize, totalAmount)} of {totalAmount} + + + { + + return ( + + ) + + }} + onChange={(e, value) => { + if (value < 1) { + return + } + + const newPage = value-1 + console.log("New page: ", value) + // handleChangePage() + + setPage(newPage) + }} + /> + + {selectedRows.length > 0 ? + + : null} +
+
-
+
) }) diff --git a/frontend/src/components/Navbar.jsx b/frontend/src/components/Navbar.jsx index 07be2cb8..41b80efe 100644 --- a/frontend/src/components/Navbar.jsx +++ b/frontend/src/components/Navbar.jsx @@ -49,7 +49,7 @@ const menuData = { { title: "Shuffle", description: - "The most versatile automation engine with focus on security.", + "The most versatile automation engine. Focused on cybersecurity.", icon: "/images/icons/shuffleLogo.svg", path: "/docs/about", gaData: { @@ -61,7 +61,7 @@ const menuData = { { title: "Singul", description: - "Connect your favorite services with a singul line of code.", + "Connect to your favorite services with a singul line of code.", icon: "/images/logos/singul.svg", path: "https://singul.io", gaData: { @@ -70,6 +70,7 @@ const menuData = { label: "singul_click" } }, + /* { title: "API Explorer", description: @@ -82,6 +83,7 @@ const menuData = { label: "api_explorer_click" } }, + */ ], Services: [ { @@ -1245,23 +1247,6 @@ const Navbar = (props) => { > {menuItem.title} - {menuItem.title === "Singul" && ( - - Beta: Coming Soon - - )} { adminTab, selectedStatus, setSelectedStatus, - isEditOrgTab + isEditOrgTab, + handleStatusChange } = props; const classes = useStyles(); @@ -68,7 +69,7 @@ const OrgHeaderexpandedNew = (props) => { style: { maxHeight: ITEM_HEIGHT * 4.5 + ITEM_PADDING_TOP, width: 300, - borderRadius: 20, + borderRadius: 4, overflowY: "scroll", }, }, @@ -93,40 +94,6 @@ const OrgHeaderexpandedNew = (props) => { const { themeMode, supportEmail, brandColor } = useContext(Context); const theme = getTheme(themeMode, brandColor); - const handleStatusChange = (event) => { - const { value } = event.target; - handleEditOrg( - orgName, - orgDescription, - selectedOrganization.id, - selectedOrganization?.image, - { - app_download_repo: selectedOrganization?.defaults?.app_download_repo, - app_download_branch: selectedOrganization?.defaults?.app_download_branch, - workflow_download_repo: selectedOrganization?.defaults?.workflow_download_repo, - workflow_download_branch: selectedOrganization?.defaults?.workflow_download_branch, - notification_workflow: selectedOrganization?.defaults?.notification_workflow, - documentation_reference: selectedOrganization?.defaults?.documentation_reference, - workflow_upload_repo: selectedOrganization?.defaults?.workflow_upload_repo, - workflow_upload_branch: selectedOrganization?.defaults?.workflow_upload_branch, - workflow_upload_username: selectedOrganization?.defaults?.workflow_upload_username, - workflow_upload_token: selectedOrganization?.defaults?.workflow_upload_token, - newsletter: selectedOrganization?.defaults?.newsletter, - weekly_recommendations: selectedOrganization?.defaults?.weekly_recommendations, - }, - { - sso_entrypoint: selectedOrganization?.sso_config?.sso_entrypoint, - sso_certificate: selectedOrganization?.sso_config?.sso_certificate, - client_id: selectedOrganization?.sso_config?.client_id, - client_secret: selectedOrganization?.sso_config?.client_secret, - openid_authorization: selectedOrganization?.sso_config?.openid_authorization, - openid_token: selectedOrganization?.sso_config?.openid_token, - SSORequired: selectedOrganization?.sso_config?.SSORequired, - auto_provision: selectedOrganization?.sso_config?.auto_provision, - }, - value.length === 0 ? ["none"] : value, - ) - } const [appDownloadBranch, setAppDownloadBranch] = React.useState( selectedOrganization.defaults === undefined ? defaultBranch @@ -544,7 +511,7 @@ const OrgHeaderexpandedNew = (props) => { renderValue={(selected) => selected.join(', ')} MenuProps={MenuProps} > - {["contacted", "lead", "demo done", "pov", "customer", "open source", "student", "internal", "creator", "tech partner", "integration partner", "distribution partner", "service partner", "old customer", "old lead"].map((name) => ( + {["contacted", "lead", "demo done", "pov", "customer", "open source", "student", "internal", "creator", "tech partner", "integration partner", "distribution partner", "channel partner", "service partner", "old customer", "old lead"].map((name) => ( -1} /> diff --git a/frontend/src/components/OrganizationTab.jsx b/frontend/src/components/OrganizationTab.jsx index d7f2acc3..ce4e5c9e 100644 --- a/frontend/src/components/OrganizationTab.jsx +++ b/frontend/src/components/OrganizationTab.jsx @@ -36,7 +36,7 @@ const OrganizationTab = (props) => { const [billingInfo, setBillingInfo] = useState({}); const [orgRequest, setOrgRequest] = React.useState(true); const [curIndex, setCurIndex] = React.useState(0); - const items = ['Org Configuration', "SSO", "Notifications", 'Billing & Stats', 'Branding']; + const items = ['Org Configuration', "SSO", "Notifications", 'Billing & Stats']; const [visibleTabs, setVisibleTabs] = useState(items); const [unreadNotifications, setUnreadNotifications] = React.useState( notifications?.filter((notification) => notification.read === false)?.length @@ -157,16 +157,16 @@ const OrganizationTab = (props) => { isLoaded={isLoaded} /> ); - case 'branding': - return ; + // case 'branding': + // return ; // case 'analytics': // return ; default: diff --git a/frontend/src/components/ParsedAction.jsx b/frontend/src/components/ParsedAction.jsx index fe7b5649..dcc64043 100755 --- a/frontend/src/components/ParsedAction.jsx +++ b/frontend/src/components/ParsedAction.jsx @@ -904,13 +904,12 @@ const ParsedAction = (props) => { if (paramvalue.includes("$")) { let actions = workflow.actions?.map((action) => { - return "$" + action.label?.toLowerCase(); + return "$" + action.label?.toLowerCase().replaceAll(" ", "_"); }) if (newActionList?.length > 0) { - let appParentActions = parentActionList?.map(action => "$" + action.name.toLowerCase()); + let appParentActions = parentActionList?.map(action => "$" + action.name.toLowerCase().replaceAll(" ", "_")); let notPresentAction = actions?.filter((action) => !appParentActions?.includes(action)) - // Extract all variable references from paramvalue // Examples of what it matches: // - Simple variables: $test, $myVar, $x diff --git a/frontend/src/components/PartnerDetails.jsx b/frontend/src/components/PartnerDetails.jsx index 755e187d..397af9eb 100644 --- a/frontend/src/components/PartnerDetails.jsx +++ b/frontend/src/components/PartnerDetails.jsx @@ -358,6 +358,21 @@ const PartnerDetails = (props) => { animation="wave" /> +
+ + Contact Email + + +
@@ -824,7 +839,7 @@ const PartnerDetails = (props) => { value={partnerData?.article_url} onBlur={() => {}} onChange={(e) => { - if (e.target.value.length > 100) { + if (e.target.value.length > 1000) { toast("Choose a shorter article URL."); return; } @@ -851,7 +866,65 @@ const PartnerDetails = (props) => { }, }} /> - + +
+ + Contact Email + + {}} + onChange={(e) => { + setPartnerData({ + ...partnerData, + contact_email: e.target.value, + }); + }} + color="primary" + InputProps={{ + style: { + color: theme.palette.textFieldStyle.color, + height: "35px", + fontSize: "1em", + borderRadius: 4, + backgroundColor: + theme.palette.textFieldStyle.backgroundColor, + }, + classes: { + notchedOutline: isEditOrgTab + ? null + : classes.notchedOutline, + }, + }} + /> +
diff --git a/frontend/src/components/PartnerSettings.jsx b/frontend/src/components/PartnerSettings.jsx index a979c7d7..53ef9bee 100644 --- a/frontend/src/components/PartnerSettings.jsx +++ b/frontend/src/components/PartnerSettings.jsx @@ -54,7 +54,8 @@ const PartnerSettings = (props) => { "tech_partner": "#ff8544", "distribution_partner": "#2BC07E", "service_partner": "#a99cf9", - "integration_partner": "#fb47a0" + "integration_partner": "#fb47a0", + "channel_partner": "#4caf50", } const handleSendUpdateRequest = () => { @@ -85,6 +86,7 @@ const PartnerSettings = (props) => { { field: partnerData?.landscape_image_url, name: "Landscape Image" }, { field: partnerData?.website_url, name: "Website URL" }, { field: partnerData?.article_url, name: "Article URL" }, + { field: partnerData?.contact_email, name: "Contact Email" }, { field: partnerData?.country, name: "Country" }, { field: partnerData?.region, name: "Region" }, ]; @@ -129,6 +131,7 @@ const PartnerSettings = (props) => { description: partnerData.description?.trim(), website_url: partnerData.website_url?.trim(), article_url: partnerData.article_url?.trim(), + contact_email: partnerData.contact_email?.trim(), partner_type: partnerTypes, expertise: partnerData?.expertise || [], services: partnerData?.services || [], @@ -177,6 +180,7 @@ const PartnerSettings = (props) => { description: partnerData.description?.trim(), website_url: partnerData.website_url?.trim(), article_url: partnerData.article_url?.trim(), + contact_email: partnerData.contact_email?.trim(), partner_type: partnerTypes, usecases: partnerData?.usecases || [], expertise: partnerData?.expertise || [], diff --git a/frontend/src/components/PartnerTab.jsx b/frontend/src/components/PartnerTab.jsx index 10c21f06..4a17cba5 100644 --- a/frontend/src/components/PartnerTab.jsx +++ b/frontend/src/components/PartnerTab.jsx @@ -195,14 +195,17 @@ const PartnerTab = (props) => { // Enable the tab by default return false; } + + const isSupportOnlyTab = (tabName) => { + return tabName === "Apps" || tabName === "Articles" || tabName === "AI Agents"; + } return (
{tabsOnPartnerTab?.map((tabName, index) => ( -
+
+ {isSupportOnlyTab(tabName) && userdata?.support && ( +
+ S +
+ )}
))}
diff --git a/frontend/src/components/PartnersUsecasesTab.jsx b/frontend/src/components/PartnersUsecasesTab.jsx index 87e34b52..07866df9 100644 --- a/frontend/src/components/PartnersUsecasesTab.jsx +++ b/frontend/src/components/PartnersUsecasesTab.jsx @@ -29,9 +29,9 @@ import CloseIcon from "@mui/icons-material/Close"; import { toast } from "react-toastify"; import MoreVertIcon from "@mui/icons-material/MoreVert"; import EditIcon from "@mui/icons-material/Edit"; -import StarIcon from "@mui/icons-material/Star"; import DeleteIcon from "@mui/icons-material/Delete"; import AddCircleOutlineIcon from "@mui/icons-material/AddCircleOutline"; +import OpenInNewIcon from '@mui/icons-material/OpenInNew'; import DeleteOutlineIcon from "@mui/icons-material/DeleteOutline"; import KeyboardArrowUpIcon from "@mui/icons-material/KeyboardArrowUp"; import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown"; @@ -1083,16 +1083,40 @@ const PartnersUsecasesTab = ({ isCloud, globalUrl, userdata, partnerData, setPar }} > - - Public Workflow - + + Public Workflow + + { + // Open workflow in new tab - adjust URL as needed + window.open(`${window.location.origin}/workflows/${formData.mainContent.publicWorkflowId}`, "_blank"); + }} + > + + + { + setMaxExecutionCount(e.target.value) + submitSearch(workflowId, status, startTime, endTime, rowCursor, e.target.value, suborgWorkflowRuns) + }} + style={{ + height: 50, + backgroundColor: theme.palette.backgroundColor, + color: theme.palette.textColor, + }} + > + 10 + 25 + 50 + 100 + 200 + 500 + + +
{userdata?.active_org?.creator_org?.length === 0 ? (
@@ -956,7 +999,8 @@ const RuntimeDebugger = (props) => { setStartTime("") setEndTime("") setSearchQuery("") - submitSearch("", "", "", "", rowCursor, rowsPerPage, !suborgWorkflowRuns)} + setMaxExecutionCount(50) + submitSearch("", "", "", "", rowCursor, 50, !suborgWorkflowRuns)} } color="secondary" /> @@ -967,7 +1011,7 @@ const RuntimeDebugger = (props) => {
{ - submitSearch(workflowId, status, startTime, endTime, rowCursor, rowsPerPage, suborgWorkflowRuns) + submitSearch(workflowId, status, startTime, endTime, rowCursor, maxExecutionCount, suborgWorkflowRuns) }} style={{display: "flex", justifyContent: "center", alignItems: "center", }}> Status @@ -1158,7 +1202,8 @@ const RuntimeDebugger = (props) => { setEndTime("") setSearchQuery("") setSuborgWorkflowRuns(false) - submitSearch("", "", "", "", rowCursor, rowsPerPage, false) + setMaxExecutionCount(50) + submitSearch("", "", "", "", rowCursor, 50, false) }} > @@ -1169,7 +1214,7 @@ const RuntimeDebugger = (props) => { variant="outlined" color="primary" onClick={() => { - submitSearch(workflowId, status, startTime, endTime, rowCursor, rowsPerPage, suborgWorkflowRuns) + submitSearch(workflowId, status, startTime, endTime, rowCursor, maxExecutionCount, suborgWorkflowRuns) }} disabled={searchLoading} style={{height: 50, minWidth: 100, marginTop: 15, }} @@ -1181,20 +1226,18 @@ const RuntimeDebugger = (props) => { { - setRowsPerPage(newPageSize) - submitSearch(workflowId, status, startTime, endTime, rowCursor, newPageSize, suborgWorkflowRuns) + + onPaginationModelChange={(newPaginationModel) => { + setPaginationModel(newPaginationModel) + setRowsPerPage(newPaginationModel.pageSize) + // No API call needed - this is just for client-side pagination }} - // event for when clicking next page - // Hide page changer - onPageChange={(params) => { - console.log("params: ", params) - }} - onSelectionModelChange={(newSelection) => { + + onRowSelectionModelChange={(newSelection) => { //console.log("newSelection: ", newSelection) //setSelectedWorkflowExecutionsIndexes(newSelection) var found = [] diff --git a/frontend/src/components/ShuffleCodeEditor1.jsx b/frontend/src/components/ShuffleCodeEditor1.jsx index 043c5de2..1490d72e 100644 --- a/frontend/src/components/ShuffleCodeEditor1.jsx +++ b/frontend/src/components/ShuffleCodeEditor1.jsx @@ -2476,9 +2476,14 @@ const CodeEditor = (props) => { }} displayDataTypes={false} onSelect={(select) => { - //HandleJsonCopy(executionResult.result, select, "exec"); + var basename = "exec" + if (selectedAction !== undefined && selectedAction !== null && Object.keys(selectedAction).length !== 0) { + basename = selectedAction.label.toLowerCase().replaceAll(" ", "_") + } + + HandleJsonCopy(executionResult.result, select, basename) }} - name={"Test result"} + name={"Run Output"} /> : diff --git a/frontend/src/components/TenantsTab.jsx b/frontend/src/components/TenantsTab.jsx index ffd15912..16cd5f11 100644 --- a/frontend/src/components/TenantsTab.jsx +++ b/frontend/src/components/TenantsTab.jsx @@ -667,13 +667,12 @@ const TenantsTab = memo((props) => { const handleDeleteAccount = () => { const baseURL = globalUrl; - const url = `${baseURL}/api/v1/orgs/${selectedOrganization?.id}`; + const url = `${baseURL}/api/v1/orgs/${selectedSuborg?.id}`; const data = { - suborg_id : selectedSuborg?.id, password: password, - } - + }; + fetch(url, { mode: "cors", method: "DELETE", diff --git a/frontend/src/components/UserManagmentTab.jsx b/frontend/src/components/UserManagmentTab.jsx index 81b65b94..6293b3ce 100644 --- a/frontend/src/components/UserManagmentTab.jsx +++ b/frontend/src/components/UserManagmentTab.jsx @@ -1465,7 +1465,11 @@ const UserManagmentTab = memo((props) => { {isCloud ? ( )} + primary={( + userRegion ? ( + {data?.user_geo_info?.country?.iso_code} + ) : null + )} style={{ display: 'table-cell', verticalAlign: 'middle', textAlign: 'center' }} />) : null} { leads.push("distribution partner"); } + if (responseJson.lead_info.channel_partner) { + leads.push("channel partner"); + } + if (responseJson.lead_info.service_partner) { leads.push("service partner"); } diff --git a/frontend/src/views/AgentUI.jsx b/frontend/src/views/AgentUI.jsx index ebc5a558..f97f31f5 100644 --- a/frontend/src/views/AgentUI.jsx +++ b/frontend/src/views/AgentUI.jsx @@ -1,11 +1,14 @@ import React, { useState, useEffect, useContext, memo } from "react"; import { Context } from "../context/ContextApi.jsx"; +import { useNavigate, Link, useLocation } from "react-router-dom"; import { getTheme } from "../theme.jsx"; import { toast } from "react-toastify" import ReactJson from "react-json-view-ssr"; +import { v4 as uuidv4} from "uuid"; import { validateJson, collapseField, handleReactJsonClipboard, HandleJsonCopy } from "../views/Workflows.jsx"; import { + Box, Button, ButtonGroup, Typography, @@ -13,6 +16,7 @@ import { CircularProgress, Tooltip, IconButton, + TextField, } from '@mui/material' import { @@ -21,6 +25,8 @@ import { RestartAlt as RestartAltIcon, ExpandMore as ExpandMoreIcon, ExpandLess as ExpandLessIcon, + Send as SendIcon, + Error as ErrorIcon, } from '@mui/icons-material' import { @@ -39,9 +45,12 @@ const AgentUI = (props) => { const [originalStartTime, setOriginalStartTime] = useState(0) const [latestEndTime, setLatestEndTime] = useState(0) + const [showAgentStarter, setShowAgentStarter] = useState(false) + const [actionInput, setActionInput] = useState("") const {themeMode} = useContext(Context) const theme = getTheme(themeMode) + const navigate = useNavigate(); const agentWrapperStyle = { width: 1000, @@ -64,6 +73,10 @@ const AgentUI = (props) => { return } + if (node_id === undefined || node_id === null || node_id === "") { + return + } + var found = false for (var key in execution_data.results) { const item = execution_data.results[key] @@ -86,6 +99,16 @@ const AgentUI = (props) => { if (found === false) { toast.warn("Failed to find the relevant AI Agent result") + + if (execution_data?.results?.length === 1) { + setAgentActionResult(execution_data.results[0]) + const validatedData = validateJson(execution_data.results[0].result) + if (validatedData.valid) { + setData(validatedData.result) + } else { + toast.warn("Action output result is not valid JSON!") + } + } } } @@ -95,10 +118,10 @@ const AgentUI = (props) => { return } - if (node_id === undefined || node_id === null) { - toast.error("No node ID provided. Please provide node_id in the URL.") - return - } + //if (node_id === undefined || node_id === null || node_id === "") { + // toast.error("No node ID provided. Please provide node_id in the URL.") + // return + //} if (authorization === undefined || authorization === null) { toast.error("No authorization provided. Please provide authorization in the URL.") @@ -166,6 +189,7 @@ const AgentUI = (props) => { const url = `${globalUrl}/api/v1/apps/agent/run?rerun=true&decision_id=${decision?.run_details?.id}` var body = agentActionResult.action + console.log("BODY: ", body) body.source_execution = execution.execution_id body.source_workflow = execution.workflow.id @@ -202,10 +226,11 @@ const AgentUI = (props) => { const executionId = params.get("execution_id") const nodeId = params.get("node_id") const authorization = params.get("authorization") - if (executionId !== undefined && executionId !== null && nodeId !== undefined && nodeId !== null && authorization !== undefined && authorization !== null) { + if (executionId !== undefined && executionId !== null && authorization !== undefined && authorization !== null) { GetExecution(executionId, nodeId, authorization) } else { - toast.warn("No execution ID or node ID provided. Please provide execution_id and node_id in the URL.") + setShowAgentStarter(true) + //toast.warn("No execution ID or node ID provided. Please provide execution_id and node_id in the URL.") } }, []) @@ -221,6 +246,11 @@ const AgentUI = (props) => { + : + item.status === "ABORTED" || item.status === "FAILURE" ? + + + : @@ -246,11 +276,13 @@ const AgentUI = (props) => { const validate = validateJson(item.details) const itemStartTime = item.start_time var itemEndTime = item.end_time - if (itemStartTime !== undefined && (itemStartTime < originalStartTime || originalStartTime === 0)) { - setOriginalStartTime(itemStartTime) + if (itemStartTime !== undefined && itemStartTime !== originalStartTime && (itemStartTime < originalStartTime || originalStartTime === 0)) { + console.log("Rerender 1") + //setOriginalStartTime(itemStartTime) } if (itemEndTime !== undefined && itemEndTime > latestEndTime) { + console.log("Rerender 2") setLatestEndTime(itemEndTime) } @@ -280,23 +312,47 @@ const AgentUI = (props) => { borderTop: "1px solid " + theme.palette.surfaceColor, borderRadius: theme.palette.borderRadius, }} + onMouseEnter={() => { + if (!hovered) { + console.log("HOVER") + setHovered(true) + } + }} + onMouseLeave={() => { + if (hovered) { + setHovered(false) + } + }} >
setHovered(true)} - onMouseLeave={() => setHovered(false)} onClick={(e) => { if (item.details === undefined || item.details === null || item.details === "") { - toast("No details to open") + + if (item?.category === "agent" && item?.type === "agent") { + // Show all the data + if (openIndexes.includes(index)) { + console.log("Rerender 3") + setOpenIndexes(openIndexes.filter((i) => i !== index)) + } else { + console.log("Rerender 4") + setOpenIndexes([...openIndexes, index]) + } + } else { + toast.warn("No details to open") + } + return } if (openIndexes.includes(index)) { + console.log("Rerender 5") setOpenIndexes(openIndexes.filter((i) => i !== index)) } else { + console.log("Rerender 6") setOpenIndexes([...openIndexes, index]) } }} @@ -421,18 +477,31 @@ const AgentUI = (props) => { const TimelineRender = (props) => { const { agent_data } = props; + + const actionResult = execution?.results?.length > 0 ? execution.results[0] : execution var timelineItems = [ { "label": "AI Agent 2", "type": "agent", "category": "agent", + "details": actionResult?.result, - "status": agent_data.status, - "start_time": agent_data.started_at, - "end_time": agent_data.completed_at, + "status": agent_data?.status, + "start_time": agent_data?.started_at, + "end_time": agent_data?.completed_at, }, ] + // Autofixer for result lol + if ((agent_data?.decisions === undefined || agent_data?.decisions === null)) { + const verifiedInput = validateJson(actionResult?.result) + if (verifiedInput.valid === true && verifiedInput.result?.decisions !== undefined && verifiedInput.result?.decisions !== null) { + agent_data.decisions = verifiedInput.result?.decisions + + setAgentActionResult(actionResult) + } + } + var sortedTimelineItems = [] for (var key in agent_data?.decisions) { const item = agent_data.decisions[key] @@ -501,39 +570,136 @@ const AgentUI = (props) => { ) } + const submitInput = (inputText) => { + toast.info("Submitting AI Agent input: " + inputText); + + //setShowAgentStarter(false); + //GetExecution(execution?.execution_id, execution?.node_id, execution?.authorization); + + if (inputText === undefined || inputText === null || inputText === "") { + toast.error("Please provide a valid input for the AI Agent.") + return + } + + // 1. Run the execution. Can this be a single-action run? + // 2. Get the execution ID and node ID from the response. + const uuid = uuidv4() + const data = { + "id": uuid, + "name":"agent", + //"app_name":"Shuffle AI", + "app_name":"AI Agent", // Failover for rerun + "app_id":"shuffle_agent", + "app_version":"1.0.0", + + "environment":"cloud", + "parameters":[ + { + "name":"app_name", + "value":"openai" + }, + { + "name":"input", + "value": inputText + }, + { + "name":"action", + "value":"list_tickets" + } + ]} + + const url = `${globalUrl}/api/v1/apps/agent_starter/run` + fetch(url, { + method: "POST", + body: JSON.stringify(data), + credentials: "include", + }) + .then((response) => { + return response.json() + }) + .then((responseJson) => { + toast.success("Got response!") + console.log("Agent run response: ", responseJson) + + if (responseJson.success === true && responseJson.authorization !== undefined && responseJson.execution_id !== undefined) { + navigate("?execution_id=" + responseJson.execution_id + "&authorization=" + responseJson.authorization) + setShowAgentStarter(false) + GetExecution(responseJson.execution_id, "", responseJson.authorization) + } + }) + .catch((error) => { + toast.error("Error: " + error) + }) + + } + return (
- {/* - - Agent Input: {data.input} - - */} - - - - + {showAgentStarter ? + { + e.preventDefault(); + submitInput(actionInput); + }}> + +
- {buttonState === "timeline" ? - - : - null + + Shuffle AI Agents + + { + setActionInput(e.target.value) + }} + InputProps={{ + endAdornment: ( + + + + + + ), + }} + /> + + : +
+ + + + + + {buttonState === "timeline" ? + + : + null + } +
}
) diff --git a/frontend/src/views/AngularWorkflow.jsx b/frontend/src/views/AngularWorkflow.jsx index 0466fb85..cd1be3dd 100755 --- a/frontend/src/views/AngularWorkflow.jsx +++ b/frontend/src/views/AngularWorkflow.jsx @@ -401,7 +401,6 @@ export function SetJsonDotnotation(jsonInput, inputKey) { //export const green = "#86c142"; export const green = "#02CB70" export const yellow = "#FECC00"; -//export const red = "#ff3632"; export const red = "#F53434"; export const grey = "#b0b0b0"; @@ -729,6 +728,8 @@ const AngularWorkflow = (defaultprops) => { "List tickets", "Send Email", "Get specific ticket", + "Update ticket", + "Add ticket comment", ], "multiselect": true, }, @@ -2258,7 +2259,7 @@ const AngularWorkflow = (defaultprops) => { currentnode.removeClass("shuffle-hover-highlight"); currentnode.removeClass("awaiting-data-highlight"); currentnode.addClass("success-highlight"); - incomingEdges.addClass("success-highlight"); + outgoingEdges.addClass("success-highlight"); if (visited !== undefined && visited !== null && !visited.includes(label)) { @@ -5368,7 +5369,8 @@ const AngularWorkflow = (defaultprops) => { if (!branchFound) { var relevantNodes = [] - const minDistance = 185 + //const minDistance = 185 + const minDistance = 85 const draggedNode = event.target const allnodes = cy.nodes().jsons() for (var nodekey in allnodes) { @@ -5398,7 +5400,7 @@ const AngularWorkflow = (defaultprops) => { if (decoratorNodeIds.includes(node.data.id)) { // Drag a little farther to remove it - if (distance > minDistance + 75) { + if (distance > minDistance + 125) { // Remove the branch? Why? const edgeToRemove = cy.getElementById(branches[branchkey].data.id) if (edgeToRemove !== null && edgeToRemove !== undefined) { @@ -18187,8 +18189,8 @@ const AngularWorkflow = (defaultprops) => {
- const defaultEnvironment = environments && environments?.find( - (env) => env?.default && env?.Name?.toLowerCase() !== "cloud" + const defaultEnvironment = environments.find( + (env) => env.default && env.Name.toLowerCase() !== "cloud" ); if (selectedTrigger.trigger_type === "PIPELINE" && selectedTrigger.environment === "onprem" && defaultEnvironment !== undefined) { @@ -20997,7 +20999,10 @@ const AngularWorkflow = (defaultprops) => { */} - {userdata.support === true || (userdata.avatar !== undefined && (userdata.avatar === creatorProfile.github_avatar || allowList.includes(userdata.public_username))) ? + {userdata.support || + (userdata.avatar !== undefined && (userdata.avatar === creatorProfile.github_avatar || allowList.includes(userdata.public_username))) || + (workflow?.owner?.length > 0 && workflow.owner === userdata?.active_org?.id && userdata?.active_org.role === "admin") || + (workflow?.owner?.length > 0 && workflow.owner === userdata?.id)?
You can see these buttons because you may have the correct access rights as a creator to help modify this workflow. @@ -22738,7 +22743,7 @@ const AngularWorkflow = (defaultprops) => { style={{ zIndex: 50000 }} > 0 ? yellow : theme.palette.textColor, + color: relevant_errors.length > 0 ? red : theme.palette.textColor, }} /> @@ -22783,6 +22788,26 @@ const AngularWorkflow = (defaultprops) => { : null} + {data?.action?.name === "run_schemaless" || data?.action?.name === "run_singul" || data?.action?.name === "singul" && data?.action?.parameters?.length > 4 ? + + : null} + {data.action.app_name === "shuffle-subflow" && validate.result.success !== undefined && validate.result.success === true ? ( @@ -22944,7 +22969,7 @@ const AngularWorkflow = (defaultprops) => { > Action Logs - + More log details for this action are not available without an onprem environment with the SHUFFLE_LOGS_DISABLED environment variable set to false: SHUFFLE_LOGS_DISABLED=false. Logs are enabled by default, except in scale mode.
@@ -23002,7 +23027,9 @@ const AngularWorkflow = (defaultprops) => { variant="body1" style={{}} > - {data.name}: {showVariable ? data.value : null} + {data.name}: + {showVariable ? data.value : null} + } {open ? @@ -23024,8 +23051,8 @@ const AngularWorkflow = (defaultprops) => { { @@ -23035,7 +23062,7 @@ const AngularWorkflow = (defaultprops) => { window.open(data.value, "_blank") } }} - color={showlink ? "inherit" : "textSecondary"} + color={showlink ? "#ff8544" : "textSecondary"} > {data.value} @@ -23199,8 +23226,9 @@ const AngularWorkflow = (defaultprops) => { sx: { pointerEvents: "auto", color: theme.palette.text.primary, - minWidth: isMobile ? "90%" : "750px", - maxHeight: "550px", + minWidth: isMobile ? "90%" : 900, + minHeight: 500, + maxHeight: 650, overflowY: "auto", overflowX: "hidden", border: theme.palette.defaultBorder, @@ -23418,7 +23446,9 @@ const AngularWorkflow = (defaultprops) => { > {selectedResult.action.label.replaceAll("_", " ")} -
{selectedResult.action.name}
+ + {selectedResult.action.name} + @@ -25588,12 +25618,14 @@ const AngularWorkflow = (defaultprops) => { const actionIndex = workflow?.actions.findIndex(action => action.id === actionId); if (actionIndex >= 0) { // Find the parameter with matching name - console.log("fieldName", fieldName) const paramIndex = workflow.actions[actionIndex].parameters.findIndex(param => param.name === fieldName); if (paramIndex >= 0) { // Update the parameter value workflow.actions[actionIndex].parameters[paramIndex].value = newData; - + if(selectedAction !== undefined && selectedAction !== null && selectedAction.id === actionId) { + selectedAction.parameters[paramIndex].value = newData; + setSelectedAction(selectedAction) + } // Update workflow state to trigger re-render setWorkflow({...workflow}); setLastSaved(false); diff --git a/frontend/src/views/AppExplorer.jsx b/frontend/src/views/AppExplorer.jsx index f0069e4f..f0f70021 100644 --- a/frontend/src/views/AppExplorer.jsx +++ b/frontend/src/views/AppExplorer.jsx @@ -233,6 +233,7 @@ const buttonBackground = "linear-gradient(to right, #f86a3e, #f34079)"; const [secondaryApp, setSecondaryApp] = useState({}); const [firstRequest, setFirstRequest] = useState(true); const [publishModalOpen, setPublishModalOpen] = React.useState(false); + const [showDistributionPopup, setShowDistributionPopup] = React.useState(false); const [categories, setCategories] = useState(appCategories) const [newWorkflowCategories, setNewWorkflowCategories] = React.useState([]); @@ -743,7 +744,7 @@ const buttonBackground = "linear-gradient(to right, #f86a3e, #f34079)"; } }; - const activateApp = (action) => { + const activateApp = (action, org_id, multiple_request = false) => { if (serverside === true) { return } @@ -753,11 +754,14 @@ const buttonBackground = "linear-gradient(to right, #f86a3e, #f34079)"; if (action !== undefined && action !== null) { url = `${globalUrl}/api/v1/apps/${appId}/${action}` } - fetch(url, { + + + fetch(url, { method: "GET", headers: { "Content-Type": "application/json", - Accept: "application/json", + "Accept": "application/json", + "Org-Id": org_id !== undefined && org_id !== null && org_id?.length > 0 ? org_id : userdata.active_org.id }, credentials: "include", }) @@ -784,7 +788,7 @@ const buttonBackground = "linear-gradient(to right, #f86a3e, #f34079)"; } } } else { - if (checkLogin !== undefined && checkLogin !== null) { + if ((checkLogin !== undefined && checkLogin !== null) && !multiple_request) { checkLogin() } @@ -795,6 +799,9 @@ const buttonBackground = "linear-gradient(to right, #f86a3e, #f34079)"; toast("App activated for your organization!") } } else { + if (responseJson.success && !multiple_request &&(responseJson.reason !== undefined || responseJson.reason !== null)) { + toast.success(`${responseJson.reason}`); + } } } }) @@ -3909,6 +3916,212 @@ const buttonBackground = "linear-gradient(to right, #f86a3e, #f34079)"; ) : null; + + const handleActivateApp = (id, action) => { + if (action === "activate_all") { + const childOrgs = userdata.orgs.filter( + (data) => data.creator_org === userdata.active_org.id + ); + const orgIds = childOrgs.map((data) => data.id); + // run app activation requset for each org + orgIds.forEach((orgId) => { + activateApp("activate", orgId, true) + }); + setTimeout(() => { + toast.success("App activated for all sub-orgs"); + }, 5000); + } else if (action === "deactivate_all") { + const childOrgs = userdata.orgs.filter( + (data) => data.creator_org === userdata.active_org.id + ); + const orgIds = childOrgs.map((data) => data.id); + // run app deactivation request for each org + orgIds.forEach((orgId) => { + activateApp("deactivate", orgId, true) + }); + + setTimeout(() => { + toast.success("App deactivated for all sub-orgs"); + }, 5000); + + } else if (action === "activate_single") { + if (id === null) { + toast.error("Please select a sub-org to activate the app for."); + return; + } + activateApp("activate", id); + } else if (action === "deactivate_single") { + if (id === null) { + toast.error("Please select a sub-org to deactivate the app for."); + return; + } + activateApp("deactivate", id); + } + }; + + + + + const appDistributinModal = showDistributionPopup ? ( + setShowDistributionPopup(false)} + PaperProps={{ + sx: { + borderRadius: theme?.palette?.DialogStyle?.borderRadius || 3, + border: theme?.palette?.DialogStyle?.border, + fontFamily: theme?.typography?.fontFamily, + backgroundColor: theme?.palette?.DialogStyle?.backgroundColor, + zIndex: 1000, + minWidth: "600px", + minHeight: "320px", + overflow: "auto", + '& .MuiDialogContent-root': { + backgroundColor: theme?.palette?.DialogStyle?.backgroundColor, + }, + '& .MuiDialogTitle-root': { + backgroundColor: theme?.palette?.DialogStyle?.backgroundColor, + p: 2, + }, + '& .MuiDialogActions-root': { + backgroundColor: theme?.palette?.DialogStyle?.backgroundColor, + }, + }, + }} + > + + + Select sub-org to distribute App + + setShowDistributionPopup(false)} + sx={{ + color: theme.palette.text.primary, + }} + > + + + + + + handleActivateApp(null, "deactivate_all")} + sx={{ + borderRadius: 1, + px: 2, + '&:hover': { + backgroundColor: 'rgba(255,255,255,0.08)', + }, + }} + > + Deactivate for all suborgs + + + handleActivateApp(null, "activate_all")} + sx={{ + borderRadius: 1, + px: 2, + '&:hover': { + backgroundColor: 'rgba(255,255,255,0.08)', + }, + }} + > + Activate for all suborgs + + + {userdata.orgs.map((data, index) => { + if (data.creator_org !== userdata.active_org.id) return null; + + const imageSize = 28; + const imageStyle = { + width: imageSize, + height: imageSize, + borderRadius: '50%', + objectFit: 'cover', + marginRight: 12, + }; + + const imageSrc = data.image || theme.palette.defaultImage; + + return ( + + + {data.name} + + {data.name} + + + + + + + + ); + })} + + +) : null; + + const landingpageDataBrowser = (
{publishModal} + {appDistributinModal}
{isMobile ? null : ( } - {isMobile || app?.reference_org === userdata?.active_org?.id ? null : ( + {isMobile || app?.reference_org === userdata?.active_org?.id || (app?.suborg_distribution?.includes(userdata?.active_org?.id)) ? null : ( + )}
) : ( { // What data do we fill in here? Idk const Dashboard = (props) => { - const { globalUrl, isLoggedIn } = props; + const { globalUrl, userdata, isLoggedIn } = props; //const alert = useAlert(); const [bigChartData, setBgChartData] = useState("data1"); const [dayAmount, setDayAmount] = useState(7); @@ -323,10 +323,10 @@ const Dashboard = (props) => { const [stats, setStats] = useState({}); const [changeme, setChangeme] = useState(""); const [statsRan, setStatsRan] = useState(false); - const [keys, setKeys] = useState([]) - const [treeKeys, setTreeKeys] = useState([]) + const [keys, setKeys] = useState([]) + const [treeKeys, setTreeKeys] = useState([]) - const [selectedUsecaseCategory, setSelectedUsecaseCategory] = useState(""); + const [selectedUsecaseCategory, setSelectedUsecaseCategory] = useState("") const [selectedUsecases, setSelectedUsecases] = useState([]); const [usecases, setUsecases] = useState([]); const [workflows, setWorkflows] = useState([]); @@ -750,8 +750,6 @@ const Dashboard = (props) => { const newname = data.key !== undefined ? data.key.replaceAll("_", " ") : "" - console.log("KEYDATA: ", data) - const loadNewStats = (newkey) => { const resp = LoadStats(globalUrl, newkey) if (resp !== undefined) { @@ -807,7 +805,7 @@ const Dashboard = (props) => { backgroundColor: theme.palette.inputColor, color: "white", height: 40, - maxWidth: 150, + maxWidth: 200, borderRadius: theme.palette?.borderRadius, }} > @@ -829,10 +827,12 @@ const Dashboard = (props) => { }
+ + ) @@ -848,14 +848,6 @@ const Dashboard = (props) => { : null} - {/*widgetData === undefined || widgetData === null || widgetData === [] || widgetData.length === 0 ? null : - - - - - - */} - {newWidgetData === undefined || newWidgetData === null || newWidgetData === [] ? null : newWidgetData.map((data, index) => { @@ -872,7 +864,9 @@ const Dashboard = (props) => { ); const dataWrapper = ( -
{data}
+
+ {data} +
); return dataWrapper; diff --git a/frontend/src/views/Workflows.jsx b/frontend/src/views/Workflows.jsx index 3fe551f4..32859549 100755 --- a/frontend/src/views/Workflows.jsx +++ b/frontend/src/views/Workflows.jsx @@ -558,7 +558,7 @@ export const handleReactJsonClipboard = (copy) => { document.execCommand("copy"); console.log("COPYING!"); - toast("Copied value to clipboard, NOT json path.") + toast.success("Copied Value, NOT json path.") } else { console.log("Failed to copy from " + elementName + ": ", copyText); }