Filesync w 2.0.0-rc3

This commit is contained in:
Frikky
2025-01-20 01:05:57 +01:00
parent 3fa3620004
commit 09c9a9a716
17 changed files with 797 additions and 377 deletions
+22 -9
View File
@@ -100,6 +100,8 @@ const ApiExplorer = memo(({ openapi, globalUrl, userdata, HandleApiExecution, se
const [ExampleBody, setExampleBody] = useState({});
const [filteredActions, setFilteredActions] = useState([]);
const [firstSendDone, setFirstSendDone] = useState(false)
const getJsonObject = (properties) => {
let jsonObject = {};
@@ -1425,18 +1427,20 @@ const ActionsList = memo(({
};
const handleSearch = (e) => {
const query = e.target.value;
setSearchQuery(query);
const query = e?.target?.value?.toLowerCase().replaceAll("_", " ");
setSearchQuery(query)
if (query.length === 0) {
setVisibleActions(actions);
setVisibleActions(actions)
} else {
setVisibleActions(
actions.filter((action) =>
action.name.toLowerCase().includes(searchQuery.toLowerCase())
actions?.filter((action) =>
action?.name?.toLowerCase()?.replaceAll("_", " ")?.includes(searchQuery)
)
);
)
}
};
}
return (
<div style={{ maxWidth: '350px', width: "25%", overflow: 'hidden', marginLeft: !(isLoggedIn || isLoaded) ? 5 : 0}}>
<div style={{ borderBottom: '1px solid #494949', paddingTop: 10, paddingBottom: 10}}>
@@ -1465,7 +1469,7 @@ const ActionsList = memo(({
</div>
<TextField
value={searchQuery}
placeholder="Search here"
placeholder="Search endpoints"
onChange={handleSearch}
InputProps={{
startAdornment: (
@@ -2035,6 +2039,7 @@ const Action = memo((
}, [editorRef.current]);
const actionname = action?.name.charAt(0).toUpperCase() + action?.name.slice(1).replaceAll("_", " ")
return (
<div
ref={actionRef}
@@ -2042,6 +2047,7 @@ const Action = memo((
data-index={index}
key={action.name.replace(/ /g, "-").replace(/_/g, "-")}
style={{
marginTop: 50,
display: "flex",
flexDirection: "row",
minHeight: 400,
@@ -2066,7 +2072,7 @@ const Action = memo((
color: "rgba(241, 241, 241, 1)",
}}
>
{action.name}
{actionname}
</Typography>
<div
style={{
@@ -2259,6 +2265,13 @@ const Action = memo((
}}
disabled={disableExecuteButton}
onClick={() => {
/*
if (!firstSendDone) {
setFirstSendDone(true)
setCurTab(2)
}
*/
if (actionUrl.length === 0) {
toast.error("URL cannot be empty");
return;
+5 -3
View File
@@ -215,8 +215,8 @@ const Billing = memo((props) => {
var priceItem = "price_1MROFrDzMUgUjxHShcSxgHO1"
const successUrl = `${window.location.origin}/admin?admin_tab=billing&payment=success`
const failUrl = `${window.location.origin}/admin?admin_tab=billing&payment=failure`
const successUrl = `${window.location.origin}/admin?admin_tab=billingstats&payment=success`
const failUrl = `${window.location.origin}/admin?admin_tab=billingstats&payment=failure`
var checkoutObject = {
lineItems: [
{
@@ -2361,7 +2361,8 @@ const Billing = memo((props) => {
</div>
</div>
)}
<div style={{ marginTop: 40, marginLeft: 10 }}>
{isCloud ? (
<div style={{ marginTop: 40, marginLeft: 10 }}>
<Typography
style={{ marginBottom: 5, fontSize: 24, fontWeight: "bold" }}
>
@@ -2545,6 +2546,7 @@ const Billing = memo((props) => {
</Button>
</div>
): null}
</div>
<div style={{ marginTop: 40, display: 'flex', flexDirection: 'column' }}>
<Typography
+267 -16
View File
@@ -20,6 +20,10 @@ import {
DialogTitle,
DialogActions,
Skeleton,
Chip,
Checkbox,
MenuItem,
DialogContent,
} from "@mui/material";
import {
@@ -46,6 +50,8 @@ import {
Business as BusinessIcon,
Visibility as VisibilityIcon,
VisibilityOff as VisibilityOffIcon,
CheckBox,
Key,
} from "@mui/icons-material";
import { validateJson, } from "../views/Workflows.jsx";
import { Context } from "../context/ContextApi.jsx";
@@ -68,7 +74,7 @@ const scrollStyle2 = {
const CacheView = memo((props) => {
const { globalUrl, userdata, serverside, orgId, isSelectedDataStore } = props;
const { globalUrl, userdata, serverside, orgId, isSelectedDataStore, selectedOrganization } = props;
const [orgCache, setOrgCache] = React.useState("");
const [listCache, setListCache] = React.useState([]);
const [addCache, setAddCache] = React.useState("");
@@ -82,12 +88,16 @@ const CacheView = memo((props) => {
const [editCache, setEditCache] = React.useState(false);
const [cachedLoaded, setCachedLoaded] = React.useState(false);
const [show, setShow] = useState({});
const [showDistributionPopup, setShowDistributionPopup] = useState(false);
const [selectedSubOrg, setSelectedSubOrg] = useState([]);
const [selectedCacheKey, setSelectedCacheKey] = useState("");
useEffect(() => {
if(orgId?.length >0){
listOrgCache(orgId);
}
}, [orgId]);
const listOrgCache = (orgId) => {
fetch(globalUrl + `/api/v1/orgs/${orgId}/list_cache`, {
method: "GET",
@@ -214,7 +224,7 @@ const CacheView = memo((props) => {
})
.then((responseJson) => {
setAddCache(responseJson);
toast("New Cache Added Successfully!");
toast("New key Added Successfully!");
listOrgCache(orgId);
setModalOpen(false);
})
@@ -296,7 +306,7 @@ const CacheView = memo((props) => {
>
<DialogTitle>
<span style={{ color: "white" }}>
{ editCache ? "Edit Cache" : "Add Cache" }
{ editCache ? "Edit Key" : "Add Key" }
</span>
</DialogTitle>
<div style={{ paddingLeft: "30px", paddingRight: '30px', backgroundColor: "#212121", }}>
@@ -368,6 +378,7 @@ const CacheView = memo((props) => {
style={{ borderRadius: "2px", fontSize: 16, color: "#ff8544", textTransform:"none" }}
onClick={() => {
setModalOpen(false)
setKey("")
setValue("")
setDataValue({})
}}
@@ -380,7 +391,7 @@ const CacheView = memo((props) => {
style={{ borderRadius: "2px", backgroundColor: "#ff8544",color: "#1a1a1a", textTransform:"none" }}
onClick={() => {
{editCache ? editOrgCache(orgId) : addOrgCache(orgId)}
setKey("")
setValue("")
setDataValue({})
}}
@@ -392,9 +403,175 @@ const CacheView = memo((props) => {
</Dialog>
);
const handleSelectSubOrg = (id, action) => {
if (action === "all") {
const childOrgs = userdata.orgs.filter(
(data) => data.creator_org === userdata.active_org.id
);
setSelectedSubOrg((prev) => {
if (prev.length === childOrgs.length) {
// If all child orgs are already selected, clear the selection
return [];
} else {
// Otherwise, select all child org IDs
return childOrgs.map((data) => data.id);
}
});
} else if (action === "none") {
setSelectedSubOrg([]);
} else {
setSelectedSubOrg((prev) => {
if (prev.includes(id)) {
return prev.filter((data) => data !== id);
} else {
return [...prev, id];
}
});
}
};
const changeDistribution = (id, selectedSubOrg) => {
editFileConfig(id, [...new Set(selectedSubOrg)])
}
const editFileConfig = (id, selectedSubOrg, cacheKey) => {
const data = {
Key: id,
action: "suborg_distribute",
selected_suborgs: selectedSubOrg,
}
console.log("data: ", data);
const url = `${globalUrl}/api/v1/orgs/${orgId}/cache/config`;
fetch(url, {
mode: "cors",
method: "POST",
body: JSON.stringify(data),
credentials: "include",
crossDomain: true,
withCredentials: true,
headers: {
"Content-Type": "application/json; charset=utf-8",
},
})
.then((response) =>
response.json().then((responseJson) => {
if (responseJson["success"] === false) {
toast("Failed overwriting datastore");
} else {
toast("Successfully updated datastore!");
setTimeout(() => {
listOrgCache(orgId);
setShowDistributionPopup(false);
}, 1000);
}
})
)
.catch((error) => {
toast("Err: " + error.toString());
});
};
const cacheDistributionModal = showDistributionPopup ? (
<Dialog
open={showDistributionPopup}
onClose={() => setShowDistributionPopup(false)}
PaperProps={{
sx: {
borderRadius: theme?.palette?.DialogStyle?.borderRadius,
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,
},
'& .MuiDialogActions-root': {
backgroundColor: theme?.palette?.DialogStyle?.backgroundColor,
},
},
}}
>
<DialogTitle>
<div style={{ color: "rgba(255,255,255,0.9)" }}>
Select sub-org to distribute files
</div>
</DialogTitle>
<DialogContent style={{ color: "rgba(255,255,255,0.65)" }}>
<MenuItem value="none" onClick={()=> {handleSelectSubOrg(null, "none")}}>None</MenuItem>
<MenuItem value="all" onClick={()=> {handleSelectSubOrg(null, "all")}}>All</MenuItem>
{userdata.orgs.map((data, index) => {
if (data.creator_org !== userdata.active_org.id) {
return null;
}
const imagesize = 22;
const imageStyle = {
width: imagesize,
height: imagesize,
pointerEvents: "none",
marginRight: 10,
marginLeft: data.id === userdata.active_org.id ? 0 : 20,
};
const image = data.image === "" ? (
<img alt={data.name} src={theme.palette.defaultImage} style={imageStyle} />
) : (
<img alt={data.name} src={data.image} style={imageStyle} />
);
return (
<MenuItem
key={index}
value={data.id}
onClick={() => handleSelectSubOrg(data.id)}
style={{ display: "flex", alignItems: "center" }}
>
<Checkbox
checked={selectedSubOrg.includes(data.id)}
/>
{image}
<span style={{ marginLeft: 8 }}>{data.name}</span>
</MenuItem>
);
})}
<div style={{ display: "flex", marginTop: 20 }}>
<Button
style={{ borderRadius: "2px", textTransform: 'none', fontSize:16, color: "#ff8544" }}
onClick={() => setShowDistributionPopup(false)}
color="primary"
>
Cancel
</Button>
<Button
variant="contained"
style={{ borderRadius: "2px", textTransform: 'none', fontSize:16, color: "#1a1a1a", backgroundColor: "#ff8544", marginLeft: 10 }}
onClick={() => {
changeDistribution(selectedCacheKey, selectedSubOrg);
}}
color="primary"
>
Submit
</Button>
</div>
</DialogContent>
</Dialog>
) : null;
return (
<div style={{paddingBottom: isSelectedDataStore?null:250, minHeight: 1000, boxSizing: "border-box", width: isSelectedDataStore? "100%" :null, transition: "width 0.3s ease", padding:isSelectedDataStore?"27px 10px 27px 27px":null, height: isSelectedDataStore?"100%":null, color: isSelectedDataStore?'#ffffff':null, backgroundColor: isSelectedDataStore?'#212121':null, borderTopRightRadius: isSelectedDataStore?'8px':null, borderBottomRightRadius: isSelectedDataStore?'8px':null, borderLeft: "1px solid #494949" }}>
{modalView}
{cacheDistributionModal}
<div style={{height: "100%", maxHeight: 1700, overflowY: "auto", scrollbarColor: '#494949 transparent', scrollbarWidth: 'thin'}}>
<div style={{ height: "100%", width: "calc(100% - 20px)", scrollbarColor: '#494949 transparent', scrollbarWidth: 'thin' }}>
<div style={{ marginTop: isSelectedDataStore?null:20, marginBottom: 20 }}>
@@ -422,7 +599,7 @@ const CacheView = memo((props) => {
setValue("")
}}
>
Add Cache
Add Key
</Button>
<Button
style={{ marginLeft: 16, marginRight: 15, backgroundColor: isSelectedDataStore?"#2F2F2F":null, boxShadow: isSelectedDataStore ? "none":null,textTransform: isSelectedDataStore ? 'capitalize':null,borderRadius:isSelectedDataStore?4:null, width:isSelectedDataStore?81:null, height:isSelectedDataStore?40:null, }}
@@ -459,7 +636,7 @@ const CacheView = memo((props) => {
overflowX: "auto",
}}>
<ListItem style={{width: isSelectedDataStore?"100%":null, borderBottom:isSelectedDataStore?"1px solid #494949":null, display: "table-row"}}>
{["Key", "Value", "Actions", "Updated"].map((header, index) => (
{["Key", "Value", "Actions", "Updated", "Distribution"].map((header, index) => (
<ListItemText
key={index}
primary={header}
@@ -483,7 +660,7 @@ const CacheView = memo((props) => {
backgroundColor: "#212121",
}}
>
{Array(4)
{Array(5)
.fill()
.map((_, colIndex) => (
<ListItemText
@@ -507,9 +684,21 @@ const CacheView = memo((props) => {
</ListItem>
))
: listCache?.length === 0 ? (
<Typography style={{ textAlign: "center", marginTop: 20, marginBottom: 20, minWidth: 1000, }}>
No Keys Found
</Typography>
<ListItem style={{ display: "table-row" }}>
{Array(5).fill().map((_, index) => (
<ListItemText
key={index}
style={{
display: "table-cell",
padding: "8px",
textAlign: index === 0 ? "center" : "left",
}}
primary={index === 2 ? "No key found." : null}
colSpan={index === 0 ? 5 : undefined}
/>
))}
</ListItem>
): listCache?.map((data, index) => {
var bgColor = isSelectedDataStore? "#212121":"#27292d";
if (index % 2 === 0) {
@@ -517,6 +706,7 @@ const CacheView = memo((props) => {
}
const validate = validateJson(data.value);
const isDistributed = data?.suborg_distribution?.length > 0 ? true : false;
return (
<ListItem key={index} style={{display:'table-row', backgroundColor: bgColor, maxHeight: 300, overflow: "auto", borderBottomLeftRadius: listCache?.length - 1 === index ? 8 : 0, borderBottomRightRadius: listCache?.length - 1 === index ? 8 : 0,}}>
<ListItemText
@@ -576,13 +766,14 @@ const CacheView = memo((props) => {
primary={(
<span style={{ display: "inline" }}>
<Tooltip
title="Edit item"
title={data?.org_id !== selectedOrganization.id ? "You can not edit this cache as it is controlled by parent organization." : "Edit this key" }
style={{}}
aria-label={"Edit"}
>
<span>
<IconButton
style={{ padding: "6px" }}
disabled={data.org_id !== selectedOrganization.id ? true : false}
onClick={() => {
setEditCache(true)
setDataValue({
@@ -598,14 +789,14 @@ const CacheView = memo((props) => {
</span>
</Tooltip>
<Tooltip
title={"Public URL (types: text, raw, json)"}
title={data?.org_id !== selectedOrganization.id ? "You can not access public URL for this key as it is controlled by parent organization." : "Public URL (types: text, raw, json)" }
style={{ marginLeft: 0, }}
aria-label={"Public URL"}
>
<span>
<IconButton
style={{ padding: "6px" }}
disabled={data.public_authorization === undefined || data.public_authorization === null || data.public_authorization === "" ? true : false}
disabled={data.public_authorization === undefined || data.public_authorization === null || data.public_authorization === "" || data.org_id !== selectedOrganization.id ? true : false}
onClick={() => {
window.open(`${globalUrl}/api/v1/orgs/${orgId}/cache/${data.key}?type=text&authorization=${data.public_authorization}`, "_blank");
}}
@@ -616,18 +807,40 @@ const CacheView = memo((props) => {
</span>
</Tooltip>
<Tooltip
title={"Delete item"}
title={data?.org_id !== selectedOrganization.id ? "You can not delete this key as it is controlled by parent organization." : "Delete this key" }
aria-label={"Delete"}
>
<span>
<IconButton
style={{ padding: "6px" }}
disabled={data.org_id !== selectedOrganization.id ? true : false}
onClick={() => {
deleteCache(orgId, data.key);
//deleteFile(orgId);
}}
>
<img src="/icons/deleteIcon.svg" alt="delete" />
<svg
width="24"
height="24"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
style={{
stroke: data.org_id === selectedOrganization.id ? "#fd4c62" : "#c8c8c8",
}}
>
<path
d="M5 7.20001H6.6H19.4"
strokeLinecap="round"
strokeLinejoin="round"
fill="none"
/>
<path
d="M17.7996 7.2V18.4C17.7996 18.8243 17.631 19.2313 17.331 19.5314C17.0309 19.8314 16.624 20 16.1996 20H8.19961C7.77526 20 7.3683 19.8314 7.06824 19.5314C6.76818 19.2313 6.59961 18.8243 6.59961 18.4V7.2M8.99961 7.2V5.6C8.99961 5.17565 9.16818 4.76869 9.46824 4.46863C9.7683 4.16857 10.1753 4 10.5996 4H13.7996C14.224 4 14.6309 4.16857 14.931 4.46863C15.231 4.76869 15.3996 5.17565 15.3996 5.6V7.2"
strokeLinecap="round"
strokeLinejoin="round"
fill="none"
/>
</svg>
</IconButton>
</span>
</Tooltip>
@@ -642,6 +855,44 @@ const CacheView = memo((props) => {
}}
primary={new Date(data.edited * 1000).toISOString()}
/>
{selectedOrganization.id !== undefined && data?.org_id !== selectedOrganization.id ?
<ListItemText
primary={
<Tooltip
title="Parent organization controlled datastore. You can use, but not modify this key. Contact an admin of your parent organization if you need changes to this."
placement="top"
>
<Chip
label={"Parent"}
variant="contained"
color="secondary"
/>
</Tooltip>
}
style={{display: "table-cell", textAlign: 'center', verticalAlign: 'middle', }}
/>
:
<Tooltip
title="Distributed to sub-organizations. This means the sub organizations can use this datastore key, but can not modify it."
placement="top"
>
<Checkbox
disabled={ userdata?.active_org?.role !== "admin" || (selectedOrganization.creator_org !== undefined && selectedOrganization.creator_org !== null && selectedOrganization.creator_org !== "" )? true : false}
checked={isDistributed}
style={{ }}
color="secondary"
onClick={() => {
setShowDistributionPopup(true)
if(data?.suborg_distribution?.length > 0){
setSelectedSubOrg(data.suborg_distribution)
}else{
setSelectedSubOrg([])
}
setSelectedCacheKey(data.key)
}}
/>
</Tooltip>
}
</ListItem>
);
})}
@@ -653,4 +904,4 @@ const CacheView = memo((props) => {
);
});
export default memo(CacheView);
export default memo(CacheView);
@@ -36,7 +36,8 @@ const handleDirectoryChange = (folderDisabled, setFolderDisabled, globalUrl, isD
}
const action = folderDisabled ? "enable_folder" : "disable_folder";
const url = `${globalUrl}/api/v1/detections/${action}`;
//const url = `${globalUrl}/api/v1/detections/${detectionType}/selected_rules/${action}`;
const url = `${globalUrl}/api/v1/detections/sigma/selected_rules/${action}`;
fetch(url, {
method: "PUT",
@@ -258,7 +259,7 @@ const DetectionExplorer = (props) => {
rule.description.toLowerCase().includes(searchQuery.toLowerCase())
)
const lakeNodes = environmentList !== undefined && environmentList !== null ? environmentList.filter((env) => env?.data_lake?.enabled === true).length : 0
const lakeNodes = environmentList !== undefined && environmentList !== null ? environmentList.filter((env) => env?.archived === false && env?.data_lake?.enabled === true).length : 0
return (
<Container>
@@ -284,7 +285,8 @@ const DetectionExplorer = (props) => {
</Typography>
<div style={{display: "flex", }}>
{workflow !== undefined && workflow !== null && workflow.id !== undefined && workflow.id !== null && workflow.id.length > 0 ?
{/*workflow !== undefined && workflow !== null && workflow.id !== undefined && workflow.id !== null && workflow.id.length > 0 ?
<div style={{display: "flex", }}>
<div style={{minWidth: 400, maxWidth: 400, }}>
<WorkflowValidationTimeline
@@ -313,7 +315,7 @@ const DetectionExplorer = (props) => {
</IconButton>
</div>
:
: */}
<Button
variant="contained"
onClick={() => {
@@ -331,11 +333,11 @@ const DetectionExplorer = (props) => {
detectionWorkflowId === "" ? `Connect to ${detectionInfo?.category}` :
isDetectionValid ? `Connected to ${detectionInfo?.category}` : `Fix ${detectionInfo?.category} connection`}
</Button>
}
{/**/}
{detectionInfo?.category === "SIGMA" || detectionInfo?.category === "SIEM" ?
<Tooltip title={`You have ${lakeNodes} available Data Lake node(s)`}>
<a href="/admin?tab=environments" style={{textDecoration: "none", color: "inherit", }} target="_blank" rel="noreferrer">
<a href="/admin?tab=Locations" style={{textDecoration: "none", color: "inherit", }} target="_blank" rel="noreferrer">
<FmdGoodIcon style={{marginLeft: 15, marginTop: 5, color: lakeNodes > 0 ? green : red}} />
</a>
</Tooltip>
@@ -137,6 +137,8 @@ const RuleCard = ({ ruleName, description, file_id, globalUrl, folderDisabled, i
setResponseValue(e.target.value)
toast.error("The automatic response system is NOT available for you yet. Please contact support@shuffler.io if you want to try this feature.")
// FIXME: Handle:
// 1. Get the current cache for the detection
// 2. Create a new mapping for Detection -> Response
@@ -187,7 +189,7 @@ const RuleCard = ({ ruleName, description, file_id, globalUrl, folderDisabled, i
<Switch
checked={isEnabled && !folderDisabled}
onChange={handleSwitchChange}
disabled={false}
disabled={true}
/>
</Tooltip>
</div>
+3 -3
View File
@@ -647,7 +647,7 @@ const EditWorkflow = (props) => {
userdata.orgs.filter(org => org.creator_org === userdata.active_org.id).length === 0 ?
userdata.active_org.creator_org === undefined || userdata.active_org.creator_org === null || userdata.active_org.creator_org === "" ?
<Typography variant="body2" style={{ marginTop: 10, color: "rgba(255,255,255,0.7)" }}>
Your organization does not have any suborgs yet OR your user may not have access to available suborgs. Please <a href="/admin?tab=suborgs" style={{ textDecoration: "none", color: "#f86a3e" }} target="_blank">make one</a> or get access to suborgs by another admin, then try again.
Your organization does not have any suborgs yet OR your user may not have access to available suborgs. Please <a href="/admin?tab=tenants" style={{ textDecoration: "none", color: "#f86a3e" }} target="_blank">make one</a> or get access to suborgs by another admin, then try again.
</Typography>
:
<Typography variant="body2" style={{ marginTop: 10, color: "rgba(255,255,255,0.7)" }}>
@@ -738,7 +738,7 @@ const EditWorkflow = (props) => {
})}
</Select>
:
<Link to={"/admin?tab=suborgs"} style={{ textDecoration: "none", color: "#f86a3e" }} target="_blank">
<Link to={"/admin?tab=tenants"} style={{ textDecoration: "none", color: "#f86a3e" }} target="_blank">
<Typography variant="body2" style={{ marginTop: 10, }}>
Create a sub-org to distribute workflows to suborgs.
</Typography>
@@ -753,7 +753,7 @@ const EditWorkflow = (props) => {
Git Backup Repository
</Typography>
<Typography variant="body2" style={{ textAlign: "left", marginTop: 5, }} color="textSecondary">
Decide where this workflow is backed up in a Git repository. Will create logs and notifications if upload fails. <b>The repository and branch must already have been initialized</b>. Files will show up in the root folder in the format 'orgid/workflow status/workflow id.json' without images. Overrides your <a href="/admin?admin_tab=organization" style={{ textDecoration: "none", color: "#f86a3e" }} target="_blank">default backup repository</a>. <a href="/docs/configuration#environment-variables" style={{ textDecoration: "none", color: "#f86a3e" }} target="_blank">Credentials are encrypted.</a> Creates <a href="/admin?admin_tab=priorities" style={{ textDecoration: "none", color: "#f86a3e" }} target="_blank">notifications</a> if it fails.
Decide where this workflow is backed up in a Git repository. Will create logs and notifications if upload fails. <b>The repository and branch must already have been initialized</b>. Files will show up in the root folder in the format 'orgid/workflow status/workflow id.json' without images. Overrides your <a href="/admin?admin_tab=org_config" style={{ textDecoration: "none", color: "#f86a3e" }} target="_blank">default backup repository</a>. <a href="/docs/configuration#environment-variables" style={{ textDecoration: "none", color: "#f86a3e" }} target="_blank">Credentials are encrypted.</a> Creates <a href="/admin?admin_tab=notifications" style={{ textDecoration: "none", color: "#f86a3e" }} target="_blank">notifications</a> if it fails.
</Typography>
<Grid container style={{ marginTop: 10, }} spacing={2}>
<Grid item xs={6} style={{}}>
+1 -1
View File
@@ -1567,7 +1567,7 @@ const Files = memo((props) => {
placement="top"
>
<Checkbox
disabled={selectedOrganization.creator_org !== undefined && selectedOrganization.creator_org !== null && selectedOrganization.creator_org !== "" ? true : false}
disabled={userdata?.active_org?.role !== "admin" || (selectedOrganization.creator_org !== undefined && selectedOrganization.creator_org !== null && selectedOrganization.creator_org !== "" ) ? true : false}
checked={isDistributed}
style={{ }}
color="secondary"
+3 -3
View File
@@ -869,8 +869,8 @@ const LicencePopup = (props) => {
:
shuffleVariant === 0 ? "price_1PZPSSEJjT17t98NLJoTMYja" : "price_1PZPQuEJjT17t98N3yORUtd9"
const successUrl = `${window.location.origin}/admin?admin_tab=billing&payment=success`
const failUrl = `${window.location.origin}/pricing?admin_tab=billing&payment=failure`
const successUrl = `${window.location.origin}/admin?admin_tab=billingstats&payment=success`
const failUrl = `${window.location.origin}/admin?admin_tab=billingstats&payment=failure`
console.log("Priceitem: ", priceItem, shuffleVariant)
var checkoutObject = {
@@ -888,7 +888,7 @@ const LicencePopup = (props) => {
}
if (stripe === undefined || stripe === null || stripe.redirectToCheckout === undefined) {
window.open("https://shuffler.io/admin?admin_tab=billing&payment=stripe_error", "_self")
window.open("https://shuffler.io/admin?admin_tab=billingstats&payment=stripe_error", "_self")
}
stripe.redirectToCheckout(checkoutObject)
+2 -2
View File
@@ -465,7 +465,7 @@ const Header = (props) => {
</Link>
<Divider style={{ marginTop: 10, marginBottom: 10, }} />
<Link to="/admin?admin_tab=priorities" style={hrefStyle}>
<Link to="/admin?admin_tab=notifications" style={hrefStyle}>
<MenuItem
onClick={(event) => {
handleClose();
@@ -1107,7 +1107,7 @@ const Header = (props) => {
);
})}
<Divider />
<Link to="/admin?tab=suborgs" style={hrefStyle}>
<Link to="/admin?tab=tenants" style={hrefStyle}>
<MenuItem
key={"add suborgs"}
style={{
+51 -95
View File
@@ -92,6 +92,7 @@ import {
SquareFoot as SquareFootIcon,
Storage as StorageIcon,
Check as CheckIcon,
PriorityHigh as PriorityHighIcon,
} from '@mui/icons-material';
export const useStyles = makeStyles({
@@ -165,6 +166,7 @@ const ParsedAction = (props) => {
expansionModalOpen,
setExpansionModalOpen,
fixExample,
listCache,
setActiveDialog,
@@ -258,7 +260,7 @@ const ParsedAction = (props) => {
if (param.required === false && param.name.startsWith("${") && param.name.endsWith("}")) {
// Check if it's a required param
param.autocompleted = false
param.autocompleted = true
if (selectedAction.required_body_fields !== undefined && selectedAction.required_body_fields !== null && selectedAction.required_body_fields.length > 0) {
if (selectedAction.required_body_fields.includes(param.name)) {
param.required = true
@@ -281,6 +283,7 @@ const ParsedAction = (props) => {
}
if (param.field_active === true) {
param.autocompleted = true
generated_optional.push(param)
continue
}
@@ -298,8 +301,8 @@ const ParsedAction = (props) => {
const newparams = auth
.concat(bodyfield)
.concat(required)
.concat(special_optional)
.concat(generated_optional)
.concat(special_optional)
.concat(optional)
var newkeyorder = []
@@ -1492,6 +1495,7 @@ const ParsedAction = (props) => {
newAppname = newAppname.replaceAll("_", " ")
}
var optionalFound = false
return (
<div style={appApiViewStyle} id="parsed_action_view">
@@ -3122,91 +3126,6 @@ const ParsedAction = (props) => {
}}
/>
</Tabs>
{/*
<ButtonGroup fullWidth>
<Tooltip
color="secondary"
title={
"Show all body fields instead of the body itself"
}
placement="top"
>
<Button
variant={hideBody === true ? "outlined" : "contained"}
color="primary"
style={{textTransform: "none",}}
onClick={() => {
// Set localstorage
localStorage.setItem("hideBody", "true")
setHideBody(false)
const updatedParameters = selectedActionParameters.map((param) => {
if (param.name === "body") {
return {
...param,
id: "UNTOGGLED",
}
}
if (param.description === openApiFieldDesc) {
// Check required fields here
if (selectedAction.required_body_fields !== undefined && selectedAction.required_body_fields !== null && selectedAction.required_body_fields.length > 0) {
// Look for the field name in the required_body_fields
if (selectedAction.required_body_fields.includes(param.name)) {
param.required = true
} else {
param.required = false
}
}
return { ...param, field_active: true }
}
return param
})
setSelectedActionParameters(updatedParameters)
}}>
Simplified
</Button>
</Tooltip>
<Tooltip
color="secondary"
title={
"Show the body as it is"
}
placement="top"
>
<Button
variant={hideBody === true ? "contained" : "outlined" }
color="primary"
style={{textTransform: "none",}}
onClick={() => {
localStorage.setItem("hideBody", "false")
setHideBody(true)
// Make sure the body field is shown
const updatedParameters = selectedActionParameters.map((param) => {
if (param.name === "body") {
return {
...param,
id: "TOGGLED",
}
}
if (param.description === openApiFieldDesc) {
return { ...param, field_active: false }
}
return param
})
setSelectedActionParameters(updatedParameters)
}}>
Advanced
</Button>
</Tooltip>
</ButtonGroup>
*/}
</div>
)
@@ -3551,7 +3470,7 @@ const ParsedAction = (props) => {
}}
/>
</Tooltip>
);
)
// Finds headers from a string to be used for autocompletion
const findHeaders = (inputdata) => {
@@ -4087,9 +4006,24 @@ const ParsedAction = (props) => {
};
var parsedPaths = [];
if (typeof innerdata.example === "object") {
parsedPaths = GetParsedPaths(innerdata.example, "");
}
if (innerdata.type === "workflow_variable") {
// Try to parse the value if it's a string that could be JSON
if (typeof innerdata.value === "string") {
try {
const parsedValue = JSON.parse(innerdata.value)
if (typeof parsedValue === "object") {
parsedPaths = GetParsedPaths(parsedValue, "");
}
} catch (e) {
// Not valid JSON, use the value directly
parsedPaths = GetParsedPaths(innerdata.value, "");
}
} else if (typeof innerdata.value === "object") {
parsedPaths = GetParsedPaths(innerdata.value, "");
}
} else if (typeof innerdata.example === "object") {
parsedPaths = GetParsedPaths(innerdata.example, "");
}
const coverColor = "#82ccc3"
//menuPosition.left -= 50
@@ -4270,8 +4204,14 @@ const ParsedAction = (props) => {
data.variant = "STATIC_VALUE"
}
const isFirstOptional = optionalFound === false && data.configuration === false && data.required === false ? true : false
if (optionalFound === false && data.configuration === false && data.required === false) {
optionalFound = true
}
return (
<div key={data.name}>
<div key={data.name} style={{marginTop: isFirstOptional ? 55 : 5, }}>
{isFirstOptional ? <Divider style={{backgroundColor: "rgba(255,255,255,0.1)", marginBottom: 20, }} /> : null}
{showButtonField === true ? hideBodyButtonValue : null}
<div
style={{ marginTop: 20, marginBottom: 0, display: "flex" }}
@@ -4297,6 +4237,20 @@ const ParsedAction = (props) => {
) : null}
{hasAutocomplete === true ?
data.field_active === true ?
<Tooltip
color="primary"
title={"This is an advanced field, simplified to make it easier to use. NOT required according to the API documentation."}
placement="top"
>
<PriorityHighIcon
style={{
color: "rgba(255,255,255,0.5)" ,
marginRight: 0,
}}/>
</Tooltip>
:
<Tooltip
color="primary"
title={"Field was autocompleted by Shuffle based on previous actions (same fields or parent nodes)"}
@@ -4316,7 +4270,7 @@ const ParsedAction = (props) => {
title={"Explore your keys in Datastore"}
placement="top"
>
<a href="/admin?tab=cache" target="_blank" style={{textDecoration: "none"}}>
<a href="/admin?tab=datastore" target="_blank" style={{textDecoration: "none"}}>
<StorageIcon style={{
color: "#FF8544",
marginRight: 10,
@@ -4351,14 +4305,16 @@ const ParsedAction = (props) => {
parsedvalue = ""
}
//console.log("Required fields: ", selectedActionParameters[count])
setEditorData({
"name": data.name,
"value": parsedvalue,
"value": fixExample(parsedvalue),
"field_number": count,
"actionlist": actionlist,
"field_id": clickedFieldId,
"example": selectedActionParameters[count].example,
"example": fixExample(selectedActionParameters[count].example),
})
}}
/>
+1 -1
View File
@@ -387,7 +387,7 @@ const SearchData = props => {
if (responseJson.success === false) {
toast(`Failed to ${type} the app for your organization. Please try again or contact support@shuffler.io for more info`)
} else {
toast(`App successfully ${type}d. Please refresh the page to use it.`)
toast(`App successfully ${type}d. It may now be used in your workflows.`)
}
})
.catch(error => {
+191 -15
View File
@@ -114,7 +114,9 @@ const CodeEditor = (props) => {
editorData,
setAiQueryModalOpen,
fullScreenMode
fullScreenMode,
environment,
fixExample,
} = props
const [localcodedata, setlocalcodedata] = React.useState(codedata === undefined || codedata === null || codedata.length === 0 ? "" : codedata);
@@ -189,8 +191,27 @@ const CodeEditor = (props) => {
tmpVariables.push('$' + actionlist[i].autocomplete.toLowerCase())
var parsedPaths = []
if (typeof actionlist[i].example === "object") {
parsedPaths = GetParsedPaths(actionlist[i].example, "");
if (actionlist[i].type === "workflow_variable") {
// Try to parse the value if it's a string that could be JSON
if (typeof actionlist[i].value === "string") {
try {
const parsedValue = JSON.parse(actionlist[i].value)
if (typeof parsedValue === "object") {
parsedPaths = GetParsedPaths(parsedValue, "");
}
} catch (e) {
// Not valid JSON, skip parsing
continue
}
} else if (typeof actionlist[i].value === "object") {
// Direct object/array value
parsedPaths = GetParsedPaths(actionlist[i].value, "");
}
} else {
// Handle regular action results
if (typeof actionlist[i].example === "object") {
parsedPaths = GetParsedPaths(actionlist[i].example, "");
}
}
for (var key in parsedPaths) {
@@ -596,7 +617,7 @@ const CodeEditor = (props) => {
var code_lines = value.split('\n')
for (var i = 0; i < code_lines.length; i++) {
var current_code_line = code_lines[i]
var variable_occurence = current_code_line.match(/[\\]{0,1}[$]{1}([a-zA-Z0-9_@-]+\.?){1}([a-zA-Z0-9#_@-]+\.?){0,}/g);
var variable_occurence = current_code_line.match(/[\\]{0,1}[$]{1}([a-zA-Z0-9_@-]+\.?){1}([a-zA-Z0-9#_@-]+\.?){0,}/g)
if (!variable_occurence) {
continue;
@@ -664,6 +685,75 @@ const CodeEditor = (props) => {
}
}
var code_lines = value.split('\n')
for (var i = 0; i < code_lines.length; i++) {
var current_code_line = code_lines[i]
// Look for REPLACE_ME
var variable_occurence = current_code_line.match(/REPLACE_ME/g)
if (!variable_occurence) {
continue;
}
var new_occurences = variable_occurence.filter((occurrence) => occurrence[0])
variable_occurence = new_occurences
// Find the start position of REPLACE_ME and highlight it
var dollar_occurence = []
for (let ch = 0; ch < current_code_line.length; ch++) {
// Not allowing it then lol
if (ch + 9 >= current_code_line.length) {
continue
}
// Rofl - at least it is specific
if (current_code_line[ch] === 'R' && current_code_line[ch + 1] === 'E' && current_code_line[ch + 2] === 'P' && current_code_line[ch + 3] === 'L' && current_code_line[ch + 4] === 'A' && current_code_line[ch + 5] === 'C' && current_code_line[ch + 6] === 'E' && current_code_line[ch + 7] === '_' && current_code_line[ch + 8] === 'M' && current_code_line[ch + 9] === 'E') {
dollar_occurence.push(ch)
}
}
try {
if (variable_occurence.length === 0) {
//value.markText({line:i, ch:0}, {line:i, ch:code_lines[i].length-1}, {"css": "background-color: #282828; border-radius: 0px; color: #b8bb26"})
//value.markText({line:i, ch:0}, {line:i, ch:code_lines[i].length-1}, {"css": "background-color: #; border-radius: 0px; color: inherit"})
}
for (let occ = 0; occ < variable_occurence.length; occ++) {
const fixedVariable = variable_occurence[occ]
var startCh = dollar_occurence[occ]
var endCh = dollar_occurence[occ] + 10
try {
newMarkers.push({
startRow: i,
startCol: startCh,
endRow: i,
endCol: endCh,
className: "bad-marker",
type: "text",
})
} catch (e) {
console.log("Error in color highlighting: ", e);
newMarkers.push({
startRow: i,
startCol: startCh,
endRow: i,
endCol: endCh,
className: "bad-marker",
type: "text",
})
}
setMarkers(newMarkers)
}
} catch (e) {
console.log("Error in color highlighting: ", e);
}
}
setMarkers(newMarkers)
}
@@ -720,6 +810,34 @@ const CodeEditor = (props) => {
const fixedVariable = fixVariable(found[i])
var valuefound = false
// First check if it's a workflow variable
if (actionlist !== undefined && actionlist.length > 0) {
const workflowVar = actionlist?.find(item =>
item.type === "workflow_variable" &&
`$${item.autocomplete.toLowerCase()}` === fixedVariable.toLowerCase()
)
if (workflowVar && workflowVar.example) {
valuefound = true
try {
// Try to parse the example value if it's stored as a JSON string
if (typeof workflowVar.example === "string" &&
(workflowVar.example.startsWith("[") || workflowVar.example.startsWith("{"))) {
const parsedExample = JSON.parse(workflowVar.example)
input = input.replace(found[i], JSON.stringify(parsedExample), -1)
} else {
input = input.replace(found[i], workflowVar.example, -1)
}
continue
} catch (e) {
console.log("Error parsing workflow variable:", e)
input = input.replace(found[i], workflowVar.example, -1)
continue
}
}
}
for (var j = 0; j < actionlist.length; j++) {
if (fixedVariable.slice(1,).toLowerCase() !== actionlist[j].autocomplete.toLowerCase()) {
continue
@@ -750,9 +868,26 @@ const CodeEditor = (props) => {
var shouldbreak = false
for (var k = 0; k < actionlist.length; k++) {
var parsedPaths = []
if (typeof actionlist[k].example === "object") {
parsedPaths = GetParsedPaths(actionlist[k].example, "");
}
// Handle both workflow variables and regular actions
if (actionlist[k].type === "workflow_variable") {
// Try to parse the value if it's a string that could be JSON
if (typeof actionlist[k].value === "string") {
try {
const parsedValue = JSON.parse(actionlist[k].value)
if (typeof parsedValue === "object") {
parsedPaths = GetParsedPaths(parsedValue, "");
}
} catch (e) {
// Not valid JSON, use the value directly
parsedPaths = GetParsedPaths(actionlist[k].value, "");
}
} else if (typeof actionlist[k].value === "object") {
parsedPaths = GetParsedPaths(actionlist[k].value, "");
}
} else if (typeof actionlist[k].example === "object") {
parsedPaths = GetParsedPaths(actionlist[k].example, "");
}
for (var key in parsedPaths) {
const fullpath = "$" + actionlist[k].autocomplete.toLowerCase() + parsedPaths[key].autocomplete.toLowerCase()
@@ -766,7 +901,22 @@ const CodeEditor = (props) => {
var new_input = ""
try {
new_input = FindJsonPath(fullpath, actionlist[k].example)
const sourceData = actionlist[k].type === "workflow_variable" ?
(() => {
// Try to parse the value if it's a JSON string
if (typeof actionlist[k].value === "string") {
try {
return JSON.parse(actionlist[k].value);
} catch (e) {
// If parsing fails, return the original string value
return actionlist[k].value;
}
}
return actionlist[k].value;
})() :
actionlist[k].example;
new_input = FindJsonPath(fullpath, sourceData)
} catch (e) {
console.log("ERR IN INPUT: ", e)
}
@@ -789,8 +939,9 @@ const CodeEditor = (props) => {
}
}
input = input.replace(fixedVariable, new_input, -1)
input = input.replace(found[i], new_input, -1)
// Replace both the fixed and original variable to handle both #0 and # cases
input = input.replace(found[i], new_input)
input = input.replace(fixedVariable, new_input)
shouldbreak = true
break
@@ -876,8 +1027,12 @@ const CodeEditor = (props) => {
}
if (edited === false) {
if (!item.value.includes("{%") && !item.value.includes("{{")) {
setlocalcodedata(localcodedata + " | " + item.value + " }}")
if (item.value.includes("{%") || item.value.includes("{{")) {
if (!item.value.includes("}}") && !item.value.includes("%}")) {
setlocalcodedata(localcodedata + " | " + item.value + " }}")
} else {
setlocalcodedata(localcodedata + item.value)
}
} else {
setlocalcodedata(localcodedata + item.value)
}
@@ -899,7 +1054,7 @@ const CodeEditor = (props) => {
const actionname = selectedAction.name === "execute_python" && !inputdata.replaceAll(" ", "").includes("{%python%}") ? "execute_python" : selectedAction.name === "execute_bash" ? "execute_bash" : "repeat_back_to_me"
const params = actionname === "execute_python" ? [{ "name": "code", "value": inputdata }] : actionname === "execute_bash" ? [{ "name": "code", "value": inputdata }, { "name": "shuffle_input", "value": "", }] : [{ "name": "call", "value": inputdata }]
const actiondata = { "description": "Repeats the call parameter", "id": "", "name": actionname, "label": "", "node_type": "", "environment": "", "sharing": false, "private_id": "", "public_id": "", "app_id": appid, "tags": null, "authentication": [], "tested": false, "parameters": params, "execution_variable": { "description": "", "id": "", "name": "", "value": "" }, "returns": { "description": "", "example": "", "id": "", "schema": { "type": "string" } }, "authentication_id": "", "example": "", "auth_not_required": false, "source_workflow": "", "run_magic_output": false, "run_magic_input": false, "execution_delay": 0, "app_name": "Shuffle Tools", "app_version": "1.2.0", "selectedAuthentication": {} }
const actiondata = { "description": "Repeats the call parameter", "id": "", "name": actionname, "label": "", "node_type": "", "environment": environment?.Name, "sharing": false, "private_id": "", "public_id": "", "app_id": appid, "tags": null, "authentication": [], "tested": false, "parameters": params, "execution_variable": { "description": "", "id": "", "name": "", "value": "" }, "returns": { "description": "", "example": "", "id": "", "schema": { "type": "string" } }, "authentication_id": "", "example": "", "auth_not_required": false, "source_workflow": "", "run_magic_output": false, "run_magic_input": false, "execution_delay": 0, "app_name": "Shuffle Tools", "app_version": "1.2.0", "selectedAuthentication": {} }
setExecutionResult({
"valid": false,
@@ -1396,7 +1551,22 @@ const CodeEditor = (props) => {
};
var parsedPaths = [];
if (typeof innerdata.example === "object") {
if (innerdata.type === "workflow_variable") {
// Try to parse the value if it's a string that could be JSON
if (typeof innerdata.value === "string") {
try {
const parsedValue = JSON.parse(innerdata.value)
if (typeof parsedValue === "object") {
parsedPaths = GetParsedPaths(parsedValue, "");
}
} catch (e) {
// Not valid JSON, use the value directly
parsedPaths = GetParsedPaths(innerdata.value, "");
}
} else if (typeof innerdata.value === "object") {
parsedPaths = GetParsedPaths(innerdata.value, "");
}
} else if (typeof innerdata.example === "object") {
parsedPaths = GetParsedPaths(innerdata.example, "");
}
@@ -1585,7 +1755,13 @@ const CodeEditor = (props) => {
}}
disabled={editorData === undefined || editorData.example === undefined || editorData.example === null || editorData.example.length === 0}
onClick={() => {
setlocalcodedata(editorData.example)
if (fixExample !== undefined) {
const newExample = fixExample(editorData.example)
setlocalcodedata(newExample)
} else {
console.log("No fix example available!")
setlocalcodedata(editorData.example)
}
}}
color="secondary"
>