import React, { memo, useContext, useEffect, useState } from 'react'; import { getTheme } from "../theme.jsx"; import { Tooltip, Typography, Switch, TextField, Button, ButtonGroup, List, ListItem, ListItemText, IconButton, Dialog, DialogTitle, DialogActions, DialogContent, Checkbox, Divider, Tab, Tabs, Collapse, Skeleton, Grid, Chip, MenuItem, } from "@mui/material"; import { CopyToClipboard } from "../views/Docs.jsx" import { FileCopy as FileCopyIcon, CheckCircle as CheckCircleIcon, Cached as CachedIcon, Cloud as CloudIcon, Cancel as CancelIcon, Help as HelpIcon, ExpandLess as ExpandLessIcon, ExpandMore as ExpandMoreIcon, } from "@mui/icons-material"; import { toast } from 'react-toastify'; import { Context } from '../context/ContextApi.jsx'; import { green, red } from '../views/AngularWorkflow.jsx' import AppSearch from "../components/AppSearch1.jsx"; const EnvironmentTab = memo((props) => { const { globalUrl, isCloud, userdata, selectedOrganization } = props; const [environments, setEnvironments] = React.useState([]); const [showArchived, setShowArchived] = React.useState(false); const [modalUser, setModalUser] = React.useState({}); const [loginInfo, setLoginInfo] = React.useState(""); const [modalOpen, setModalOpen] = React.useState(false); const [showLoader, setShowLoader] = useState(true) const [commandController, setCommandController] = React.useState({ pipelines: false, proxies: false, }) const [installationTab, setInstallationTab] = React.useState(0); const [isExpanded, setIsExpanded] = React.useState(false); const [listItemExpanded, setListItemExpanded] = React.useState(-1); const [, setUpdate] = React.useState(0); const [showDistributionPopup, setShowDistributionPopup] = React.useState(false); const [selectedEnvironment, setSelectedEnvironment] = React.useState(null); const [selectedSubOrg, setSelectedSubOrg] = React.useState([]); const [showLocationActionModal, setShowLocationActionModal] = React.useState(undefined) const { themeMode, supportEmail, brandColor } = useContext(Context); const theme = getTheme(themeMode, brandColor); useEffect(() => { getEnvironments(); setModalUser({}); }, []); const changeModalData = (field, value) => { modalUser[field] = value; }; // Horrible frontend fix for environments const setDefaultEnvironment = (environment) => { // FIXME - add more checks to this toast("Setting default location to " + environment.Name); 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 rerunCloudWorkflows = (environment) => { toast("Starting execution reruns. This can run in the background."); 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 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); setShowLoader(false) // 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 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 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. Change the default environment first."); 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 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 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) { 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 ${supportEmail} 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 ) { return; } const elementName = "copy_element_shuffle"; var auth = environment.auth === "" ? "cb5st3d3Z!3X3zaJ*Pc" : environment.auth // Escape exclamation marks for copying auth = auth.replace("\\!", "!").replace(/!/g, "\\!") 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="${auth}" \\ -e ENVIRONMENT_NAME="${environment.Name}" \\ -e ORG="${environment.org_id}" \\ -e SHUFFLE_WORKER_IMAGE="ghcr.io/shuffle/shuffle-worker:latest" \\ -e SHUFFLE_SWARM_CONFIG=run \\ -e SHUFFLE_LOGS_DISABLED=true \\ -e BASE_URL="${newUrl}" \\${addProxy ? ` -e HTTPS_PROXY=IP:PORT \\` : ""}${skipPipeline ? ` -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 ? ` -e HTTPS_PROXY=IP:PORT \\` : ""}${skipPipeline ? ` -e SHUFFLE_SKIP_PIPELINES=true \\` : ""} ghcr.io/shuffle/shuffle-orborus:latest` return commandData }; 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); }); }; const modalView = ( { setModalOpen(false); }} PaperProps={{ sx: { borderRadius: theme?.palette?.DialogStyle?.borderRadius, border: theme?.palette?.DialogStyle?.border, minWidth: "800px", minHeight: "320px", fontFamily: theme?.typography?.fontFamily, backgroundColor: theme?.palette?.DialogStyle?.backgroundColor, zIndex: 1000, '& .MuiDialogContent-root': { backgroundColor: theme?.palette?.DialogStyle?.backgroundColor, }, '& .MuiDialogTitle-root': { backgroundColor: theme?.palette?.DialogStyle?.backgroundColor, }, '& .MuiDialogActions-root': { backgroundColor: theme?.palette?.DialogStyle?.backgroundColor, }, }, }} > Add Location
Location Name changeModalData("environment", event.target.value) } />
{loginInfo}
); const textColor = "#9E9E9E !important"; const handleSelectSubOrg = (id, action) => { if (action === "all") { const childOrgs = userdata.orgs.filter( (data) => data.creator_org === userdata.active_org.id ); setSelectedSubOrg((prev) => { if (prev.length === childOrgs.length) { // If all child orgs are already selected, clear the selection return []; } else { // Otherwise, select all child org IDs return childOrgs.map((data) => data.id); } }); } else if (action === "none") { setSelectedSubOrg([]); } else { setSelectedSubOrg((prev) => { if (prev.includes(id)) { return prev.filter((data) => data !== id); } else { return [...prev, id]; } }); } }; const queueSizeText = (queue) => { if (queue === undefined || queue === null) return 0; if (queue < 0) return 0; if (queue > 1000) return ">1000"; return queue; }; const LocationActionModal = (props) => { const { showLocationActionModal } = props const [searchQuery, setSearchQuery] = React.useState(""); if (showLocationActionModal === undefined || showLocationActionModal === null) { return null } if (showLocationActionModal?.open !== true) { return null } return ( { setShowLocationActionModal(undefined) }} PaperProps={{ sx: { borderRadius: theme?.palette?.DialogStyle?.borderRadius, border: theme?.palette?.DialogStyle?.border, fontFamily: theme?.typography?.fontFamily, backgroundColor: theme?.palette?.DialogStyle?.backgroundColor, zIndex: 1000, minWidth: 600, minHeight: 500, overflow: "auto", '& .MuiDialogContent-root': { backgroundColor: theme?.palette?.DialogStyle?.backgroundColor, }, '& .MuiDialogTitle-root': { backgroundColor: theme?.palette?.DialogStyle?.backgroundColor, }, '& .MuiDialogActions-root': { backgroundColor: theme?.palette?.DialogStyle?.backgroundColor, }, }, }} >
Select a job to send
Find app to re-download
) } const editEnvironmentConfig = (id, selectedSubOrg, cacheKey) => { const data = { action: "suborg_distribute", selected_suborgs: selectedSubOrg, } const url = `${globalUrl}/api/v1/environments/${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 environments"); } else { toast("Successfully updated environments!"); setTimeout(() => { getEnvironments(); setShowDistributionPopup(false); }, 1000); } }) ) .catch((error) => { toast("Err: " + error.toString()); }); }; const changeDistribution = (id, selectedSubOrg) => { editEnvironmentConfig(id, [...new Set(selectedSubOrg)]) } const EnvironmentDistributionModal = showDistributionPopup ? ( setShowDistributionPopup(false)} PaperProps={{ sx: { borderRadius: theme?.palette?.DialogStyle?.borderRadius, border: theme?.palette?.DialogStyle?.border, fontFamily: theme?.typography?.fontFamily, backgroundColor: theme?.palette?.DialogStyle?.backgroundColor, zIndex: 1000, minWidth: "600px", minHeight: "320px", overflow: "auto", '& .MuiDialogContent-root': { backgroundColor: theme?.palette?.DialogStyle?.backgroundColor, }, '& .MuiDialogTitle-root': { backgroundColor: theme?.palette?.DialogStyle?.backgroundColor, }, '& .MuiDialogActions-root': { backgroundColor: theme?.palette?.DialogStyle?.backgroundColor, }, }, }} > Select sub-org to distribute Environments {handleSelectSubOrg(null, "none")}}>None {handleSelectSubOrg(null, "all")}}>All {userdata.orgs.map((data, index) => { if (data.creator_org !== userdata.active_org.id) { return null; } const imagesize = 22; const imageStyle = { width: imagesize, height: imagesize, pointerEvents: "none", marginRight: 10, marginLeft: data.id === userdata.active_org.id ? 0 : 20, }; const image = data.image === "" ? ( {data.name} ) : ( {data.name} ); return ( handleSelectSubOrg(data.id)} style={{ display: "flex", alignItems: "center" }} > {image} {data.name} ); })}
) : null; return (
{modalView} {EnvironmentDistributionModal}
Runtime Locations Decides which Orborus runtime location to run your workflows in. Previously called Environments.
If you have scale problems, check the docs or talk to our team: {supportEmail}.  Learn more
setShowArchived(!showArchived)} />{" "} Show disabled {/* */}
{["Type", "Status", "Scale", "Pipeline", "Name", "Type", "Queue", "Actions", "Distribution"].map((header, index) => { return ( ) })} {showLoader ? [...Array(6)].map((_, rowIndex) => ( {Array(9).fill(null).map((_, colIndex) => ( ))} )) : environments?.length === 0 ? ( No Locations Found ):( environments?.map((environment, index) => { if (!showArchived && environment.archived) { return null; } if (environment.archived === undefined) { return null; } var bgColor = themeMode === "dark" ? "#212121" : "#FFFFFF"; if (index % 2 === 0) { bgColor = themeMode === "dark" ? "#1A1A1A" : "#EAEAEA"; } // 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 } const isDistributed = environment?.suborg_distribution?.length > 0 ? true : false; return ( <> { 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) }} > ) : environment.run_type === "docker" ? ( ) : environment.run_type === "k8s" ? ( ) : ( ) } style={{ minWidth: 80, padding: "8px 8px 8px 0", overflow: "hidden", whiteSpace: "normal", wordWrap: "break-word", textAlign: "center", display: "table-cell", }} /> {environment.Type !== "cloud" ? environment.running_ip === undefined || environment.running_ip === null || environment.running_ip.length === 0 ? "Not running. Click to get the start command that can be ran on your server." : IP / label: {environment?.running_ip?.split(":")[0]}. May stay running up to a minute after stopping Orborus. : `Cloud is automatically configured. Reachout to ${supportEmail} if you have any questions.` }

Last checkin: {environment?.checkin !== undefined && environment.checkin !== null && environment?.checkin > 0 ? new Date(environment?.checkin * 1000).toLocaleString() : "Never"} } placement="top"> {environment.Type !== "cloud" && (environment.running_ip === undefined || environment.running_ip === null || environment.running_ip.length === 0) ? { //handleChipClick }} variant="outlined" color="primary" /> : { //handleChipClick }} variant="outlined" color="primary" /> } } /> ) : ( ) } style={{ minWidth: 60, marginLeft: 20, overflow: "hidden", whiteSpace: "normal", wordWrap: "break-word", padding: 8, display: "table-cell", }} /> : environment?.data_lake?.enabled && environment?.archived !== true ? ( ) : ( { e.preventDefault() e.stopPropagation() window.open("/detections/Sigma", "_blank") }} > ) } style={{ minWidth: 60, marginLeft: 40, overflow: "hidden", whiteSpace: "normal", wordWrap: "break-word", display: "table-cell", }} /> {environment.Name} )} primaryTypographyProps={{ style:{ maxWidth: 150, whiteSpace: 'nowrap', overflow: "hidden", textOverflow: 'ellipsis', wordWrap: "break-word", transition: "all 0.3s ease", }}} style={{ minWidth: 120, maxWidth: 150, display: "table-cell", }} />
{environment.Type === "cloud" ? null : } {setIsExpanded(prev => !prev)}}> {listItemExpanded === index ? : }
{selectedOrganization.id !== undefined && environment?.org_id !== selectedOrganization.id ? } style={{ textAlign: 'center', verticalAlign: 'middle', }} /> : { e.stopPropagation() setShowDistributionPopup(true) if(environment?.suborg_distribution?.length > 0){ setSelectedSubOrg(environment.suborg_distribution) }else{ setSelectedSubOrg([]) } setSelectedEnvironment(environment.id) }}> }
Self-Hosted Orborus instance 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. { setInstallationTab(inputValue) }} aria-label="disabled tabs example" variant="scrollable" scrollButtons="auto" style={{textAlign: "center", marginTop: 25, }} > Verbose (default) /> Scale /> k8s /> {installationTab === 2 ? Check our Kubernetes documentation for more information on how to run Shuffle on Kubernetes. The status of the node will change when connected. : 1. Ensure Docker is installed and the target server can reach '{globalUrl}' } {installationTab === 2 ? null : "2. Run this command on the server you want to run workflows or store Pipeline data on"} {installationTab === 2 ? null :
{getOrborusCommand(environment)}
{ navigator.clipboard.writeText(orborusCommandWrapper()) toast("Copied to clipboard") }} >
Configure HTTP Proxies: { if (commandController.proxies === undefined) { commandController.proxies = true } else { commandController.proxies = !commandController.proxies } setCommandController(commandController) setUpdate(Math.random()) }} />
Disable Pipelines & Data Lake: { if (commandController.pipelines === undefined) { commandController.pipelines = true } else { commandController.pipelines = !commandController.pipelines } setCommandController(commandController) setUpdate(Math.random()) }} />
} {installationTab === 2 ? null : 3. Verify if the node is running. Try to refresh the page a little while after running the command. }
{showCPUAlert === false ? null : (
90% CPU the server(s) hosting the Shuffle App Runner (Orborus) was found. Need help with High Availability and Scale?{" "} Read documentation {" "} and{" "} Get in touch .
)} ); }) ) }
{showLocationActionModal !== undefined && showLocationActionModal !== null && showLocationActionModal?.open === true ?
: null }
) }); export default EnvironmentTab;