Tons of minor fixes from cloud sync

This commit is contained in:
Frikky
2025-07-11 16:04:14 +02:00
parent 4f48d71252
commit 7e9cdce4b6
29 changed files with 1788 additions and 1015 deletions
+11 -5
View File
@@ -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();
-1
View File
@@ -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';
+77 -22
View File
@@ -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) => {
}}
/>
<Typography style={{marginTop: 10, fontSize: 16,}} color="textSecondary">
You have used <strong>{currentAppRunsInPercentage}%</strong> of total app execution limit or <strong>{userdata.app_execution_usage}</strong> app runs out of <strong>{userdata.app_execution_limit}</strong> app runs.
You have used <strong>{currentAppRunsInPercentage}%</strong> of total app execution limit or <strong>{userdata.app_execution_usage + userdata.app_executions_suborgs}</strong> app runs out of <strong>{userdata.app_execution_limit}</strong> app runs.
</Typography>
{userdata?.active_org?.creator_org?.length > 0 ? null :
(
<>
<Typography color="textSecondary" style={{ marginTop: 20, fontSize: 16 }}>
Parent Organization App Executions: <strong>{userdata.app_execution_usage}</strong>
</Typography>
<Typography color="textSecondary" style={{ fontSize: 16 }}>
Sub-Organization App Executions: <strong>{userdata.app_executions_suborgs || "N/A"}</strong>
</Typography>
</>
)}
<div>
<Typography style={{ marginTop: 20, fontSize: 18 }}>
Set email alert thresholds for app runs
@@ -2442,8 +2441,8 @@ const Billing = memo((props) => {
: " " + 0 + " "}
app runs.
</Typography>
<Typography color="textSecondary" style={{ fontSize: 16 }}>
<span style={{fontWeight: 'bold'}}>Please note</span>: Once your app runs reach the set alert threshold, all admins in the organization will receive an email notification.
<Typography color="textSecondary" style={{ fontSize: 16, marginTop: 10 }}>
<span style={{fontWeight: 'bold'}}>Please note</span>: 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.
</Typography>
<div style={{ marginTop: 15 }}>
{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 (
<>
<Typography style={{ fontSize: 16 }}>
{params.row.app_runs_hard_limit}
</Typography>
<IconButton
style={{ color: theme.palette.primary.main }}
onClick={() => {
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)
}
}}
>
<Edit/>
</IconButton>
</>
)
}
}
]
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
}
}}
>
<DialogTitle style={{ fontSize: 24, fontWeight: "bold" }}>
{editing === "app_executions_hard_limit" ? (
<DialogTitle style={{ fontSize: 24, fontWeight: "bold" }}>
Add {editing.replaceAll("_", " ").replace(/\b\w/g, c => c.toUpperCase())}
</DialogTitle>
) : (
<DialogTitle style={{ fontSize: 24, fontWeight: "bold" }}>
Increase {editing.replaceAll("_", " ").replace(/\b\w/g, c => c.toUpperCase())} Limit
</DialogTitle>
)}
<DialogContent>
{ editing === "app_executions_hard_limit" ? (
<Typography style={{ marginRight: 20, marginBottom: 20, fontSize: 16, color: theme.palette.text.secondary }}>
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.
</Typography>
) : null}
<TextField
value={currentLimit}
onChange={(e) => setCurrentLimit(e.target.value)}
+1 -48
View File
@@ -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 (
<div style={{color: "white", border: "1px solid rgba(255,255,255,0.3)", borderRadius: theme.palette?.borderRadius, padding: 30, marginTop: 15, backgroundColor: theme.palette.platformColor, overflow: "hidden", }}>
<Typography variant="h6" style={{marginBotton: 30, }}>
{inputname}
</Typography>
<BarChart
style={{marginTop: 100, }}
width={"100%"}
height={height}
data={inputdata}
series={
<BarSeries
bar={
<Bar />
}
/>
}
gridlines={
<GridlineSeries line={<Gridline direction="all" />} />
}
/>
</div>
)
}
const AppStats = (defaultprops) => {
const {
+241 -67
View File
@@ -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": <AirIcon />,
"enabled": false,
},
{
"name": "Correlate Categories",
"description": "",
"type": "singul",
"options": [{
"key": "datastore_categories",
"value": "",
}],
"icon": <CompareArrowsIcon />,
"enabled": false,
"disabled": false,
},
{
"name": "Run AI Agent",
"description": "",
"options": [{
"key": "",
"value": "",
}],
"icon": <SmartToyIcon />,
"enabled": false,
"disabled": true,
},
{
"name": "Send webhook",
"description": "Sends the updated value to a specified webhook URL.",
"options": [{
"key": "webhook_url",
"value": "",
}],
"icon": <WebhookIcon />,
"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": <SmartToyIcon />,
"enabled": false,
"disabled": true,
},
{
"name": "Send webhook",
"description": "Sends the updated value to a specified webhook URL.",
"options": [{
"key": "webhook_url",
"value": "",
}],
"icon": <WebhookIcon />,
"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 (
<Typography key={optionIndex} style={{ color: theme.palette.text.secondary, marginTop: 10 }}>
No categories available. Please add categories in the settings.
</Typography>
)
}
return (
<Autocomplete
key={optionIndex}
multiple
label="Choose Datastore Categories"
id="datastore_category_search"
autoHighlight
freeSolo
value={datastoreCategories?.filter(c => 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 (
<Tooltip arrow placement="left" title={
<span style={{}}>
{data.image !== undefined && data.image !== null && data.image.length > 0 ?
<img src={data.image} alt={data.name} style={{ backgroundColor: theme.palette.surfaceColor, maxHeight: 200, minHeigth: 200, borderRadius: theme.palette?.borderRadius, }} />
: null}
<Typography>
Choose {data}
</Typography>
</span>
} >
<MenuItem
{...props}
style={{
backgroundColor: theme.palette.surfaceColor,
}}
value={data}
>
<Typography style={{
display: "flex",
marginTop: 5,
color: keyfound ? red : theme.palette.text.primary,
}}>
<div style={{marginRight: 10, }}>
{iconDetails?.originalIcon && (
iconDetails?.originalIcon
)}
</div>
{fixedname}
</Typography>
</MenuItem>
</Tooltip>
)
}}
renderInput={(params) => {
return (
<TextField
{...params}
style={{
backgroundColor: theme.palette.textFieldStyle.backgroundColor,
color: theme.palette.textFieldStyle.color,
borderRadius: theme.palette.textFieldStyle.borderRadius,
height: 35,
fontSize: 16,
marginTop: "16px"
}}
InputProps={{
...params.InputProps,
style: {
height: 35,
display: "flex",
alignItems: "center",
padding: "0px 8px",
fontSize: 16,
borderRadius: 4,
},
inputProps: {
...params.inputProps,
style: {
height: "100%",
boxSizing: "border-box",
}
}
}}
variant="outlined"
placeholder="Select Categories to Correlate"
/>
)
}}
/>
)
} else if (option?.key === "workflow_id") {
return (
<Autocomplete
key={optionIndex}
@@ -1181,8 +1346,7 @@ const CacheView = memo((props) => {
{...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) => {
<IconButton
style={{ padding: "6px" }}
disabled={data.org_id !== selectedOrganization.id ? true : false}
onClick={() => {
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) => {
<IconButton
style={{ padding: "6px" }}
disabled={data.public_authorization === undefined || data.public_authorization === null || data.public_authorization === "" || data.org_id !== selectedOrganization.id ? true : false}
onClick={() => {
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) => {
<IconButton
style={{ padding: "6px" }}
disabled={selectedOrganization?.id === undefined ? false : data.org_id !== selectedOrganization.id ? true : false}
onClick={() => {
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) => {
<DataGrid
rows={listCache}
columns={columns}
checkboxSelection
disableRowSelectionOnClick
rowSelectionModel={selectedRows}
onSelectionModelChange={(newSelection) => {
setSelectedRows(newSelection);
}}
onRowSelectionModelChange={(newSelection) => {
setSelectedRows(newSelection)
setSelectedRows(newSelection);
}}
keepNonExistentRowsSelected={false}
getRowId={(row) => row.key}
@@ -2245,6 +2414,11 @@ const CacheView = memo((props) => {
</div>
</div>
<TextField
id="copy_element_shuffle"
value={to_be_copied}
style={{ display: "none" }}
/>
</div>
);
})
+184 -23
View File
@@ -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 (
//<Grid item xs={hovering ? 12 : 5.9}
<Grid item xs={12}
style={{
minHeight: hovering ? 225 : 145,
maxHeight: hovering ? 225 : 145,
minHeight: hovering ? 250 : 140,
maxHeight: hovering ? "auto" : 140,
cursor: "pointer",
position: "relative",
transition: "all 0.3s ease-in-out",
borderRadius: theme.palette.borderRadius,
border: hovering ? `2px solid ${theme.palette.primary.main}` : isFinished ? `2px solid ${theme.palette.success.main}` : `2px solid ${theme.palette.secondary.main}`,
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,
overflow: "hidden",
}}
onMouseEnter={() => setHovering(true)}
onMouseEnter={() => {
//if (foundMatchingWorkflow !== null) {
//} else {
setHovering(true)
//}
}}
onMouseLeave={() => setHovering(false)}
>
<div style={{marginTop: 35, marginBottom: 35, }}>
@@ -116,18 +200,92 @@ const CollectIngestModal = (props) => {
</Typography>
</div>
<Button variant="contained" onClick={() => {
toast.info("Starting ingest for relevant apps")
startIngestion(appname, index)
}}>
Start Ingestion
</Button>
<div style={{display: "flex", width: 400, margin: "auto", }}>
{matchingapps.length > 0 ?
<Autocomplete
style={{flex: 1, }}
multiple
filterSelectedOptions
options={matchingapps}
value={selectedApps}
onChange={(event, value) => {
setSelectedApps(value)
}}
getOptionLabel={(option) => {
const parsedname = option.name.replaceAll("_", " ")
return (
<div>
<img src={option?.large_image} alt={option.name} style={{ width: 24, height: 24, marginRight: 10, borderRadius: 5, }} />
<Typography variant="body1" style={{ display: "inline-block", verticalAlign: "middle", marginTop: -12, }}>
{parsedname}
</Typography>
</div>
)
}}
renderInput={(params) => {
return (
<TextField
{...params}
variant="outlined"
label="Select apps"
/>
)
}}
/>
: null}
<Button
style={{flex: 1, }}
variant={foundMatchingWorkflow !== null ? "outlined" : "contained"}
onClick={() => {
//if (foundMatchingWorkflow !== null) {
// toast.error("Deletion not implemented for this POC. Please delete the workflow.")
//}
//else {
toast.info("Starting ingest for relevant apps")
var newapps = ""
for (var key in selectedApps) {
const app = selectedApps[key]
if (newapps.length > 0) {
newapps += ","
}
newapps += app.name
}
startIngestion(appname, newapps, appCategory, index)
//}
}}>
{foundMatchingWorkflow !== null ?
"Re-Create Ingestion"
:
"Start Ingestion"
}
</Button>
</div>
{foundMatchingWorkflow !== null ?
<a href={`/workflows/${foundMatchingWorkflow.id}`} target="_blank" rel="noopener noreferrer">
<Tooltip title="View Workflow" placement="right">
<IconButton style={{position: "absolute", top: 10, right: 10, marginLeft: 15, }}>
<RocketIcon style={{ }} />
</IconButton>
</Tooltip>
</a>
: null}
{hovering ?
<div>
</div>
: null}
{isFinished ?
{foundMatchingWorkflow !== null ?
<div>
<Typography variant="body1" style={{
position: "absolute",
@@ -137,6 +295,8 @@ const CollectIngestModal = (props) => {
}}>
{ingestedAmount} / X
</Typography>
{/*
<LinearProgress
style={{
width: "100%",
@@ -146,6 +306,7 @@ const CollectIngestModal = (props) => {
variant="determinate"
fullWidth value={{ingestedAmount}}
/>
*/}
</div>
: null}
</Grid>
@@ -189,13 +350,13 @@ const CollectIngestModal = (props) => {
</Typography>
<Grid container>
<IngestItem type="Ingest Tickets" index={1} />
<IngestItem type="Ingest Tickets" appCategory={"cases"} index={1} />
<IngestItem type="Enable Threat feeds" index={2} />
<IngestItem type="Track Assets" index={2} />
<IngestItem type="Enable Search" index={2} />
<IngestItem type="Enable Mitre Att&ck techniques" index={2} />
<IngestItem type="Enable Detection Rules" index={2} />
<IngestItem type="Ingest Logs" index={2} />
<IngestItem type="Track Assets" appCategory={"assets"} index={2} />
</Grid>
</DialogContent>
</Dialog>
+7 -77
View File
@@ -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 (
// <Bar
// data={timelineData}
// options={barOptions}
// height={inputHeight}
// getElementAtEvent={(elements) => {
// if (elements && elements.length > 0) {
// //toast("Click event")
// console.log("Clicked: ", elements)
// }
// }}
// />
// )
return (
<LineChartWrapper keys={timelineData} height={150} width={"100%"} border={false} />
)
}
export default DashboardBarchart;
+2 -37
View File
@@ -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}
/>
</div>
</div >
File diff suppressed because it is too large Load Diff
+4 -19
View File
@@ -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}
</Typography>
{menuItem.title === "Singul" && (
<Typography
sx={{
fontSize: '10px',
color: '#FF8544',
border: '1px solid #FF8544',
borderRadius: '4px',
padding: '2px 6px',
lineHeight: 1,
fontWeight: 500,
textTransform: 'uppercase',
letterSpacing: '0.5px',
}}
>
Beta: Coming Soon
</Typography>
)}
</Box>
<Typography
variant="body2"
@@ -56,7 +56,8 @@ const OrgHeaderexpandedNew = (props) => {
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) => (
<MenuItem key={name} value={name}>
<Checkbox checked={selectedStatus.indexOf(name) > -1} />
<ListItemText primary={name} />
+11 -11
View File
@@ -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 <Branding
isCloud={isCloud}
userdata={userdata}
globalUrl={globalUrl}
handleGetOrg={handleGetOrg}
selectedOrganization={selectedOrganization}
clickedFromOrgTab={true}
setSelectedOrganization={setSelectedOrganization}
/>;
// case 'branding':
// return <Branding
// isCloud={isCloud}
// userdata={userdata}
// globalUrl={globalUrl}
// handleGetOrg={handleGetOrg}
// selectedOrganization={selectedOrganization}
// clickedFromOrgTab={true}
// setSelectedOrganization={setSelectedOrganization}
// />;
// case 'analytics':
// return <AnalyticsTab isCloud={isCloud} userdata={userdata} globalUrl={globalUrl} />;
default:
+2 -3
View File
@@ -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
+75 -2
View File
@@ -358,6 +358,21 @@ const PartnerDetails = (props) => {
animation="wave"
/>
</div>
<div style={{ width: "100%", maxWidth: 500, marginRight: 10, marginTop: 10 }}>
<Typography variant="text" style={{ color: theme?.palette?.text?.primary }}>
Contact Email
</Typography>
<Skeleton
variant="rounded"
height={35}
width="100%"
style={{
marginTop: 5,
borderRadius: 4
}}
animation="wave"
/>
</div>
</div>
</div>
</div>
@@ -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) => {
},
}}
/>
</div>
</div>
<div
style={{ width: "100%", maxWidth: 500, marginRight: 10, marginTop: 20 }}
>
<Typography
variant="text"
style={{ color: theme.palette.text.primary }}
>
Contact Email
</Typography>
<TextField
required
disabled={isDisabled}
style={{
flex: "1",
display: "flex",
height: 35,
width: "100%",
maxWidth: 500,
marginTop: "5px",
marginRight: "15px",
color: theme.palette.textFieldStyle.color,
backgroundColor: isEditOrgTab
? theme.palette.textFieldStyle.backgroundColor
: theme.palette.inputColor,
cursor: isDisabled ? "not-allowed" : "pointer",
}}
fullWidth={true}
placeholder="https://www.example.com"
type="name"
id="standard-required"
margin="normal"
variant="outlined"
value={partnerData?.contact_email}
onBlur={() => {}}
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,
},
}}
/>
</div>
</div>
</div>
</div>
+5 -1
View File
@@ -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 || [],
+25 -2
View File
@@ -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 (
<div style={{ height: "100%", width: "100%", color: theme.palette.platformColor, backgroundColor: theme.palette.platformColor, borderTopRightRadius: '8px', borderBottomRightRadius: 8, borderLeft: theme.palette.defaultBorder, boxSizing: 'border-box' }}>
<div style={{ display: 'flex', justifyContent: 'space-around', width: "100%", borderBottom: theme.palette.defaultBorder ,boxSizing: 'border-box' }}>
{tabsOnPartnerTab?.map((tabName, index) => (
<div style={{ pointerEvents: 'auto', width: '100%',}}>
<div key={tabName} style={{ pointerEvents: 'auto', width: '100%', position: 'relative'}}>
<Button
key={tabName}
onClick={() => {
setCurIndex(index);
handleTabClick(index === 0 ? "partner_settings" : tabName.toLowerCase().replace(/[\s&]+/g, ''));
@@ -232,6 +235,26 @@ const PartnerTab = (props) => {
>
{tabName}
</Button>
{isSupportOnlyTab(tabName) && userdata?.support && (
<div style={{
position: 'absolute',
top: '8px',
right: '8px',
backgroundColor: '#4D4D4D',
color: 'white',
borderRadius: '50%',
width: '20px',
height: '20px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontSize: '12px',
fontWeight: 'bold',
zIndex: 1
}}>
S
</div>
)}
</div>
))}
</div>
@@ -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
}}
>
<FormControl>
<Typography
<Box
sx={{
color: theme.palette.accentColor,
display: "flex",
flexDirection: "row",
gap: 1,
mb: 2,
fontSize: "16px",
fontWeight: 600,
alignItems: "center",
}}
>
Public Workflow
</Typography>
<Typography
sx={{
color: theme.palette.accentColor,
fontSize: "16px",
fontWeight: 600,
}}
>
Public Workflow
</Typography>
<IconButton
size="small"
sx={{
color: theme.palette.primary.main,
"&:hover": {
backgroundColor: theme.palette.action.hover,
},
}}
onClick={() => {
// Open workflow in new tab - adjust URL as needed
window.open(`${window.location.origin}/workflows/${formData.mainContent.publicWorkflowId}`, "_blank");
}}
>
<OpenInNewIcon fontSize="small" />
</IconButton>
</Box>
<Select
displayEmpty
value={
+5 -4
View File
@@ -176,11 +176,12 @@ const Priorities = memo((props) => {
credentials: "include",
}).then((response) => {
if (response.status !== 200) {
toast(`Failed getting config for ${item.id}: `, response.reason);
//toast.error(`Failed getting config for ${item.id}: `, response.reason)
console.log("Status not 200 for app config :O!");
return;
return
}
return response.json();
return response.json()
}).then((responseJson) => {
if (!responseJson.success) {
console.log("Could not get app config")
@@ -209,7 +210,7 @@ const Priorities = memo((props) => {
}).catch((error) => {
console.log("Error getting app config: " + error);
toast("Error getting app config: " + error);
//toast.error("Error getting app config: " + error);
})
})
})
+65 -22
View File
@@ -79,9 +79,14 @@ const RuntimeDebugger = (props) => {
const [searchLoading, setSearchLoading] = useState(false)
const [rowCursor, setCursor] = useState("")
const [rowsPerPage, setRowsPerPage] = useState(10)
const [maxExecutionCount, setMaxExecutionCount] = useState(50)
const [resultRows, setResultRows] = useState([])
const [selectedWorkflowExecutions, setSelectedWorkflowExecutions] = useState([])
const [suborgWorkflowRuns, setSuborgWorkflowRuns] = useState(false)
const [paginationModel, setPaginationModel] = useState({
page: 0,
pageSize: 10,
})
const [openWorkflowMenu, setOpenWorkflowMenu] = useState(false)
const [workflows, setWorkflows] = useState([
{"id": "", "name": "All Workflows",}
@@ -665,6 +670,13 @@ const RuntimeDebugger = (props) => {
},
]
useEffect(() => {
setPaginationModel(prev => ({
...prev,
pageSize: rowsPerPage
}))
}, [rowsPerPage])
useEffect(() => {
// Check if the user is currently focusing a texxtfield or not
// If they are, don't submit the search
@@ -672,7 +684,7 @@ const RuntimeDebugger = (props) => {
return
}
submitSearch(workflowId, status, startTime, endTime, rowCursor, rowsPerPage, suborgWorkflowRuns)
submitSearch(workflowId, status, startTime, endTime, rowCursor, maxExecutionCount, suborgWorkflowRuns)
}, [workflowId, status, startTime, endTime])
const textfieldStyle = {
@@ -723,7 +735,7 @@ const RuntimeDebugger = (props) => {
setWorkflowId(e.target.value.id)
setSuborgWorkflowRuns(false)
submitSearch(e.target.value.id, status, startTime, endTime, rowCursor, rowsPerPage, false)
submitSearch(e.target.value.id, status, startTime, endTime, rowCursor, maxExecutionCount, false)
}
const executeWorkflow = (execution) => {
@@ -819,9 +831,12 @@ const RuntimeDebugger = (props) => {
<div style={{display: "flex", paddingTop: 50, }}>
<div style={{display: 'flex', flexDirection: 'column'}}>
<div style={{display: "flex", width: "100%", }}>
<Typography variant="h3" style={{flex: 3, whiteSpace: "nowrap" }}>Workflow Run Debugger {totalCount !== 0 ? ` (~${totalCount})` : ""}</Typography>
<Typography variant="h3" style={{flex: 3, whiteSpace: "nowrap" }}>
Workflow Run Debugger {totalCount !== 0 ? ` (~${totalCount})` : ""}
</Typography>
{selectedWorkflowExecutions.length > 0 ?
<ButtonGroup>
<ButtonGroup>
<Tooltip title="Reruns ALL selected workflows. This will make a new execution for them, and not continue the existing.">
<Button
variant="outlined"
@@ -864,7 +879,7 @@ const RuntimeDebugger = (props) => {
} else {
toast("Aborted "+aborted+" workflows.")
// Research
submitSearch(workflowId, status, startTime, endTime, rowCursor, rowsPerPage, suborgWorkflowRuns)
submitSearch(workflowId, status, startTime, endTime, rowCursor, maxExecutionCount, suborgWorkflowRuns)
setSelectedWorkflowExecutions([])
}
@@ -902,7 +917,7 @@ const RuntimeDebugger = (props) => {
marginTop: 20,
marginLeft: 10,
marginRight: 12,
width: 693,
width: 643,
height: 51,
borderRadius: 4,
fontSize: 16,
@@ -913,7 +928,7 @@ const RuntimeDebugger = (props) => {
color: theme.palette.textColor,
fontSize: "1em",
height: 51,
width: 693,
width: 643,
borderRadius: 4,
},
startAdornment: (
@@ -942,6 +957,34 @@ const RuntimeDebugger = (props) => {
placeholder="Filter by Workflow Name, Status, Execution Argument, Results"
id="shuffle_search_field"
/>
<Tooltip title="Set the maximum number of workflow executions to retrieve in search results" placement="top">
<FormControl style={{ minWidth: 120, marginTop: 20 }}>
<InputLabel id="max-execution-count-label" style={{ fontSize: '0.875rem' }}>Max Results</InputLabel>
<Select
labelId="max-execution-count-label"
id="max-execution-count-select"
value={maxExecutionCount}
label="Max Results"
size="small"
onChange={(e) => {
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,
}}
>
<MenuItem value={10}>10</MenuItem>
<MenuItem value={25}>25</MenuItem>
<MenuItem value={50}>50</MenuItem>
<MenuItem value={100}>100</MenuItem>
<MenuItem value={200}>200</MenuItem>
<MenuItem value={500}>500</MenuItem>
</Select>
</FormControl>
</Tooltip>
</div>
{userdata?.active_org?.creator_org?.length === 0 ? (
<div style={{display: "flex", margin: 'auto',marginTop: 20,justifyContent: 'center', alignItems: 'center', }}>
@@ -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) => {
</div>
</div>
<form onSubmit={(e) => {
submitSearch(workflowId, status, startTime, endTime, rowCursor, rowsPerPage, suborgWorkflowRuns)
submitSearch(workflowId, status, startTime, endTime, rowCursor, maxExecutionCount, suborgWorkflowRuns)
}} style={{display: "flex", justifyContent: "center", alignItems: "center", }}>
<FormControl fullWidth style={{marginTop: 5, }}>
<InputLabel id="status-label">Status</InputLabel>
@@ -1158,7 +1202,8 @@ const RuntimeDebugger = (props) => {
setEndTime("")
setSearchQuery("")
setSuborgWorkflowRuns(false)
submitSearch("", "", "", "", rowCursor, rowsPerPage, false)
setMaxExecutionCount(50)
submitSearch("", "", "", "", rowCursor, 50, false)
}}
>
<FilterAltOffIcon />
@@ -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) => {
<DataGrid
rows={filteredRows}
columns={columns}
pageSize={rowsPerPage}
rowsPerPageOptions={[10, 20, 50, 100]}
paginationModel={paginationModel}
pageSizeOptions={[10, 20, 50, 75, 100]}
checkboxSelection
disableSelectionOnClick
onPageSizeChange={(newPageSize) => {
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 = []
@@ -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"}
/>
:
<span style={{ maxHeight: 190, minHeight: 190, }}>
+3 -4
View File
@@ -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",
+5 -1
View File
@@ -1465,7 +1465,11 @@ const UserManagmentTab = memo((props) => {
<ListItem key={index} style={{ backgroundColor: bgColor, display: 'table-row', borderBottomLeftRadius: users?.length - 1 === index ? 8 : 0, borderBottomRightRadius: users?.length - 1 === index ? 8 : 0 }}>
{isCloud ? (
<ListItemText
primary={(<img src={`https://flagcdn.com/48x36/${userRegion.toLowerCase()}.png`} alt={data?.user_geo_info?.country?.iso_code} style={{ marginRight: 30, width: 25, height: 23, }} />)}
primary={(
userRegion ? (
<img src={`https://flagcdn.com/48x36/${userRegion.toLowerCase()}.png`} alt={data?.user_geo_info?.country?.iso_code} style={{ marginRight: 30, width: 25, height: 23, }} />
) : null
)}
style={{ display: 'table-cell', verticalAlign: 'middle', textAlign: 'center' }}
/>) : null}
<ListItemText