Files
shuffle-cracked/frontend/src/views/Admin.jsx
T

7445 lines
225 KiB
React
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import React, { useState, useEffect } from "react";
import theme from "../theme.jsx";
import { makeStyles } from "@mui/styles";
import { useNavigate, Link } from "react-router-dom";
import countries from "../components/Countries.jsx";
import CacheView from "../components/CacheView.jsx";
import { CopyToClipboard } from "../views/Docs.jsx";
import {
FormControl,
InputLabel,
Paper,
OutlinedInput,
Checkbox,
Card,
Chip,
Tooltip,
FormControlLabel,
Typography,
Switch,
Select,
MenuItem,
Divider,
TextField,
Button,
ButtonGroup,
Tabs,
Tab,
Grid,
List,
ListItem,
ListItemText,
ListItemAvatar,
ListItemSecondaryAction,
IconButton,
Avatar,
Zoom,
Dialog,
DialogTitle,
DialogActions,
DialogContent,
CircularProgress,
Box,
InputAdornment,
Autocomplete,
Modal
} from "@mui/material";
import {
Add as AddIcon,
Clear as ClearIcon,
Storage as StorageIcon,
Edit as EditIcon,
FileCopy as FileCopyIcon,
SelectAll as SelectAllIcon,
OpenInNew as OpenInNewIcon,
CloudDownload as CloudDownloadIcon,
Description as DescriptionIcon,
Polyline as PolylineIcon,
CheckCircle as CheckCircleIcon,
Close as CloseIcon,
Apps as AppsIcon,
Image as ImageIcon,
Delete as DeleteIcon,
Cached as CachedIcon,
AccessibilityNew as AccessibilityNewIcon,
Lock as LockIcon,
Schedule as ScheduleIcon,
Cloud as CloudIcon,
Business as BusinessIcon,
Visibility as VisibilityIcon,
VisibilityOff as VisibilityOffIcon,
Cancel as CancelIcon,
Dns as DnsIcon,
Help as HelpIcon,
Flag as FlagIcon,
FmdGood as FmdGoodIcon,
Warning as WarningIcon,
ExpandLess as ExpandLessIcon,
ExpandMore as ExpandMoreIcon,
} from "@mui/icons-material";
//import { useAlert
import { ToastContainer, toast } from "react-toastify";
import Dropzone from "../components/Dropzone.jsx";
import HandlePaymentNew from "../views/HandlePaymentNew.jsx";
import OrgHeader from "../components/OrgHeader.jsx";
import OrgHeaderexpanded from "../components/OrgHeaderexpanded.jsx";
import Billing from "../components/Billing.jsx";
import Priorities from "../components/Priorities.jsx";
import Branding from "../components/Branding.jsx";
import Files from "../components/Files.jsx";
import { display, style } from "@mui/system";
const useStyles = makeStyles({
notchedOutline: {
borderColor: "#f85a3e !important",
},
});
const ITEM_HEIGHT = 48;
const ITEM_PADDING_TOP = 8;
const MenuProps = {
PaperProps: {
style: {
maxHeight: ITEM_HEIGHT * 4.5 + ITEM_PADDING_TOP,
width: 500,
},
},
getContentAnchorEl: () => null,
};
const FileCategoryInput = (props) => {
const isSet = props.isSet;
console.log("inside filecategoryinput");
console.log("isset value", isSet);
if (isSet) {
return (
<TextField
onBlur={""}
InputProps={{
style: {
color: "white",
},
}}
color="primary"
placeholder="File category name"
required
margin="dense"
defaultValue={""}
autoFocus
fullWidth
/>
);
}
};
const Admin = (props) => {
const {
globalUrl,
userdata,
serverside,
checkLogin,
notifications,
setNotifications,
} = props;
var to_be_copied = "";
const classes = useStyles();
let navigate = useNavigate();
const [logsViewModal, setLogsViewModal] = React.useState(false);
const [userLogViewing, setUserLogViewing] = React.useState({});
const [ipSelected, setIpSelected] = React.useState("");
const [logsLoading, setLogsLoading] = React.useState(true);
const [logs, setLogs] = React.useState([]);
const [firstRequest, setFirstRequest] = React.useState(true);
const [orgRequest, setOrgRequest] = React.useState(true);
const [modalUser, setModalUser] = React.useState({});
const [orgName, setOrgName] = React.useState("");
const [modalOpen, setModalOpen] = React.useState(false);
const [cloudSyncModalOpen, setCloudSyncModalOpen] = React.useState(false);
const [cloudSyncApikey, setCloudSyncApikey] = React.useState("");
const [loading, setLoading] = React.useState(false);
const [selectedOrganization, setSelectedOrganization] = React.useState({});
//console.log("Selected: ", selectedOrganization)
const [appAuthenticationGroupModalOpen, setAppAuthenticationGroupModalOpen] = React.useState(false);
const [appsForAppAuthGroup, setAppsForAppAuthGroup] = React.useState([]);
const [appAuthenticationGroupId, setAppAuthenticationGroupId] = React.useState("");
const [appAuthenticationGroupName, setAppAuthenticationGroupName] = React.useState("");
const [appAuthenticationGroupEnvironment, setAppAuthenticationGroupEnvironment] = React.useState("");
const [appAuthenticationGroupDescription, setAppAuthenticationGroupDescription] = React.useState("");
const [appAuthenticationGroups, setAppAuthenticationGroups] = React.useState([]);
const [organizationFeatures, setOrganizationFeatures] = React.useState({});
const [loginInfo, setLoginInfo] = React.useState("");
const [curTab, setCurTab] = React.useState(0);
const [users, setUsers] = React.useState([]);
const [subOrgs, setSubOrgs] = useState([]);
const [parentOrg, setParentOrg] = React.useState(null);
const [organizations, setOrganizations] = React.useState([]);
const [orgSyncResponse, setOrgSyncResponse] = React.useState("");
const [userSettings, setUserSettings] = React.useState({});
const [matchingOrganizations, setMatchingOrganizations] = React.useState([]);
const [environments, setEnvironments] = React.useState([]);
const [authentication, setAuthentication] = React.useState([]);
const [schedules, setSchedules] = React.useState([]);
const [selectedUser, setSelectedUser] = React.useState({});
const [newUsername, setNewUsername] = React.useState("");
const [newPassword, setNewPassword] = React.useState("");
const [selectedUserModalOpen, setSelectedUserModalOpen] =
React.useState(false);
const [selectedAuthentication, setSelectedAuthentication] = React.useState(
{},
);
const [selectedAuthenticationModalOpen, setSelectedAuthenticationModalOpen] =
React.useState(false);
const [authenticationFields, setAuthenticationFields] = React.useState([]);
const [showArchived, setShowArchived] = React.useState(false);
const [isDropzone, setIsDropzone] = React.useState(false);
const [image2FA, setImage2FA] = React.useState("");
const [value2FA, setValue2FA] = React.useState("");
const [secret2FA, setSecret2FA] = React.useState("");
const [show2faSetup, setShow2faSetup] = useState(false);
const [adminTab, setAdminTab] = React.useState(3);
const [showApiKey, setShowApiKey] = useState(false);
const [billingInfo, setBillingInfo] = React.useState({});
const [selectedStatus, setSelectedStatus] = React.useState([]);
const [webHooks, setWebHooks] = React.useState([]);
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);
const [listItemExpanded, setListItemExpanded] = React.useState(-1);
const [installationTab, setInstallationTab] = React.useState(0);
const [commandController, setCommandController] = React.useState({
pipelines: false,
proxies: false,
})
const [, setUpdate] = React.useState(0);
useEffect(() => {
if (selectedOrganization.mfa_required !== undefined) {
setMFARequired(selectedOrganization.mfa_required);
}
}, [selectedOrganization.mfa_required]);
const [showDeleteAccountTextbox, setShowDeleteAccountTextbox] =
React.useState(false);
const [deleteAccountText, setDeleteAccountText] = React.useState("");
const [regionChangeModalOpen, setRegionChangeModalOpen] =
React.useState(false);
useEffect(() => {
getUsers();
setTimeout(() => {
const urlSearchParams = new URLSearchParams(window.location.search);
const params = Object.fromEntries(urlSearchParams.entries());
const foundOrgID = params["org_id"]
if (foundOrgID !== null && foundOrgID !== undefined) {
handleClickChangeOrg(foundOrgID)
}
const foundTab = params["admin_tab"]
if (foundTab !== null && foundTab !== undefined) {
if (adminTab === 3) {
window.scroll({
top: 450,
left: 0,
behavior: "smooth",
});
}
}
}, 1500);
}, []);
useEffect(() => {
const urlSearchParams = new URLSearchParams(window.location.search);
const params = Object.fromEntries(urlSearchParams.entries());
const foundTab = params["admin_tab"]
if (foundTab !== null && foundTab !== undefined) {
window.scroll({
top: 450,
left: 0,
behavior: "smooth",
})
}
}, [adminTab]);
useEffect(() => {
if (isDropzone) {
//redirectOpenApi();
setIsDropzone(false);
}
}, [isDropzone]);
useEffect(() => {
if (
userdata.orgs !== undefined &&
userdata.orgs !== null &&
userdata.orgs.length > 0
) {
handleGetSubOrgs(userdata.active_org.id);
} else console.log("error in user data");
}, [userdata]);
useEffect(() => {
handleGetAllTriggers()
}, []);
const isCloud =
window.location.host === "localhost:3002" ||
window.location.host === "shuffler.io";
const setSelectedRegion = (region) => {
// send a POST request to /api/v1/orgs/{org_id}/region with the region as the body
var data = {
dst_region: region,
};
toast("Changing region to " + region + "...This may take a few minutes.");
fetch(`${globalUrl}/api/v1/orgs/${selectedOrganization.id}/change/region`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Accept: "application/json",
},
credentials: "include",
body: JSON.stringify(data),
timeOut: 1000,
}).then((response) => {
if (response.status !== 200) {
toast("Failed to change region!");
} else {
toast("Region changed successfully! Reloading in 5 seconds..");
// Reload the page in 2 seconds
setTimeout(() => {
window.location.reload();
}, 5000);
}
return response.json();
});
};
const RegionChangeModal = () => {
// Show from options: "us-west2", "europe-west2", "europe-west3", "northamerica-northeast1"
var regions = [
"us-west2",
"europe-west2",
"europe-west3",
"northamerica-northeast1",
];
return (
<Dialog
open={regionChangeModalOpen}
onClose={() => setRegionChangeModalOpen(false)}
aria-labelledby="alert-dialog-title"
aria-describedby="alert-dialog-description"
>
<DialogTitle id="alert-dialog-title">Change region</DialogTitle>
<DialogContent>
<FormControl fullWidth>
<InputLabel id="demo-simple-select-label">Region</InputLabel>
<Select
labelId="demo-simple-select-label"
id="demo-simple-select"
value={""}
onChange={(e) => {
setSelectedRegion(e.target.value);
setRegionChangeModalOpen(false);
}}
>
{regions.map((region) => {
// Set the default region if selectedOrganization.region is not set
if (selectedOrganization.region.length === 0) {
selectedOrganization.region = "europe-west2";
}
// Check if the current region matches the selected region
if (region === selectedOrganization.region) {
// If the region matches, set the MenuItem as selected
return (
<MenuItem value={region} disabled>
{region}
</MenuItem>
);
} else {
// Otherwise, render a regular MenuItem
return <MenuItem value={region}>{region}</MenuItem>;
}
})}
</Select>
</FormControl>
</DialogContent>
<DialogActions>
<Button
onClick={() => setRegionChangeModalOpen(false)}
color="primary"
>
Cancel
</Button>
</DialogActions>
</Dialog>
);
};
const get2faCode = (userId) => {
fetch(`${globalUrl}/api/v1/users/${userId}/get2fa`, {
method: "GET",
headers: {
"Content-Type": "application/json",
Accept: "application/json",
},
credentials: "include",
})
.then((response) => {
if (response.status !== 200) {
console.log("Status not 200 for apps :O!");
}
return response.json();
})
.then((responseJson) => {
//console.log("RESPONSE: ", responseJson)
if (responseJson.success === true) {
//toast(responseJson.reason)
setImage2FA(responseJson.reason);
setSecret2FA(responseJson.extra);
}
})
.catch((error) => {
toast(error.toString());
});
};
const getApps = () => {
fetch(globalUrl + "/api/v1/apps", {
method: "GET",
headers: {
"Content-Type": "application/json",
Accept: "application/json",
},
credentials: "include",
})
.then((response) => {
if (response.status !== 200) {
console.log("Status not 200 for apps :O!");
}
return response.json();
})
.then((responseJson) => {
console.log("apps: ", responseJson);
//setApps(responseJson)
//setFilteredApps(responseJson)
//if (responseJson.length > 0) {
// setSelectedApp(responseJson[0])
// if (responseJson[0].actions !== null && responseJson[0].actions.length > 0) {
// setSelectedAction(responseJson[0].actions[0])
// } else {
// setSelectedAction({})
// }
//}
})
.catch((error) => {
toast(error.toString());
});
};
const deleteAppAuthenticationGroup = (appAuthGroupId) => {
const url = `${globalUrl}/api/v1/authentication/group/${appAuthGroupId}`
fetch(url, {
method: "DELETE",
credentials: "include",
headers: {
"Content-Type": "application/json",
},
})
.then((response) => {
if (response.status !== 200) {
console.log("Status not 200 for deleting app auth group");
}
return response.json();
})
.then((responseJson) => {
if (responseJson.success === false) {
toast("Failed to delete app authentication group");
} else {
toast("App authentication group deleted")
getAppAuthenticationGroups()
}
})
.catch((error) => {
toast(error.toString())
})
}
const createAppAuthenticationGroup = (name, environment, description, appAuthIds) => {
// Makes list of ids into a full-on list of auth, but just with the ID
// The backend fills in the rest
console.log("INput auth: ", appAuthIds)
let app_auths = appAuthIds.map((appAuthId) => {
return { id: appAuthId };
})
var parsedAppGroup = {
label: name,
environment: environment,
description: description,
app_auths: app_auths
}
if (appAuthenticationGroupId !== undefined && appAuthenticationGroupId !== null && appAuthenticationGroupId !== "") {
parsedAppGroup.id = appAuthenticationGroupId
}
fetch(globalUrl + "/api/v1/authentication/group", {
method: "POST",
headers: {
"Content-Type": "application/json",
Accept: "application/json",
},
credentials: "include",
body: JSON.stringify(parsedAppGroup),
})
.then((response) => {
if (response.status !== 200) {
throw new Error("Failed to create app authentication group");
}
return response.json();
})
.then((responseJson) => {
if (responseJson.success === false) {
toast("Failed to create. Please try again, or contact support@shuffler.io")
} else {
// Close the modal
setAppAuthenticationGroupModalOpen(false)
toast("App authentication group created")
getAppAuthenticationGroups()
}
})
.catch((error) => {
toast(error.toString());
});
};
const categories = [
{
name: "Ticketing",
apps: ["TheHive", "Service-Now", "SecureWorks"],
categories: ["tickets", "ticket", "ticketing"],
},
];
/*
"SIEM",
"Active Directory",
"Firewalls",
"Proxies web",
"SIEM",
"SOAR",
"Mail",
"EDR",
"AV",
"MDM/MAM",
"DNS",
"Ticketing platform",
"TIP",
"Communication",
"DDOS protection",
"VMS",
]
*/
//const alert = useAlert();
const handleStatusChange = (event) => {
const { value } = event.target;
setSelectedStatus(value);
handleEditOrg(
"",
"",
selectedOrganization.id,
"",
{},
{},
value.length === 0 ? ["none"] : value,
);
};
// Basically just a simple way to get a generated email
// This also may help understand how to communicate with users
// both inside and outside Shuffle
// This could also be generated on the backend
const mailsendingButton = (org) => {
if (org === undefined || org === null) {
return "";
}
if (users.length === 0) {
return "";
}
// 1 mail based on users that have only apps
// Another based on those doing workflows
// Another based on those trying usecases(?) or templates
//
// Start based on edr, siem & ticketing
// Talk about enrichment?
// Check suggested usecases
// Check suggested workflows
var your_apps = "- Connecting ";
var subject_add = 0;
var subject = "POC to automate ";
if (
org.security_framework !== undefined &&
org.security_framework !== null
) {
if (
org.security_framework.cases.name !== undefined &&
org.security_framework.cases.name !== null &&
org.security_framework.cases.name !== ""
) {
your_apps +=
org.security_framework.cases.name
.replace("_", " ", -1)
.replace(" API", "", -1) + ", ";
if (subject_add < 2) {
if (subject_add === 1) {
subject += " and ";
}
subject_add += 1;
subject += org.security_framework.cases.name
.replace("_", " ", -1)
.replace(" API", "", -1);
}
}
if (
org.security_framework.siem.name !== undefined &&
org.security_framework.siem.name !== null &&
org.security_framework.siem.name !== ""
) {
your_apps +=
org.security_framework.siem.name
.replace("_", " ", -1)
.replace(" API", "", -1) + ", ";
if (subject_add < 2) {
if (subject_add === 1) {
subject += " and ";
}
subject_add += 1;
subject += org.security_framework.siem.name
.replace("_", " ", -1)
.replace(" API", "", -1);
}
}
if (
org.security_framework.communication.name !== undefined &&
org.security_framework.communication.name !== null &&
org.security_framework.communication.name !== ""
) {
your_apps +=
org.security_framework.communication.name
.replace("_", " ", -1)
.replace(" API", "", -1) + ", ";
if (subject_add < 2) {
if (subject_add === 1) {
subject += " and ";
}
subject_add += 1;
subject += org.security_framework.communication.name
.replace("_", " ", -1)
.replace(" API", "", -1);
}
}
if (
org.security_framework.edr.name !== undefined &&
org.security_framework.edr.name !== null &&
org.security_framework.edr.name !== ""
) {
your_apps +=
org.security_framework.edr.name
.replace("_", " ", -1)
.replace(" API", "", -1) + ", ";
if (subject_add < 2) {
if (subject_add === 1) {
subject += " and ";
}
subject_add += 1;
subject += org.security_framework.edr.name
.replace("_", " ", -1)
.replace(" API", "", -1);
}
}
if (
org.security_framework.intel.name !== undefined &&
org.security_framework.intel.name !== null &&
org.security_framework.intel.name !== ""
) {
your_apps +=
org.security_framework.intel.name
.replace("_", " ", -1)
.replace(" API", "", -1) + ", ";
if (subject_add < 2) {
if (subject_add === 1) {
subject += " and ";
}
subject_add += 1;
subject += org.security_framework.intel.name
.replace("_", " ", -1)
.replace(" API", "", -1);
}
}
// Remove comma
//subject += "?"
your_apps = your_apps.substring(0, your_apps.length - 2);
}
// Add usecases they may not have tried (from recommendations): org.priorities where item type is usecase
var usecases = "- Building usecases like ";
const active_usecase = org.priorities.filter(
(item) => item.type === "usecase" && item.active === true,
);
if (active_usecase.length > 0) {
for (var i = 0; i < active_usecase.length; i++) {
if (active_usecase[i].name.includes("Suggested Usecase: ")) {
usecases +=
active_usecase[i].name.replace("Suggested Usecase: ", "", -1) +
", ";
} else {
usecases += active_usecase[i].name + ", ";
}
}
usecases = usecases.substring(0, usecases.length - 2);
}
if (your_apps.length <= 15) {
your_apps = "";
}
if (usecases.length <= 30) {
usecases = "";
}
var workflow_amount = "a few";
var admins = "";
// Loop users
var lastLogin = 0;
for (var i = 0; i < users.length; i++) {
if (users[i].username.includes("shuffler")) {
continue;
}
if (users[i].role === "admin") {
admins += users[i].username + ",";
}
const data = users[i];
for (var i = 0; i < data.login_info.length; i++) {
if (data.login_info[i].timestamp > lastLogin) {
lastLogin = data.login_info[i].timestamp;
}
}
}
// Remove last comma
admins = admins.substring(0, admins.length - 1);
if (your_apps.length > 5) {
your_apps += "%0D%0A";
}
if (usecases.length > 5) {
usecases += "%0D%0A";
}
// Get drift username from userdata.username before @ in email
const username = userdata.username.substring(
0,
userdata.username.indexOf("@"),
);
// Check if timestamp is more than 2 weeks ago and add "a while back" to the message
const timeComparison = 1209600;
const extra_timestamp_text =
lastLogin === 0
? 0
: Date.now() / 1000 - lastLogin > timeComparison
? " a while back"
: "";
console.log("LAST LOGIN: " + lastLogin, extra_timestamp_text);
// Check if cloud sync is active, and if so, add a message about it
const cloudSyncInfo =
selectedOrganization.cloud_sync === true
? "- Scale your onprem installation"
: "";
var body = `Hey,%0D%0A%0D%0AI noticed you tried to use Shuffle${extra_timestamp_text}, and thought you may be interested in a POC. It looks like you have ${workflow_amount} workflows made, but it still doesn't look like you are getting what you wanted out of Shuffle. If you're interested, I'd love to set up a quick call to see if we can help you get more out of Shuffle. %0D%0A%0D%0A
Some of the things we can help with:%0D%0A
${your_apps}
- Configuring and authenticating your apps%0D%0A
${usecases}
- Multi-Tenancy and creating special usecases%0D%0A
${cloudSyncInfo}%0D%0A
If you're interested, please let me know a time that works for you, or set up a call here: https://drift.me/${username}`;
return `mailto:${admins}?bcc=frikky@shuffler.io,binu@shuffler.io&subject=${subject}&body=${body}`;
};
const changeDistribution = (data) => {
//changeDistributed(data, !isDistributed)
console.log("Should change distribution to be shared among suborgs");
editAuthenticationConfig(data.id, "suborg_distribute");
};
const deleteAuthentication = (data) => {
toast("Deleting auth " + data.label);
// Just use this one?
const url = globalUrl + "/api/v1/apps/authentication/" + data.id;
console.log("URL: ", url);
fetch(url, {
method: "DELETE",
credentials: "include",
headers: {
"Content-Type": "application/json",
},
})
.then((response) =>
response.json().then((responseJson) => {
if (responseJson.success === false) {
toast("Failed deleting auth");
} else {
// Need to wait because query in ES is too fast
setTimeout(() => {
getAppAuthentication();
}, 1000);
//toast("Successfully deleted authentication!")
}
}),
)
.catch((error) => {
console.log("Error in userdata: ", error);
});
};
const handleGetAllTriggers = () => {
fetch(globalUrl + "/api/v1/triggers", {
method: "GET",
headers: {
"Content-Type": "application/json",
Accept: "application/json",
},
credentials: "include",
})
.then((response) => {
if (response.status !== 200) {
console.log("Status not 200 for getting all triggers");
}
return response.json();
})
.then((responseJson) => {
setWebHooks(responseJson.webhooks || []); // Handling the case where the result is null or undefined
setAllSchedules(responseJson.schedules || []);
setPipelines(responseJson.pipelines || []);
})
.catch((error) => {
// toast(error.toString());
});
};
const deleteSchedule = (data) => {
// FIXME - add some check here ROFL
console.log("INPUT: ", data);
// Just use this one?
const url =
globalUrl +
"/api/v1/workflows/" +
data["workflow_id"] +
"/schedule/" +
data.id;
console.log("URL: ", url);
fetch(url, {
method: "DELETE",
credentials: "include",
headers: {
"Content-Type": "application/json",
},
})
.then((response) =>
response.json().then((responseJson) => {
console.log("RESP: ", responseJson);
if (responseJson["success"] === false) {
toast("Failed stopping schedule");
} else {
toast("Successfully stopped schedule!");
}
setTimeout(handleGetAllTriggers, 1000);
}),
)
.catch((error) => {
console.log("Error in userdata: ", error);
});
};
const startSchedule = (trigger) => {
if (trigger.name.length <= 0) {
toast("Error: name can't be empty");
return;
}
toast("Creating schedule");
const data = {
name: trigger.name,
frequency: trigger.frequency,
execution_argument: trigger.argument,
environment: trigger.environment,
id: trigger.id,
start: trigger.start_node,
};
fetch(`${globalUrl}/api/v1/workflows/${trigger.workflow_id}/schedule`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Accept: "application/json",
},
body: JSON.stringify(data),
credentials: "include",
})
.then((response) => {
if (response.status !== 200) {
console.log("Status not 200 for stream results :O!");
}
return response.json();
})
.then((responseJson) => {
if (!responseJson.success) {
toast("Failed to set schedule: " + responseJson.reason);
} else {
toast("Successfully created schedule");
}
setTimeout(handleGetAllTriggers, 1000);
})
.catch((error) => {
//toast(error.toString());
console.log("Get schedule error: ", error.toString());
});
};
const deleteWebhook = (trigger) => {
if (trigger === undefined) {
return;
}
fetch(globalUrl + "/api/v1/hooks/" + trigger.id + "/delete", {
method: "DELETE",
headers: {
"Content-Type": "application/json",
Accept: "application/json",
},
credentials: "include",
})
.then((response) => {
if (response.status !== 200) {
console.log("Status not 200 for stream results :O!");
}
return response.json();
})
.then((responseJson) => {
if (responseJson.success) {
toast("Successfully stopped webhook");
} else {
if (responseJson.reason !== undefined) {
toast("Failed stopping webhook: " + responseJson.reason);
}
}
setTimeout(handleGetAllTriggers, 1000);
})
.catch((error) => {
toast(
"Delete webhook error. Contact support or check logs if this persists.",
);
});
};
const startWebHook = (trigger) => {
const hookname = trigger.info.name;
if (hookname.length === 0) {
toast("Missing name");
return;
}
if (trigger.id.length !== 36) {
toast("Missing id");
return;
}
toast("Starting webhook");
const data = {
name: hookname,
type: "webhook",
id: trigger.id,
workflow: trigger.workflows[0],
start: trigger.start,
environment: trigger.environment,
auth: trigger.auth,
custom_response: trigger.custom_response,
version: trigger.version,
version_timeout: 15,
};
console.log("Trigger data: ", data);
fetch(globalUrl + "/api/v1/hooks/new", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify(data),
credentials: "include",
})
.then((response) => response.json())
.then((responseJson) => {
if (responseJson.success) {
// Set the status
toast("Successfully started webhook");
} else {
toast("Failed starting webhook: " + responseJson.reason);
}
setTimeout(handleGetAllTriggers, 1000);
})
.catch((error) => {
//console.log(error.toString());
console.log("New webhook error: ", error.toString());
});
};
const changePipelineState = (pipeline, state) => {
if (state.trim() === "") {
toast("state is not defined");
return;
}
const data = {
name: pipeline.name,
id: pipeline.id,
type: state,
command: pipeline.definition,
environment: pipeline.environment,
};
if (state === "start") toast("starting the pipeline");
else toast.info("Stopping the pipeline. This may take a few minutes to propagate.")
const url = `${globalUrl}/api/v1/triggers/pipeline`;
fetch(url, {
method: "POST",
headers: {
"Content-Type": "application/json",
Accept: "application/json",
},
body: JSON.stringify(data),
credentials: "include",
})
.then((response) => {
if (response.status !== 200) {
console.log("Status not 200 for stream results :O!");
toast("Failed to update the pipeline state");
}
return response.json();
})
.then((responseJson) => {
if (!responseJson.success) {
toast.error("Failed to update the pipeline: " + responseJson.reason);
} else {
setTimeout(() => {
handleGetAllTriggers()
}, 5000)
setTimeout(() => {
handleGetAllTriggers()
}, 20000)
setTimeout(() => {
handleGetAllTriggers()
}, 120000)
/*
if (state === "start") {
toast("Successfully created pipeline");
} else {
toast("Sucessfully stopped the pipeline");
}
*/
}
})
.catch((error) => {
//toast(error.toString());
console.log("Get schedule error: ", error.toString());
});
};
if (
userdata.support === true &&
selectedOrganization.id !== "" &&
selectedOrganization.id !== undefined &&
selectedOrganization.id !== null &&
selectedOrganization.id !== userdata.active_org.id
) {
toast("Refreshing window to fix org support access");
window.location.reload();
return null;
}
const handleVerify2FA = (userId, code) => {
const data = {
code: code,
user_id: userId,
};
toast("Verifying 2fa code. Please wait...");
fetch(`${globalUrl}/api/v1/users/${userId}/set2fa`, {
mode: "cors",
method: "POST",
body: JSON.stringify(data),
credentials: "include",
crossDomain: true,
withCredentials: true,
headers: {
"Content-Type": "application/json; charset=utf-8",
},
})
.then((response) => {
if (response.status === 200) {
} else {
//toast("Wrong code sent.")
//toast("Wrong code sent. Please try again.")
}
return response.json();
})
.then((responseJson) => {
if (responseJson.success === true) {
if (responseJson.MFAActive === true) {
toast.success("Successfully enabled 2fa");
}
if (responseJson.MFAActive === false) {
toast.success("Successfully disabled 2fa");
}
setTimeout(() => {
getUsers();
setImage2FA("");
setValue2FA("");
setSecret2FA("");
setShow2faSetup(false);
setSelectedUserModalOpen(false);
}, 1000);
} else {
toast("Wrong code sent. Please try again.");
//toast("Failed setting 2fa: ", responseJson.reason)
}
})
.catch((error) => {
toast("Wrong code sent. Please try again.");
//toast("Err: " + error.toString())
});
};
const handleStopOrgSync = (org_id) => {
if (org_id === undefined || org_id === null) {
toast("Couldn't get org " + org_id);
return;
}
const data = {};
const url = globalUrl + "/api/v1/orgs/" + org_id + "/stop_sync";
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) => {
if (response.status === 200) {
console.log("Cloud sync success?");
toast("Successfully stopped cloud sync");
} else {
console.log("Cloud sync fail?");
toast(
"Failed stopping sync. Try again, and contact support if this persists.",
);
}
return response.json();
})
.then((responseJson) => {
setTimeout(() => {
handleGetOrg(org_id);
}, 1000);
})
.catch((error) => {
toast("Err: " + error.toString());
});
};
const enableCloudSync = (apikey, organization, disableSync) => {
setOrgSyncResponse("");
const data = {
apikey: apikey,
organization: organization,
disable: disableSync,
};
const url = globalUrl + "/api/v1/cloud/setup";
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) => {
setLoading(false);
if (response.status === 200) {
console.log("Cloud sync success?");
} else {
console.log("Cloud sync fail?");
}
return response.json();
//setTimeout(() => {
//}, 1000)
})
.then((responseJson) => {
console.log("RESP: ", responseJson);
if (
responseJson.success === false &&
responseJson.reason !== undefined
) {
setOrgSyncResponse(responseJson.reason);
toast("Failed to handle sync: " + responseJson.reason);
} else if (!responseJson.success) {
toast("Failed to handle sync.");
} else {
getOrgs();
if (disableSync) {
toast("Successfully disabled sync!");
setOrgSyncResponse("Successfully disabled syncronization");
} else {
toast("Cloud Syncronization successfully set up!");
setOrgSyncResponse(
"Successfully started syncronization. Cloud features you now have access to can be seen below.",
);
}
selectedOrganization.cloud_sync = !selectedOrganization.cloud_sync;
setSelectedOrganization(selectedOrganization)
setCloudSyncApikey("")
handleGetOrg(userdata.active_org.id)
}
})
.catch((error) => {
setLoading(false);
toast("Err: " + error.toString());
});
};
const saveAuthentication = (authentication) => {
const data = authentication;
const url = globalUrl + "/api/v1/apps/authentication";
fetch(url, {
mode: "cors",
method: "PUT",
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) {
// Check if .reason exists
if (responseJson.reason !== undefined) {
toast("Failed changing authentication: " + responseJson.reason);
} else {
toast("Failed changing authentication");
}
} else {
//toast("Successfully password!")
setSelectedUserModalOpen(false);
getAppAuthentication();
setSelectedAuthentication({});
setSelectedAuthenticationModalOpen(false);
}
}),
)
.catch((error) => {
toast("Err: " + error.toString());
});
};
const handleEditOrg = (
name,
description,
orgId,
image,
defaults,
sso_config,
lead_info,
) => {
const data = {
name: name,
description: description,
org_id: orgId,
image: image,
defaults: defaults,
sso_config: sso_config,
lead_info: lead_info,
};
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) => {
if (responseJson["success"] === false) {
toast("Failed updating org: ", responseJson.reason);
} else {
if (
lead_info === undefined ||
lead_info === null ||
lead_info === []
) {
toast("Successfully edited org!");
}
}
}),
)
.catch((error) => {
toast("Err: " + error.toString());
});
};
const editAuthenticationConfig = (id, parentAction) => {
const data = {
id: id,
action:
parentAction !== undefined && parentAction !== null
? parentAction
: "assign_everywhere",
};
const url = globalUrl + "/api/v1/apps/authentication/" + id + "/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 appauth");
} else {
toast("Successfully updated auth!");
setSelectedUserModalOpen(false);
setTimeout(() => {
getAppAuthentication();
}, 1000);
}
}),
)
.catch((error) => {
toast("Err: " + error.toString());
});
};
const createSubOrg = (currentOrgId, name) => {
const data = { name: name, org_id: currentOrgId };
console.log(data);
const url = globalUrl + `/api/v1/orgs/${currentOrgId}/create_sub_org`;
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) {
if (responseJson.reason !== undefined) {
toast(responseJson.reason);
} else {
toast("Failed creating suborg. Please try again");
}
} else {
toast("Successfully created suborg. Reloading in 3 seconds!");
setSelectedUserModalOpen(false);
setTimeout(() => {
window.location.reload();
}, 2500);
}
setOrgName("");
setModalOpen(false);
}),
)
.catch((error) => {
toast("Err: " + error.toString());
});
};
const onPasswordChange = () => {
const data = { username: selectedUser.username, newpassword: newPassword };
const url = globalUrl + "/api/v1/users/passwordchange";
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) {
if (responseJson.reason !== undefined) {
toast(responseJson.reason);
} else {
toast("Failed setting new password");
}
} else {
toast("Successfully updated password!");
setSelectedUserModalOpen(false);
}
}),
)
.catch((error) => {
toast("Err: " + error.toString());
});
};
const deleteUser = (data) => {
// Just use this one?
const userId = data.id;
const url = globalUrl + "/api/v1/users/" + userId;
fetch(url, {
method: "DELETE",
credentials: "include",
headers: {
"Content-Type": "application/json",
},
})
.then((response) => {
if (response.status === 200) {
getUsers();
}
return response.json();
})
.then((responseJson) => {
if (!responseJson.success && responseJson.reason !== undefined) {
toast("Failed to deactivate user: " + responseJson.reason);
} else if (responseJson.success === false) {
toast(
"Failed to deactivate user. Please contact support@shuffler.io if this persists.",
);
} else {
toast("Changed activation for user " + data.id);
}
})
.catch((error) => {
console.log("Error in userdata: ", error);
});
};
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 && foundorgid.length === 36) {
orgId = foundorgid
}
}
if (orgId.length === 0) {
toast("Organization ID not defined. Please contact us on https://shuffler.io if this persists logout.")
return;
}
// Just use this one?
localStorage.setItem("globalUrl", "");
localStorage.setItem("getting_started_sidebar", "open");
fetch(`${globalUrl}/api/v1/orgs/${orgId}`, {
method: "GET",
credentials: "include",
headers: {
"Content-Type": "application/json",
},
})
.then((response) => {
if (response.status === 401) {
} else {
localStorage.removeItem("apps")
localStorage.removeItem("workflows")
localStorage.removeItem("userinfo")
}
return response.json();
})
.then((responseJson) => {
if (responseJson["success"] === false) {
toast(
"Failed getting your org. If this persists, please contact support.",
);
} else {
if (
responseJson.sync_features === undefined ||
responseJson.sync_features === null
) {
responseJson.sync_features = {};
}
if (
responseJson.lead_info !== undefined &&
responseJson.lead_info !== null
) {
var leads = [];
if (responseJson.lead_info.testing_shuffle) {
leads.push("testing shuffle");
}
if (responseJson.lead_info.contacted) {
leads.push("contacted");
}
if (responseJson.lead_info.customer) {
leads.push("customer");
}
if (responseJson.lead_info.old_customer) {
leads.push("old customer");
}
if (responseJson.lead_info.old_lead) {
leads.push("old lead");
}
if (responseJson.lead_info.tech_partner) {
leads.push("tech partner");
}
if (responseJson.lead_info.creator) {
leads.push("creator");
}
if (responseJson.lead_info.opensource) {
leads.push("open source");
}
if (responseJson.lead_info.demo_done) {
leads.push("demo done");
}
if (responseJson.lead_info.pov) {
leads.push("pov");
}
if (responseJson.lead_info.lead) {
leads.push("lead");
}
if (responseJson.lead_info.student) {
leads.push("student");
}
if (responseJson.lead_info.internal) {
leads.push("internal");
}
if (responseJson.lead_info.sub_org) {
leads.push("sub_org");
}
setSelectedStatus(leads);
}
setSelectedOrganization(responseJson);
var lists = {
active: {
triggers: [],
features: [],
sync: [],
},
inactive: {
triggers: [],
features: [],
sync: [],
},
};
// FIXME: Set up features
//Object.keys(responseJson.sync_features).map(function(key, index) {
// //console.log(responseJson.sync_features[key])
//})
//setOrgName(responseJson.name)
//setOrgDescription(responseJson.description)
setOrganizationFeatures(lists);
}
})
.catch((error) => {
console.log("Error getting org: ", error);
toast("Error getting current organization");
});
};
const handleGetSubOrgs = (orgId) => {
if (orgId.length === 0) {
toast(
"Organization ID not defined. Please contact us on https://shuffler.io if this persists logout.",
);
return;
}
fetch(`${globalUrl}/api/v1/orgs/${orgId}/suborgs`, {
method: "GET",
credentials: "include",
headers: {
"Content-Type": "application/json",
},
})
.then((response) => {
if (!response.ok) {
throw new Error("Failed to fetch sub organizations");
}
return response.json();
})
.then((responseJson) => {
if (responseJson.success === false) {
//toast("Failed getting your org. If this persists, please contact support.");
} else {
const { subOrgs, parentOrg } = responseJson;
setSubOrgs(subOrgs);
setParentOrg(parentOrg);
}
})
.catch((error) => {
console.log("Error getting sub orgs: ", error);
//toast("Error getting sub organizations");
});
};
const handleClickChangeOrg = (orgId) => {
// Don't really care about the logout
//name: org.name,
//orgId = "asd"
const data = {
org_id: orgId,
};
localStorage.setItem("globalUrl", "");
localStorage.setItem("getting_started_sidebar", "open");
fetch(`${globalUrl}/api/v1/orgs/${orgId}/change`, {
mode: "cors",
credentials: "include",
crossDomain: true,
method: "POST",
body: JSON.stringify(data),
withCredentials: true,
headers: {
"Content-Type": "application/json; charset=utf-8",
},
})
.then(function (response) {
if (response.status !== 200) {
console.log("Error in response");
} else {
localStorage.removeItem("apps")
localStorage.removeItem("workflows")
localStorage.removeItem("userinfo")
}
return response.json();
})
.then(function (responseJson) {
if (responseJson.success === true) {
if (responseJson.region_url !== undefined && responseJson.region_url !== null && responseJson.region_url.length > 0) {
localStorage.setItem("globalUrl", responseJson.region_url)
//globalUrl = responseJson.region_url
}
setTimeout(() => {
window.location.reload()
}, 2000);
toast.success("Successfully changed active organization - refreshing!");
if (responseJson.org_id !== undefined && responseJson.org_id !== null && responseJson.org_id.length === 36) {
navigate(`/admin?org_id=${responseJson.org_id}`)
} else {
if (orgId !== undefined && orgId !== null && orgId?.includes("@")) {
navigate(`/admin`)
} else {
toast("No pivot?")
}
}
} else {
if (responseJson.reason !== undefined && responseJson.reason !== null) {
if (!responseJson.reason.includes("already")) {
toast("Failed changing org: " + responseJson.reason);
}
} else {
toast("Failed changing org")
}
}
})
.catch((error) => {
console.log("error changing: ", error);
//removeCookie("session_token", {path: "/"})
});
};
const inviteUser = (data) => {
//console.log("INPUT: ", data);
setLoginInfo("");
// Just use this one?
var data = {
username: data.Username,
type: "invite",
org_id: selectedOrganization.id,
};
var baseurl = globalUrl;
const url = baseurl + "/api/v1/users/register_org";
fetch(url, {
method: "POST",
credentials: "include",
body: JSON.stringify(data),
headers: {
"Content-Type": "application/json",
},
})
.then((response) =>
response.json().then((responseJson) => {
if (responseJson["success"] === false) {
setLoginInfo("Error: " + responseJson.reason);
toast(
"Failed to send email (2). Please try again and contact support if this persists.",
);
} else {
setLoginInfo("");
setModalOpen(false);
setTimeout(() => {
getUsers();
}, 1000);
toast(
"Invite sent! They will show up in the list when they have accepted the invite.",
);
}
}),
)
.catch((error) => {
console.log("Error in userdata: ", error);
toast(
"Failed to send email. Please try again and contact support if this persists.",
);
});
};
const submitUser = (data) => {
console.log("INPUT: ", data);
setLoginInfo("");
// Just use this one?
var data = { username: data.Username, password: data.Password };
var baseurl = globalUrl;
const url = baseurl + "/api/v1/users/register";
fetch(url, {
method: "POST",
credentials: "include",
body: JSON.stringify(data),
headers: {
"Content-Type": "application/json",
},
})
.then((response) =>
response.json().then((responseJson) => {
if (responseJson["success"] === false) {
setLoginInfo("Error: " + responseJson.reason);
} else {
setLoginInfo("");
setModalOpen(false);
setTimeout(() => {
getUsers();
}, 1000);
}
}),
)
.catch((error) => {
console.log("Error in userdata: ", error);
});
};
// Horrible frontend fix for environments
const setDefaultEnvironment = (environment) => {
// FIXME - add more checks to this
toast("Changing default env")
var newEnv = [];
for (var key in environments) {
if (environments[key].id == environment.id) {
if (environments[key].archived) {
toast("Can't set archived to default");
return;
}
environments[key].default = true;
} else if (
environments[key].default == true &&
environments[key].id !== environment.id
) {
environments[key].default = false;
}
newEnv.push(environments[key]);
}
// Just use this one?
const url = globalUrl + "/api/v1/setenvironments";
fetch(url, {
method: "PUT",
credentials: "include",
body: JSON.stringify(newEnv),
headers: {
"Content-Type": "application/json",
},
})
.then((response) =>
response.json().then((responseJson) => {
if (responseJson["success"] === false) {
toast(responseJson.reason);
setTimeout(() => {
getEnvironments();
}, 1500);
} else {
setLoginInfo("");
setModalOpen(false);
setTimeout(() => {
getEnvironments();
}, 1500);
}
}),
)
.catch((error) => {
console.log("Error in backend data: ", error);
});
};
const flushQueue = (name) => {
// Just use this one?
const url = globalUrl + "/api/v1/flush_queue";
fetch(url, {
method: "DELETE",
credentials: "include",
headers: {
"Content-Type": "application/json",
},
})
.then((response) =>
response.json().then((responseJson) => {
if (responseJson["success"] === false) {
toast(responseJson.reason);
getEnvironments();
} else {
setLoginInfo("");
setModalOpen(false);
getEnvironments();
}
}),
)
.catch((error) => {
console.log("Error when deleting: ", error);
});
};
const rerunCloudWorkflows = (environment) => {
toast("Starting execution reruns. This runs in the background. Check the /debug view to see the progress.");
fetch(`${globalUrl}/api/v1/environments/${environment.id}/rerun`, {
method: "GET",
credentials: "include",
})
.then((response) => {
if (response.status !== 200) {
console.log("Status not 200 for apps :O!");
return;
} else {
toast(response.reason);
//toast("Aborted all dangling workflows");
}
return response.json();
})
.then((responseJson) => {
console.log("Got response for execution: ", responseJson);
//console.log("RESPONSE: ", responseJson)
//setFiles(responseJson)
})
.catch((error) => {
//toast(error.toString())
});
};
const abortEnvironmentWorkflows = (environment) => {
//console.log("Aborting all workflows started >10 minutes ago, not finished");
toast(
"Clearing the queue - this may take some time. A new will show up when finished.",
);
fetch(
`${globalUrl}/api/v1/environments/${environment.id}/stop?deleteall=true`,
{
method: "GET",
credentials: "include",
},
)
.then((response) => {
if (response.status !== 200) {
console.log("Status not 200 for apps :O!");
toast("Failed aborting dangling workflows");
return;
} else {
toast("Successfully cleared the queue");
getEnvironments();
}
return response.json();
})
.then((responseJson) => {
console.log("Got response for execution: ", responseJson);
//console.log("RESPONSE: ", responseJson)
//setFiles(responseJson)
})
.catch((error) => {
//toast(error.toString())
});
};
const deleteEnvironment = (environment) => {
// FIXME - add some check here ROFL
//const name = environment.name
//toast("Modifying environment " + name)
//var newEnv = []
//for (var key in environments) {
// if (environments[key].Name == name) {
// if (environments[key].default) {
// toast("Can't modify the default environment")
// return
// }
// if (environments[key].type === "cloud" && !environments[key].archived) {
// toast("Can't modify cloud environments")
// return
// }
// environments[key].archived = !environments[key].archived
// }
// newEnv.push(environments[key])
//}
const id = environment.id;
//toast("Modifying environment " + environment.Name)
var newEnv = [];
for (var key in environments) {
if (environments[key].id == id) {
if (environments[key].default) {
toast("Can't modify the default environment");
return;
}
if (environments[key].type === "cloud" && !environments[key].archived) {
toast("Can't modify cloud environments");
return;
}
environments[key].archived = !environments[key].archived;
}
newEnv.push(environments[key]);
}
// Just use this one?
const url = globalUrl + "/api/v1/setenvironments";
fetch(url, {
method: "PUT",
credentials: "include",
body: JSON.stringify(newEnv),
headers: {
"Content-Type": "application/json",
},
})
.then((response) =>
response.json().then((responseJson) => {
if (responseJson["success"] === false) {
toast(responseJson.reason);
getEnvironments();
} else {
setLoginInfo("");
setModalOpen(false);
getEnvironments();
}
}),
)
.catch((error) => {
console.log("Error when deleting: ", error);
});
};
const submitEnvironment = (data) => {
// FIXME - add some check here ROFL
environments.push({
name: data.environment,
type: "onprem",
});
// Just use this one?
var baseurl = globalUrl;
const url = baseurl + "/api/v1/setenvironments";
fetch(url, {
method: "PUT",
credentials: "include",
body: JSON.stringify(environments),
headers: {
"Content-Type": "application/json",
},
})
.then((response) =>
response.json().then((responseJson) => {
if (responseJson["success"] === false) {
setLoginInfo("Error in input: " + responseJson.reason);
getEnvironments();
} else {
setLoginInfo("");
setModalOpen(false);
getEnvironments();
}
}),
)
.catch((error) => {
console.log("Error in userdata: ", error);
});
};
var localData = "";
const handleDeleteAccount = (userID) => {
if (userID === undefined || userID === null || userID === "") {
return;
}
const url = `${globalUrl}/api/v1/users/${userID}/remove`;
fetch(url, {
mode: "cors",
method: "DELETE",
credentials: "include",
crossDomain: true,
withCredentials: true,
headers: {
"Content-Type": "application/json",
},
})
.then((response) => response.json())
.then((data) => {
if (data.success) {
toast.success(
"Deleted their account. Would reload users in a few seconds.",
);
setTimeout(() => {
getUsers();
});
} else {
toast.error(`${data.reason}`);
}
})
.catch((error) => {
console.error(
"There was a problem with deleting the account. Please try again:",
error,
);
toast.error(
"There was a problem with the delete request. Please try again",
);
});
};
const getSchedules = () => {
fetch(globalUrl + "/api/v1/workflows/schedules", {
method: "GET",
headers: {
"Content-Type": "application/json",
Accept: "application/json",
},
credentials: "include",
})
.then((response) => {
if (response.status !== 200) {
console.log("Status not 200 for apps :O!");
return;
}
return response.json();
})
.then((responseJson) => {
setSchedules(responseJson);
})
.catch((error) => {
toast(error.toString());
});
};
const getAppAuthenticationGroups = () => {
//console.log("DEBUG: Skipping app auth group loading")
//return
fetch(globalUrl + "/api/v1/authentication/group", {
method: "GET",
headers: {
"Content-Type": "application/json",
Accept: "application/json",
},
credentials: "include",
})
.then((response) => {
if (response.status !== 200) {
console.log("Status not 200 for apps :O!");
return;
}
return response.json();
})
.then((responseJson) => {
if (responseJson.success === true) {
setAppAuthenticationGroups(responseJson.data);
}
})
.catch((error) => {
toast(error.toString());
});
};
const getAppAuthentication = () => {
fetch(globalUrl + "/api/v1/apps/authentication", {
method: "GET",
headers: {
"Content-Type": "application/json",
Accept: "application/json",
},
credentials: "include",
})
.then((response) => {
if (response.status !== 200) {
console.log("Status not 200 for apps :O!");
return;
}
return response.json();
})
.then((responseJson) => {
if (responseJson.success === true) {
//console.log(responseJson.data)
//console.log(responseJson)
setAuthentication(responseJson.data);
} else {
toast("Failed getting authentications");
}
})
.catch((error) => {
toast(error.toString());
});
};
const getEnvironments = () => {
fetch(globalUrl + "/api/v1/getenvironments", {
method: "GET",
headers: {
"Content-Type": "application/json",
Accept: "application/json",
},
credentials: "include",
})
.then((response) => {
if (response.status !== 200) {
console.log("Status not 200 for apps :O!");
return;
}
return response.json();
})
.then((responseJson) => {
setEnvironments(responseJson);
// Helper info for users in case they have a large queue and don't know about queue flushing
if (responseJson !== undefined && responseJson !== null && responseJson.length > 0) {
if (responseJson.length === 1 && responseJson[0].Type !== "cloud") {
setListItemExpanded(0)
}
for (var i = 0; i < responseJson.length; i++) {
const env = responseJson[i];
// Check if queuesize is too large
if (
env.queue !== undefined &&
env.queue !== null &&
env.queue > 100
) {
toast(
"Queue size for " +
env.name +
" is very large. We recommend you to reduce it by flushing the queue before continuing.",
);
break;
}
}
}
})
.catch((error) => {
toast(error.toString());
});
};
const getOrgs = () => {
// API no longer in use, as it's in handleInfo request
return;
fetch(globalUrl + "/api/v1/orgs", {
method: "GET",
headers: {
"Content-Type": "application/json",
Accept: "application/json",
},
credentials: "include",
})
.then((response) => {
if (response.status !== 200) {
console.log("Status not 200 for apps :O!");
return;
}
return response.json();
})
.then((responseJson) => {
setOrganizations(responseJson);
})
.catch((error) => {
toast(error.toString());
});
};
const getUsers = () => {
fetch(globalUrl + "/api/v1/getusers", {
method: "GET",
headers: {
"Content-Type": "application/json",
Accept: "application/json",
},
credentials: "include",
})
.then((response) => {
if (response.status !== 200) {
// Ahh, this happens because they're not admin
// window.location.pathname = "/workflows"
return;
}
return response.json();
})
.then((responseJson) => {
setUsers(responseJson);
})
.catch((error) => {
toast(error.toString());
});
};
const getSettings = () => {
fetch(globalUrl + "/api/v1/getsettings", {
method: "GET",
headers: {
"Content-Type": "application/json",
Accept: "application/json",
},
credentials: "include",
})
.then((response) => {
if (response.status !== 200) {
console.log("Status not 200 when getting settings :O!");
}
return response.json();
})
.then((responseJson) => {
setUserSettings(responseJson);
})
.catch((error) => {
console.log(error);
});
};
const views = {
0: "organization",
1: "users",
2: "app_auth",
3: "files",
4: "cache",
5: "triggers",
6: "locations",
7: "suborgs",
};
const admin_views = {
0: "organization",
1: "cloud_sync",
2: "priorities",
3: "billing",
4: "partner",
};
const setConfig = (event, inputValue) => {
const newValue = parseInt(inputValue);
setCurTab(newValue);
if (newValue === 1) {
document.title = "Shuffle - admin - users";
getUsers();
} else if (newValue === 2) {
document.title = "Shuffle - admin - app authentication";
getAppAuthentication();
getAppAuthenticationGroups();
} else if (newValue === 3) {
document.title = "Shuffle - admin - Files";
} else if (newValue === 4) {
document.title = "Shuffle - admin - Datastore";
//listOrgCache("3fd181b9-fb29-41b7-b2f5-15292265d420");
} else if (newValue === 5) {
document.title = "Shuffle - admin - schedules";
getSchedules();
} else if (newValue === 6) {
document.title = "Shuffle - admin - environments";
getEnvironments();
} else if (newValue === 7) {
document.title = "Shuffle - admin - orgs";
getOrgs();
} else {
document.title = "Shuffle - admin";
}
if (newValue === 8) {
console.log("Should get apps for categories.");
}
navigate(`/admin?tab=${views[newValue]}`);
setModalUser({});
};
if (firstRequest) {
setFirstRequest(false);
document.title = "Shuffle - admin";
getEnvironments();
if (!isCloud) {
getUsers();
} else {
getSettings();
}
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 adminTab = params["admin_tab"];
if (adminTab !== null && adminTab !== undefined) {
for (var key in Object.keys(admin_views)) {
const value = admin_views[key];
if (value === adminTab) {
setAdminTab(parseInt(key));
setConfig("", 0);
break;
}
}
} else {
const foundTab = params["tab"];
if (foundTab !== null && foundTab !== undefined) {
for (var key in Object.keys(views)) {
const value = views[key];
if (value === foundTab) {
setConfig("", key);
break;
}
}
}
}
}
}
if ( selectedOrganization.id === undefined && userdata !== undefined && userdata.active_org !== undefined && orgRequest) {
setOrgRequest(false);
handleGetOrg(userdata.active_org.id)
}
const paperStyle = {
maxWidth: 1250,
margin: "auto",
color: "white",
backgroundColor: theme.palette.surfaceColor,
marginBottom: 10,
padding: 20,
};
const changeModalData = (field, value) => {
modalUser[field] = value;
};
const setUser = (userId, field, value) => {
const data = { user_id: userId };
data[field] = value;
fetch(globalUrl + "/api/v1/users/updateuser", {
method: "PUT",
headers: {
"Content-Type": "application/json",
Accept: "application/json",
},
body: JSON.stringify(data),
credentials: "include",
})
.then((response) => {
if (response.status !== 200) {
console.log("Status not 200 for WORKFLOW EXECUTION :O!");
} else {
getUsers();
}
return response.json();
})
.then((responseJson) => {
if (!responseJson.success && responseJson.reason !== undefined) {
toast("Failed setting user: " + responseJson.reason);
} else if (responseJson.success === false) {
toast("Failed to update user");
} else {
//toast("Set the user field " + field + " to " + value);
toast("Successfully updated user field " + field);
if (field !== "suborgs") {
setSelectedUserModalOpen(false);
}
}
})
.catch((error) => {
console.log(error);
});
};
const generateApikey = (user) => {
const userId = user.id;
const data = { user_id: userId };
toast("Generating new API key");
var fetchdata = {
method: "POST",
headers: {
"Content-Type": "application/json",
Accept: "application/json",
},
credentials: "include",
};
if (userId === userdata.id) {
fetchdata.method = "GET";
} else {
fetchdata.body = JSON.stringify(data);
}
fetch(globalUrl + "/api/v1/generateapikey", fetchdata)
.then((response) => {
if (response.status !== 200) {
console.log("Status not 200 for WORKFLOW EXECUTION :O!");
} else {
getUsers();
}
return response.json();
})
.then((responseJson) => {
console.log("RESP: ", responseJson);
if (!responseJson.success && responseJson.reason !== undefined) {
toast("Failed getting new: " + responseJson.reason);
} else {
toast("Got new API key");
}
})
.catch((error) => {
console.log(error);
});
};
const editAuthenticationModal = selectedAuthenticationModalOpen ? (
<Dialog
open={selectedAuthenticationModalOpen}
onClose={() => {
setSelectedAuthenticationModalOpen(false);
}}
PaperProps={{
style: {
backgroundColor: theme.palette.surfaceColor,
color: "white",
minWidth: "800px",
minHeight: "320px",
},
}}
>
<DialogTitle>
<span style={{ color: "white" }}>
Edit authentication for{" "}
{selectedAuthentication.app.name.replaceAll("_", " ")} (
{selectedAuthentication.label})
</span>
<Typography
variant="body1"
color="textSecondary"
style={{ marginTop: 10 }}
>
You can <b>not</b> see the previous values for an authentication while
editing. This is to keep your data secure. You can overwrite one- or
multiple fields at a time.
</Typography>
</DialogTitle>
<DialogContent>
<Typography style={{ marginBottom: 0, marginTop: 10 }}>
Authentication Label
</Typography>
<TextField
style={{
backgroundColor: theme.palette.inputColor,
marginTop: 0,
}}
InputProps={{
style: {
height: 50,
color: "white",
},
}}
color="primary"
required
fullWidth={true}
placeholder={selectedAuthentication.label}
defaultValue={selectedAuthentication.label}
type="text"
margin="normal"
variant="outlined"
onChange={(e) => {
selectedAuthentication.label = e.target.value;
}}
/>
<Divider />
{selectedAuthentication.type === "oauth" ||
selectedAuthentication.type === "oauth2" ||
selectedAuthentication.type === "oauth2-app" ? (
<div>
<Typography
variant="body1"
color="textSecondary"
style={{ marginBottom: 0, marginTop: 10 }}
>
Only the name and url can be modified for Oauth2/OpenID connect.
Please remake the authentication if you want to change the other
fields like Client ID, Secret, Scopes etc.
</Typography>
</div>
) : null}
{selectedAuthentication.fields.map((data, index) => {
var fieldname = data.key.replaceAll("_", " ");
if (fieldname.endsWith(" basic")) {
fieldname = fieldname.substring(0, fieldname.length - 6);
}
if (
selectedAuthentication.type === "oauth" ||
selectedAuthentication.type === "oauth2" ||
selectedAuthentication.type === "oauth2-app"
) {
if (selectedAuthentication.fields[index].key !== "url") {
return null;
}
}
//console.log("DATA: ", data, selectedAuthentication)
return (
<div key={index}>
<Typography style={{ marginBottom: 0, marginTop: 10 }}>
{fieldname}
</Typography>
<TextField
style={{
backgroundColor: theme.palette.inputColor,
marginTop: 0,
}}
InputProps={{
style: {
height: 50,
color: "white",
},
}}
color="primary"
required
fullWidth={true}
placeholder={fieldname}
type="text"
id={`authentication-${index}`}
margin="normal"
variant="outlined"
onChange={(e) => {
authenticationFields[index].value = e.target.value;
setAuthenticationFields(authenticationFields);
}}
/>
</div>
);
})}
</DialogContent>
<DialogActions>
<Button
style={{ borderRadius: "0px" }}
onClick={() => setSelectedAuthenticationModalOpen(false)}
color="primary"
>
Cancel
</Button>
<Button
variant="contained"
style={{ borderRadius: "0px" }}
onClick={() => {
var error = false;
var fails = 0;
for (var key in authenticationFields) {
const item = authenticationFields[key];
if (item.value.length === 0) {
fails += 1;
console.log("ITEM: ", item);
//var currentnode = cy.getElementById(data.id)
var textfield = document.getElementById(
`authentication-${key}`,
);
if (textfield !== null && textfield !== undefined) {
console.log("HANDLE ERROR FOR KEY ", key);
}
error = true;
}
}
if (
selectedAuthentication.type === "oauth" ||
selectedAuthentication.type === "oauth2" ||
selectedAuthentication.type === "oauth2-app"
) {
selectedAuthentication.fields = [];
}
if (error && fails === authenticationFields.length) {
toast("Updating auth with new name only");
saveAuthentication(selectedAuthentication);
} else {
toast("Saving new version of this authentication");
selectedAuthentication.fields = authenticationFields;
saveAuthentication(selectedAuthentication);
}
}}
color="primary"
>
Submit
</Button>
</DialogActions>
</Dialog>
) : null;
const handleOrgEditChange = (event) => {
if (userdata.id === selectedUser.id) {
toast("Can't remove orgs from yourself");
return;
}
console.log("event: ", event.target.value);
setMatchingOrganizations(event.target.value);
// Workaround for empty orgs
if (event.target.value.length === 0) {
event.target.value.push("REMOVE");
}
setUser(selectedUser.id, "suborgs", event.target.value);
//setUser(selectedUser.id, "suborgs", matchingOrganizations)
};
const userOrgEdit =
selectedUser.id !== undefined &&
selectedUser.orgs !== undefined &&
selectedUser.orgs !== null &&
selectedOrganization.child_orgs !== undefined &&
selectedOrganization.child_orgs !== null &&
selectedOrganization.child_orgs.length > 0 ? (
<FormControl fullWidth sx={{ m: 1 }}>
<InputLabel id="demo-multiple-checkbox-label" style={{ padding: 5 }}>
Accessible Sub-Organizations (
{selectedUser.orgs ? selectedUser.orgs.length - 1 : 0})
</InputLabel>
<Select
fullWidth
style={{ width: "100%" }}
disabled={selectedUser.id === userdata.id}
labelId="demo-multiple-checkbox-label"
id="demo-multiple-checkbox"
multiple
value={matchingOrganizations}
onChange={handleOrgEditChange}
input={<OutlinedInput label="Tag" />}
renderValue={(selected) => {
return selected.join(", ");
}}
MenuProps={MenuProps}
>
{selectedOrganization.child_orgs.map((org, index) => (
<MenuItem key={index} value={org.id}>
<Checkbox checked={matchingOrganizations.indexOf(org.id) > -1} />
<ListItemText primary={org.name} />
</MenuItem>
))}
</Select>
</FormControl>
) : null;
const editUserModal = (
<Dialog
open={selectedUserModalOpen}
onClose={() => {
setSelectedUserModalOpen(false);
setImage2FA("");
setValue2FA("");
setSecret2FA("");
setShow2faSetup(false);
}}
PaperProps={{
style: {
backgroundColor: theme.palette.platformColor,
color: "white",
minWidth: "800px",
minHeight: "320px",
padding: 50,
},
}}
>
<DialogTitle style={{ maxWidth: 450, margin: "auto" }}>
<span style={{ color: "white" }}>
<EditIcon style={{ marginTop: 5 }} /> Editing {selectedUser.username}
</span>
</DialogTitle>
<DialogContent>
{isCloud ? null : (
<div style={{ display: "flex" }}>
<TextField
style={{
marginTop: 0,
backgroundColor: theme.palette.inputColor,
flex: 3,
marginRight: 10,
}}
InputProps={{
style: {
height: 50,
color: "white",
},
}}
color="primary"
required
fullWidth={true}
placeholder="New username"
type="text"
id="standard-required"
autoComplete="username"
margin="normal"
variant="outlined"
defaultValue={selectedUser.username}
onChange={(e) => {
setNewUsername(e.target.value);
}}
/>
<Button
style={{ maxHeight: 50, flex: 1 }}
variant="outlined"
color="primary"
disabled={selectedUser.role === "admin"}
onClick={() => {
setUser(selectedUser.id, "username", newUsername);
}}
>
Submit
</Button>
</div>
)}
{isCloud ? null : (
<div style={{ display: "flex" }}>
<TextField
style={{
marginTop: 0,
backgroundColor: theme.palette.inputColor,
flex: 3,
marginRight: 10,
}}
InputProps={{
style: {
height: 50,
color: "white",
},
}}
color="primary"
required
fullWidth={true}
placeholder="New password"
type="password"
id="standard-required"
autoComplete="password"
margin="normal"
variant="outlined"
onChange={(e) => setNewPassword(e.target.value)}
/>
<Button
style={{ maxHeight: 50, flex: 1 }}
variant="outlined"
color="primary"
disabled={selectedUser.role === "admin"}
onClick={() => onPasswordChange()}
>
Submit
</Button>
</div>
)}
{userOrgEdit}
<Divider
style={{
marginTop: 20,
marginBottom: 20,
backgroundColor: theme.palette.inputColor,
}}
/>
<div style={{ margin: "auto", maxWidth: 450 }}>
<Button
style={{}}
variant="outlined"
color="primary"
disabled={selectedUser.username === userdata.username}
onClick={() => {
deleteUser(selectedUser);
setSelectedUserModalOpen(false);
}}
>
{selectedUser.active ? "Delete from org" : "Delete from org"}
</Button>
<Button
style={{}}
variant="outlined"
color="primary"
disabled={
selectedUser.role === "admin" &&
selectedUser.username !== userdata.username
}
onClick={() => generateApikey(selectedUser)}
>
Renew API-key
</Button>
<Button
onClick={() => {
run2FASetup(userdata);
}}
disabled={
selectedUser.role === "admin" &&
selectedUser.username !== userdata.username
}
variant="outlined"
color="primary"
>
{selectedUser.mfa_info !== undefined &&
selectedUser.mfa_info !== null &&
selectedUser.mfa_info.active === true
? "Disable 2FA"
: "Enable 2FA"}
</Button>
{isCloud && userdata.support && selectedUser.id != userdata.id ? (
<Button
style={{
width: "100%",
height: 60,
marginTop: 50,
border: "1px solid #d52b2b",
textTransform: "none",
color:
showDeleteAccountTextbox === true &&
deleteAccountText.length > 0 &&
deleteAccountText === selectedUser.username
? "white"
: "#d52b2b",
backgroundColor:
showDeleteAccountTextbox === true &&
deleteAccountText.length > 0 &&
deleteAccountText === selectedUser.username
? "#d52b2b"
: "transparent",
}}
disabled={
showDeleteAccountTextbox === false
? false
: showDeleteAccountTextbox === true &&
deleteAccountText.length > 0 &&
deleteAccountText === selectedUser.username
? false
: true
}
onClick={() => {
if (
deleteAccountText.length > 0 &&
deleteAccountText === selectedUser.username
) {
console.log("Should delete: ", selectedUser.username);
handleDeleteAccount(selectedUser.id);
setShowDeleteAccountTextbox(false);
setDeleteAccountText("");
setSelectedUserModalOpen(false);
} else {
setShowDeleteAccountTextbox(true);
}
}}
>
Delete Account Permanently <br />
(support users - confirmation needed)
</Button>
) : null}
{showDeleteAccountTextbox ? (
<TextField
style={{
marginTop: 10,
}}
InputProps={{
style: {
height: 50,
color: "white",
},
}}
color="primary"
required
fullWidth={true}
label="Type the users' username"
placeholder="username@example.com"
value={deleteAccountText}
onChange={(e) => {
setDeleteAccountText(e.target.value);
}}
/>
) : null}
</div>
{show2faSetup ? (
<div
style={{
margin: "auto",
maxWidth: 300,
minWidth: 300,
marginTop: 25,
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
}}
>
{secret2FA !== undefined &&
secret2FA !== null &&
secret2FA.length > 0 ? (
<span>
<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.
</Typography>
</span>
) : null}
{image2FA !== undefined &&
image2FA !== null &&
image2FA.length > 0 ? (
<img
alt="2 factor img"
src={image2FA}
style={{
margin: "15px auto",
maxHeight: 200,
maxWidth: 200,
display: 'block',
}}
/>
) : (
<CircularProgress />
)}
<Typography variant="body2" color="textSecondary">
After scanning the QR code image, the app will display a code that
you can enter below.
</Typography>
<div style={{ display: "flex", width: '100%', marginTop: 10 }}>
<TextField
color="primary"
style={{
flex: 2,
backgroundColor: theme.palette.inputColor,
marginRight: 10,
}}
InputProps={{
style: {
height: 50,
color: "white",
fontSize: "1em",
},
maxLength: 6,
}}
required
fullWidth={true}
id="2fa_key"
margin="normal"
placeholder="6-digit code"
variant="outlined"
onChange={(event) => {
if (event.target.value.length > 6) {
return;
}
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, false);
}}
color="primary"
>
Submit
</Button>
</div>
</div>
) : null}
</DialogContent>
</Dialog>
);
const GridItem = (props) => {
const [expanded, setExpanded] = React.useState(false);
const [showEdit, setShowEdit] = React.useState(false);
const [newValue, setNewValue] = React.useState(-100);
const primary = props.data.primary;
const secondary = props.data.secondary;
const primaryIcon = props.data.icon;
const secondaryIcon = props.data.active ? (
<CheckCircleIcon style={{ color: "green" }} />
) : (
<CloseIcon style={{ color: "red" }} />
);
const submitFeatureEdit = (sync_features) => {
if (!userdata.support) {
console.log(
"User does not have support access and can't edit features",
);
return;
}
sync_features.editing = true;
const data = {
org_id: selectedOrganization.id,
sync_features: sync_features,
};
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) => {
if (responseJson["success"] === false) {
toast("Failed updating org: ", responseJson.reason);
} else {
toast("Successfully edited org!");
}
}),
)
.catch((error) => {
toast("Err: " + error.toString());
});
};
const enableFeature = () => {
console.log("Enabling " + primary);
console.log(selectedOrganization.sync_features);
// Check if primary is in sync_features
var tmpprimary = primary.replaceAll(" ", "_");
if (!(tmpprimary in selectedOrganization.sync_features)) {
console.log("Primary not in sync_features: " + tmpprimary);
return;
}
if (props.data.active) {
selectedOrganization.sync_features[tmpprimary].active = false;
} else {
selectedOrganization.sync_features[tmpprimary].active = true;
}
setSelectedOrganization(selectedOrganization);
forceUpdate(Math.random());
submitFeatureEdit(selectedOrganization.sync_features);
};
const submitEdit = (e) => {
e.preventDefault();
e.stopPropagation();
// Check if primary is in sync_features
var tmpprimary = primary.replaceAll(" ", "_");
if (!(tmpprimary in selectedOrganization.sync_features)) {
console.log("Primary not in sync_features: " + tmpprimary);
return;
}
// Make it into a number
var tmp = parseInt(newValue);
if (isNaN(tmp)) {
console.log("Not a number: " + newValue);
return;
}
selectedOrganization.sync_features[tmpprimary].limit = tmp;
setSelectedOrganization(selectedOrganization);
forceUpdate(Math.random());
submitFeatureEdit(selectedOrganization.sync_features);
};
return (
<Grid item xs={4}>
<Card
style={{
margin: 4,
backgroundColor: theme.palette.platformColor,
borderRadius: theme.palette?.borderRadius,
border: "1px solid rgba(255,255,255,0.3)",
color: "white",
minHeight: expanded ? 250 : "inherit",
maxHeight: expanded ? 300 : "inherit",
}}
>
<ListItem
style={{ cursor: "pointer" }}
onClick={() => {
setExpanded(!expanded);
}}
>
<ListItemAvatar>
<Avatar>{primaryIcon}</Avatar>
</ListItemAvatar>
<ListItemText
style={{ textTransform: "capitalize" }}
primary={props.data.newname ? props.data.newname : primary}
/>
{isCloud && userdata.support === true ? (
<Tooltip title="Edit features (support users only)">
<EditIcon
color="secondary"
style={{ marginRight: 10, cursor: "pointer" }}
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
if (showEdit) {
setShowEdit(false);
return;
}
console.log("Edit");
setShowEdit(true);
}}
/>
</Tooltip>
) : null}
<Tooltip
title={props.data.active ? "Disable feature" : "Enable feature"}
>
<span
style={{ cursor: "pointer", marginTop: 5 }}
onClick={(e) => {
if (!isCloud || userdata.support !== true) {
return;
}
e.preventDefault();
e.stopPropagation();
enableFeature();
}}
>
{secondaryIcon}
</span>
</Tooltip>
</ListItem>
{expanded ? (
<div style={{ padding: 15 }}>
<Typography>
<b>Usage:&nbsp;</b>
{props.data.limit === 0 ? (
"Unlimited"
) : (
<span>
{props.data.usage} /{" "}
{props.data.limit === "" ? "Unlimited" : props.data.limit}
</span>
)}
</Typography>
{/*<Typography>
Data sharing: {props.data.data_collection}
</Typography>*/}
<Typography
style={{
maxHeight: 150,
overflowX: "hidden",
overflowY: "auto",
}}
>
<b>Description:</b> {secondary}
</Typography>
</div>
) : null}
{showEdit ? (
<FormControl
fullWidth
onSubmit={(e) => {
console.log("Submit");
submitEdit(e);
}}
>
<span style={{ display: "flex" }}>
<TextField
style={{ flex: 3 }}
color="primary"
label={"Edit value"}
defaultValue={props.data.limit}
style={{}}
onChange={(event) => {
setNewValue(event.target.value);
}}
/>
<Button
style={{ flex: 1 }}
variant="contained"
disabled={newValue < -1}
onClick={(e) => {
console.log("Submit 2");
submitEdit(e);
}}
>
Submit
</Button>
</span>
</FormControl>
) : null}
</Card>
</Grid>
);
};
const itemColor = "black";
var syncList = [
{
primary: "Workflows",
secondary: "",
active: true,
icon: <PolylineIcon style={{ color: itemColor }} />,
},
{
primary: "Apps",
secondary: "",
active: true,
icon: <AppsIcon style={{ color: itemColor }} />,
},
{
primary: "Organization",
secondary: "",
active: true,
icon: <BusinessIcon style={{ color: itemColor }} />,
},
];
const cloudSyncModal = (
<Dialog
open={cloudSyncModalOpen}
onClose={() => {
setCloudSyncModalOpen(false);
}}
PaperProps={{
style: {
backgroundColor: theme.palette.surfaceColor,
color: "white",
minWidth: "800px",
minHeight: "320px",
},
}}
>
<DialogTitle>
<span style={{ color: "white" }}>Enable cloud features</span>
</DialogTitle>
<DialogContent color="textSecondary">
What does{" "}
<a
href="/docs/organizations#cloud_sync"
target="_blank"
rel="noopener noreferrer"
style={{ textDecoration: "none", color: "#f85a3e" }}
>
cloud sync
</a>{" "}
do?
<div style={{ display: "flex", marginBottom: 20 }}>
<TextField
color="primary"
style={{
backgroundColor: theme.palette.inputColor,
marginRight: 10,
}}
InputProps={{
style: {
height: "50px",
color: "white",
fontSize: "1em",
},
}}
required
fullWidth={true}
disabled={selectedOrganization.cloud_sync}
autoComplete="cloud apikey"
id="apikey_field"
margin="normal"
placeholder="Cloud Apikey"
variant="outlined"
onChange={(event) => {
setCloudSyncApikey(event.target.value);
}}
/>
<Button
disabled={
(!selectedOrganization.cloud_sync &&
cloudSyncApikey.length === 0) ||
loading
}
style={{ marginLeft: 15, height: 50, borderRadius: "0px" }}
onClick={() => {
setLoading(true);
enableCloudSync(
cloudSyncApikey,
selectedOrganization,
selectedOrganization.cloud_sync,
);
}}
color="primary"
variant={
selectedOrganization.cloud_sync === true
? "outlined"
: "contained"
}
>
{selectedOrganization.cloud_sync ? "Stop sync" : "Start sync"}
</Button>
</div>
{orgSyncResponse.length > 0 ? (
<Typography style={{ marginTop: 5, marginBottom: 10 }}>
Error: {orgSyncResponse}
</Typography>
) : null}
<Grid container style={{ width: "100%", marginBottom: 15 }}>
{syncList.map((data, index) => {
return <GridItem key={index} data={data} />;
})}
</Grid>
* New triggers (userinput, hotmail realtime)
<div />
* Execute in the cloud rather than onprem
<div />
* Apps can be built in the cloud
<div />
* Easily share apps and workflows
<div />* Access to powerful cloud search
</DialogContent>
</Dialog>
);
var regiontag = "eu";
if (
userdata.region_url !== undefined &&
userdata.region_url !== null &&
userdata.region_url.length > 0
) {
const regionsplit = userdata.region_url.split(".");
if (regionsplit.length > 2 && !regionsplit[0].includes("shuffler")) {
const namesplit = regionsplit[0].split("/");
regiontag = namesplit[namesplit.length - 1];
if (regiontag === "california") {
regiontag = "us";
}
}
}
const organizationView =
curTab === 0 && selectedOrganization.id !== undefined ? (
<div style={{ position: "relative" }}>
<div style={{ marginTop: 20, marginBottom: 20 }}>
<h2 style={{ display: "inline" }}>Organization overview</h2>
<Typography
variant="body1"
color="textSecondary"
style={{ marginLeft: 0 }}
>
On this page organization admins can configure organizations, and
sub-orgs (MSSP).{" "}
<a
target="_blank"
rel="noopener noreferrer"
href="/docs/organizations#organization"
style={{ textDecoration: "none", color: "#f85a3e" }}
>
Learn more
</a>
</Typography>
</div>
{selectedOrganization.id === undefined ? (
<div
style={{
paddingTop: 250,
width: 250,
margin: "auto",
textAlign: "center",
}}
>
<CircularProgress />
<Typography>Loading Organization</Typography>
</div>
) : (
<div>
{/*
<Tooltip
title={"Go to Organization document"}
style={{}}
aria-label={"Organization doc"}
>
<IconButton
style={{ top: -10, right: 50, position: "absolute" }}
onClick={() => {
console.log("Should go to icon")
}}
>
<FileCopyIcon style={{ color: "rgba(255,255,255,0.8)" }} />
</IconButton>
</Tooltip>
*/}
{userdata.support === true ? (
<span
style={{
display: "flex",
top: -10,
right: -50,
position: "absolute",
}}
>
{/*<a href={mailsendingButton(selectedOrganization)} target="_blank" rel="noopener noreferrer" style={{textDecoration: "none"}} disabled={selectedStatus.length !== 0}>*/}
<Button
variant="outlined"
color="primary"
disabled={selectedStatus.length !== 0}
style={{ minWidth: 80, maxWidth: 80, height: "100%" }}
onClick={() => {
console.log(
"Should send mail to admins of org with context",
);
handleStatusChange({ target: { value: ["contacted"] } });
// Open a new tab
window.open(
mailsendingButton(selectedOrganization),
"_blank",
);
}}
>
Sales mail
</Button>
<FormControl sx={{ m: 1, width: 300 }} style={{}}>
<InputLabel id="">Status</InputLabel>
<Select
style={{ minWidth: 150, maxWidth: 150, }}
labelId="multiselect-status"
id="multiselect-status"
multiple
value={selectedStatus}
onChange={handleStatusChange}
input={<OutlinedInput label="Status" />}
renderValue={(selected) => selected.join(", ")}
MenuProps={MenuProps}
>
{[
"testing shuffle",
"contacted",
"lead",
//"demo done",
"pov",
"customer",
"open source",
"student",
"internal",
"creator",
"tech partner",
"old customer",
"old lead",
].map((name) => (
<MenuItem key={name} value={name}>
<Checkbox checked={selectedStatus.indexOf(name) > -1} />
<ListItemText primary={name} />
</MenuItem>
))}
</Select>
</FormControl>
</span>
) : null}
{isCloud ? (
<Tooltip
title={`Your organization is in ${regiontag}. Click to change!`}
style={{}}
>
<Avatar
style={{
cursor: "pointer",
top: -10,
right: 50,
position: "absolute",
}}
onClick={() => {
if (userdata.support === false) {
toast(
"Region change is not directly implemented yet, and requires support help.",
);
if (window.drift !== undefined) {
window.drift.api.startInteraction({
interactionId: 386411,
});
}
} else {
// Show region change modal
console.log("Should open region change modal");
setRegionChangeModalOpen(true);
}
}}
>
{regiontag}
</Avatar>
</Tooltip>
) : null}
<RegionChangeModal />
<Tooltip
title={"Copy Organization ID"}
style={{}}
aria-label={"Copy orgid"}
>
<IconButton
style={{ top: -10, right: 0, position: "absolute" }}
onClick={() => {
const elementName = "copy_element_shuffle";
const org_id = selectedOrganization.id;
var copyText = document.getElementById(elementName);
if (copyText !== null && copyText !== undefined) {
const clipboard = navigator.clipboard;
if (clipboard === undefined) {
toast("Can only copy over HTTPS (port 3443)");
return;
}
navigator.clipboard.writeText(org_id);
copyText.select();
copyText.setSelectionRange(
0,
99999,
); /* For mobile devices */
/* Copy the text inside the text field */
document.execCommand("copy");
toast(org_id + " copied to clipboard");
}
}}
>
<FileCopyIcon style={{ color: "rgba(255,255,255,0.8)" }} />
</IconButton>
</Tooltip>
{selectedOrganization.defaults !== undefined &&
selectedOrganization.defaults.documentation_reference !==
undefined &&
selectedOrganization.defaults.documentation_reference !== null &&
selectedOrganization.defaults.documentation_reference.includes(
"http",
) ? (
<Tooltip
title={"Open Organization Documentation"}
style={{ top: -10, right: 50, position: "absolute" }}
aria-label={"Open org docs"}
>
<a
href={selectedOrganization.defaults.documentation_reference}
target="_blank"
style={{ textDecoration: "none" }}
rel="noopener noreferrer"
>
<IconButton
style={{ top: -10, right: 50, position: "absolute" }}
>
<DescriptionIcon
style={{ color: "rgba(255,255,255,0.8)" }}
/>
</IconButton>
</a>
</Tooltip>
) : null}
{selectedOrganization.name.length > 0 ? (
<OrgHeader
isCloud={isCloud}
userdata={userdata}
setSelectedOrganization={setSelectedOrganization}
globalUrl={globalUrl}
selectedOrganization={selectedOrganization}
adminTab={adminTab}
handleEditOrg={handleEditOrg}
/>
) : (
<div
style={{
paddingTop: 250,
width: 250,
margin: "auto",
textAlign: "center",
}}
>
<CircularProgress />
<Typography>Loading Organization</Typography>
</div>
)}
<Tabs
id="admin_tabs"
value={adminTab}
indicatorColor="primary"
textColor="secondary"
style={{ marginTop: 50 }}
onChange={(event, inputValue) => {
const newValue = parseInt(inputValue);
setAdminTab(newValue);
//const setConfig = (event, inputValue) => {
navigate(`/admin?admin_tab=${admin_views[newValue]}`);
}}
aria-label="disabled tabs example"
>
<Tab label=<span>Edit Details</span> />
<Tab label=<span>Limits & Cloud Sync</span> />
<Tab label=<span>Notifications</span> />
<Tab label=<span>Billing & Stats</span> />
<Tab disabled={!isCloud} label=<span>Partner</span> />
</Tabs>
<Divider
style={{
marginTop: 20,
marginBottom: 20,
backgroundColor: theme.palette.inputColor,
}}
/>
{adminTab === 0 ? (
<OrgHeaderexpanded
isCloud={isCloud}
userdata={userdata}
setSelectedOrganization={setSelectedOrganization}
globalUrl={globalUrl}
selectedOrganization={selectedOrganization}
adminTab={adminTab}
/>
) : adminTab === 1 ? (
<div>
<Typography
variant="h6"
style={{ marginBottom: "10px", color: "white" }}
>
Cloud syncronization
</Typography>
What does{" "}
<a
href="/docs/organizations#cloud_sync"
target="_blank"
rel="noopener noreferrer"
style={{ textDecoration: "none", color: "#f85a3e" }}
>
cloud sync
</a>{" "}
do? Cloud syncronization is a way of getting more out of
Shuffle. Shuffle will <b>ALWAYS</b> make every option open
source, but features relying on other users can't be done
without a collaborative approach.
{isCloud ? (
<div style={{ marginTop: 15, display: "flex" }}>
<div style={{ flex: 1 }}>
<Typography style={{}}>
Currently syncronizing:{" "}
{selectedOrganization.cloud_sync_active === true
? "True"
: "False"}
</Typography>
{selectedOrganization.cloud_sync_active ? (
<Typography style={{}}>
Syncronization interval:{" "}
{selectedOrganization.sync_config.interval === 0
? "60"
: selectedOrganization.sync_config.interval}
</Typography>
) : null}
<Typography
style={{
whiteSpace: "nowrap",
marginTop: 25,
marginRight: 10,
}}
>
Your Apikey
</Typography>
<div style={{ display: "flex" }}>
<TextField
color="primary"
style={{
backgroundColor: theme.palette.inputColor,
maxWidth: 500,
}}
InputProps={{
style: {
height: "50px",
color: "white",
fontSize: "1em",
},
endAdornment: (
<InputAdornment position="end">
<IconButton
aria-label="toggle password visibility"
onClick={() => {
setShowApiKey(!showApiKey);
}}
>
{showApiKey ? (
<VisibilityIcon />
) : (
<VisibilityOffIcon />
)}
</IconButton>
</InputAdornment>
),
}}
required
fullWidth={true}
disabled={true}
autoComplete="cloud apikey"
id="apikey_field"
margin="normal"
placeholder="Cloud Apikey"
variant="outlined"
defaultValue={userSettings.apikey}
type={!isCloud || showApiKey ? "text" : "password"}
/>
{selectedOrganization.cloud_sync_active ? (
<Button
style={{
width: 150,
height: 50,
marginLeft: 10,
marginTop: 17,
}}
variant={
selectedOrganization.cloud_sync_active === true
? "outlined"
: "contained"
}
color="primary"
onClick={() => {
handleStopOrgSync(selectedOrganization.id);
}}
>
Stop Sync
</Button>
) : null}
</div>
</div>
</div>
) : (
<div>
<div style={{ display: "flex", marginBottom: 20 }}>
<TextField
color="primary"
style={{
backgroundColor: theme.palette.inputColor,
marginRight: 10,
}}
InputProps={{
style: {
height: "50px",
color: "white",
fontSize: "1em",
},
}}
required
fullWidth={true}
disabled={selectedOrganization.cloud_sync}
autoComplete="cloud apikey"
id="apikey_field"
margin="normal"
placeholder="Cloud Apikey"
variant="outlined"
onChange={(event) => {
setCloudSyncApikey(event.target.value);
}}
/>
<Button
disabled={
(!selectedOrganization.cloud_sync &&
cloudSyncApikey.length === 0) ||
loading
}
style={{ marginTop: 15, height: 50, width: 150 }}
onClick={() => {
setLoading(true);
enableCloudSync(
cloudSyncApikey,
selectedOrganization,
selectedOrganization.cloud_sync,
);
}}
color="primary"
variant={
selectedOrganization.cloud_sync === true
? "outlined"
: "contained"
}
>
{selectedOrganization.cloud_sync
? "Stop sync"
: "Start sync"}
</Button>
</div>
{orgSyncResponse.length > 0 ? (
<Typography style={{ marginTop: 5, marginBottom: 10 }}>
Message from Shuffle Cloud: <b>{orgSyncResponse}</b>
</Typography>
) : null}
</div>
)}
<Typography
variant="h6"
style={{ marginLeft: 5, marginTop: 40, marginBottom: 5 }}
>
Features
</Typography>
<Typography
variant="body2"
color="textSecondary"
style={{ marginBottom: 10, marginLeft: 5 }}
>
Features and Limitations that are currently available to you
in your Cloud or Hybrid Organization. App Executions (App
Runs) reset monthly. If the organization is a customer or in a
trial, these features limitations are not always enforced.
</Typography>
<Grid
container
style={{
width: "100%",
marginBottom: 15,
paddingBottom: 150,
}}
>
{selectedOrganization.sync_features === undefined ||
selectedOrganization.sync_features === null
? null
: Object.keys(selectedOrganization.sync_features).map(
function (key, index) {
if (
key === "schedule" ||
key === "apps" ||
key === "updates" ||
key === "editing"
) {
return null;
}
const item = selectedOrganization.sync_features[key];
if (item === null || item === undefined) {
return null;
}
const newkey = key.replaceAll("_", " ");
// Name rewrites as these are structs
var newname = "";
if (newkey.toLowerCase() === "shuffle gpt") {
newname = "Shuffle AI";
}
const griditem = {
primary: newkey,
secondary:
item.description === undefined ||
item.description === null ||
item.description.length === 0
? "Not defined yet"
: item.description,
limit: item.limit,
usage:
item.usage === undefined || item.usage === null
? 0
: item.usage,
data_collection: "None",
active: item.active,
icon: <PolylineIcon style={{ color: itemColor }} />,
newname: newname,
};
return (
<Zoom key={index}>
<GridItem data={griditem} />
</Zoom>
);
},
)}
</Grid>
</div>
) : adminTab === 2 ? (
<Priorities
isCloud={isCloud}
userdata={userdata}
adminTab={adminTab}
globalUrl={globalUrl}
checkLogin={checkLogin}
setAdminTab={setAdminTab}
setCurTab={setCurTab}
notifications={notifications}
setNotifications={setNotifications}
/>
) : adminTab === 3 ? (
<Billing
isCloud={isCloud}
userdata={userdata}
setSelectedOrganization={setSelectedOrganization}
globalUrl={globalUrl}
selectedOrganization={selectedOrganization}
adminTab={adminTab}
billingInfo={billingInfo}
selectedOrganization={selectedOrganization}
stripeKey={props.stripeKey}
handleGetOrg={handleGetOrg}
/>
) : adminTab === 4 ? (
<Branding
isCloud={isCloud}
userdata={userdata}
adminTab={adminTab}
globalUrl={globalUrl}
handleGetOrg={handleGetOrg}
selectedOrganization={selectedOrganization}
setSelectedOrganization={setSelectedOrganization}
/>
) : null}
<Divider
style={{
marginTop: 20,
marginBottom: 20,
backgroundColor: theme.palette.inputColor,
}}
/>
</div>
)}
</div>
) : null;
const modalView = (
<Dialog
open={modalOpen}
onClose={() => {
setModalOpen(false);
}}
PaperProps={{
style: {
backgroundColor: theme.palette.surfaceColor,
color: "white",
minWidth: "800px",
minHeight: "320px",
},
}}
>
<DialogTitle>
<span style={{ color: "white" }}>
{curTab === 1
? "Add user"
: curTab === 7
? "Add Sub-Organization"
: "Add environment"}
</span>
</DialogTitle>
<DialogContent>
{curTab === 1 && isCloud ? (
<Typography variant="body1" style={{ marginBottom: 10 }}>
We will send an email to invite them to your organization.
</Typography>
) : curTab === 7 ? (
<Typography variant="body1" style={{ marginBottom: 10 }}>
The organization created will become a child of your current
organization, and be available to you.
</Typography>
) : null}
{curTab === 1 ? (
<div>
Username
<TextField
color="primary"
style={{ backgroundColor: theme.palette.inputColor }}
autoFocus
InputProps={{
style: {
height: "50px",
color: "white",
fontSize: "1em",
},
}}
required
fullWidth={true}
autoComplete="username"
placeholder="username@example.com"
id="emailfield"
margin="normal"
variant="outlined"
onChange={(event) =>
changeModalData("Username", event.target.value)
}
/>
{isCloud ? null : (
<span>
Password
<TextField
color="primary"
style={{ backgroundColor: theme.palette.inputColor }}
InputProps={{
style: {
height: "50px",
color: "white",
fontSize: "1em",
},
}}
required
fullWidth={true}
autoComplete="password"
type="password"
placeholder="********"
id="pwfield"
margin="normal"
variant="outlined"
onChange={(event) =>
changeModalData("Password", event.target.value)
}
/>
</span>
)}
</div>
) : curTab === 7 ? (
<div>
Name
<TextField
color="primary"
style={{ backgroundColor: theme.palette.inputColor }}
autoFocus
InputProps={{
style: {
height: "50px",
color: "white",
fontSize: "1em",
},
}}
required
fullWidth={true}
placeholder={`${selectedOrganization.name} Copycat Inc.`}
id="orgname"
margin="normal"
variant="outlined"
onChange={(event) => {
setOrgName(event.target.value);
}}
/>
</div>
) : curTab === 6 ? (
<div>
Environment Name
<TextField
color="primary"
style={{ backgroundColor: theme.palette.inputColor }}
autoFocus
InputProps={{
style: {
height: "50px",
color: "white",
fontSize: "1em",
},
}}
required
fullWidth={true}
placeholder="datacenter froglantern"
id="environment_name"
margin="normal"
variant="outlined"
onChange={(event) =>
changeModalData("environment", event.target.value)
}
/>
</div>
) : null}
{loginInfo}
</DialogContent>
<DialogActions>
<Button
style={{ borderRadius: "0px" }}
onClick={() => setModalOpen(false)}
color="primary"
>
Cancel
</Button>
<Button
variant="contained"
style={{ borderRadius: "0px" }}
onClick={() => {
if (curTab === 1) {
if (isCloud) {
inviteUser(modalUser);
} else {
submitUser(modalUser);
}
} else if (curTab === 7) {
createSubOrg(selectedOrganization.id, orgName);
} else if (curTab === 6) {
submitEnvironment(modalUser);
}
}}
color="primary"
>
Submit
</Button>
</DialogActions>
</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={{ 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>
<Divider
style={{
marginTop: 20,
marginBottom: 20,
backgroundColor: theme.palette.inputColor,
}}
/>
{logsViewModal ? (
<Dialog
open={logsViewModal}
onClose={() => {
setLogsViewModal(false);
}}
PaperProps={{
style: {
backgroundColor: theme.palette.surfaceColor,
color: "white",
minWidth: "1200px",
minHeight: "320px",
},
}}
>
<DialogTitle>
<span style={{ color: "white" }}>User Logs</span>
</DialogTitle>
<DialogContent>
{/* ask user for which IP they want to see logs for by iterating of user.login_info */}
<FormControl fullWidth>
<InputLabel
style={{ size: 10 }}
id="user-ip-simple-select-label"
>
User IP
</InputLabel>
<Select
labelId="user-ip-simple-select-label"
id="user-ip-simple-select"
onChange={(event) => {
setIpSelected(event.target.value);
getLogs(event.target.value, userLogViewing.id);
}}
>
{(() => {
const uniqueIPs = new Set();
return userLogViewing.login_info.map((data, index) => {
if (
data.ip.includes("127.0.0.1") ||
uniqueIPs.has(data.ip)
) {
return null;
}
uniqueIPs.add(data.ip);
return (
<MenuItem key={index} value={data.ip}>
{data.ip}
</MenuItem>
);
});
})()}
</Select>
</FormControl>
{logsLoading && ipSelected.length !== 0 ? (
<div
style={{
marginTop: 20,
marginBottom: 20,
display: "flex",
alignItems: "center",
}}
>
<CircularProgress style={{ marginRight: 10 }} />
<Typography>Loading logs</Typography>
</div>
) : null}
<List>
<ListItem>
<ListItemText
primary={
"Timestamp"
}
style={{
minWidth: 200,
maxWidth: 200,
}}
/>
<ListItemText
primary={"Referer"}
style={{
minWidth: 300,
maxWidth: 300,
overflow: "hidden",
}}
/>
<ListItemText
primary={"URL"}
style={{
minWidth: 700,
maxWidth: 700,
overflow: "hidden",
marginLeft: 10,
}}
/>
</ListItem>
{logs.map((data, index) => {
//console.log("LOG: ", data)
return (
// redirect user to logs
// using request id or trace id
<ListItem
key={index}
style={{
backgroundColor: index % 2 === 0 ? "#1f2023" : "#27292d",
}}
>
<ListItemText
primary={new Date(
data.timestamp * 1000,
).toLocaleString("en-US", {
year: "numeric",
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
hour12: false,
})}
style={{
minWidth: 200,
maxWidth: 200,
}}
/>
<ListItemText
primary={data.referer}
style={{
minWidth: 300,
maxWidth: 300,
overflow: "hidden",
}}
/>
<ListItemText
primary={data.url}
style={{
minWidth: 700,
maxWidth: 700,
overflow: "hidden",
marginLeft: 10,
}}
/>
</ListItem>
)})}
</List>
</DialogContent>
</Dialog>
) : null}
<List>
<ListItem>
<ListItemText
primary="Username"
style={{ minWidth: 350, maxWidth: 350 }}
/>
<ListItemText
primary="Role"
style={{ minWidth: 150, maxWidth: 150 }}
/>
<ListItemText
primary="Active"
style={{ minWidth: 100, maxWidth: 100, marginLeft: 5 }}
/>
<ListItemText
primary="Type"
style={{ minWidth: 100, maxWidth: 100 }}
/>
<ListItemText
primary="MFA"
style={{ minWidth: 100, maxWidth: 100 }}
/>
{selectedOrganization.child_orgs !== undefined &&
selectedOrganization.child_orgs !== null &&
selectedOrganization.child_orgs.length > 0 ? (
<ListItemText
primary="Suborgs"
style={{ minWidth: 100, maxWidth: 100 }}
/>
) : null}
<ListItemText
primary="Actions"
style={{ minWidth: 100, maxWidth: 100 }}
/>
<ListItemText
primary="Last Login"
style={{ minWidth: 150, maxWidth: 150 }}
/>
</ListItem>
{users === undefined || users === null
? null
: users.map((data, index) => {
var bgColor = "#27292d";
if (index % 2 === 0) {
bgColor = "#1f2023";
}
const timeNow = new Date().getTime();
// Get the highest timestamp in data.login_info
var lastLogin = "N/A";
if (data.login_info !== undefined && data.login_info !== null) {
var loginInfo = 0;
for (var i = 0; i < data.login_info.length; i++) {
if (data.login_info[i].timestamp > loginInfo) {
loginInfo = data.login_info[i].timestamp;
}
}
if (loginInfo > 0) {
lastLogin =
new Date(loginInfo * 1000).toISOString().slice(0, 10) +
" (" +
data.login_info.length +
")";
}
}
var userData = data.username;
if (userdata.support === true) {
userData = (
<a
style={{
cursor: "pointer",
textDecoration: "underline",
textDecorationColor: "#F76742",
color: "#F76742",
}}
onClick={() => {
setLogsViewModal(true);
setUserLogViewing(data);
if (userLogViewing.login_info !== undefined && userLogViewing.login_info !== null && userLogViewing.login_info.length > 0) {
getLogs(userLogViewing.login_info[0].ip, userLogViewing.id)
setIpSelected(userLogViewing.login_info[0].ip);
}
}}
>
{data.username}
</a>
);
}
return (
<ListItem key={index} style={{ backgroundColor: bgColor }}>
<ListItemText
primary={userData}
style={{
minWidth: 350,
maxWidth: 350,
overflow: "hidden",
}}
/>
<ListItemText
primary={
<Select
SelectDisplayProps={{
style: {
marginLeft: 10,
},
}}
value={data.role}
fullWidth
onChange={(e) => {
console.log("VALUE: ", e.target.value);
setUser(data.id, "role", e.target.value);
}}
style={{
backgroundColor: theme.palette.surfaceColor,
color: "white",
height: "50px",
}}
>
<MenuItem
style={{
backgroundColor: theme.palette.inputColor,
color: "white",
}}
value={"admin"}
>
Org Admin
</MenuItem>
<MenuItem
style={{
backgroundColor: theme.palette.inputColor,
color: "white",
}}
value={"user"}
>
Org User
</MenuItem>
<MenuItem
style={{
backgroundColor: theme.palette.inputColor,
color: "white",
}}
value={"org-reader"}
>
Org Reader
</MenuItem>
</Select>
}
style={{ minWidth: 135, maxWidth: 135, marginRight: 15 }}
/>
<ListItemText
primary={data.active ? "True" : "False"}
style={{ minWidth: 100, maxWidth: 100 }}
/>
<ListItemText
primary={
data.login_type === undefined ||
data.login_type === null ||
data.login_type.length === 0
? "Normal"
: data.login_type
}
style={{ minWidth: 100, maxWidth: 100 }}
/>
<ListItemText
primary={
data.mfa_info !== undefined &&
data.mfa_info !== null &&
data.mfa_info.active === true
? "Active"
: "Inactive"
}
style={{ minWidth: 100, maxWidth: 100 }}
/>
{selectedOrganization.child_orgs !== undefined &&
selectedOrganization.child_orgs !== null &&
selectedOrganization.child_orgs.length > 0 ? (
<ListItemText
style={{ display: "flex" }}
primary={
data.orgs === undefined || data.orgs === null
? 0
: data.orgs.length - 1
}
/>
) : null}
<ListItemText
style={{ display: "flex", minWidth: 100, maxWidth: 100 }}
>
<IconButton
onClick={() => {
setSelectedUserModalOpen(true);
setSelectedUser(data);
// Find matching orgs between current org and current user's access to those orgs
if (
userdata.orgs !== undefined &&
userdata.orgs !== null &&
userdata.orgs.length > 0 &&
selectedOrganization.child_orgs !== undefined &&
selectedOrganization.child_orgs !== null &&
selectedOrganization.child_orgs.length > 0
) {
var active = [];
for (var key in userdata.orgs) {
const found =
selectedOrganization.child_orgs.find(
(item) => item.id === userdata.orgs[key].id,
);
if (found !== null && found !== undefined) {
if (
data.orgs === undefined ||
data.orgs === null
) {
continue;
}
const subfound = data.orgs.find(
(item) => item === found.id,
);
if (
subfound !== null &&
subfound !== undefined
) {
active.push(subfound);
}
}
}
setMatchingOrganizations(active);
}
}}
>
<EditIcon color="primary" />
</IconButton>
{/*<Button
onClick={() => {
generateApikey(data)
}}
disabled={data.role === "admin" && data.username !== userdata.username}
variant="outlined"
color="primary"
>
New apikey
</Button>*/}
</ListItemText>
<ListItemText
style={{ minWidth: 150, maxWidth: 150 }}
primary={lastLogin}
>
<span />
</ListItemText>
</ListItem>
);
})}
</List>
</div>
) : 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 {
// Should remove?
setImage2FA("");
setSecret2FA("");
}
setShow2faSetup(!show2faSetup);
//setShow2faSetup(true);
};
const filesView =
curTab !== 3 ? null : (
<Files
isCloud={isCloud}
globalUrl={globalUrl}
userdata={userdata}
serverside={serverside}
selectedOrganization={selectedOrganization}
/>
);
const schedulesView =
curTab === 5 ? (
<div>
<div style={{ marginTop: 20, marginBottom: 20 }}>
<h2 style={{ display: "inline" }}>Schedules</h2>
<span style={{ marginLeft: 25 }}>
Schedules used in Workflows. Makes locating and control easier.{" "}
<a
target="_blank"
rel="noopener noreferrer"
href="/docs/triggers#schedules"
style={{ textDecoration: "none", color: "#f85a3e" }}
>
Learn more
</a>
</span>
</div>
<Divider
style={{
marginTop: 20,
marginBottom: 20,
backgroundColor: theme.palette.inputColor,
}}
/>
{allSchedules === undefined ||
allSchedules === null ||
allSchedules.length === 0 ? (
<div
style={{
textAlign: "center",
padding: "20px",
color: "#666",
borderRadius: "5px",
}}
>
No schedules found.
</div>
) : (
<List>
<ListItem>
<ListItemText
primary="Interval"
style={{ maxWidth: 200, minWidth: 200 }}
/>
<ListItemText
primary="Environment"
style={{ maxWidth: 150, minWidth: 150 }}
/>
<ListItemText
primary="Workflow"
style={{ maxWidth: 315, minWidth: 315 }}
/>
<ListItemText
primary="Argument"
style={{ minWidth: 300, maxWidth: 300, overflow: "hidden" }}
/>
<ListItemText primary="Actions" />
<ListItemText primary="Delegation" />
</ListItem>
{allSchedules.map((schedule, index) => {
var bgColor = "#27292d";
if (index % 2 === 0) {
bgColor = "#1f2023";
}
return (
<ListItem key={index} style={{ backgroundColor: bgColor }}>
<ListItemText
style={{ maxWidth: 200, minWidth: 200 }}
primary={
schedule.environment === "cloud" ||
schedule.environment === "" ||
schedule.frequency.length > 0 ? (
schedule.frequency
) : (
<span>{schedule.seconds} seconds</span>
)
}
/>
<ListItemText
style={{ maxWidth: 150, minWidth: 150 }}
primary={schedule.environment}
/>
<ListItemText
style={{ maxWidth: 315, minWidth: 315 }}
primary={
<a
style={{ textDecoration: "none", color: "#f85a3e" }}
href={`/workflows/${schedule.workflow_id}`}
target="_blank"
rel="noopener noreferrer"
>
{schedule.workflow_id}
</a>
}
/>
<ListItemText
primary={schedule.wrapped_argument.replaceAll('\\"', '"')}
style={{
minWidth: 300,
maxWidth: 300,
overflow: "hidden",
}}
/>
<ListItemText>
<Button
style={{}}
variant={
schedule.status === "running" ? "contained" : "outlined"
}
disabled={schedule.status === "uninitialized"}
onClick={() => {
if (schedule.status === "running") {
deleteSchedule(schedule);
} else startSchedule(schedule);
}}
>
{schedule.status === "running"
? "Stop Schedule"
: "Start Schedule"}
</Button>
</ListItemText>
</ListItem>
);
})}
</List>
)}
<div style={{ marginTop: 50, marginBottom: 20 }}>
<h2 style={{ display: "inline" }}>Webhooks</h2>
<span style={{ marginLeft: 25 }}>
Current HTTP Webhook endpoints in your organization.&nbsp;
<a
target="_blank"
rel="noopener noreferrer"
href="/docs/triggers#webhooks"
style={{ textDecoration: "none", color: "#f85a3e" }}
>
Learn more
</a>
</span>
</div>
<Divider
style={{
marginTop: 20,
marginBottom: 20,
backgroundColor: theme.palette.inputColor,
}}
/>
{webHooks === undefined || webHooks === null || webHooks.length === 0 ? (
<div
style={{
textAlign: "center",
padding: "20px",
color: "#666",
borderRadius: "5px",
}}
>
No webhooks found.
</div>
) : (
<List>
<ListItem>
<ListItemText
primary="Name"
style={{ maxWidth: 200, minWidth: 200 }}
/>
<ListItemText
primary="Environment"
style={{ maxWidth: 150, minWidth: 150 }}
/>
<ListItemText
primary="Workflow"
style={{ maxWidth: 315, minWidth: 315 }}
/>
<ListItemText
primary="Url"
style={{ minWidth: 300, maxWidth: 300, overflow: "hidden" }}
/>
<ListItemText primary="Actions" />
</ListItem>
{webHooks.map((webhook, index) => {
var bgColor = "#27292d";
if (index % 2 === 0) {
bgColor = "#1f2023";
}
return (
<ListItem key={index} style={{ backgroundColor: bgColor }}>
<ListItemText
style={{ maxWidth: 200, minWidth: 200 }}
primary={webhook.info.name}
/>
<ListItemText
style={{ maxWidth: 150, minWidth: 150 }}
primary={webhook.environment}
/>
<ListItemText
style={{ maxWidth: 315, minWidth: 315 }}
primary={
<a
style={{ textDecoration: "none", color: "#f85a3e" }}
href={`/workflows/${webhook.workflows[0]}`}
target="_blank"
rel="noopener noreferrer"
>
{webhook.workflows[0]}
</a>
}
/>
<ListItemText
style={{ marginLeft: 10, maxWidth: 100, minWidth: 100 }}
primary={
webhook.info.url === undefined || webhook.info.url === 0 ? (
""
) : (
<Tooltip
title={"Copy URL"}
style={{}}
aria-label={"Copy URL"}
>
<IconButton
style={{}}
onClick={() => {
const elementName = "copy_element_shuffle";
var copyText = document.getElementById(elementName);
if (copyText !== null && copyText !== undefined) {
const clipboard = navigator.clipboard;
if (clipboard === undefined) {
toast("Can only copy over HTTPS (port 3443)");
return;
}
navigator.clipboard.writeText(webhook.info.url);
copyText.select();
copyText.setSelectionRange(
0,
99999,
); /* For mobile devices */
/* Copy the text inside the text field */
document.execCommand("copy");
toast("URL copied to clipboard");
}
}}
>
<FileCopyIcon
style={{ color: "rgba(255,255,255,0.8)" }}
/>
</IconButton>
</Tooltip>
)
}
/>
<ListItemText>
<Button
style={{ marginLeft: "18%" }}
variant={
webhook.status === "running" ? "contained" : "outlined"
}
disabled={webhook.status === "uninitialized"}
onClick={() => {
if (webhook.status === "running") {
deleteWebhook(webhook);
} else startWebHook(webhook);
}}
>
{webhook.status === "running"
? "Stop webhook"
: "Start Webhook"}
</Button>
</ListItemText>
</ListItem>
);
})}
</List>
)}
<div style={{ marginTop: 50, marginBottom: 20 }}>
<h2 style={{ display: "inline" }}>Pipelines</h2>
<span style={{ marginLeft: 25 }}>
Controls a pipeline to e.g. connect to Kafka queues or search in the Tenzir storage.{" "}
<a
target="_blank"
rel="noopener noreferrer"
href="/docs/triggers#pipelines"
style={{ textDecoration: "none", color: "#f85a3e" }}
>
Learn more
</a>
</span>
</div>
<Divider
style={{
marginTop: 20,
marginBottom: 20,
backgroundColor: theme.palette.inputColor,
}}
/>
{pipelines === undefined ||
pipelines === null ||
pipelines.length === 0 ? (
<div
style={{
textAlign: "center",
padding: "20px",
color: "#666",
borderRadius: "5px",
}}
>
No pipelines found.
</div>
) : (
<List>
<ListItem>
<ListItemText
primary="Name"
style={{ maxWidth: 250, minWidth: 250 }}
/>
<ListItemText
primary="Environment"
style={{ maxWidth: 150, minWidth: 150 }}
/>
<ListItemText
primary="Total Runs"
style={{ maxWidth: 150, minWidth: 150}}
/>
<ListItemText
primary="Pipeline"
style={{ maxWidth: 300, minWidth: 300,}}
/>
<ListItemText
primary="Actions"
style={{ maxWidth: 180, minWidth: 180,}}
/>
</ListItem>
{pipelines.map((pipeline, index) => {
var bgColor = "#27292d";
if (index % 2 === 0) {
bgColor = "#1f2023";
}
return (
<ListItem key={index} style={{ backgroundColor: bgColor }}>
<ListItemText
style={{ maxWidth: 250, minWidth: 250, }}
primary={pipeline.name}
/>
<ListItemText
style={{ maxWidth: 150, minWidth: 150 }}
primary={pipeline.environment}
/>
<ListItemText
style={{ maxWidth: 150, minWidth: 150 }}
primary={pipeline.total_runs}
/>
<ListItemText
style={{ maxWidth: 300, minWidth: 300}}
primary={pipeline.definition}
/>
<ListItemText
style={{ marginleft: 30, }}
>
<Button
style={{}}
variant={
"outlined"
}
disabled={pipeline.status === "uninitialized"}
onClick={() => {
changePipelineState(pipeline, "stop")
}}
>
Stop pipeline
</Button>
</ListItemText>
</ListItem>
);
})}
</List>
)}
</div>
) : null;
const appCategoryView =
curTab === 8 ? (
<div>
<div style={{ marginTop: 20, marginBottom: 20 }}>
<h2 style={{ display: "inline" }}>Categories</h2>
<span style={{ marginLeft: 25 }}>
Categories are the categories supported by Shuffle, which are mapped
to apps and workflows
</span>
</div>
<Divider
style={{
marginTop: 20,
marginBottom: 20,
backgroundColor: theme.palette.inputColor,
}}
/>
<List>
<ListItem>
<ListItemText
primary="Category"
style={{ minWidth: 150, maxWidth: 150 }}
/>
<ListItemText
primary="Apps"
style={{ minWidth: 250, maxWidth: 250 }}
/>
<ListItemText
primary="Workflows"
style={{ minWidth: 150, maxWidth: 150 }}
/>
<ListItemText
primary="Authentication"
style={{ minWidth: 150, maxWidth: 150, overflow: "hidden" }}
/>
<ListItemText
primary="Actions"
style={{ minWidth: 150, maxWidth: 150, overflow: "hidden" }}
/>
</ListItem>
{categories.map((data, index) => {
if (data.apps.length === 0) {
return null;
}
return (
<ListItem key={index}>
<ListItemText
primary={data.name}
style={{ minWidth: 150, maxWidth: 150 }}
/>
<ListItemText
primary={""}
style={{ minWidth: 250, maxWidth: 250 }}
/>
<ListItemText
primary={""}
style={{ minWidth: 150, maxWidth: 150 }}
/>
<ListItemText
primary={""}
style={{ minWidth: 150, maxWidth: 150, overflow: "hidden" }}
/>
<ListItemText
style={{ minWidth: 150, maxWidth: 150, overflow: "hidden" }}
>
<Button
style={{}}
variant="outlined"
color="primary"
onClick={() => {
console.log("Show apps with this category");
}}
>
Find app ({data.apps === null ? 0 : data.apps.length})
</Button>
</ListItemText>
</ListItem>
);
})}
</List>
</div>
) : null;
const updateAppAuthentication = (field) => {
setSelectedAuthenticationModalOpen(true);
setSelectedAuthentication(field);
//{selectedAuthentication.fields.map((data, index) => {
var newfields = [];
for (var key in field.fields) {
newfields.push({
key: field.fields[key].key,
value: "",
});
}
setAuthenticationFields(newfields);
};
const handleAppAuthGroupCheckbox = (data) => {
//let groupApp = data.app.id
var newappauth = appsForAppAuthGroup
if (appsForAppAuthGroup.includes(data.app.id)) {
newappauth = newappauth.filter((item) => item !== data.app.id)
}
if (appsForAppAuthGroup.includes(data.id)) {
// Remove app from app auth group
newappauth = newappauth.filter((item) => item !== data.id)
setAppsForAppAuthGroup(newappauth)
return
}
for (var i = 0; i < authentication.length; i++) {
if (authentication[i].id === data.id) {
continue
}
if (!appsForAppAuthGroup.includes(authentication[i].id)) {
continue
}
if (authentication[i].app.id === data.app.id) {
// Remove app from app auth group
newappauth = newappauth.filter((item) => item !== authentication[i].id)
toast(`App ${data.app.name} is already in this group`)
}
}
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
}
}
}
var validIcon = <CheckCircleIcon style={{ color: "green" }} />
if (data.validation !== null && data.validation !== undefined && data.validation.valid === false) {
if (data.validation.changed_at === 0) {
// Warning
validIcon = "" // <WarningIcon style={{ color: "" }} />
} else {
validIcon = <CancelIcon style={{ color: "red" }} />
}
}
return (
<ListItem key={index} style={{ backgroundColor: bgColor }}>
<ListItemText
primary=
<Tooltip title={data.validation !== null && data.validation !== undefined && data.validation.valid === true ? "Valid. Click to explore." : "Configuration failed. Click to learn why"} placement="top">
<IconButton>
{validIcon}
</IconButton>
</Tooltip>
style={{ minWidth: 65, maxWidth: 65, }}
onClick={() => {
if (data.validation === null || data.validation === undefined) {
return
}
if (data.validation.workflow_id === undefined || data.validation.workflow_id === null || data.validation.workflow_id.length === 0) {
toast.warn("No workflow runs found for this auth yet. Check back later.")
return
}
const url = `/workflows/${data.validation.workflow_id}?execution_id=${data.validation.execution_id}&node=${data.validation.node_id}`
window.open(url, "_blank")
}}
/>
<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: 140,
maxWidth: 140,
overflow: "auto",
marginRight: 10,
}}
/>
<ListItemText
style={{
maxWidth: 150,
minWidth: 150,
overflow: "auto",
}}
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 ? (
<div>
{/* (appAuthenticationGroupModalOpen : { */}
{appAuthenticationGroupModalOpen && (
<Dialog
open={appAuthenticationGroupModalOpen}
onClose={() => {
setAppAuthenticationGroupModalOpen(false);
setAppAuthenticationGroupId("")
setAppAuthenticationGroupName("")
setAppAuthenticationGroupEnvironment("")
setAppAuthenticationGroupDescription("")
setAppsForAppAuthGroup([])
}}
PaperProps={{
style: {
backgroundColor: theme.palette.surfaceColor,
color: "white",
minWidth: "1000px",
minHeight: "320px",
padding: 25,
paddingLeft: 50,
},
}}
>
<DialogTitle>
<span style={{ color: "white" }}>App Authentication Groups</span>
</DialogTitle>
<DialogContent style={{marginLeft: 0, paddingLeft: 0, }}>
<div style={{display: "flex", position: "sticky", top: 0, zIndex: 1, backgroundColor: theme.palette.surfaceColor, borderRadius: theme.palette?.borderRadius, padding: 20, marginBottom: 10, }}>
<div style={{marginRight: 50, minWidth: 250, }}>
<Typography style={{marginTop: 10, }}>
Name
</Typography>
<TextField
color="primary"
label="Name"
style={{ backgroundColor: theme.palette.inputColor }}
autoFocus
InputProps={{
style: {
height: "50px",
color: "white",
fontSize: "1em",
},
}}
required
fullWidth={true}
placeholder="Name"
id="namefield"
margin="normal"
variant="outlined"
defaultValue={appAuthenticationGroupName}
onChange={(event) => {
setAppAuthenticationGroupName(event.target.value);
}}
/>
</div>
<div style={{marginRight: 50, }}>
<Typography style={{marginTop: 10, marginBottom: 10}}>
Evironment
</Typography>
{environments !== undefined && environments !== null && environments.length > 0 ?
<Select
defaultValue={appAuthenticationGroupEnvironment === "" ? environments[0].Name : appAuthenticationGroupEnvironment}
onChange={(e) => {
setAppAuthenticationGroupEnvironment(e.target.value);
}}
>
{environments.map((env, index) => {
return (
<MenuItem key={index} value={env.Name}>
{env.Name}
</MenuItem>
)
})}
</Select>
:
<Typography>
Locations failed to load. Please try again
</Typography>
}
</div>
<div style={{marginTop: 65, }}>
<Button
style={{}}
disabled={appAuthenticationGroupName === "" || appAuthenticationGroupEnvironment === "" || appsForAppAuthGroup.length === 0}
variant="contained"
color="primary"
onClick={() => {
createAppAuthenticationGroup(
appAuthenticationGroupName,
appAuthenticationGroupEnvironment,
appAuthenticationGroupDescription,
appsForAppAuthGroup,
);
}}
>
Set Group
</Button>
</div>
</div>
<Divider style={{marginTop: 10, marginBottom: 10, }}/>
<div style={{marginLeft: 25, }}>
{/* Show a check box list of all app authentications to add to the auth group */}
<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
}
if (appsForAppAuthGroup.includes(data.id)) {
checked = true
}
return (
<div key={index}>
<FormControlLabel
control={
<Tooltip
title={data.app.name}
placement="left"
>
<div style={{ display: 'flex', alignItems: 'center', marginBottom: '10px', marginLeft: '5px', }}>
<img
src={data.app.large_image ? data.app.large_image : '/images/no_image.png'}
alt=""
style={{ borderRadius: theme.palette?.borderRadius, width: 50, height: 50, marginRight: 10 }}
/>
<Checkbox
checked={checked}
onChange={(event) => {
handleAppAuthGroupCheckbox(data)
}}
name={data.label}
disabled={data.app.id in appsForAppAuthGroup}
/>
</div>
</Tooltip>
}
label={data.label}
/>
</div>
)
})}
</div>
</div>
</DialogContent>
</Dialog>
)}
<div>
<div style={{ marginTop: 20, marginBottom: 20 }}>
<h2 style={{ display: "inline" }}>App Authentication</h2>
<span style={{ marginLeft: 25 }}>
Control the authentication options for individual apps. App Groups are farther down on this page.
</span>
&nbsp;
<a
target="_blank"
rel="noopener noreferrer"
href="/docs/organizations#app_authentication"
style={{ textDecoration: "none", color: "#f85a3e" }}
>
Learn more about App Authentication
</a>
</div>
<Divider
style={{
marginTop: 20,
marginBottom: 20,
backgroundColor: theme.palette.inputColor,
}}
/>
<List>
<ListItem>
<ListItemText
primary="Valid"
style={{ minWidth: 65, maxWidth: 65 }}
/>
<ListItemText
primary="Icon"
style={{ minWidth: 75, maxWidth: 75 }}
/>
<ListItemText
primary="Label"
style={{ minWidth: 225, maxWidth: 225 }}
/>
<ListItemText
primary="App Name"
style={{ minWidth: 175, maxWidth: 175, marginLeft: 10 }}
/>
{/*<ListItemText
primary="Ready"
style={{ minWidth: 100, maxWidth: 100 }}
/>*/}
<ListItemText
primary="Workflows"
style={{ minWidth: 100, maxWidth: 100, overflow: "hidden" }}
/>
{/*
<ListItemText
primary="App Usage"
style={{ minWidth: 100, maxWidth: 100, overflow: "hidden" }}
/>
*/}
<ListItemText
primary="Fields"
style={{ minWidth: 140, maxWidth: 140, overflow: "hidden" }}
/>
<ListItemText
primary="Edited"
style={{ minWidth: 150, maxWidth: 150, overflow: "hidden" }}
/>
<ListItemText
primary="Actions"
style={{ minWidth: 150, maxWidth: 150 }}
/>
<ListItemText primary="Distribution" />
</ListItem>
{authentication === undefined || authentication === null
? null
: authentication.map((data, index) => {
//console.log("Auth data: ", data)
if (data.type === "oauth2") {
data.fields = [
{
key: "url",
value: "Secret. Replaced during app execution!",
},
{
key: "client_id",
value: "Secret. Replaced during app execution!",
},
{
key: "client_secret",
value: "Secret. Replaced during app execution!",
},
{
key: "scope",
value: "Secret. Replaced during app execution!",
},
];
}
return (
<KMSItem
data={data}
index={index}
/>
)
})}
</List>
</div>
<Divider
style={{
marginTop: 20,
marginBottom: 20,
backgroundColor: theme.palette.inputColor,
}}
/>
<div style={{marginTop: 50, }}>
<div style={{ marginTop: 20, marginBottom: 20 }}>
<h2 style={{ }}>App Authentication Groups</h2>
<span style={{ marginLeft: 0 }}>
Groups of authentication options for subflows.{" "}
<a
target="_blank"
rel="noopener noreferrer"
href="/docs/organizations#app_authentication_groups"
style={{ textDecoration: "none", color: "#f85a3e" }}
>
Learn more about App Authentication Groups
</a>
</span>
<br />
<Button
style={{ marginTop: 20 }}
variant="contained"
color="primary"
onClick={() => {
if (environments !== undefined && environments !== null && environments.length > 0) {
setAppAuthenticationGroupEnvironment(environments[0].Name)
}
setAppAuthenticationGroupModalOpen(true)
}}
>
Add Group
</Button>
<List style={{marginTop: 25, }}>
<ListItem>
<ListItemText
primary="Label"
style={{ minWidth: 250, maxWidth: 250}}
/>
<ListItemText
primary="Environment"
style={{ minWidth: 150, maxWidth: 150 }}
/>
<ListItemText
primary="App Auth"
style={{ minWidth: 250, maxWidth: 250 }}
/>
<ListItemText
primary="Created At"
style={{ minWidth: 150, maxWidth: 150 }}
/>
<ListItemText
primary="Actions"
style={{ minWidth: 150, maxWidth: 150 }}
/>
</ListItem>
{appAuthenticationGroups.map((data, index) => {
var bgColor = "#27292d";
if (index % 2 === 0) {
bgColor = "#1f2023";
}
if (data.app_auths === undefined || data.app_auths === null) {
data.app_auths = []
}
return (
<ListItem key={index} style={{ backgroundColor: bgColor }}>
<ListItemText
primary={data.label}
style={{ minWidth: 250, maxWidth: 250, }}
/>
<ListItemText
primary={data.environment}
style={{ minWidth: 150, maxWidth: 150, }}
/>
<ListItemText
primary={
<div style={{ display: 'flex' }}>
{data.app_auths.map((appAuth, index) => {
if (appAuth.app.large_image === undefined || appAuth.app.large_image === null || appAuth.app.large_image === "") {
const foundImage = authentication.find((auth) => auth.app.id === appAuth.app.id)
if (foundImage !== undefined) {
appAuth.app.large_image = foundImage.app.large_image
appAuth.app.name = foundImage.app.name
}
}
const tooltip = `${appAuth.app.name.replaceAll("_", " ")} (authname: ${appAuth.label})`
return (
<Tooltip
title={tooltip}
>
<img
key={index}
src={appAuth.app.large_image}
alt={appAuth.app.name}
style={{ width: 30, height: 30, marginRight: 5 }}
/>
</Tooltip>
)
})}
</div>
}
style={{ minWidth: 250, maxWidth: 250 }}
/>
<ListItemText
primary={new Date(data.created * 1000).toLocaleDateString('en-GB')}
style={{ minWidth: 150, maxWidth: 150 }}
/>
<ListItemText
primary={
<div style={{ display: 'flex' }}>
<IconButton
onClick={() => {
setAppAuthenticationGroupId(data.id)
setAppAuthenticationGroupName(data.label)
setAppAuthenticationGroupDescription(data.description)
setAppsForAppAuthGroup(data.app_auths.map((appAuth) => appAuth.id))
setAppAuthenticationGroupEnvironment(data.environment)
setAppAuthenticationGroupModalOpen(true)
}}
>
<EditIcon />
</IconButton>
<IconButton
onClick={() => {
deleteAppAuthenticationGroup(data.id)
}}
>
<DeleteIcon />
</IconButton>
</div>
}
style={{ minWidth: 150, maxWidth: 150 }}
/>
</ListItem>
);
}
)}
</List>
</div>
</div>
</div>
) : null;
const getLogs = (ip, userId) => {
setLogsLoading(true);
console.log("logs loading: ", logsLoading);
fetch(`${globalUrl}/api/v1/users/${userId}/audit?user_ip=${ip}`, {
mode: "cors",
method: "GET",
credentials: "include",
crossDomain: true,
withCredentials: true,
headers: {
"Content-Type": "application/json; charset=utf-8",
},
})
.then((response) => {
return response.json();
})
.then((responseJson) => {
console.log("ResponseJSON: ", responseJson);
if (responseJson.success === true) {
setLogs(responseJson.logs);
} else {
if (
responseJson.success === false ||
responseJson.reason !== undefined
) {
console.log("Reason given: ", responseJson.reason);
toast("Failed getting logs: " + responseJson.reason);
setLogs([]);
} else {
toast("Failed getting logs");
}
}
console.log("logs loading now: ", logsLoading);
setLogsLoading(false);
})
.catch((error) => {
console.log("Error: ", error);
toast("Failed getting logs. Please contact: ", error);
console.log("logs loading now: ", logsLoading);
setLogsLoading(false);
});
};
const changeRecommendation = (recommendation, action) => {
const data = {
action: action,
name: recommendation.name,
};
fetch(`${globalUrl}/api/v1/recommendations/modify`, {
mode: "cors",
method: "POST",
body: JSON.stringify(data),
credentials: "include",
crossDomain: true,
withCredentials: true,
headers: {
"Content-Type": "application/json; charset=utf-8",
},
})
.then((response) => {
if (response.status === 200) {
} else {
}
return response.json();
})
.then((responseJson) => {
if (responseJson.success === true) {
if (checkLogin !== undefined) {
checkLogin();
getEnvironments();
}
} else {
if (
responseJson.success === false &&
responseJson.reason !== undefined
) {
toast("Failed change recommendation: ", responseJson.reason);
} else {
toast("Failed change recommendation");
}
}
})
.catch((error) => {
toast(
"Failed dismissing alert. Please contact support@shuffler.io if this persists.",
);
});
};
const getOrborusCommand = (environment) => {
if (environment.Type === "cloud") {
//toast("No Orborus necessary for environment cloud. Create and use a different environment to run executions on-premises.",)
return
}
if (
props.userdata.active_org === undefined ||
props.userdata.active_org === null
) {
toast(
"No active organization yet. Are you logged in?",
);
return;
}
const elementName = "copy_element_shuffle";
const auth =
environment.auth === ""
? "cb5st3d3Z!3X3zaJ*Pc"
: environment.auth;
const newUrl =
globalUrl === "https://shuffler.io"
? "https://shuffle-backend-stbuwivzoq-nw.a.run.app"
: globalUrl;
var skipPipeline = false
if (commandController.pipelines === true) {
skipPipeline = true
}
var addProxy = false
if (commandController.proxies === true) {
addProxy = true
}
if (installationTab === 1) {
return (`docker run -d \\
--restart=always \\
--name="shuffle-orborus" \\
--pull=always \\
--volume "/var/run/docker.sock:/var/run/docker.sock" \\
-e AUTH="${environment.auth}" \\
-e ENVIRONMENT_NAME="${environment.Name}" \\
-e ORG="${environment.org_id}" \\
-e SHUFFLE_WORKER_IMAGE="ghcr.io/shuffle/shuffle-worker:nightly" \\
-e SHUFFLE_SWARM_CONFIG=run \\
-e SHUFFLE_LOGS_DISABLED=true \\
-e BASE_URL="${newUrl}" \\${addProxy ? "\n -e HTTPS_PROXY=IP:PORT \\" : ""}${skipPipeline ? "\n -e SHUFFLE_SKIP_PIPELINES=true \\" : ""}
ghcr.io/shuffle/shuffle-orborus:latest
`)
} else if (installationTab === 2) {
return `https://shuffler.io/docs/configuration#kubernetes`
}
const commandData = `docker rm shuffle-orborus --force; \\\ndocker run -d \\
--restart=always \\
--name="shuffle-orborus" \\
--pull=always \\
--volume "/var/run/docker.sock:/var/run/docker.sock" \\
-e AUTH="${auth}" \\
-e ENVIRONMENT_NAME="${environment.Name}" \\
-e ORG="${props.userdata.active_org.id}" \\
-e BASE_URL="${newUrl}" \\${addProxy ? "\n -e HTTPS_PROXY=IP:PORT \\" : ""}${skipPipeline ? "\n -e SHUFFLE_SKIP_PIPELINES=true \\" : ""}
ghcr.io/shuffle/shuffle-orborus:latest`
return commandData
var copyText =
document.getElementById(elementName);
if (
copyText !== null &&
copyText !== undefined
) {
const clipboard = navigator.clipboard;
if (clipboard === undefined) {
toast(
"Can only copy over HTTPS (port 3443)",
);
return;
}
navigator.clipboard.writeText(commandData);
copyText.select();
copyText.setSelectionRange(
0,
99999,
); /* For mobile devices */
/* Copy the text inside the text field */
document.execCommand("copy");
toast("Orborus command copied to clipboard");
}
}
const environmentView =
curTab === 6 ? (
<div>
<div style={{ marginTop: 20, marginBottom: 20 }}>
<h2 style={{ display: "inline" }}>Locations</h2>
<span style={{ marginLeft: 25 }}>
Decides where to run your workflows and actions. Uses Shuffle's Orborus runner to handle queued jobs onprem. Previously "Environments".
If you have scale problems, talk to our team: support@shuffler.io.&nbsp;
<a
target="_blank"
rel="noopener noreferrer"
href="/docs/organizations#environments"
style={{ textDecoration: "none", color: "#f85a3e" }}
>
Learn more
</a>
</span>
</div>
<Button
style={{}}
variant="contained"
color="primary"
onClick={() => setModalOpen(true)}
>
Add location
</Button>
<Button
style={{ marginLeft: 5, marginRight: 15 }}
variant="contained"
color="primary"
onClick={() => getEnvironments()}
>
<CachedIcon />
</Button>
<Switch
checked={showArchived}
onChange={() => {
setShowArchived(!showArchived);
}}
/>{" "}
Show disabled
<Divider
style={{
marginTop: 20,
marginBottom: 20,
backgroundColor: theme.palette.inputColor,
}}
/>
<List>
<ListItem style={{ paddingLeft: 10 }}>
<ListItemText
primary="Type"
style={{ minWidth: 50, maxWidth: 50 }}
/>
<ListItemText
primary="Scale"
style={{ minWidth: 60, maxWidth: 60}}
/>
<ListItemText
primary="Lake"
style={{ minWidth: 60, maxWidth: 60 }}
/>
<ListItemText
primary="Name"
style={{ minWidth: 200, maxWidth: 200}}
/>
<ListItemText
primary="Status"
style={{ minWidth: 150, maxWidth: 150, marginRight: 10, }}
/>
<ListItemText
primary="Type"
style={{ minWidth: 100, maxWidth: 100 }}
/>
<ListItemText
primary={"Queue"}
style={{ minWidth: 80, maxWidth: 80}}
/>
<ListItemText
primary="Actions"
style={{ minWidth: 200, maxWidth: 200 }}
/>
</ListItem>
{environments === undefined || environments === null
? null
: environments.map((environment, index) => {
if (!showArchived && environment.archived) {
return null;
}
if (environment.archived === undefined) {
return null;
}
var bgColor = "#27292d";
if (index % 2 === 0) {
bgColor = "#1f2023";
}
// Check if there's a notification for it in userdata.priorities
var showCPUAlert = false;
var foundIndex = -1;
if (
userdata !== undefined &&
userdata !== null &&
userdata.priorities !== undefined &&
userdata.priorities !== null &&
userdata.priorities.length > 0
) {
foundIndex = userdata.priorities.findIndex(
(prio) => prio.name.includes("CPU") && prio.active === true,
);
if (
foundIndex >= 0 &&
userdata.priorities[foundIndex].name.endsWith(
environment.Name,
)
) {
showCPUAlert = true;
}
}
const queueSize =
environment.queue !== undefined && environment.queue !== null
? environment.queue < 0
? 0
: environment.queue > 1000
? ">1000"
: environment.queue
: 0;
const orborusCommandWrapper = () => {
// Check the current text
const orborusCommand = document.getElementById("orborus_command")
if (orborusCommand === undefined || orborusCommand === null) {
return getOrborusCommand(environment)
}
return orborusCommand.textContent
}
return (
<span key={index}>
<ListItem
key={index}
style={{ cursor: "pointer", backgroundColor: bgColor, marginLeft: 0 }}
onClick={() => {
if (environment.Type === "cloud") {
toast("Cloud environments are not configurable. To see what is possible, create a new environment.")
return
}
setListItemExpanded(listItemExpanded === index ? -1 : index)
}}
>
<ListItemText
primary={
environment.run_type === "cloud" ||
environment.name === "Cloud" ? (
<Tooltip title="Cloud" placement="top">
<CloudIcon
style={{ color: "rgba(255,255,255,0.8)" }}
/>
</Tooltip>
) : environment.run_type === "docker" ? (
<Tooltip title="Docker" placement="top">
<img
src="/icons/docker.svg"
style={{ width: 30, height: 30 }}
/>
</Tooltip>
) : environment.run_type === "k8s" ? (
<Tooltip title="Kubernetes" placement="top">
<img
src="/icons/k8s.svg"
style={{ width: 30, height: 30 }}
/>
</Tooltip>
) : (
<Tooltip title="Unknown" placement="top">
<HelpIcon
style={{ color: "rgba(255,255,255,0.8)" }}
/>
</Tooltip>
)
}
style={{
minWidth: 50,
maxWidth: 50,
overflow: "hidden",
}}
/>
<ListItemText
primary={
environment.licensed ? (
<Tooltip title="Scale configured (auto on cloud)" placement="top">
<CheckCircleIcon style={{ color: "#4caf50" }} />
</Tooltip>
) : (
<Tooltip
title="In Verbose mode. Set SHUFFLE_SWARM_CONFIG=run to Scale. This will not be as verbose. Details: https://shuffler.io/docs/configuration#scaling-shuffle"
placement="top"
>
<a
href="/docs/configuration#scaling-shuffle"
target="_blank"
rel="noopener noreferrer"
>
<CancelIcon style={{ color: "#f85a3e" }} />
</a>
</Tooltip>
)
}
style={{
minWidth: 60,
maxWidth: 60,
overflow: "hidden",
}}
/>
<ListItemText
primary={
environment.Type === "cloud" ?
<Tooltip title={"Make a new environment to set up a Datalake node. Please contact support@shuffler.io if this is something you want to see on Cloud directly."} placement="top">
<CancelIcon style={{ color: "rgba(255,255,255,0.3)" }} />
</Tooltip>
:
environment?.data_lake?.enabled ? (
<a
href="/detections/Sigma"
target="_blank"
rel="noopener noreferrer"
>
<Tooltip title={"Data Lake node enabled. Check /detections/Sigma to learn more"} placement="top">
<CheckCircleIcon style={{ color: "#4caf50" }} />
</Tooltip>
</a>
) : (
<Tooltip
title="Data Lake node disabled. Click to enable."
placement="top"
onClick={(e) => {
e.preventDefault()
e.stopPropagation()
window.open("/detections/Sigma", "_blank")
}}
>
<a
href="/detections/Sigma"
target="_blank"
rel="noopener noreferrer"
>
<CancelIcon style={{ color: "#f85a3e" }} />
</a>
</Tooltip>
)
}
style={{
minWidth: 60,
maxWidth: 60,
overflow: "hidden",
}}
/>
<ListItemText
primary={environment.Name}
style={{
minWidth: 200,
maxWidth: 200,
overflow: "hidden",
}}
/>
<ListItemText
primary={
environment.Type !== "cloud" ? (
environment.running_ip === undefined ||
environment.running_ip === null ||
environment.running_ip.length === 0 ? (
<div>Not running</div>
) : (
environment.running_ip.split(":")[0]
)
) : (
"N/A"
)
}
style={{
minWidth: 150,
maxWidth: 150,
marginRight: 10,
overflow: "hidden",
}}
/>
<ListItemText
primary={environment.Type}
style={{ minWidth: 100, maxWidth: 100 }}
/>
<ListItemText
style={{
minWidth: 60,
maxWidth: 60,
overflow: "hidden",
marginLeft: 0,
}}
primary={queueSize}
/>
<ListItemText
style={{
minWidth: 330,
maxWidth: 330,
overflow: "hidden",
marginLeft: 10,
}}
>
<div style={{ display: "flex" }}>
<ButtonGroup
style={{ borderRadius: "5px 5px 5px 5px" }}
>
<Button
variant="outlined"
disabled={environment.default}
style={{
marginLeft: 0,
marginRight: 0,
maxWidth: 150,
}}
onClick={() => setDefaultEnvironment(environment)}
color="primary"
>
Make Default
</Button>
<Button
variant={
environment.archived ? "contained" : "outlined"
}
style={{}}
onClick={() => deleteEnvironment(environment)}
color="primary"
>
{environment.archived ? "Activate" : "Disable"}
</Button>
<Button
variant={"outlined"}
style={{}}
onClick={(e) => {
e.preventDefault()
e.stopPropagation()
console.log(
"Should clear executions for: ",
environment,
);
if (
isCloud &&
environment.Name.toLowerCase() === "cloud"
) {
rerunCloudWorkflows(environment);
} else {
abortEnvironmentWorkflows(environment);
}
}}
color="primary"
>
{isCloud &&
environment.Name.toLowerCase() === "cloud"
? "Rerun"
: "Clear"}
</Button>
</ButtonGroup>
</div>
</ListItemText>
<IconButton
disabled={environment.Type === "cloud"}
>
{listItemExpanded === index ? <ExpandLessIcon /> : <ExpandMoreIcon />}
</IconButton>
</ListItem>
{listItemExpanded === index ? (
<div style={{minHeight: 250, width: "100%", backgroundColor: bgColor, }}>
<div style={{width: 775, margin: "auto", paddingTop: 50, paddingBottom: 100, }}>
<Typography variant="h6">
Your Onprem Orborus instance
</Typography>
<Typography variant="body2" color="textSecondary">
Orborus is the Shuffle queue handler that runs your hybrid workflows and manages pipelines. It can be run in Docker/k8s container on your server or in your cluster. Follow the steps below, and configure as need be.
</Typography>
<Tabs
value={installationTab}
indicatorColor="primary"
textColor="secondary"
onChange={(e, inputValue) => {
setInstallationTab(inputValue)
}}
aria-label="disabled tabs example"
variant="scrollable"
scrollButtons="auto"
style={{textAlign: "center", marginTop: 25, }}
>
<Tab
value={0}
label=<span>
<img
src="/icons/docker.svg"
style={{ width: 20, height: 20, marginRight: 10, }}
/> Verbose (default)
</span>
/>
<Tab
value={1}
label=<span>
<img
src="/icons/docker.svg"
style={{ width: 20, height: 20, marginRight: 10, }}
/> Scale
</span>
/>
<Tab
value={2}
label=<span>
<img
src="/icons/k8s.svg"
style={{ width: 20, height: 20, marginRight: 10, }}
/> k8s
</span>
/>
</Tabs>
<Typography variant="body1" color="textSecondary" style={{marginTop: 15, }}>
{installationTab === 2 ?
<span>
Check our <a href="https://docs.docker.com/get-started/get-docker/" target="_blank" rel="noopener noreferrer" style={{textDecoration: "none", color: "#f85a3e",}}>Kubernetes documentation</a> for more information on how to run Shuffle on Kubernetes. The status of the node will change when connected.
</span>
:
<span>
1. <a href="https://docs.docker.com/get-started/get-docker/" target="_blank" rel="noopener noreferrer" style={{textDecoration: "none", color: "#f85a3e",}}>Ensure Docker is installed</a> and the target server can reach '{globalUrl}'
</span>
}
</Typography>
<Typography variant="body1" color="textSecondary">
{installationTab === 2 ? null :
"2. Run this command on the server you want to run workflows or store Pipeline data on"}
</Typography>
{installationTab === 2 ? null :
<div
style={{
marginTop: 10,
padding: 15,
minWidth: "50%",
maxWidth: "100%",
backgroundColor: theme.palette.inputColor,
overflowY: "auto",
// Have it inline
borderRadius: theme.palette?.borderRadius,
}}
>
<div style={{ display: "flex", position: "relative", }}>
<code
contenteditable="true"
id="orborus_command"
style={{
// Wrap if larger than X
whiteSpace: "pre-wrap",
overflow: "auto",
marginRight: 30,
}}
>
{getOrborusCommand(environment)}
</code>
<CopyToClipboard
text={orborusCommandWrapper()}
/>
</div>
<Divider style={{marginTop: 25, marginBottom: 10, }}/>
Configure HTTP Proxies: <Checkbox
id="shuffle_skip_proxies"
onClick={() => {
if (commandController.proxies === undefined) {
commandController.proxies = true
} else {
commandController.proxies = !commandController.proxies
}
setCommandController(commandController)
setUpdate(Math.random())
}}
/>
<div />
Disable Pipelines & Data Lake: <Checkbox
id="shuffle_skip_pipelines"
onClick={() => {
if (commandController.pipelines === undefined) {
commandController.pipelines = true
} else {
commandController.pipelines = !commandController.pipelines
}
setCommandController(commandController)
setUpdate(Math.random())
}}
/>
</div>
}
<Typography variant="body1" color="textSecondary" style={{marginTop: 15, }}>
{installationTab === 2 ? null :
<span>
3. Verify if the node is running. Try to refresh the page a little while after running the command.
</span>
}
</Typography>
</div>
</div>
) : null}
{showCPUAlert === false ? null : (
<ListItem
key={index + "_cpu"}
style={{ backgroundColor: bgColor }}
>
<div
style={{
border: "1px solid #f85a3e",
borderRadius: theme.palette?.borderRadius,
marginTop: 10,
marginBottom: 10,
padding: 15,
textAlign: "center",
height: 70,
textAlign: "left",
backgroundColor: theme.palette.surfaceColor,
display: "flex",
}}
>
<div style={{ flex: 2, overflow: "hidden" }}>
<Typography variant="body1">
90% CPU the server(s) hosting the Shuffle App
Runner (Orborus) was found.
</Typography>
<Typography variant="body2" color="textSecondary">
Need help with High Availability and Scale?{" "}
<a
href="/docs/configuration#scale"
target="_blank"
rel="noopener noreferrer"
style={{
textDecoration: "none",
color: "#f85a3e",
}}
>
Read documentation
</a>{" "}
and{" "}
<a
href="https://shuffler.io/contact"
target="_blank"
rel="noopener noreferrer"
style={{
textDecoration: "none",
color: "#f85a3e",
}}
>
Get in touch
</a>
.
</Typography>
</div>
<div
style={{ flex: 1, display: "flex", marginLeft: 30 }}
>
<Button
style={{
borderRadius: 25,
width: 200,
height: 50,
marginTop: 8,
}}
variant="outlined"
color="secondary"
onClick={() => {
// dismiss -> get envs
changeRecommendation(
userdata.priorities[foundIndex],
"dismiss",
);
}}
>
Dismiss
</Button>
</div>
</div>
</ListItem>
)}
</span>
);
})}
</List>
{/*<EnvironmentStats />*/}
</div>
) : null;
const imagesize = 40;
const imageStyle = {
width: imagesize,
height: imagesize,
pointerEvents: "none",
};
const organizationsTab =
curTab === 7 ? (
<div>
<div style={{ marginTop: 20, marginBottom: 20 }}>
<h2 style={{ display: "inline" }}>Organizations</h2>
<span style={{ marginLeft: 25 }}>
Control sub organizations (tenants)!{" "}
{isCloud
? "You can only make a sub organization if you are a customer of shuffle or running a POC of the platform. Please contact support@shuffler.io to try it out."
: ""}
.{" "}
<a
href="/docs/organizations"
target="_blank"
rel="noopener noreferrer"
style={{
textDecoration: "none",
color: theme.palette.primary.main,
}}
>
Learn more
</a>
</span>
</div>
<Button
style={{}}
variant="contained"
color="primary"
disabled={userdata.admin !== "true"}
onClick={() => {
setModalOpen(true);
}}
>
Add suborganization
</Button>
{parentOrg ? (
<span>
<Divider
style={{
marginTop: 20,
marginBottom: 20,
backgroundColor: theme.palette.inputColor,
}}
/>
<div
style={{
textAlign: "center",
width: "100%",
padding: "10px",
marginTop: 20,
}}
>
<h3
style={{
margin: 0,
fontSize: "1.2rem",
fontWeight: "bold",
letterSpacing: "1px",
}}
>
{" "}
Your Parent Organization
</h3>
</div>
<Divider
style={{
marginTop: 20,
marginBottom: 20,
backgroundColor: theme.palette.inputColor,
}}
/>
{(() => {
const image =
parentOrg.image === "" ? (
<img
alt={parentOrg.name}
src={theme.palette.defaultImage}
style={imageStyle}
/>
) : (
<img
alt={parentOrg.name}
src={parentOrg.image}
style={imageStyle}
/>
);
const bgColor = "#27292d";
return (
<List>
<ListItem>
<ListItemText
primary="Logo"
style={{ minWidth: 100, maxWidth: 100 }}
/>
<ListItemText
primary="Name"
style={{ minWidth: 350, maxWidth: 350 }}
/>
<ListItemText
primary="id"
style={{ minWidth: 400, maxWidth: 400 }}
/>
</ListItem>
<ListItem style={{ backgroundColor: bgColor }}>
<ListItemText
primary={image}
style={{ minWidth: 100, maxWidth: 100 }}
/>
<ListItemText
primary={parentOrg.name}
style={{ minWidth: 350, maxWidth: 350 }}
/>
<ListItemText
primary={parentOrg.id}
style={{ minWidth: 400, maxWidth: 400 }}
/>
<Button
variant="contained"
color="primary"
disabled={parentOrg.id === selectedOrganization.id}
onClick={() => {
handleClickChangeOrg(parentOrg.id);
}}
>
Switch to Parent
</Button>
</ListItem>
</List>
);
})()}
</span>
) : null}
{subOrgs.length > 0 ? (
<span>
<Divider
style={{
marginTop: 20,
marginBottom: 20,
backgroundColor: theme.palette.inputColor,
}}
/>
<div
style={{
textAlign: "center",
width: "100%",
padding: "10px",
marginTop: 20,
}}
>
<h3
style={{
margin: 0,
fontSize: "1.2rem",
fontWeight: "bold",
letterSpacing: "1px",
}}
>
Sub Organizations of the Current Organization ({subOrgs.length})
</h3>
</div>
<Divider
style={{
marginTop: 20,
marginBottom: 20,
backgroundColor: theme.palette.inputColor,
}}
/>
<List>
<ListItem>
<ListItemText
primary="Logo"
style={{ minWidth: 100, maxWidth: 100 }}
/>
<ListItemText
primary="Name"
style={{ minWidth: 350, maxWidth: 350 }}
/>
<ListItemText
primary="id"
style={{ minWidth: 400, maxWidth: 400 }}
/>
</ListItem>
<span>
{subOrgs.map((data, index) => {
const image =
data.image === "" ? (
<img
alt={data.name}
src={theme.palette.defaultImage}
style={imageStyle}
/>
) : (
<img
alt={data.name}
src={data.image}
style={imageStyle}
/>
);
var bgColor = "#27292d";
if (index % 2 === 0) {
bgColor = "#1f2023";
}
return (
<ListItem key={index} style={{ backgroundColor: bgColor }}>
<ListItemText
primary={image}
style={{ minWidth: 100, maxWidth: 100 }}
/>
<ListItemText
primary={data.name}
style={{ minWidth: 350, maxWidth: 350 }}
/>
<ListItemText
primary={data.id}
style={{ minWidth: 400, maxWidth: 400 }}
/>
<Button
variant="outlined"
color="primary"
disabled={data.id === selectedOrganization.id}
onClick={() => {
handleClickChangeOrg(data.id);
}}
>
Change Active Org
</Button>
</ListItem>
);
})}
</span>
</List>
</span>
) : null}
<Divider
style={{
marginTop: 20,
marginBottom: 20,
backgroundColor: theme.palette.inputColor,
}}
/>
<div
style={{
textAlign: "center",
width: "100%",
padding: "10px",
marginTop: 20,
}}
>
<h3
style={{
margin: 0,
fontSize: "1.2rem",
fontWeight: "bold",
letterSpacing: "1px",
}}
>
All Your Organizations
</h3>
</div>
<Divider
style={{
marginTop: 20,
marginBottom: 20,
backgroundColor: theme.palette.inputColor,
}}
/>
<List>
<ListItem>
<ListItemText
primary="Logo"
style={{ minWidth: 100, maxWidth: 100 }}
/>
<ListItemText
primary="Name"
style={{ minWidth: 350, maxWidth: 350 }}
/>
<ListItemText
primary="id"
style={{ minWidth: 400, maxWidth: 400 }}
/>
<ListItemText
primary="action"
style={{ minWidth: 200, maxWidth: 200 }}
/>
</ListItem>
{userdata.orgs !== undefined &&
userdata.orgs !== null &&
userdata.orgs.length > 0 ? (
<span>
{userdata.orgs.map((data, index) => {
const isSelected =
props.userdata.active_org.id === undefined
? "False"
: props.userdata.active_org.id === data.id
? "True"
: "False";
const image =
data.image === "" ? (
<img
alt={data.name}
src={theme.palette.defaultImage}
style={imageStyle}
/>
) : (
<img alt={data.name} src={data.image} style={imageStyle} />
);
var bgColor = "#27292d";
if (index % 2 === 0) {
bgColor = "#1f2023";
}
return (
<ListItem key={index} style={{ backgroundColor: bgColor }}>
<ListItemText
primary={image}
style={{ minWidth: 100, maxWidth: 100 }}
/>
<ListItemText
primary={data.name}
style={{ minWidth: 350, maxWidth: 350}}
/>
<ListItemText
primary={data.id}
style={{ minWidth: 400, maxWidth: 400}}
/>
<ListItemText
style={{ minWidth: 200, maxWidth: 200 }}
primary={
<Button
variant="outlined"
color="primary"
disabled={data.id === selectedOrganization.id}
onClick={() => {
handleClickChangeOrg(data.id)
}}
>
Change Active Org
</Button>
}
/>
</ListItem>
)
})}
</span>
) : null}
</List>
</div>
) : null;
const hybridTab =
curTab === 8 ? (
<div>
<div style={{ marginTop: 20, marginBottom: 20 }}>
<h2 style={{ display: "inline" }}>Hybrid</h2>
<span style={{ marginLeft: 25 }}></span>
</div>
<Divider
style={{
marginTop: 20,
marginBottom: 20,
backgroundColor: theme.palette.inputColor,
}}
/>
<List>
<ListItem>
<ListItemText
primary="Name"
style={{ minWidth: 150, maxWidth: 150 }}
/>
<ListItemText
primary="Orborus source"
style={{ minWidth: 200, maxWidth: 200 }}
/>
<ListItemText
primary="Actions"
style={{ minWidth: 150, maxWidth: 150 }}
/>
</ListItem>
<ListItem>
<ListItemText
primary="Enabled"
style={{ minWidth: 150, maxWidth: 150 }}
/>
<ListItemText
primary="false"
style={{ minWidth: 200, maxWidth: 200 }}
/>
<ListItemText
primary=<Switch
checked={false}
onChange={() => {
console.log("INVERT");
}}
/>
style={{ minWidth: 150, maxWidth: 150 }}
/>
</ListItem>
</List>
</div>
) : null;
const cacheOrgView =
curTab === 4 ? (
<div>
<CacheView globalUrl={globalUrl} orgId={selectedOrganization.id} />
</div>
) : null;
// primary={environment.Registered ? "true" : "false"}
const iconStyle = { marginRight: 10 };
const data = (
<div
style={{
width: 1300,
margin: "auto",
overflowX: "hidden",
marginTop: 25,
}}
>
<Paper style={paperStyle}>
<Tabs
value={curTab}
indicatorColor="primary"
textColor="secondary"
onChange={setConfig}
aria-label="disabled tabs example"
variant="scrollable"
scrollButtons="auto"
>
<Tab
label=<span>
<BusinessIcon style={iconStyle} /> Organization
</span>
/>
<Tab
disabled={userdata.admin !== "true"}
label=<span>
<AccessibilityNewIcon style={iconStyle} />
Users
</span>
/>
<Tab
label=<span>
<LockIcon style={iconStyle} />
App Auth
</span>
/>
<Tab
disabled={userdata.admin !== "true"}
label=<span>
<DescriptionIcon style={iconStyle} />
Files
</span>
/>
<Tab
label=<span>
<StorageIcon style={iconStyle} /> Datastore
</span>
/>
<Tab
disabled={userdata.admin !== "true"}
label=<span>
<ScheduleIcon style={iconStyle} />
Triggers
</span>
/>
<Tab
disabled={userdata.admin !== "true"}
label=<span>
<FmdGoodIcon style={iconStyle} />
Locations
</span>
/>
<Tab
label=<span>
<BusinessIcon style={iconStyle} /> Tenants
</span>
/>
{/*window.location.protocol == "http:" && window.location.port === "3000" ? <Tab label=<span><CloudIcon style={iconStyle} /> Hybrid</span>/> : null*/}
{/*window.location.protocol === "http:" && window.location.port === "3000" ? <Tab label=<span><LockIcon style={iconStyle} />Categories</span>/> : null*/}
</Tabs>
<Divider
style={{
marginTop: 0,
marginBottom: 10,
backgroundColor: "rgb(91, 96, 100)",
}}
/>
<div style={{ padding: 15 }}>
{organizationView}
{authenticationView}
{usersView}
{environmentView}
{schedulesView}
{filesView}
{hybridTab}
{organizationsTab}
{appCategoryView}
{cacheOrgView}
</div>
</Paper>
</div>
);
return (
<div style={{}} >
&nbsp;
{modalView}
{cloudSyncModal}
{editUserModal}
{editAuthenticationModal}
{data}
<TextField
id="copy_element_shuffle"
value={to_be_copied}
style={{ display: "none" }}
/>
</div>
);
};
export default Admin;