7445 lines
225 KiB
React
Executable File
7445 lines
225 KiB
React
Executable File
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 can’t 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: </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.
|
||
<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>
|
||
|
||
<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.
|
||
<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={{}} >
|
||
|
||
{modalView}
|
||
{cloudSyncModal}
|
||
{editUserModal}
|
||
{editAuthenticationModal}
|
||
{data}
|
||
<TextField
|
||
id="copy_element_shuffle"
|
||
value={to_be_copied}
|
||
style={{ display: "none" }}
|
||
/>
|
||
</div>
|
||
);
|
||
};
|
||
|
||
export default Admin;
|