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"
>
+1 -22
View File
@@ -11,29 +11,8 @@ const Admin2 = (props) => {
const [organizationFeatures, setOrganizationFeatures] = useState({});
const [orgRequest, setOrgRequest] = React.useState(true);
const isCloud = window.location.host === "localhost:3002" || window.location.host === "shuffler.io";
const handleGetOrg = (orgId) => {
// if (
// serverside !== true &&
// window.location.search !== undefined &&
// window.location.search !== null
// ) {
// const urlSearchParams = new URLSearchParams(window.location.search);
// const params = Object.fromEntries(urlSearchParams.entries());
// const foundorgid = params["org_id"];
// if (foundorgid !== undefined && foundorgid !== null) {
// orgId = foundorgid;
// }
// }
console.log("getting organization details for: ", orgId);
// if (orgId === undefined) {
// toast(
// "Organization ID not defined. Please contact us on https://shuffler.io if this persists logout.",
// );
// return;
// }
// Just use this one?
fetch(`${globalUrl}/api/v1/orgs/${orgId}`, {
method: "GET",
+210 -89
View File
@@ -568,6 +568,9 @@ const AngularWorkflow = (defaultprops) => {
}
}, [editWorkflowModalOpen])
const dragRef = React.useRef(false);
// New for generated stuff
const releaseToConnectLabel = "Release to Connect"
const integrationApps = [{
@@ -696,11 +699,32 @@ const releaseToConnectLabel = "Release to Connect"
}
if (loadedApps.includes(appId)) {
return
console.log("App already loaded: ", appId)
// 1. Find the app and check if it has actions
// 2. If it doesn't have actions, reload once again
var should_reload = false
for (var i = 0; i < apps.length; i++) {
const curapp = apps[i]
if (curapp.id !== appId) {
continue
}
if (curapp.actions === undefined || curapp.actions === null || curapp.actions.length === 0 || curapp.actions.length === 1) {
should_reload = true
break
}
}
if (!should_reload) {
return
}
}
loadedApps.push(appId)
setLoadedApps(loadedApps)
if (!loadedApps.includes(appId)) {
loadedApps.push(appId)
setLoadedApps(loadedApps)
}
const appUrl = `${globalUrl}/api/v1/apps/${appId}/config?openapi=false`
fetch(appUrl, {
@@ -2331,6 +2355,35 @@ const releaseToConnectLabel = "Release to Connect"
return success
};
const fixExample = (input, required) => {
if (input === undefined || input === null || input.length === 0) {
return ""
}
// 1. Find anything matching ${variable}
// 2. If it is required, replace it with REQUIRED
// 3. If it is not required, replace it with empty
// Check if it is a string or not
var newExample = input
if (typeof newExample !== "string") {
return newExample
}
const found = newExample.match(/\${(.*?)}/g)
if (found === null || found === undefined || found.length === 0) {
return newExample
}
for (var i = 0; i < found.length; i++) {
//newExample = newExample.replace(found[i], "REQUIRED")
newExample = newExample.replace(found[i], "REPLACE_ME")
}
return newExample
}
const monitorUpdates = () => {
var firstnode = cy.getElementById(workflow.start);
if (firstnode.length === 0) {
@@ -3656,7 +3709,7 @@ const releaseToConnectLabel = "Release to Connect"
setWorkflows([responseJson])
} else {
getAppAuthentication();
getEnvironments();
getEnvironments(responseJson.org_id)
getSettings();
getFiles()
@@ -6864,7 +6917,9 @@ const releaseToConnectLabel = "Release to Connect"
}
}
if (showEnvCnt > 1) {
// Always showing for now
//if (showEnvCnt > 1) {
if (showEnvCnt > 0) {
setShowEnvironment(true)
}
@@ -10018,15 +10073,37 @@ const releaseToConnectLabel = "Release to Connect"
const ParsedAppPaper = (props) => {
const app = props.app
const small = props.small
const actionString = props.action
const small = props.small
const actionString = props.action
const [hover, setHover] = React.useState(false);
React.useEffect(() => {
if(app.name === "Shuffle Tools"){
if (app.actions !== undefined && (app.actions === null || app.actions.length === 1)) {
loadAppConfig(app.id, false)
// Prevent hover effects during drag
const handleMouseMove = React.useCallback((e) => {
if (dragRef.current) {
setHover(false);
}
}, []);
React.useEffect(() => {
// Add mousemove listener to track dragging
document.addEventListener('mousemove', handleMouseMove);
return () => {
document.removeEventListener('mousemove', handleMouseMove);
};
}, [handleMouseMove]);
React.useEffect(() => {
if (props.skip_load === true) {
return
}
if (app.name === "Shuffle Tools"){
if (app.actions !== undefined && (app.actions === null || app.actions.length === 1)) {
loadAppConfig(app.id, false)
}
}
}
}, [])
if (app === undefined || app === null) {
@@ -10115,13 +10192,20 @@ const releaseToConnectLabel = "Release to Connect"
}
newAppStyle.backgroundColor = theme.palette.backgroundColor
return (
<Draggable
onStart={() => {
dragRef.current = true;
newAppStyle.zIndex = 9999
}}
onDrag={(e) => {
newAppStyle.zIndex = 9999
handleAppDrag(e, app)
}}
onStop={(e) => {
dragRef.current = false;
newAppStyle.zIndex = "none"
handleDragStop(e, app)
}}
key={app.id}
@@ -10132,18 +10216,18 @@ const releaseToConnectLabel = "Release to Connect"
<Paper
square
style={newAppStyle}
onMouseOver={(e) => {
e.preventDefault()
e.stopPropagation()
setHover(true)
if (app.actions !== undefined && (app.actions === null || app.actions.length === 1)) {
loadAppConfig(app.id, false)
}
onMouseEnter={(e) => {
if (!dragRef.current) {
e.preventDefault();
e.stopPropagation();
setHover(true);
if (app.actions !== undefined && (app.actions === null || app.actions.length === 1)) {
loadAppConfig(app.id, false);
}
}
}}
onMouseOut={() => {
onMouseLeave={() => {
setHover(false);
}}
onClick={() => {
@@ -10586,6 +10670,27 @@ const releaseToConnectLabel = "Release to Connect"
}
var viewedApps = []
const QuickAccessSection = ({title, items, renderItem}) => (
<div style={{marginBottom: 25}}>
<Typography
variant="body1"
color="textSecondary"
style={{marginTop: 20, marginLeft: 5}}
>
{title}
</Typography>
<div style={{display: "flex", flexWrap: "wrap", gap: 5}}>
{items.map((item, index) => renderItem(item, index))}
</div>
</div>
);
// Popular Shuffle Tools actions
const popularActions = [
["repeat_back_to_me", "filter_list", "execute_python", "parse_ioc"],
["set_cache_value", "get_file_meta", "merge_lists", "send_sms_shuffle"]
];
return (
<div style={appViewStyle}>
<div style={{ flex: "1", zoom: 0.9, }}>
@@ -10628,59 +10733,37 @@ const releaseToConnectLabel = "Release to Connect"
}}
/>
{shuffleToolsApp !== undefined && shuffleToolsApp !== null && document?.getElementById("appsearch")?.value?.length === 0 ?
<div style={{marginBottom: 25, }}>
<Typography variant="body1" color="textSecondary" style={{marginTop: 20, marginLeft: 5, }}>
Popular Actions
</Typography>
<div style={{
display: "flex",
}}>
<ParsedAppPaper small={true} action={"repeat_back_to_me"} app={shuffleToolsApp} />
<div style={{marginLeft: 5, }}/>
<ParsedAppPaper small={true} action={"filter_list"} app={shuffleToolsApp} />
<div style={{marginLeft: 5, }}/>
<ParsedAppPaper small={true} action={"execute_python"} app={shuffleToolsApp} />
<div style={{marginLeft: 5, }}/>
<ParsedAppPaper small={true} action={"parse_ioc"} app={shuffleToolsApp} />
</div>
<div style={{
display: "flex",
}}>
<ParsedAppPaper small={true} action={"set_cache_value"} app={shuffleToolsApp} />
<div style={{marginLeft: 5, }}/>
<ParsedAppPaper small={true} action={"get_file_meta"} app={shuffleToolsApp} />
<div style={{marginLeft: 5, }}/>
<ParsedAppPaper small={true} action={"merge_lists"} app={shuffleToolsApp} />
<div style={{marginLeft: 5, }}/>
<ParsedAppPaper small={true} action={"send_sms_shuffle"} app={shuffleToolsApp} />
</div>
</div>
: null}
{document?.getElementById("appsearch")?.value?.length === 0 ?
<div style={{marginBottom: 25, }}>
<Typography variant="body1" color="textSecondary" style={{marginTop: 20, marginLeft: 5, }}>
Triggers
</Typography>
<div style={{
display: "flex",
}}>
{triggers.map((trigger, index) => {
if (trigger.trigger_type === "PIPELINE") {
return null
}
return(
<div key={index} style={{marginLeft: index !== 0 ? 5 : 0, }}>
<ParsedAppPaper small={true} action={trigger.name} app={JSON.parse(JSON.stringify(trigger))} />
</div>
)
})}
</div>
</div>
: null}
{shuffleToolsApp && !document?.getElementById("appsearch")?.value?.length && (
<QuickAccessSection
title="Popular Actions"
items={popularActions.flat()}
renderItem={(action) => (
<ParsedAppPaper
key={action}
small={true}
action={action}
app={shuffleToolsApp}
skip_load={true}
/>
)}
/>
)}
{!document?.getElementById("appsearch")?.value?.length && (
<QuickAccessSection
title="Triggers"
items={triggers.filter(t => t.trigger_type !== "PIPELINE")}
renderItem={(trigger, index) => (
<ParsedAppPaper
key={index}
small={true}
action={trigger.name}
app={JSON.parse(JSON.stringify(trigger))}
/>
)}
/>
)}
<Typography variant="body1" color="textSecondary" style={{marginTop: 20, marginLeft: 5, }}>
Your Apps
@@ -13942,8 +14025,23 @@ const releaseToConnectLabel = "Release to Connect"
};
var parsedPaths = [];
console.log("Found example data: ", innerdata.example)
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, "");
}
@@ -16531,7 +16629,7 @@ const releaseToConnectLabel = "Release to Connect"
}
</div>
{showEnvironment === true && environments.length > 1 && selectedActionEnvironment !== undefined && selectedActionEnvironment !== null && selectedActionEnvironment.Name !== undefined && selectedActionEnvironment.Name !== null ?
{showEnvironment === true && environments.length > 0 && selectedActionEnvironment !== undefined && selectedActionEnvironment !== null && selectedActionEnvironment.Name !== undefined && selectedActionEnvironment.Name !== null ?
<FormControl fullWidth style={{marginTop: 15, pointerEvents: "auto", maxWidth: 250, }}>
<InputLabel
@@ -16556,6 +16654,10 @@ const releaseToConnectLabel = "Release to Connect"
height: 40,
}
}}
onClick={(e) => {
console.log("CLICK!")
getEnvironments(workflow.org_id)
}}
onChange={(e) => {
setLastSaved(false)
const env = environments.find((a) => a.Name === e.target.value);
@@ -16600,8 +16702,9 @@ const releaseToConnectLabel = "Release to Connect"
>
{data.Name === "cloud" || data.Name === "Cloud" ? null : !isRunning ?
<a href={`/admin?tab=locations&env=${data.Name}`} target="_blank" style={{textDecoration: "none",}}>
<Tooltip title={"Click to configure this location"} placement="top">
<Tooltip title={"Click to configure this runtime location"} placement="top">
<Chip
style={{marginLeft: 0, padding: 0, marginRight: 10, cursor: "pointer", backgroundColor: red, }}
label={"Stopped"}
@@ -16610,6 +16713,7 @@ const releaseToConnectLabel = "Release to Connect"
onClick={(e) => {
e.preventDefault()
e.stopPropagation()
window.open(`/admin?tab=locations&env=${data.Name}`, "_blank", "noopener,noreferrer")
}}
@@ -16617,7 +16721,21 @@ const releaseToConnectLabel = "Release to Connect"
/>
</Tooltip>
</a>
: null}
:
<Chip
key={index}
style={{
color: green,
borderColor: green,
}}
label={"Running"}
onClick={() => {
//handleChipClick
}}
variant="outlined"
color="primary"
/>
}
{data.default === true ?
<Chip
@@ -18000,7 +18118,7 @@ const releaseToConnectLabel = "Release to Connect"
setWorkflow(workflow)
getAppAuthentication();
getEnvironments();
getEnvironments(workflow.org_id)
getWorkflowExecution(props.match.params.key, "");
getAvailableWorkflows(-1);
getSettings();
@@ -19020,7 +19138,7 @@ const releaseToConnectLabel = "Release to Connect"
onClick={(e) => {
e.preventDefault()
e.stopPropagation()
window.open(`/admin?admin_tab=priorities&workflow=${data.workflow.id}&execution_id=${data.execution_id}`, "_blank")
window.open(`/admin?admin_tab=notifications&workflow=${data.workflow.id}&execution_id=${data.execution_id}`, "_blank")
}}
/>
</Tooltip>
@@ -19075,7 +19193,7 @@ const releaseToConnectLabel = "Release to Connect"
)}
</div>
) : (
<div style={{ padding: isMobile ? "0px 10px 25px 10px" : "25px 15px 25px 15px", maxWidth: isMobile ? "100%" : "100%", overflowX: "hidden" }}>
<div style={{ padding: isMobile ? "0px 10px 50px 10px" : "25px 15px 150px 15px", maxWidth: isMobile ? "100%" : "100%", overflowX: "hidden", }}>
<Breadcrumbs
@@ -19129,7 +19247,7 @@ const releaseToConnectLabel = "Release to Connect"
marginBottom: 10,
}}
/>
<div style={{ display: "flex", marginLeft: 10, }}>
<div style={{ display: "flex", paddingLeft: 10, paddingRight: 10, position: "sticky", top: 0, zIndex: 12500, backgroundColor: theme.palette.platformColor, borderRadius: theme.palette.borderRadius, border: "2px solid rgba(255,255,255,0.3)", marginBottom: 10, }}>
<h2>Details</h2>
<Tooltip
color="primary"
@@ -19269,7 +19387,7 @@ const releaseToConnectLabel = "Release to Connect"
onClick={(e) => {
e.preventDefault()
e.stopPropagation()
window.open(`/admin?admin_tab=priorities&workflow=${executionData.workflow.id}&execution_id=${executionData.execution_id}`, "_blank")
window.open(`/admin?admin_tab=notifications&workflow=${executionData.workflow.id}&execution_id=${executionData.execution_id}`, "_blank")
}}
/>
</Button>
@@ -20190,7 +20308,7 @@ const releaseToConnectLabel = "Release to Connect"
}
if (stringjson.includes("kms/")) {
return "KMS authentication most likely failed. Check your notifications for more details on this page: /admin?admin_tab=priorities. If you need help with KMS, please contact support@shuffler.io"
return "KMS authentication most likely failed. Check your notifications for more details on this page: /admin?admin_tab=notifications. If you need help with KMS, please contact support@shuffler.io"
}
if (stringjson.includes("invalidurl")) {
@@ -20208,7 +20326,7 @@ const releaseToConnectLabel = "Release to Connect"
}
if (isCloud && stringjson.toLowerCase().includes("timeout error")) {
return "Run this workflow in a local environment to increase the timeout. Go to https://shuffler.io/admin?tab=locationsto create an environment to connect to"
return "Run this workflow in a local environment to increase the timeout. Go to https://shuffler.io/admin?tab=locations to create an environment to connect to"
}
if (stringjson.toLowerCase().includes("invalid header")) {
@@ -20218,7 +20336,7 @@ const releaseToConnectLabel = "Release to Connect"
if (stringjson.includes("connectionerror")) {
if (stringjson.includes("kms")) {
return "KMS authentication most likely failed (2). Check your notifications for more details on this page: /admin?admin_tab=priorities&kms=true. If you need help with KMS, please contact support@shuffler.io"
return "KMS authentication most likely failed (2). Check your notifications for more details on this page: /admin?admin_tab=notifications&kms=true. If you need help with KMS, please contact support@shuffler.io"
}
return "The URL is incorrect, or Shuffle can't reach it. Set up a Shuffle Environment in the same VLAN, or whitelist Shuffle's IPs."
@@ -20696,6 +20814,7 @@ const releaseToConnectLabel = "Release to Connect"
setExpansionModalOpen={setCodeEditorModalOpen}
setEditorData={setEditorData}
setAiQueryModalOpen={setAiQueryModalOpen}
fixExample={fixExample}
/>
</div>
</Fade>
@@ -22583,6 +22702,8 @@ const releaseToConnectLabel = "Release to Connect"
changeActionParameterCodeMirror={changeActionParameterCodeMirror}
activeDialog={activeDialog}
setActiveDialog={setActiveDialog}
environment={selectedActionEnvironment}
setAiQueryModalOpen={setAiQueryModalOpen}
/>
+16 -7
View File
@@ -101,8 +101,8 @@ const ApiExplorerWrapper = (props) => {
}, [selectedAppData, openapi])
useEffect(() => {
getAppData(appid)
if (appid !== undefined && appid !== null && appid.length !== 0) {
getAppData(appid)
HandleGetLocations()
}
@@ -157,13 +157,17 @@ const ApiExplorerWrapper = (props) => {
}
if (!found) {
toast.error("Failed to get app data or App doesn't exist (1). Redirecting..");
toast.error(`Failed to get API data for '${appname}' (1). Contact support@shuffler.io if this persists.`, {
"autoClose": 10000,
})
setTimeout(()=>{
navigate("/search?tab=apps");
},3000)
}
} else {
toast.error("Failed to get app data or App doesn't exist (2). Redirecting..");
toast.error(`Failed to get API data for '${appname}' (2). Contact support@shuffler.io if this persists.`, {
"autoClose": 10000,
})
setTimeout(()=>{
navigate("/search?tab=apps");
},3000)
@@ -177,6 +181,11 @@ const ApiExplorerWrapper = (props) => {
// Fetch data when appid is available
const getAppData = useCallback((appid) => {
if (appid === undefined || appid === null || appid.length === 0) {
toast.error("No API data to load (4). Please contact support@shuffler.io if this persists.")
setTimeout(() => {
navigate("/search?tab=apps")
}, 3000)
return
}
@@ -197,9 +206,9 @@ const ApiExplorerWrapper = (props) => {
.then((response) => {
if (response.status !== 200) {
toast.error("Failed to get app data or App doesn't exist (3). Redirecting..");
setTimeout(()=>{
navigate("/search?tab=apps");
},3000)
setTimeout(() => {
navigate("/search?tab=apps")
}, 3000)
return;
}
return response.json();
@@ -1790,7 +1799,7 @@ const Wrapper = ({children, isLoaded,isLoggedIn})=>{
return(
<div className="api-explorer-wrapper" style={{ paddingLeft: (isLoggedIn && isLoaded) ? leftSideBarOpenByClick ? 280 : 100 : 0, transition: 'padding-left 0.3s ease' }}>
<div className="api-explorer-wrapper" style={{ zoom: 0.8, paddingLeft: (isLoggedIn && isLoaded) ? leftSideBarOpenByClick ? 280 : 100 : 0, transition: 'padding-left 0.3s ease' }}>
{children}
</div>
)
+13 -94
View File
@@ -383,6 +383,7 @@ const Docs = (defaultprops) => {
if (hash.includes('?')) {
hash = hash.split('?')[0]
}
if (hash) {
const element = document.getElementById(hash.toLowerCase())
if (element) {
@@ -526,7 +527,7 @@ const Docs = (defaultprops) => {
backgroundColor: theme.palette.inputColor,
padding: 15,
borderRadius: theme.palette?.borderRadius,
marginBottom: 30,
marginBottom: 25,
display: "flex",
}}
>
@@ -618,7 +619,8 @@ const Docs = (defaultprops) => {
<Divider
style={{
width: "90%",
marginTop: 40,
marginTop: 60,
marginBottom: 20,
backgroundColor: theme.palette.inputColor,
}}
/>
@@ -664,6 +666,8 @@ const Docs = (defaultprops) => {
minHeight: "93vh",
maxHeight: "93vh",
marginTop: 70,
maxWidth: 250,
overflow: "hidden",
}
const fetchDocList = () => {
@@ -705,8 +709,7 @@ const Docs = (defaultprops) => {
// Find <img> tags and translate them into ![]() format
const imgRegex = /<img.*?src="(.*?)"/g;
const tocRegex = /^## Table of contents[\s\S]*?(?=^## )|^## Table of contents[\s\S]*$/gm;
const newdata = responseJson.reason.replace(imgRegex, '![]($1)')
.replace(tocRegex, "");
const newdata = responseJson.reason.replace(imgRegex, '![]($1)').replace(tocRegex, "");
setData(newdata);
if (docId === undefined) {
document.title = "Shuffle documentation introduction";
@@ -782,88 +785,6 @@ const Docs = (defaultprops) => {
fetchDocs(props.match.params.key);
}
// const parseElementScroll = () => {
// const offset = 45;
// var parent = document.getElementById("markdown_wrapper_outer");
// if (parent !== null) {
// //console.log("IN PARENT")
// var elements = parent.getElementsByTagName("h2");
//
// const name = window.location.hash
// .slice(1, window.location.hash.length)
// .toLowerCase()
// .split("%20")
// .join(" ")
// .split("_")
// .join(" ")
// .split("-")
// .join(" ")
// .split("?")[0]
//
// //console.log(name)
// var found = false;
// for (var key in elements) {
// const element = elements[key];
// if (element.innerHTML === undefined) {
// continue;
// }
//
// // Fix location..
// if (element.innerHTML.toLowerCase() === name) {
// //console.log(element.offsetTop)
// element.scrollIntoView({ behavior: "smooth" });
// //element.scrollTo({
// // top: element.offsetTop+offset,
// // behavior: "smooth"
// //})
// found = true;
// //element.scrollTo({
// // top: element.offsetTop-100,
// // behavior: "smooth"
// //})
// }
// }
//
// // H#
// if (!found) {
// elements = parent.getElementsByTagName("h3");
// //console.log("NAMe: ", name)
// found = false;
// for (key in elements) {
// const element = elements[key];
// if (element.innerHTML === undefined) {
// continue;
// }
//
// // Fix location..
// if (element.innerHTML.toLowerCase() === name) {
// element.scrollIntoView({ behavior: "smooth" });
// //element.scrollTo({
// // top: element.offsetTop-offset,
// // behavior: "smooth"
// //})
// found = true;
// //element.scrollTo({
// // top: element.offsetTop-100,
// // behavior: "smooth"
// //})
// }
// }
// }
// }
// //console.log(element)
//
// //console.log("NAME: ", name)
// //console.log(document.body.innerHTML)
// // parent = document.getElementById(parent);
//
// //var descendants = parent.getElementsByTagName(tagname);
//
// // this.scrollDiv.current.scrollIntoView({ behavior: 'smooth' });
//
// //$(".parent").find("h2:contains('Statistics')").parent();
// };
const markdownStyle = {
color: "rgba(255, 255, 255, 0.90)",
overflow: "hidden",
@@ -872,7 +793,7 @@ const Docs = (defaultprops) => {
maxWidth: "100%",
minWidth: "100%",
overflow: "hidden",
fontSize: isMobile ? "1.3rem" : "1.1rem",
fontSize: isMobile ? "1.3rem" : "0.8rem",
};
const alertNote = {
@@ -1084,25 +1005,23 @@ const Docs = (defaultprops) => {
<div style={IndexBar}>
{tocLines.length > 0 ?
(
<h4 style={{ fontWeight: 600, margin: 0, fontSize: "16px", marginBottom: "8px" }}>Table Of Content</h4>
<h4 style={{ fontWeight: 600, margin: 0, fontSize: "16px", marginBottom: "8px" }}>Table of Content</h4>
) : null}
<nav>
{tocLines.map((data, index) => {
return (
<div className="toc">
<div className="toc"
>
<ListItemButton
key={data.text}
href={`#${data.id}`}
style={{
color: activeId === data.id ? "#f86a3e" : "inherit",
textDecoration: "none",
fontSize: "14px",
fontWeight: 400,
padding: "4px 0",
paddingLeft: "8px",
paddingRight: "8px",
padding: "4px 8px 4px 8px",
lineHeight: "20px",
color: activeId === data.id ? "#f86a3e" : "rgba(255,255,255,0.6)",
}}
onClick={(e) => {
handleCollapse(index)
-10
View File
@@ -80,25 +80,15 @@ const useStyles = makeStyles((theme) => ({
datagrid: {
border: 0,
"& .MuiDataGrid-columnsContainer": {
backgroundColor:
theme.palette.type === "light" ? "#fafafa" : theme.palette.inputColor,
},
"& .MuiDataGrid-iconSeparator": {
display: "none",
},
"& .MuiDataGrid-colCell, .MuiDataGrid-cell": {
borderRight: `1px solid ${
theme.palette.type === "light" ? "white" : "#303030"
}`,
},
"& .MuiDataGrid-columnsContainer, .MuiDataGrid-cell": {
borderBottom: `1px solid ${
theme.palette.type === "light" ? "#f0f0f0" : "#303030"
}`,
},
"& .MuiDataGrid-cell": {
color:
theme.palette.type === "light" ? "white" : "rgba(255,255,255,0.65)",
},
"& .MuiPaginationItem-root, .MuiTablePagination-actions, .MuiTablePagination-caption":
{