MSSP multi tenant workflow features ayy

This commit is contained in:
Frikky
2024-06-17 01:39:52 +02:00
parent fe43b683b9
commit 3f0ed868cc
14 changed files with 1064 additions and 364 deletions
+420 -234
View File
@@ -44,6 +44,7 @@ import {
Box,
InputAdornment,
Autocomplete,
Modal
} from "@mui/material";
import {
@@ -164,7 +165,7 @@ const Admin = (props) => {
const [selectedOrganization, setSelectedOrganization] = React.useState({});
//console.log("Selected: ", selectedOrganization)
const [appAuthenticationGroupModalOpen , setAppAuthenticationGroupModalOpen] = React.useState(false);
const [appAuthenticationGroupModalOpen, setAppAuthenticationGroupModalOpen] = React.useState(false);
const [appsForAppAuthGroup, setAppsForAppAuthGroup] = React.useState([]);
const [appAuthenticationGroupId, setAppAuthenticationGroupId] = React.useState("");
const [appAuthenticationGroupName, setAppAuthenticationGroupName] = React.useState("");
@@ -212,6 +213,13 @@ const Admin = (props) => {
const [allSchedules, setAllSchedules] = React.useState([]);
const [pipelines, setPipelines] = React.useState([]);
const [, forceUpdate] = React.useState();
const [MFARequired, setMFARequired] = React.useState(selectedOrganization.mfa_required === undefined ? false : selectedOrganization.mfa_required);
useEffect(() => {
if (selectedOrganization.mfa_required !== undefined) {
setMFARequired(selectedOrganization.mfa_required);
}
}, [selectedOrganization.mfa_required]);
const [showDeleteAccountTextbox, setShowDeleteAccountTextbox] =
React.useState(false);
@@ -1097,6 +1105,7 @@ If you're interested, please let me know a time that works for you, or set up a
code: code,
user_id: userId,
};
toast("Verifying 2fa code. Please wait...");
fetch(`${globalUrl}/api/v1/users/${userId}/set2fa`, {
mode: "cors",
@@ -1120,11 +1129,15 @@ If you're interested, please let me know a time that works for you, or set up a
})
.then((responseJson) => {
if (responseJson.success === true) {
toast("Successfully enabled 2fa");
if (responseJson.MFAActive === true) {
toast.success("Successfully enabled 2fa");
}
if (responseJson.MFAActive === false) {
toast.success("Successfully disabled 2fa");
}
setTimeout(() => {
getUsers();
setImage2FA("");
setValue2FA("");
setSecret2FA("");
@@ -2922,8 +2935,8 @@ If you're interested, please let me know a time that works for you, or set up a
color="primary"
>
{selectedUser.mfa_info !== undefined &&
selectedUser.mfa_info !== null &&
selectedUser.mfa_info.active === true
selectedUser.mfa_info !== null &&
selectedUser.mfa_info.active === true
? "Disable 2FA"
: "Enable 2FA"}
</Button>
@@ -3009,15 +3022,16 @@ If you're interested, please let me know a time that works for you, or set up a
maxWidth: 300,
minWidth: 300,
marginTop: 25,
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
}}
>
{/*<Divider style={{marginTop: 20, marginBottom: 20}} />*/}
{secret2FA !== undefined &&
secret2FA !== null &&
secret2FA.length > 0 ? (
secret2FA !== null &&
secret2FA.length > 0 ? (
<span>
<Typography variant="body2" color="textSecondary">
<Typography variant="body2" color="textSecondary" style={{ textAlign: 'left' }}>
Scan the image below with the two-factor authentication app on
your phone. If you cant use a QR code, use the code{" "}
{secret2FA} instead.
@@ -3025,18 +3039,16 @@ If you're interested, please let me know a time that works for you, or set up a
</span>
) : null}
{image2FA !== undefined &&
image2FA !== null &&
image2FA.length > 0 ? (
image2FA !== null &&
image2FA.length > 0 ? (
<img
alt={"2 factor img"}
alt="2 factor img"
src={image2FA}
style={{
margin: "auto",
marginTop: 25,
margin: "15px auto",
maxHeight: 200,
maxWidth: 200,
minWidth: 200,
maxWidth: 200,
display: 'block',
}}
/>
) : (
@@ -3047,7 +3059,7 @@ If you're interested, please let me know a time that works for you, or set up a
After scanning the QR code image, the app will display a code that
you can enter below.
</Typography>
<div style={{ display: "flex" }}>
<div style={{ display: "flex", width: '100%', marginTop: 10 }}>
<TextField
color="primary"
style={{
@@ -3076,13 +3088,18 @@ If you're interested, please let me know a time that works for you, or set up a
setValue2FA(event.target.value);
}}
onKeyPress={(event) => {
if (event.key === 'Enter' && event.target.value.length === 6) {
handleVerify2FA(userdata.id, event.target.value, false);
}
}}
/>
<Button
disabled={value2FA.length !== 6}
variant="contained"
style={{ marginTop: 15, height: 50, flex: 1 }}
onClick={() => {
handleVerify2FA(userdata.id, value2FA);
handleVerify2FA(userdata.id, value2FA, false);
}}
color="primary"
>
@@ -4225,40 +4242,101 @@ If you're interested, please let me know a time that works for you, or set up a
</Dialog>
);
const UpdateMFAInUserOrg = (org_id) => {
if (MFARequired === false) {
toast("Making MFA required for your organization. Please wait...");
} else {
toast("Making MFA optional for your organization. Please wait...");
}
const data = {
mfa_required: !selectedOrganization.mfa_required,
org_id: selectedOrganization.id,
}
const url = globalUrl + `/api/v1/orgs/${selectedOrganization.id}`;
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) => {
console.log(responseJson)
if (responseJson["success"] === false) {
toast.error("Failed updating org: ", responseJson.reason);
} else {
if (MFARequired === false) {
setMFARequired(true)
toast.success("Successfully make MFA required for your organization!");
} else {
setMFARequired(false)
toast.success("Successfully make MFA optional for your organization!")
}
}
}),
)
.catch((error) => {
toast("Err: " + error.toString());
});
}
const usersView =
curTab === 1 ? (
<div>
<div style={{ marginTop: 20, marginBottom: 20 }}>
<h2 style={{ display: "inline" }}>User Management</h2>
<span style={{ marginLeft: 25 }}>
Add, edit, block or change passwords.{" "}
<a
target="_blank"
rel="noopener noreferrer"
href="/docs/organizations#user_management"
style={{ textDecoration: "none", color: "#f85a3e" }}
>
Learn more
</a>
</span>
<div style={{ display: 'flex', flexDirection: 'row', justifyContent: 'space-between', marginTop: 20, marginBottom: 20 }}>
<div style={{ display: 'flex', flexDirection: 'column', }}>
<div>
<h2 style={{ display: "inline" }}>User Management</h2>
<span style={{ marginLeft: 25 }}>
Add, edit, block or change passwords.{" "}
<a
target="_blank"
rel="noopener noreferrer"
href="/docs/organizations#user_management"
style={{ textDecoration: "none", color: "#f85a3e" }}
>
Learn more
</a>
</span>
</div>
<div style={{ display: 'flex', flexDirection: 'row', marginTop: 20, }}>
<Button
style={{}}
variant="contained"
color="primary"
onClick={() => setModalOpen(true)}
>
Add user
</Button>
<Button
style={{ marginLeft: 5, marginRight: 15 }}
variant="contained"
color="primary"
onClick={() => getUsers()}
>
<CachedIcon />
</Button>
</div>
</div>
<div />
<div style={{ marginleft: 20, maxWidth: 500 }}>
<Typography variant="body1">MFA Required</Typography>
<Switch
checked={MFARequired}
onChange={(event) => {
UpdateMFAInUserOrg(selectedOrganization.id);
}}
/>
</div>
</div>
<div />
<Button
style={{}}
variant="contained"
color="primary"
onClick={() => setModalOpen(true)}
>
Add user
</Button>
<Button
style={{ marginLeft: 5, marginRight: 15 }}
variant="contained"
color="primary"
onClick={() => getUsers()}
>
<CachedIcon />
</Button>
<Divider
style={{
marginTop: 20,
@@ -4714,6 +4792,12 @@ If you're interested, please let me know a time that works for you, or set up a
) : null;
const run2FASetup = (data) => {
if (MFARequired === true && (selectedUser.mfa_info && selectedUser.mfa_info.active === true)) {
toast("MFA is required for your organization. You can't disable it.");
return;
}
if (!show2faSetup) {
get2faCode(data.id);
} else {
@@ -5247,6 +5331,282 @@ If you're interested, please let me know a time that works for you, or set up a
setAppsForAppAuthGroup(newappauth.concat(data.id))
}
const KMSItem = (props) => {
const {
data,
index,
} = props
const [showEnvironmentDropdown, setShowEnvironmentDropdown] = React.useState(false)
var bgColor = "#27292d";
if (index % 2 === 0) {
bgColor = "#1f2023";
}
const isDistributed =
data.suborg_distributed === true ? true : false;
const isKms = data.label !== undefined && data.label !== null && data.label.toLowerCase() === "kms shuffle storage"
var selectedEnvironment = ""
if (data.environment !== undefined && data.environment !== null && data.environment.length > 0) {
selectedEnvironment = data.environment
}
if (selectedEnvironment === "" && environments !== undefined && environments !== null && environments.length > 0) {
for (var i = 0; i < environments.length; i++) {
if (environments[i].default === true) {
selectedEnvironment = environments[i].Name
break
}
}
}
console.log("AUTH: ", data)
return (
<ListItem key={index} style={{ backgroundColor: bgColor }}>
<ListItemText
primary=<img
alt=""
src={data.app.large_image}
style={{
maxWidth: 50,
borderRadius: theme.palette.borderRadius,
}}
/>
style={{ minWidth: 75, maxWidth: 75 }}
/>
<ListItemText
primary={!isKms ? data.label :
<div style={{display: "flex", flexDirection: "column", maxWidth: 200, }}>
<Chip
label={"KMS Shuffle Storage"}
variant="contained"
color="secondary"
style={{cursor: "pointer"}}
onClick={() => {
setShowEnvironmentDropdown(true)
if (environments === undefined || environments === null || environments.length === 0) {
toast.error("No environments found. Please try again in a second, or reload to configure environment to use for KMS")
}
}}
/>
{showEnvironmentDropdown === true && environments !== undefined && environments !== null && environments.length > 0 ?
<FormControl fullWidth sx={{ m: 1 }}>
<InputLabel id="envselect" style={{ padding: 5 }}>
Environment
</InputLabel>
<Select
labelId="envselect"
defaultValue={selectedEnvironment}
onChange={(e) => {
if (e.target.value === "") {
return
}
if (e.target.value === selectedEnvironment) {
return
}
toast.info("Updating environment KMS runs on to " + e.target.value)
data.environment = e.target.value
const envIndex = environments.findIndex((env) => env.Name === e.target.value)
if (envIndex === -1) {
toast.error("Environment not found")
return
}
environments[envIndex].environment = e.target.value
setEnvironments(environments)
setShowEnvironmentDropdown(false)
saveAuthentication(data)
}}
>
{environments.map((env, index) => {
if (env.archived === true) {
return null
}
return (
<MenuItem key={index} value={env.Name}>
{env.default === true ? "Default - " : ""}{env.Name}
</MenuItem>
)
})}
</Select>
</FormControl>
: null}
</div>
}
style={{
minWidth: 225,
maxWidth: 225,
overflow: "hidden",
}}
/>
<ListItemText
primary={data.app.name.replaceAll("_", " ")}
style={{ minWidth: 175, maxWidth: 175, marginLeft: 10 }}
/>
{/*
<ListItemText
primary={data.defined === false ? "No" : "Yes"}
style={{ minWidth: 100, maxWidth: 100, }}
/>
*/}
<ListItemText
primary={
data.workflow_count === null ? 0 : data.workflow_count
}
style={{
minWidth: 100,
maxWidth: 100,
textAlign: "center",
overflow: "hidden",
}}
/>
{/*
<ListItemText
primary={data.node_count}
style={{
minWidth: 110,
maxWidth: 110,
textAlign: "center",
overflow: "hidden",
}}
/>
*/}
<ListItemText
primary={
data.fields === null || data.fields === undefined
? ""
: data.fields
.map((data) => {
return data.key;
})
.join(", ")
}
style={{
minWidth: 125,
maxWidth: 125,
overflow: "auto",
marginRight: 10,
}}
/>
<ListItemText
style={{
maxWidth: 230,
minWidth: 230,
overflow: "hidden",
}}
primary={new Date(data.edited * 1000).toISOString()}
/>
<ListItemText>
<IconButton
onClick={() => {
updateAppAuthentication(data);
}}
disabled={
data.org_id !== selectedOrganization.id ? true : false
}
>
<EditIcon color="secondary" />
</IconButton>
{data.defined ? (
<Tooltip
color="primary"
title="Set for EVERY instance of this App being used in this organization"
placement="top"
>
<IconButton
style={{ marginRight: 10 }}
disabled={
data.defined === false ||
data.org_id !== selectedOrganization.id
? true
: false
}
onClick={() => {
editAuthenticationConfig(data.id);
}}
>
<SelectAllIcon color={"secondary"} />
</IconButton>
</Tooltip>
) : (
<Tooltip
color="primary"
title="Must edit before you can set in all workflows"
placement="top"
>
<IconButton
style={{}}
onClick={() => {}}
disabled={
data.org_id !== selectedOrganization.id
? true
: false
}
>
<SelectAllIcon color="secondary" />
</IconButton>
</Tooltip>
)}
<IconButton
style={{ marginLeft: 0 }}
disabled={
data.org_id !== selectedOrganization.id ? true : false
}
onClick={() => {
deleteAuthentication(data);
}}
>
<DeleteIcon color="secondary" />
</IconButton>
</ListItemText>
<ListItemText>
{selectedOrganization.id !== undefined &&
data.org_id !== selectedOrganization.id ? (
<Tooltip
title="Parent organization controlled auth. You can use, but not modify this auth. Contact an admin of your parent organization if you need changes to this."
placement="top"
>
<Chip
label={"Parent"}
variant="contained"
color="secondary"
/>
</Tooltip>
) : (
<Tooltip
title="Distributed to sub-organizations. This means the sub organizations can use this authentication, but not modify it."
placement="top"
>
<Checkbox
disabled={
selectedOrganization.creator_org !== undefined &&
selectedOrganization.creator_org !== null &&
selectedOrganization.creator_org !== ""
? true
: false
}
checked={isDistributed}
color="secondary"
onClick={() => {
changeDistribution(data, !isDistributed);
}}
/>
</Tooltip>
)}
</ListItemText>
</ListItem>
)
}
const authenticationView =
curTab === 2 ? (
@@ -5361,6 +5721,10 @@ If you're interested, please let me know a time that works for you, or set up a
<div>
{authentication.map((data, index) => {
var checked = data.checked
if (data.label !== undefined && data.label !== null && data.label.toLowerCase() === "kms shuffle storage") {
return null
}
if (checked === undefined || checked === null) {
checked = false
}
@@ -5476,11 +5840,6 @@ If you're interested, please let me know a time that works for you, or set up a
{authentication === undefined || authentication === null
? null
: authentication.map((data, index) => {
var bgColor = "#27292d";
if (index % 2 === 0) {
bgColor = "#1f2023";
}
//console.log("Auth data: ", data)
if (data.type === "oauth2") {
data.fields = [
@@ -5503,187 +5862,14 @@ If you're interested, please let me know a time that works for you, or set up a
];
}
const isDistributed =
data.suborg_distributed === true ? true : false;
return (
<ListItem key={index} style={{ backgroundColor: bgColor }}>
<ListItemText
primary=<img
alt=""
src={data.app.large_image}
style={{
maxWidth: 50,
borderRadius: theme.palette.borderRadius,
}}
/>
style={{ minWidth: 75, maxWidth: 75 }}
/>
<ListItemText
primary={data.label}
style={{
minWidth: 225,
maxWidth: 225,
overflow: "hidden",
}}
/>
<ListItemText
primary={data.app.name.replaceAll("_", " ")}
style={{ minWidth: 175, maxWidth: 175, marginLeft: 10 }}
/>
{/*
<ListItemText
primary={data.defined === false ? "No" : "Yes"}
style={{ minWidth: 100, maxWidth: 100, }}
/>
*/}
<ListItemText
primary={
data.workflow_count === null ? 0 : data.workflow_count
}
style={{
minWidth: 100,
maxWidth: 100,
textAlign: "center",
overflow: "hidden",
}}
/>
{/*
<ListItemText
primary={data.node_count}
style={{
minWidth: 110,
maxWidth: 110,
textAlign: "center",
overflow: "hidden",
}}
/>
*/}
<ListItemText
primary={
data.fields === null || data.fields === undefined
? ""
: data.fields
.map((data) => {
return data.key;
})
.join(", ")
}
style={{
minWidth: 125,
maxWidth: 125,
overflow: "auto",
marginRight: 10,
}}
/>
<ListItemText
style={{
maxWidth: 230,
minWidth: 230,
overflow: "hidden",
}}
primary={new Date(data.edited * 1000).toISOString()}
/>
<ListItemText>
<IconButton
onClick={() => {
updateAppAuthentication(data);
}}
disabled={
data.org_id !== selectedOrganization.id ? true : false
}
>
<EditIcon color="secondary" />
</IconButton>
{data.defined ? (
<Tooltip
color="primary"
title="Set for EVERY instance of this App being used in this organization"
placement="top"
>
<IconButton
style={{ marginRight: 10 }}
disabled={
data.defined === false ||
data.org_id !== selectedOrganization.id
? true
: false
}
onClick={() => {
editAuthenticationConfig(data.id);
}}
>
<SelectAllIcon color={"secondary"} />
</IconButton>
</Tooltip>
) : (
<Tooltip
color="primary"
title="Must edit before you can set in all workflows"
placement="top"
>
<IconButton
style={{}}
onClick={() => {}}
disabled={
data.org_id !== selectedOrganization.id
? true
: false
}
>
<SelectAllIcon color="secondary" />
</IconButton>
</Tooltip>
)}
<IconButton
style={{ marginLeft: 0 }}
disabled={
data.org_id !== selectedOrganization.id ? true : false
}
onClick={() => {
deleteAuthentication(data);
}}
>
<DeleteIcon color="secondary" />
</IconButton>
</ListItemText>
<ListItemText>
{selectedOrganization.id !== undefined &&
data.org_id !== selectedOrganization.id ? (
<Tooltip
title="Parent organization controlled auth. You can use, but not modify this auth. Contact an admin of your parent organization if you need changes to this."
placement="top"
>
<Chip
label={"Parent"}
variant="contained"
color="secondary"
/>
</Tooltip>
) : (
<Tooltip
title="Distributed to sub-organizations. This means the sub organizations can use this authentication, but not modify it."
placement="top"
>
<Checkbox
disabled={
selectedOrganization.creator_org !== undefined &&
selectedOrganization.creator_org !== null &&
selectedOrganization.creator_org !== ""
? true
: false
}
checked={isDistributed}
color="secondary"
onClick={() => {
changeDistribution(data, !isDistributed);
}}
/>
</Tooltip>
)}
</ListItemText>
</ListItem>
);
<KMSItem
data={data}
index={index}
/>
)
})}
</List>
</div>
+388 -42
View File
@@ -88,6 +88,7 @@ import {
Error as ErrorIcon,
Warning as WarningIcon,
ArrowLeft as ArrowLeftIcon,
ArrowRight as ArrowRightIcon,
Cached as CachedIcon,
DirectionsRun as DirectionsRunIcon,
FormatListNumbered as FormatListNumberedIcon,
@@ -123,6 +124,8 @@ import {
Add as AddIcon,
ErrorOutline as ErrorOutlineIcon,
ArrowForward as ArrowForwardIcon,
} from "@mui/icons-material";
//import * as cytoscape from "cytoscape";
@@ -378,7 +381,7 @@ const svgSize = 24;
const searchClient = algoliasearch("JNSS5CFDZZ", "db08e40265e2941b9a7d8f644b6e5240")
const AngularWorkflow = (defaultprops) => {
const { globalUrl, setCookie, isLoggedIn, isLoaded, userdata, data_id } = defaultprops;
const { globalUrl, setCookie, isLoggedIn, isLoaded, userdata, data_id, ReactGA, } = defaultprops;
const referenceUrl = globalUrl + "/api/v1/hooks/";
//const alert = useAlert()
let navigate = useNavigate();
@@ -499,6 +502,7 @@ const AngularWorkflow = (defaultprops) => {
const [selectedApp, setSelectedApp] = React.useState({});
const [selectedAction, setSelectedAction] = React.useState({});
const [selectedActionEnvironment, setSelectedActionEnvironment] = React.useState({});
const [selectedMeta, setSelectedMeta] = React.useState(undefined);
// Disabled streaming for now
const [streamDisabled, setStreamDisabled] = React.useState(true)
@@ -950,6 +954,10 @@ const AngularWorkflow = (defaultprops) => {
})
.then((responseJson) => {
if (responseJson.success === true) {
if (responseJson.meta !== undefined && responseJson.meta !== null && Object.getOwnPropertyNames(responseJson.meta).length > 0) {
setSelectedMeta(responseJson.meta)
}
if (responseJson.reason !== undefined && responseJson.reason !== undefined && responseJson.reason.length > 0) {
if (!responseJson.reason.includes("404: Not Found") && responseJson.reason.length > 25) {
// Translate <img> into markdown ![]()
@@ -8018,10 +8026,10 @@ const AngularWorkflow = (defaultprops) => {
backgroundColor: theme.palette.surfaceColor,
cursor: "pointer",
display: "flex",
};
}
const VariableItem = (props) => {
const { variable, index, type } = props;
const VariableItem = (props) => {
const { variable, index, type } = props;
const [open, setOpen] = React.useState(false);
const [anchorEl, setAnchorEl] = React.useState(null);
@@ -8792,7 +8800,25 @@ const AngularWorkflow = (defaultprops) => {
? "cloud"
: environments[defaultEnvironmentIndex] === undefined
? "cloud"
: environments[defaultEnvironmentIndex].Name;
: environments[defaultEnvironmentIndex].Name
// Basic automatic auth mapping
var authId = ""
if (appAuthentication !== undefined && appAuthentication !== null && appAuthentication.length > 0) {
const appname = app.name.toLowerCase().replace(" ", "_")
for (var key in appAuthentication) {
const authKey = appAuthentication[key]
if (authKey.app.id === app.id) {
authId = authKey.id
break
}
const appauthname = authKey.app.name.toLowerCase().replace(" ", "_")
if (appauthname === appname) {
authId = authKey.id
}
}
}
// List other nodes in the workflow and see if they have an environment set. If they do, use that as the default
if (cy !== undefined && cy !== null) {
@@ -8840,7 +8866,7 @@ const AngularWorkflow = (defaultprops) => {
app.categories.length > 0
? app.categories[0]
: "",
authentication_id: "",
authentication_id: authId,
finished: false,
template: app.template === true ? true : false,
};
@@ -9722,30 +9748,49 @@ const AngularWorkflow = (defaultprops) => {
}
}
setSelectedAction(newSelectedAction);
setUpdate(Math.random());
// Last fix for params
if (newSelectedAction.parameters !== undefined && newSelectedAction.parameters !== null && newSelectedAction.parameters.length > 0) {
for (let paramkey in newSelectedAction.parameters) {
const param = newSelectedAction.parameters[paramkey]
if (param.name !== "body") {
continue
}
const allNodes = cy.nodes().jsons();
if (allNodes !== undefined && allNodes !== null) {
for (let nodekey in allNodes) {
const currentNode = allNodes[nodekey];
if (
currentNode.data.attachedTo === oldaction.id &&
currentNode.data.isDescriptor
) {
const foundnode = cy.getElementById(currentNode.data.id);
if (foundnode !== null && foundnode !== undefined) {
const iconInfo = GetIconInfo(newaction);
const svg_pin = `<svg width="${svgSize}" height="${svgSize}" viewBox="0 0 ${svgSize} ${svgSize}" version="1.1" xmlns="http://www.w3.org/2000/svg"><path d="${iconInfo.icon}" fill="${iconInfo.iconColor}"></path></svg>`;
const svgpin_Url = encodeURI("data:image/svg+xml;utf-8," + svg_pin);
foundnode.data("image", svgpin_Url);
foundnode.data("imageColor", iconInfo.iconBackgroundColor);
}
break;
if (param.example !== undefined && param.example !== null && param.example.length > 0) {
if (param.value === undefined || param.value === null || param.value.length === 0) {
param.value = param.example
}
}
newSelectedAction.parameters[paramkey] = param
}
}
console.log("New selected action: ", newSelectedAction)
setSelectedAction(newSelectedAction)
setUpdate(Math.random())
const allNodes = cy.nodes().jsons()
if (allNodes !== undefined && allNodes !== null) {
for (let nodekey in allNodes) {
const currentNode = allNodes[nodekey];
if (
currentNode.data.attachedTo === oldaction.id &&
currentNode.data.isDescriptor
) {
const foundnode = cy.getElementById(currentNode.data.id);
if (foundnode !== null && foundnode !== undefined) {
const iconInfo = GetIconInfo(newaction);
const svg_pin = `<svg width="${svgSize}" height="${svgSize}" viewBox="0 0 ${svgSize} ${svgSize}" version="1.1" xmlns="http://www.w3.org/2000/svg"><path d="${iconInfo.icon}" fill="${iconInfo.iconColor}"></path></svg>`;
const svgpin_Url = encodeURI("data:image/svg+xml;utf-8," + svg_pin);
foundnode.data("image", svgpin_Url);
foundnode.data("imageColor", iconInfo.iconBackgroundColor);
}
break;
}
}
}
// Send it in here, after all fields are filled
// Disabled for now :(
@@ -17673,6 +17718,71 @@ const AngularWorkflow = (defaultprops) => {
)
}
const changeExecution = (data) => {
if ((data.result === undefined || data.result === null || data.result.length === 0) && data.status !== "FINISHED" && data.status !== "ABORTED") {
start()
setExecutionRunning(true)
setExecutionRequestStarted(false)
}
var checkStarted = false
if (data.results !== undefined && data.results !== null && data.results.length > 0) {
if (data.execution_argument !== undefined && data.execution_argument !== null && data.execution_argument.includes("too large")) {
setExecutionData({});
checkStarted = true
start();
setExecutionRunning(true);
setExecutionRequestStarted(false);
} else {
if (data.results !== undefined && data.results !== null) {
for (let resultkey in data.results) {
if (data.results[resultkey].status !== "SUCCESS") {
continue
}
if (data.results[resultkey].result.includes("too large")) {
setExecutionData({});
checkStarted = true
start();
setExecutionRunning(true);
setExecutionRequestStarted(false);
break
}
}
}
}
}
const cur_execution = {
execution_id: data.execution_id,
authorization: data.authorization,
}
setExecutionRequest(cur_execution)
setExecutionModalView(1)
if (!checkStarted) {
handleUpdateResults(data, cur_execution)
if (cy !== undefined && cy !== null) {
cy.elements().removeClass("success-highlight failure-highlight executing-highlight");
for (let actionKey in data.workflow.actions) {
var actionitem = data.workflow.actions[actionKey]
handleColoring(actionitem.id, "", actionitem.label)
}
for (let resultKey in data.results) {
var item = data.results[resultKey]
handleColoring(item.action.id, item.status, item.action.label)
}
}
setExecutionData(data)
}
}
const ShowCopyingTooltip = () => {
const [showCopying, setShowCopying] = React.useState(true)
@@ -18101,7 +18211,7 @@ const AngularWorkflow = (defaultprops) => {
<h2>Details</h2>
<Tooltip
color="primary"
title="Rerun workflow"
title="Rerun workflow (uses same startnode as the original)"
placement="top"
style={{ zIndex: 50000 }}
>
@@ -18147,7 +18257,80 @@ const AngularWorkflow = (defaultprops) => {
</span>
</Tooltip>
) : null}
{isCloud ? (
<Tooltip
color="primary"
title="Previous execution"
placement="top"
style={{ zIndex: 50000, }}
>
<span style={{}}>
<Button
color="primary"
style={{ float: "right", marginTop: 20, marginLeft: 10 }}
onClick={() => {
// Find current one in execution list
var nextindex = -1
const currentIndex = workflowExecutions.findIndex((item) => item.execution_id === executionData.execution_id)
if (currentIndex === -1) {
nextindex = workflowExecutions.length - 1
} else {
nextindex = currentIndex - 1
}
if (nextindex < 0) {
toast.warn("Use the workflow run debugger to dig deeper - nothing more to show here.")
return
}
const data = workflowExecutions[nextindex]
navigate(`?execution_id=${data.execution_id}`)
changeExecution(data)
}}
>
<ArrowBackIcon color="secondary" />
</Button>
</span>
</Tooltip>
<Tooltip
color="primary"
title="Next execution"
placement="top"
style={{ zIndex: 50000, }}
>
<span style={{}}>
<Button
color="primary"
style={{ float: "right", marginTop: 20, }}
onClick={() => {
// Find current one in execution list
var nextindex = -1
const currentIndex = workflowExecutions.findIndex((item) => item.execution_id === executionData.execution_id)
if (currentIndex === -1) {
nextindex = 0
} else {
nextindex = currentIndex + 1
}
if (nextindex >= workflowExecutions.length) {
toast.warn("Use the workflow run debugger to dig deeper - nothing more to show here.")
return
}
const data = workflowExecutions[nextindex]
navigate(`?execution_id=${data.execution_id}`)
changeExecution(data)
}}
>
<ArrowForwardIcon color="secondary" />
</Button>
</span>
</Tooltip>
{isCloud ?
<Tooltip
color="primary"
title="Explore logs for the workflow"
@@ -18171,7 +18354,8 @@ const AngularWorkflow = (defaultprops) => {
</Button>
</span>
</Tooltip>
) : null}
: null}
</div>
{executionData.workflow !== undefined && executionData.workflow !== null && executionData.workflow.actions !== undefined && executionData.workflow.actions !== null && executionData.workflow.actions.length > 0 ?
<div style={{ display: "flex", marginLeft: 10, }}>
@@ -18801,21 +18985,49 @@ const AngularWorkflow = (defaultprops) => {
validate.result = JSON.parse(validate.result)
}
const AppResultVariable = ({ data }) => {
const AppResultVariable = ({ data, action }) => {
const [open, setOpen] = React.useState(false)
const showVariable = data.value.length < 60
// Check if it's valid JSON
const checked = validateJson(data.value.trim())
if (data.name === "shuffle_action_logs" && data.value !== undefined && data.value !== null && data.value.length > 0 && data.value.includes("add env SHUFFLE_LOGS_DISABLED")) {
return (
<div style={{ maxWidth: 600, marginTop: 15, overflowX: "hidden", }}>
<Typography
variant="body1"
style={{}}
>
<b>Action Logs</b>
</Typography>
<Typography variant="body2" style={{ whiteSpace: 'pre-line', }}>
Logs for an action are not available without <a style={{color: "#f85a3e", }} href="/admin?tab=environments" target="_blank" rel="noopener noreferrer">an onprem environment</a> with the <a style={{color: "#f85a3e", }} href="/docs/configuration#scaling-shuffle" target="_blank" rel="noopener noreferrer">SHUFFLE_LOGS_DISABLED</a> environment variable set to false: SHUFFLE_LOGS_DISABLED=false. Logs are enabled by default, except in scale mode.
</Typography>
</div>
)
}
var showlink = false
if (data.name.endsWith("-Url")) {
//data.name = data.name.toLowerCase().replaceAll("-", "_")
if (data.value.startsWith(", ")) {
data.value = data.value.substring(2)
}
if (!data.value.startsWith("http") || (data.value.startsWith("/") && data.value.includes("?"))) {
showlink = true
}
}
return (
<div style={{ maxWidth: 600, overflowX: "hidden", }}>
{data.value.length > 60 || checked.valid ?
<IconButton
style={{
marginBottom: 0,
marginTop: 5,
cursor: "pointer",
marginTop: 5,
cursor: "pointer",
padding: 3,
border: "1px solid rgba(255,255,255,0.3)",
borderRadius: theme.palette.borderRadius,
@@ -18868,8 +19080,17 @@ const AngularWorkflow = (defaultprops) => {
variant="body2"
style={{
whiteSpace: 'pre-line',
color: showlink ? "#f85a3e" : "white",
cursor: showlink ? "pointer" : "default",
}}
color="textSecondary"
onClick={(e) => {
if (showlink) {
e.preventDefault()
e.stopPropagation()
window.open(data.value, "_blank")
}
}}
color={showlink ? "inherit" : "textSecondary"}
>
{data.value}
</Typography>
@@ -18981,6 +19202,10 @@ const AngularWorkflow = (defaultprops) => {
return "Consider whether your Orborus environment can connect to a local IP or not."
}
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"
}
if (stringjson.includes("invalidurl")) {
// IF count of "http" is more than one, 1, it's prolly invalid
var additionalinfo = ""
@@ -19006,7 +19231,7 @@ const AngularWorkflow = (defaultprops) => {
if (stringjson.includes("connectionerror")) {
if (stringjson.includes("kms")) {
return "KMS authentication failed. Check your notifications for more details."
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 "The URL is incorrect, or Shuffle can't reach it. Set up a Shuffle Environment in the same VLAN, or whitelist Shuffle's IPs."
@@ -19352,7 +19577,7 @@ const AngularWorkflow = (defaultprops) => {
}
return (
<AppResultVariable key={index} data={data} />
<AppResultVariable key={index} data={data} action={selectedResult.action} />
);
})}
</div>
@@ -20184,6 +20409,8 @@ const AngularWorkflow = (defaultprops) => {
//if (configureWorkflowModalOpen) {
// setSelectedAction({});
//}
//
setSelectedMeta(undefined)
}}
PaperProps={{
style: {
@@ -20315,11 +20542,27 @@ const AngularWorkflow = (defaultprops) => {
overflowY: "auto",
overflowX: "hidden",
}}
onLoad={() => {
/*
if (isCloud && ReactGA !== undefined) {
toast("Sending GA info")
// Google analytics info about what app people are looking at
ReactGA.event({
category: "workflow",
action: `documentation_load`,
label: selectedApp.name,
})
}
*/
}}
>
{selectedApp.documentation === undefined ||
selectedApp.documentation === null ||
selectedApp.documentation.length === 0 ? (
<span style={{ textAlign: "center" }}>
<span
style={{ textAlign: "center" }}
>
<Typography
variant="body1"
style={{ marginLeft: 25, marginRight: 25 }}
@@ -20330,15 +20573,39 @@ const AngularWorkflow = (defaultprops) => {
style={{
marginTop: 25,
marginBottom: 25,
backgroundColor: theme.palette.inputColor,
backgroundColor: "rgba(255,255,255,0.6)",
}}
/>
<Typography variant="h6">
There is currently no extended documentation available for this
app.
</Typography>
<div
style={{
backgroundColor: theme.palette.inputColor,
padding: 15,
borderRadius: theme.palette.borderRadius,
marginBottom: 30,
}}
>
<Typography variant="h6" style={{marginBottom: 25, }}>
There is no Shuffle-specific documentation for this app yet. Documentation is written custom for each app, and is a community effort. Hope to see your contribution
</Typography>
<Button
variant="contained"
color="primary"
onClick={() => {
toast.success("Opening remote Github documentation link. Thanks for contributing!")
setTimeout(() => {
window.open(`https://github.com/Shuffle/openapi-apps/new/master/docs?filename=${selectedApp.name.toLowerCase()}.md`, "_blank")
}, 2500)
}}
>
<EditIcon /> &nbsp;&nbsp;Create Docs
</Button>
</div>
<Typography variant="body1" style={{ marginTop: 25 }}>
Want to help the making of, or imrpvoe this app?{" "}
Want to help the making of, or improve this app?{" "}
<br />
<a
rel="noopener noreferrer"
target="_blank"
@@ -20385,6 +20652,84 @@ const AngularWorkflow = (defaultprops) => {
)}
</span>
) : (
<div>
{selectedMeta !== undefined && selectedMeta !== null && Object.getOwnPropertyNames(selectedMeta).length > 0 && selectedMeta.name !== undefined && selectedMeta.name !== null ?
<div
style={{
backgroundColor: theme.palette.inputColor,
padding: 15,
borderRadius: theme.palette.borderRadius,
marginBottom: 30,
display: "flex",
}}
>
<div style={{ flex: 3, display: "flex", vAlign: "center", position: "sticky", top: 50, }}>
{isMobile ? null : (
<Typography style={{ display: "inline", marginTop: 6 }}>
<a
rel="noopener noreferrer"
target="_blank"
href={selectedMeta.link}
style={{ textDecoration: "none", color: "#f85a3e" }}
>
<Button style={{ color: "white", }} variant="outlined" color="secondary">
<EditIcon /> &nbsp;&nbsp;Edit
</Button>
</a>
</Typography>
)}
{isMobile ? null : (
<div
style={{
height: "100%",
width: 1,
backgroundColor: "white",
marginLeft: 50,
marginRight: 50,
}}
/>
)}
<Typography style={{ display: "inline", marginTop: 11 }}>
{selectedMeta.read_time} minute
{selectedMeta.read_time === 1 ? "" : "s"} to read
</Typography>
</div>
<div style={{ flex: 2 }}>
{isMobile ||
selectedMeta.contributors === undefined ||
selectedMeta.contributors === null ? (
""
) : (
<div style={{ margin: 10, height: "100%", display: "inline" }}>
{selectedMeta.contributors.slice(0, 7).map((data, index) => {
return (
<a
key={index}
rel="noopener noreferrer"
target="_blank"
href={data.url}
style={{ textDecoration: "none", color: "#f85a3e" }}
>
<Tooltip title={data.url} placement="bottom">
<img
alt={data.url}
src={data.image}
style={{
marginTop: 5,
marginRight: 10,
height: 40,
borderRadius: 40,
}}
/>
</Tooltip>
</a>
);
})}
</div>
)}
</div>
</div>
: null}
<Markdown
components={{
img: Img,
@@ -20405,6 +20750,7 @@ const AngularWorkflow = (defaultprops) => {
>
{selectedApp.documentation}
</Markdown>
</div>
)}
</div>
</div>
+13 -8
View File
@@ -955,7 +955,6 @@ const Apps = (props) => {
</Link>
: null
console.log("Sharing config: ", sharingConfiguration);
const activateButton =
selectedApp.generated && !selectedApp.activated ? (
<div>
@@ -1281,10 +1280,15 @@ const Apps = (props) => {
{isCloud && !internalIds.includes(selectedApp.name.toLowerCase()) ?
<Tooltip title="Deactivates this app for the current organisation. This means the app will not be usable again until you re-activate it." placement="top">
<Button
variant="contained"
variant={selectedApp.reference_org === userdata.active_org.id ? "outlined" : "contained"}
component="label"
color="primary"
onClick={() => {
if (selectedApp.reference_org === userdata.active_org.id) {
toast.info("Can't deactivate apps made in this org. Please contact support if you want to deactivate this app.")
return
}
const tmpurl = new URL(window.location.href);
const searchParams = tmpurl.searchParams;
const queryID = searchParams.get("queryID");
@@ -1316,7 +1320,7 @@ const Apps = (props) => {
console.log("No query to handle when activating");
}
activateApp(selectedApp.id, true)
activateApp(selectedApp.id, true, true)
}}
style={{ height: 35, marginTop: 0, marginLeft: 10, }}
>
@@ -1845,9 +1849,12 @@ const Apps = (props) => {
)
}
const activateApp = (appid, refresh) => {
const activateApp = (appid, refresh, deactivate) => {
const appExists = userdata.active_apps !== undefined && userdata.active_apps !== null && userdata.active_apps.includes(appid)
const url = appExists ? `${globalUrl}/api/v1/apps/${appid}/deactivate` : `${globalUrl}/api/v1/apps/${appid}/activate`
const url = deactivate === true ?
`${globalUrl}/api/v1/apps/${appid}/deactivate`
:
appExists ? `${globalUrl}/api/v1/apps/${appid}/deactivate` : `${globalUrl}/api/v1/apps/${appid}/activate`
fetch(url, {
method: 'GET',
@@ -1876,7 +1883,7 @@ const Apps = (props) => {
if (appExists) {
toast("App deactivated for your organization! Existing workflows with the app will continue to work.")
} else {
toast("App activated for your organization!")
toast("App activation changed for your organization!")
}
if (refresh === true) {
@@ -2021,8 +2028,6 @@ const Apps = (props) => {
userToken: userdata === undefined || userdata === null || userdata.id === undefined ? "unauthenticated" : userdata.id,
}
])
} else {
console.log("No query to handle when activating")
}
activateApp(hit.objectID, true)
+7 -1
View File
@@ -156,7 +156,13 @@ export const OuterLink = (props) => {
export const Img = (props) => {
return <img style={{ borderRadius: theme.palette.borderRadius, width: 750, maxWidth: "100%", marginTop: 15, marginBottom: 15, }} alt={props.alt} src={props.src} />;
return(
<img
style={{border: "1px solid rgba(255,255,255,0.3)", borderRadius: theme.palette.borderRadius, width: 750, maxWidth: "100%", marginTop: 15, marginBottom: 15, }}
alt={props.alt}
src={props.src}
/>
)
}
export const CodeHandler = (props) => {
+86 -2
View File
@@ -10,6 +10,7 @@ import {
Divider,
TextField,
Modal,
Switch,
} from "@mui/material";
//import { useAlert
import { ToastContainer, toast } from "react-toastify";
@@ -19,6 +20,9 @@ import { FileCopy, Visibility, VisibilityOff } from "@mui/icons-material";
import IconButton from "@mui/material/IconButton";
import { Tooltip } from "@mui/material";
import CloseIcon from "@mui/icons-material/Close";
import { Box } from "@mui/system";
import CircularProgress from "@mui/material/CircularProgress";
const Settings = (props) => {
const { globalUrl, isLoaded, userdata, setUserData } = props;
@@ -32,6 +36,11 @@ const Settings = (props) => {
const [currentPassword, setCurrentPassword] = useState("");
const [newPassword, setNewPassword] = useState("");
const [newPassword2, setNewPassword2] = useState("");
const [selectedOrganization, setSelectedOrganization] = useState({});
const [MFARequired, setMFARequired] = React.useState(false);
const [image2FA, setImage2FA] = React.useState("");
const [value2FA, setValue2FA] = React.useState("");
// const [file, setFile] = React.useState("");
// const [fileBase64, setFileBase64] = React.useState(
// userdata.image === undefined || userdata.image === null
@@ -87,6 +96,82 @@ const Settings = (props) => {
flexDirection: "column",
};
useEffect(() => {
if (userdata.active_org === undefined || userdata.active_org === null || userdata.active_org.id === undefined || userdata.active_org.id === null) {
return;
}
fetch(`${globalUrl}/api/v1/orgs/${userdata.active_org.id}`, {
method: "GET",
headers: {
"Content-Type": "application/json",
Accept: "application/json",
},
credentials: "include",
})
.then((response) => {
console.log("Received response:", response);
if (response.status !== 200) {
console.error("Status not 200:", response.status);
throw new Error(`Status not 200: ${response.status}`);
}
return response.json();
})
.then((responseJson) => {
setSelectedOrganization(responseJson);
setMFARequired(responseJson.mfa_required);
})
.catch((error) => {
console.error("Error fetching organization:", error);
});
}, [userdata]);
const UpdateMFAInUserOrg = (org_id) => {
if (MFARequired === false) {
toast("Making MFA required for your organization. Please wait...");
} else {
toast("Making MFA optional for your organization. Please wait...");
}
const data = {
mfa_required: !selectedOrganization.mfa_required,
org_id: selectedOrganization.id,
}
const url = globalUrl + `/api/v1/orgs/${selectedOrganization.id}`;
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) => {
console.log(responseJson)
if (responseJson["success"] === false) {
toast.error("Failed updating org: ", responseJson.reason);
} else {
if (MFARequired === false) {
setMFARequired(true)
toast.success("Successfully make MFA required for your organization!");
} else {
setMFARequired(false)
toast.success("Successfully make MFA optional for your organization!")
}
}
}),
)
.catch((error) => {
toast("Err: " + error.toString());
});
}
const checkOwner = (data, userdata) => {
var currentOwner = false;
if (data.owner.address === userdata.eth_info.account) {
@@ -938,8 +1023,7 @@ const Settings = (props) => {
>
Submit password change
</Button>
<h3>{passwordFormMessage}</h3>
<h3>{passwordFormMessage}</h3>
{isCloud && (
<>
+10 -1
View File
@@ -2153,7 +2153,7 @@ const Workflows = (props) => {
}
return (
<div style={{width: "100%", minWidth: 321, position: "relative", border: highlightIds.includes(data.id) ? "2px solid #f85a3e" : isDistributed || hasSuborgs ? "2px solid #40E0D0" : "inherit", borderRadius: theme.palette.borderRadius, }}>
<div style={{width: "100%", minWidth: 321, position: "relative", border: highlightIds.includes(data.id) ? "2px solid #f85a3e" : isDistributed || hasSuborgs ? "1px solid #40E0D0" : "inherit", borderRadius: theme.palette.borderRadius, }}>
<Paper square style={paperAppStyle}>
{selectedCategory !== "" ?
<Tooltip title={`Usecase Category: ${selectedCategory}`} placement="bottom">
@@ -2206,8 +2206,17 @@ const Workflows = (props) => {
<Typography>
Edit '{data.name}'
</Typography>
<br />
{isDistributed || hasSuborgs ?
<Typography>
This is a parentorg-controlled workflow.
</Typography>
: null}
</div>
} placement="right">
<Typography
variant="body1"
style={{