diff --git a/backend/go-app/main.go b/backend/go-app/main.go
index 66046eac..042a30aa 100755
--- a/backend/go-app/main.go
+++ b/backend/go-app/main.go
@@ -1048,6 +1048,20 @@ func handleInfo(resp http.ResponseWriter, request *http.Request) {
licensed := shuffle.IsLicensed(ctx, *org)
+ workflowapps, err := shuffle.GetAllWorkflowApps(ctx, 1000, 0)
+ if err != nil {
+ log.Printf("{WARNING] Failed getting apps (getworkflowapps): %s", err)
+ resp.WriteHeader(401)
+ resp.Write([]byte(`{"success": false}`))
+ return
+ }
+
+ orgApps := workflowapps
+ activatedAppIds := []string{}
+ for _, app := range orgApps {
+ activatedAppIds = append(activatedAppIds, app.ID)
+ }
+
returnValue := shuffle.HandleInfo{
Success: true,
Username: userInfo.Username,
@@ -1069,6 +1083,7 @@ func handleInfo(resp http.ResponseWriter, request *http.Request) {
Interests: orgInterests,
Priorities: orgPriorities,
Licensed: licensed,
+ ActiveApps: activatedAppIds,
}
returnData, err := json.Marshal(returnValue)
@@ -3175,7 +3190,7 @@ func buildSwaggerApp(resp http.ResponseWriter, body []byte, user shuffle.User, s
err = shuffle.DeployAppToDatastore(ctx, api)
//func DeployAppToDatastore(ctx context.Context, workflowapp WorkflowApp, bucketName string) error {
if err != nil {
- log.Printf("Failed adding app to db: %s", err)
+ log.Printf("[ERROR] Failed adding app to db: %s", err)
resp.WriteHeader(500)
resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "Failed adding app to db: %s"}`, err)))
return
@@ -3184,7 +3199,7 @@ func buildSwaggerApp(resp http.ResponseWriter, body []byte, user shuffle.User, s
// 2. Get all the required code
appbase, staticBaseline, err := shuffle.GetAppbase()
if err != nil {
- log.Printf("Failed getting appbase: %s", err)
+ log.Printf("[ERROR] Failed getting appbase: %s", err)
resp.WriteHeader(500)
resp.Write([]byte(`{"success": false, "reason": "Failed getting appbase code"}`))
return
@@ -4842,7 +4857,7 @@ func makeWorkflowPublic(resp http.ResponseWriter, request *http.Request) {
// FIXME - add org check too, and not just owner
// Check workflow.Sharing == private / public / org too
if user.Id != workflow.Owner || len(user.Id) == 0 {
- if workflow.OrgId == user.ActiveOrg.Id {
+ if workflow.OrgId == user.ActiveOrg.Id {
log.Printf("[AUDIT] User %s is accessing workflow %s as admin (public)", user.Username, workflow.ID)
} else {
log.Printf("[AUDIT] Wrong user (%s) for workflow %s (public)", user.Username, workflow.ID)
@@ -5107,6 +5122,13 @@ func initHandlers() {
r.HandleFunc("/api/v1/apps/authentication/{appauthId}/config", shuffle.SetAuthenticationConfig).Methods("POST", "OPTIONS")
r.HandleFunc("/api/v1/apps/authentication/{appauthId}", shuffle.DeleteAppAuthentication).Methods("DELETE", "OPTIONS")
+ r.HandleFunc("/api/v1/authentication/group", shuffle.AddAppAuthenticationGroup).Methods("POST", "OPTIONS")
+ r.HandleFunc("/api/v1/authentication/group", shuffle.GetAppAuthenticationGroup).Methods("GET", "OPTIONS")
+ r.HandleFunc("/api/v1/authentication/group/{key}", shuffle.DeleteAppAuthenticationGroup).Methods("DELETE", "OPTIONS")
+
+ r.HandleFunc("/api/v1/authentication/groups", shuffle.AddAppAuthenticationGroup).Methods("POST", "OPTIONS")
+ r.HandleFunc("/api/v1/authentication/groups", shuffle.GetAppAuthenticationGroup).Methods("GET", "OPTIONS")
+ r.HandleFunc("/api/v1/authentication/groups/{key}", shuffle.DeleteAppAuthenticationGroup).Methods("DELETE", "OPTIONS")
// Related to use-cases that are not directly workflows.
r.HandleFunc("/api/v1/workflows/usecases/{key}", shuffle.HandleGetUsecase).Methods("GET", "OPTIONS")
r.HandleFunc("/api/v1/workflows/usecases", shuffle.LoadUsecases).Methods("GET", "OPTIONS")
@@ -5173,9 +5195,11 @@ func initHandlers() {
r.HandleFunc("/api/v1/triggers/gmail/register", shuffle.HandleNewGmailRegister).Methods("GET", "OPTIONS")
r.HandleFunc("/api/v1/triggers/gmail/getFolders", shuffle.HandleGetGmailFolders).Methods("GET", "OPTIONS")
r.HandleFunc("/api/v1/triggers/pipeline", shuffle.HandleNewPipelineRegister).Methods("POST", "OPTIONS")
+ r.HandleFunc("/api/v1/triggers/github/register", shuffle.HandleNewGithubRegister).Methods("PUT", "OPTIONS")
//r.HandleFunc("/api/v1/triggers/pipeline/save", shuffle.HandleSavePipelineInfo).Methods("PUT", "OPTIONS")
r.HandleFunc("/api/v1/pipelines/{key}", handlePipelineCallback).Methods("POST", "GET", "PATCH", "PUT", "DELETE", "OPTIONS")
r.HandleFunc("/api/v1/triggers", shuffle.HandleGetTriggers).Methods("GET", "OPTIONS")
+
//r.HandleFunc("/api/v1/triggers/gmail/routing", handleGmailRouting).Methods("POST", "OPTIONS")
r.HandleFunc("/api/v1/triggers/gmail/{key}", shuffle.HandleGetSpecificTrigger).Methods("GET", "OPTIONS")
@@ -5204,8 +5228,13 @@ func initHandlers() {
// This is a new API that validates if a key has been seen before.
// Not sure what the best course of action is for it.
+ r.HandleFunc("/api/v1/getenvironments", shuffle.HandleGetEnvironments).Methods("GET", "OPTIONS")
+ r.HandleFunc("/api/v1/setenvironments", shuffle.HandleSetEnvironments).Methods("PUT", "OPTIONS")
r.HandleFunc("/api/v1/environments/{key}/stop", shuffle.HandleStopExecutions).Methods("GET", "POST", "OPTIONS")
r.HandleFunc("/api/v1/environments/{key}/rerun", shuffle.HandleRerunExecutions).Methods("GET", "POST", "OPTIONS")
+ r.HandleFunc("/api/v1/environments/{key}/stats", shuffle.HandleGetenvStats).Methods("GET", "OPTIONS")
+ r.HandleFunc("/api/v1/environments/{key}/config", shuffle.HandleSetenvConfig).Methods("POST", "OPTIONS")
+ r.HandleFunc("/api/v1/environments", shuffle.HandleGetEnvironments).Methods("GET", "OPTIONS")
r.HandleFunc("/api/v1/orgs/{orgId}/validate_app_values", shuffle.HandleKeyValueCheck).Methods("POST", "OPTIONS")
r.HandleFunc("/api/v1/orgs/{orgId}/list_cache", shuffle.HandleListCacheKeys).Methods("GET", "OPTIONS")
@@ -5214,8 +5243,10 @@ func initHandlers() {
r.HandleFunc("/api/v1/orgs/{orgId}/set_cache", shuffle.HandleSetCacheKey).Methods("POST", "OPTIONS")
r.HandleFunc("/api/v1/orgs/{orgId}/delete_cache", shuffle.HandleDeleteCacheKeyPost).Methods("POST", "OPTIONS")
r.HandleFunc("/api/v1/orgs/{orgId}/cache/{cache_key}", shuffle.HandleDeleteCacheKey).Methods("DELETE", "OPTIONS")
+ r.HandleFunc("/api/v1/orgs/{orgId}/cache/config", shuffle.HandleCacheConfig).Methods("POST", "OPTIONS")
r.HandleFunc("/api/v1/orgs/{orgId}/stats", shuffle.HandleGetStatistics).Methods("GET", "OPTIONS")
r.HandleFunc("/api/v1/orgs/{orgId}/stats", shuffle.HandleAppendStatistics).Methods("POST", "OPTIONS")
+ r.HandleFunc("/api/v1/orgs/{orgId}/stats/{key}", shuffle.GetSpecificStats).Methods("GET", "OPTIONS")
r.HandleFunc("/api/v1/orgs/{orgId}/statistics", shuffle.HandleGetStatistics).Methods("GET", "OPTIONS")
r.HandleFunc("/api/v1/orgs/{orgId}/cache", shuffle.HandleListCacheKeys).Methods("GET", "OPTIONS")
@@ -5242,6 +5273,8 @@ func initHandlers() {
r.HandleFunc("/api/v1/files/{fileId}", shuffle.HandleGetFileMeta).Methods("GET", "OPTIONS")
r.HandleFunc("/api/v1/files/{fileId}", shuffle.HandleDeleteFile).Methods("DELETE", "OPTIONS")
r.HandleFunc("/api/v1/files", shuffle.HandleGetFiles).Methods("GET", "OPTIONS")
+ r.HandleFunc("/api/v1/files/{fileId}/config", shuffle.HandleSetFileConfig).Methods("POST", "OPTIONS")
+ r.HandleFunc("/api/v1/files/namespaces/{namespace}/share", shuffle.HandleShareNamespace).Methods("POST", "OPTIONS")
// This structure is horrendous. Needs fixing after we got the prototype up
r.HandleFunc("/api/v1/detections/{detectionType}/connect", shuffle.HandleDetectionAutoConnect).Methods("GET", "OPTIONS")
diff --git a/frontend/public/images/frameworks/attack.png b/frontend/public/images/frameworks/attack.png
new file mode 100644
index 00000000..80f2dbb2
Binary files /dev/null and b/frontend/public/images/frameworks/attack.png differ
diff --git a/frontend/public/images/frameworks/openapi.png b/frontend/public/images/frameworks/openapi.png
new file mode 100644
index 00000000..bf3fc39b
Binary files /dev/null and b/frontend/public/images/frameworks/openapi.png differ
diff --git a/frontend/public/images/frameworks/python.jpeg b/frontend/public/images/frameworks/python.jpeg
new file mode 100644
index 00000000..b72240b6
Binary files /dev/null and b/frontend/public/images/frameworks/python.jpeg differ
diff --git a/frontend/public/images/frameworks/resized/attack.png b/frontend/public/images/frameworks/resized/attack.png
new file mode 100644
index 00000000..8bbbe126
Binary files /dev/null and b/frontend/public/images/frameworks/resized/attack.png differ
diff --git a/frontend/public/images/frameworks/resized/openapi.png b/frontend/public/images/frameworks/resized/openapi.png
new file mode 100644
index 00000000..31dba79d
Binary files /dev/null and b/frontend/public/images/frameworks/resized/openapi.png differ
diff --git a/frontend/public/images/frameworks/resized/sigma.png b/frontend/public/images/frameworks/resized/sigma.png
new file mode 100644
index 00000000..2a1c7c40
Binary files /dev/null and b/frontend/public/images/frameworks/resized/sigma.png differ
diff --git a/frontend/public/images/frameworks/sigma.png b/frontend/public/images/frameworks/sigma.png
new file mode 100644
index 00000000..0bd0db14
Binary files /dev/null and b/frontend/public/images/frameworks/sigma.png differ
diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx
index 8f7ec2e1..a57927d3 100755
--- a/frontend/src/App.jsx
+++ b/frontend/src/App.jsx
@@ -4,35 +4,36 @@ import { Link, Route, Routes, BrowserRouter, useNavigate } from "react-router-do
import { CookiesProvider } from "react-cookie";
import { removeCookies, useCookies } from "react-cookie";
-import Workflows from "./views/Workflows";
-import GettingStarted from "./views/GettingStarted";
+import Workflows from "./views/Workflows.jsx";
+import GettingStarted from "./views/GettingStarted.jsx";
import AngularWorkflow from "./views/AngularWorkflow.jsx";
import Header from "./components/NewHeader.jsx";
import HealthPage from "./components/HealthPage.jsx";
//import Header from "./components/Header.jsx";
-import theme from "./theme";
-import Apps from "./views/Apps";
+import theme from "./theme.jsx";
+import Apps from "./views/Apps.jsx";
import Apps2 from "./views/Apps2.jsx";
-import AppCreator from "./views/AppCreator";
+import AppCreator from "./views/AppCreator.jsx";
import DetectionDashBoard from "./views/DetectionDashboard.jsx";
import Welcome from "./views/Welcome.jsx";
import Dashboard from "./views/Dashboard.jsx";
import DashboardView from "./views/DashboardViews.jsx";
-import AdminSetup from "./views/AdminSetup";
-import Admin from "./views/Admin";
+import AdminSetup from "./views/AdminSetup.jsx";
+import Admin from "./views/Admin.jsx";
import Docs from "./views/Docs.jsx";
import Usecases2 from "./views/Usecases2.jsx";
//import Introduction from "./views/Introduction";
-import SetAuthentication from "./views/SetAuthentication";
-import SetAuthenticationSSO from "./views/SetAuthenticationSSO";
+import SetAuthentication from "./views/SetAuthentication.jsx";
+import SetAuthenticationSSO from "./views/SetAuthenticationSSO.jsx";
import Search from "./views/Search.jsx";
import RunWorkflow from "./views/RunWorkflow.jsx";
+import Admin2 from "./views/Admin2.jsx";
-import LoginPage from "./views/LoginPage";
-import SettingsPage from "./views/SettingsPage";
+import LoginPage from "./views/LoginPage.jsx";
+import SettingsPage from "./views/SettingsPage.jsx";
import KeepAlive from "./views/KeepAlive.jsx";
import { ThemeProvider } from "@mui/material/styles";
@@ -40,8 +41,8 @@ import CssBaseline from '@mui/material/CssBaseline';
import UpdateAuthentication from "./views/UpdateAuthentication.jsx";
import FrameworkWrapper from "./views/FrameworkWrapper.jsx";
-import ScrollToTop from "./components/ScrollToTop";
-import AlertTemplate from "./components/AlertTemplate";
+import ScrollToTop from "./components/ScrollToTop.jsx";
+import AlertTemplate from "./components/AlertTemplate.js";
import { isMobile } from "react-device-detect";
import RuntimeDebugger from "./components/RuntimeDebugger.jsx"
@@ -58,6 +59,7 @@ import Drift from "react-driftjs";
import { AppContext } from './context/ContextApi.jsx';
import Workflows2 from "./views/Workflows2.jsx";
+import AppExplorer from "./views/AppExplorer.jsx";
// Production - backend proxy forwarding in nginx
var globalUrl = window.location.origin;
@@ -207,37 +209,27 @@ const App = (message, props) => {
}
- {curpath.includes("/workflows") && curpath.includes("/run") ?
-
- :
- isLoggedIn ?
-
-
+ { window?.location?.pathname === "/" || window?.location?.pathname === "/training" || !(isLoggedIn && isLoaded) ? (
+
+
- :
-
-
-
- }
+ ) : (
+
+
+
+ ) }
{/*
@@ -262,7 +254,7 @@ const App = (message, props) => {
/>
{
/>
}
/>
+
+ }
+ />
} />
{
/>
{
/>
{
} />
{
/>
{
/>
}
/>
- } />
+ {/* } />
+ } /> */}
{
/>
{
/>
{
isMobile={isMobile}
isLoaded={isLoaded}
globalUrl={globalUrl}
+ isLoggedIn={isLoggedIn}
{...props}
/>
}
@@ -567,6 +579,7 @@ const App = (message, props) => {
isMobile={isMobile}
isLoaded={isLoaded}
globalUrl={globalUrl}
+ isLoggedIn={isLoggedIn}
{...props}
/>
}
diff --git a/frontend/src/components/AdminNavBar.jsx b/frontend/src/components/AdminNavBar.jsx
new file mode 100644
index 00000000..2c538d7b
--- /dev/null
+++ b/frontend/src/components/AdminNavBar.jsx
@@ -0,0 +1,227 @@
+import React, { useState, useEffect, useContext, memo } from 'react';
+import { useNavigate, useLocation } from 'react-router-dom';
+import OrganizationTab from '../components/OrganizationTab.jsx';
+import UserManagmentTab from '../components/UserManagmentTab.jsx';
+import CacheView from "../components/CacheView.jsx";
+import Files from "../components/Files.jsx";
+import AppAuthTab from "../components/AppAuthTab.jsx";
+import SchedulesTab from "../components/SchedulesTab.jsx";
+import EnvironmentTab from "../components/EnvironmentTab.jsx";
+import TenantsTab from "../components/TenantsTab.jsx";
+
+import {
+ Business as BusinessIcon,
+ PermIdentity as PermIdentityIcon,
+ HttpsOutlined as HttpsOutlinedIcon,
+ InsertDriveFileOutlined as InsertDriveFileOutlinedIcon,
+ StorageOutlined as StorageOutlinedIcon,
+ AccessTimeOutlined as AccessTimeOutlinedIcon,
+ FmdGoodOutlined as FmdGoodOutlinedIcon,
+ GroupOutlined as GroupOutlinedIcon
+} from '@mui/icons-material';
+import theme from '../theme.jsx';
+import { Button, Tooltip } from '@mui/material';
+import { Index } from 'react-instantsearch-dom';
+import { Context } from '../context/ContextApi.jsx';
+
+const AdminNavBar = (props) => {
+ const location = useLocation();
+ const { globalUrl, userdata, isCloud, isLoaded,removeCookie, handleStatusChange, selectedStatus, setSelectedStatus, handleEditOrg, serverside, notifications, handleGetOrg, orgId, checkLogin, setNotifications, stripeKey, setSelectedOrganization, selectedOrganization } = props;
+ const [selectedItem, setSelectedItem] = useState("Organization");
+ const [isSelectedFiles, setIsSelectedFiles] = useState(true);
+ const [isSelectedDataStore, setIsSelectedDataStore] = useState(true);
+
+ const navigate = useNavigate();
+
+ useEffect(() => {
+ const queryParams = new URLSearchParams(location.search);
+ const tabName = queryParams.get('tab');
+
+ if (tabName === "environments") {
+ setSelectedItem("Locations");
+ } else if (tabName === "suborgs") {
+ setSelectedItem("Tenants");
+ }else if (tabName === "cache") {
+ setSelectedItem("Datastore");
+ }else if (tabName) {
+ setSelectedItem(tabName.charAt(0).toUpperCase() + tabName.slice(1));
+ } else {
+ setSelectedItem("Organization");
+ }
+ }, [location.search]);
+
+
+ const items = [
+ { iconSrc: , alt: "Organization Icon", text: "Organization", component: OrganizationTab, props: { globalUrl,removeCookie, selectedStatus, isLoaded, setSelectedStatus, handleStatusChange, handleEditOrg, handleGetOrg, userdata, isCloud, serverside, notifications, checkLogin, setNotifications, stripeKey, setSelectedOrganization, selectedOrganization } },
+ { iconSrc: , alt: "Users Icon", text: "Users", component: UserManagmentTab, props: { globalUrl, userdata, serverside, isCloud, selectedOrganization, setSelectedOrganization, handleEditOrg } },
+ { iconSrc: , alt: "App Auth Icon", text: "App_auth", component: AppAuthTab, props: { globalUrl, userdata, isCloud, selectedOrganization } },
+ { iconSrc: , alt: "Datastore Icon", text: "Datastore", component: CacheView, props: { globalUrl, userdata, selectedOrganization, serverside, isSelectedDataStore, orgId , isCloud} },
+ { iconSrc: , alt: "Files Icon", text: "Files", component: Files, props: { isCloud, globalUrl, userdata, serverside, selectedOrganization, isSelectedFiles } },
+ { iconSrc: , alt: "Trigger Icon", text: "Triggers", component: SchedulesTab, props: { globalUrl, userdata, isCloud, serverside } },
+ { iconSrc: , alt: "Environments Icon", text: "Locations", component: EnvironmentTab, props: { globalUrl, userdata, isCloud, selectedOrganization } },
+ { iconSrc: , alt: "Tenants Icon", text: "Tenants", component: TenantsTab, props: {isCloud, globalUrl, userdata, serverside, selectedOrganization, setSelectedOrganization, checkLogin } }
+ ];
+
+ const setConfig = (newValue) => {
+ setSelectedItem(newValue);
+ if (newValue === "App Auth") {
+ const tabName = newValue.toLowerCase().replace(/\s+/g, '_');
+ navigate(`?tab=${tabName}`, { replace: true });
+ } else {
+ const tabName = newValue.toLowerCase().replace(/\s+/g, '_');
+ navigate(`?tab=${tabName}`, { replace: true });
+ }
+ };
+
+ const renderComponent = () => {
+ const selectedItemData = items.find(item => item.text === selectedItem);
+ if (!selectedItemData) {
+ setSelectedItem("Organization");
+ // If no tab is specified, default to "Organization" tab
+ return ;
+ };
+
+ const ComponentToRender = selectedItemData.component;
+ const componentProps = selectedItemData.props;
+
+ return ;
+ };
+
+ const defaultImage = "/images/logos/orange_logo.svg"
+ const imageData =
+ selectedOrganization?.image === undefined || selectedOrganization?.image.length === 0
+ ? defaultImage
+ : selectedOrganization?.image;
+
+ return (
+
+
+
+
+
+
{selectedOrganization?.name}
+
+
+ {items.map((item, index) => (
+
+
+ setConfig(item.text)}
+ >
+ {item.text.replace(/_/g, " ")}
+
+
+
+ ))}
+
+
+
+
+ {renderComponent()}
+
+ );
+};
+
+export default AdminNavBar;
+
+const PaddingWrapper2 = memo(({ children }) => {
+
+ return (
+
+ {children}
+
+ )
+});
+
+const Wrapper2 = memo(({children}) => {
+
+ return (
+
+ {children}
+
+ );
+})
+
+const PaddingWrapper = memo(({ children }) => {
+ const { leftSideBarOpenByClick, windowWidth } = useContext(Context);
+
+ return (
+
+ {children}
+
+ );
+});
+
+const Wrapper = memo(({ children }) => {
+ return (
+
+ {children}
+
+ );
+})
diff --git a/frontend/src/components/AppAuthTab.jsx b/frontend/src/components/AppAuthTab.jsx
new file mode 100644
index 00000000..dbd29e3d
--- /dev/null
+++ b/frontend/src/components/AppAuthTab.jsx
@@ -0,0 +1,2655 @@
+import React, { memo, useContext, useEffect, useState } from 'react';
+import {
+ Edit as EditIcon,
+ SelectAll as SelectAllIcon,
+ Delete as DeleteIcon,
+ CheckCircle as CheckCircleIcon,
+ Cancel as CancelIcon,
+ Search as SearchIcon,
+ Clear as ClearIcon,
+ DragIndicator as DragIndicatorIcon,
+ Close as CloseIcon,
+ LockOpen as LockOpenIcon,
+} from "@mui/icons-material";
+import { useNavigate } from "react-router-dom";
+import { toast } from "react-toastify";
+import theme from "../theme.jsx";
+import Markdown from "react-markdown";
+import AuthenticationOauth2 from "../components/Oauth2Auth.jsx";
+import { isMobile } from "react-device-detect"
+import PaperComponent from "../components/PaperComponent.jsx";
+import { CodeHandler, Img, OuterLink, } from '../views/Docs.jsx'
+import { v4 as uuidv4} from "uuid";
+
+import {
+ Divider,
+ List,
+ ListItem,
+ ListItemText,
+ IconButton,
+ Tooltip,
+ Chip,
+ Checkbox,
+ Typography,
+ TextField,
+ Button,
+ Dialog,
+ DialogTitle,
+ DialogActions,
+ DialogContent,
+ Select,
+ FormControl,
+ InputLabel,
+ MenuItem,
+ FormControlLabel,
+ InputAdornment,
+ Grid,
+ Zoom,
+ Paper,
+ Skeleton,
+ Link,
+} from "@mui/material";
+
+import algoliasearch from "algoliasearch/lite";
+import {
+ InstantSearch,
+ Configure,
+ connectSearchBox,
+ connectHits,
+ connectHitInsights,
+ RefinementList,
+ ClearRefinements,
+ connectStateResults
+} from "react-instantsearch-dom";
+import aa from "search-insights";
+import { Context } from '../context/ContextApi.jsx';
+
+const searchClient = algoliasearch(
+ "JNSS5CFDZZ",
+ "db08e40265e2941b9a7d8f644b6e5240"
+);
+
+const AppAuthTab = memo((props) => {
+ const { globalUrl, userdata, isCloud, selectedOrganization } = props;
+ const [selectedAuthentication, setSelectedAuthentication] = React.useState({});
+ const [authentication, setAuthentication] = React.useState([]);
+ const [selectedUserModalOpen, setSelectedUserModalOpen] = React.useState(false);
+ const [authenticationFields, setAuthenticationFields] = React.useState([]);
+ const [selectedAuthenticationModalOpen, setSelectedAuthenticationModalOpen] = React.useState(false);
+ const [appAuthenticationGroupEnvironment, setAppAuthenticationGroupEnvironment] = React.useState("");
+ const [environments, setEnvironments] = React.useState([]);
+ const [listItemExpanded, setListItemExpanded] = React.useState(-1);
+ const [appAuthenticationGroupModalOpen, setAppAuthenticationGroupModalOpen] = React.useState(false);
+ const [appAuthenticationGroupId, setAppAuthenticationGroupId] = React.useState("");
+ const [appAuthenticationGroups, setAppAuthenticationGroups] = React.useState([]);
+ const [appAuthenticationGroupName, setAppAuthenticationGroupName] = React.useState("");
+ const [appAuthenticationGroupDescription, setAppAuthenticationGroupDescription] = React.useState("");
+ const [appsForAppAuthGroup, setAppsForAppAuthGroup] = React.useState([]);
+ const [searchQuery, setSearchQuery] = React.useState("");
+ const [showAppModal, setShowAppModal] = useState(false)
+ const [showAuthenticationLoader, setShowAuthenticationLoader] = useState(true)
+ const [showAppAuthGroupLoader, setShowAppAuthGroupLoader] = useState(true)
+ const changeDistribution = (data) => {
+ //changeDistributed(data, !isDistributed)
+ editAuthenticationConfig(data.id, "suborg_distribute")
+ }
+
+ useEffect(() => {
+ getAppAuthentication();
+ getAppAuthenticationGroups();
+ getEnvironments();
+ }, [])
+
+ 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) {
+ setAuthentication(responseJson.data);
+ setShowAuthenticationLoader(false)
+ } else {
+ toast("Failed getting authentications");
+ }
+ })
+ .catch((error) => {
+ toast(error.toString());
+ });
+ };
+
+ 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 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 {
+ getAppAuthentication();
+
+
+ setSelectedAuthentication({});
+ setSelectedAuthenticationModalOpen(false);
+ }
+ })
+ )
+ .catch((error) => {
+ toast("Err: " + error.toString());
+ });
+ };
+
+ 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 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 editAuthenticationModal = selectedAuthenticationModalOpen ? (
+ {
+ setSelectedAuthenticationModalOpen(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,
+ },
+ },
+ }}
+ >
+
+
+ Edit authentication for {selectedAuthentication.app.name.replaceAll("_", " ")} (
+ {selectedAuthentication.label})
+
+
+ You can not 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.
+
+
+
+
+ Authentication Label
+
+ {
+ selectedAuthentication.label = e.target.value
+ }}
+ />
+
+
+ {selectedAuthentication.type === "oauth" || selectedAuthentication.type === "oauth2" || selectedAuthentication.type === "oauth2-app" ?
+
+
+ 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.
+
+
+ : 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 (
+
+
+ {fieldname}
+
+ {
+ authenticationFields[index].value = e.target.value;
+ setAuthenticationFields(authenticationFields);
+ }}
+ />
+
+ );
+ })}
+
+
+ setSelectedAuthenticationModalOpen(false)}
+ color="primary"
+ >
+ Cancel
+
+ {
+ 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
+
+
+
+ ) : null;
+
+ 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 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);
+ setShowAppAuthGroupLoader(false)
+ }
+ })
+ .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 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 authenticationView = appAuthenticationGroupModalOpen ?
+ (
+
+ {/* (appAuthenticationGroupModalOpen : { */}
+ {appAuthenticationGroupModalOpen && (
+
{
+ setAppAuthenticationGroupModalOpen(false);
+
+ setAppAuthenticationGroupId("")
+ setAppAuthenticationGroupName("")
+ setAppAuthenticationGroupEnvironment("")
+ setAppAuthenticationGroupDescription("")
+ setAppsForAppAuthGroup([])
+ }}
+ PaperProps={{
+ sx: {
+ borderRadius: theme?.palette?.DialogStyle?.borderRadius,
+ border: theme?.palette?.DialogStyle?.border,
+ minWidth: "1000px",
+ padding: "25px",
+ paddingLeft: "50px",
+ 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,
+ },
+ },
+ }}
+ sx={{
+ "& .MuiDialog-paper": {
+ backgroundColor: "rgb(26, 26, 26)",
+ },
+ }}
+ >
+
+ App Authentication Groups
+
+
+
+
+
+
+ Name
+
+ {
+ setAppAuthenticationGroupName(event.target.value);
+ }}
+ />
+
+
+
+ Evironment
+
+ {environments !== undefined && environments !== null && environments.length > 0 ?
+ {
+ setAppAuthenticationGroupEnvironment(e.target.value);
+ }}
+ >
+ {environments.map((env, index) => {
+ return (
+
+ {env.Name}
+
+ )
+ })}
+
+ :
+
+ Locations failed to load. Please try again
+
+ }
+
+
+ {
+ createAppAuthenticationGroup(
+ appAuthenticationGroupName,
+ appAuthenticationGroupEnvironment,
+ appAuthenticationGroupDescription,
+ appsForAppAuthGroup,
+ );
+ }}
+ >
+ Set Group
+
+
+
+
+
+
+
+ {/* Show a check box list of all app authentications to add to the auth group */}
+
+ {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 (
+
+
+
+
+
{
+ handleAppAuthGroupCheckbox(data)
+ }}
+ name={data.label}
+ disabled={data.app.id in appsForAppAuthGroup}
+ />
+
+
+ }
+ label={data.label}
+ />
+
+ )
+ })}
+
+
+
+
+
+
+ )}
+
+
+
+ ): null
+
+
+
+ const appModal = showAppModal ? (
+ {
+ setShowAppModal(false);
+ }}
+ PaperProps={{
+ sx: {
+ borderRadius: theme?.palette?.DialogStyle?.borderRadius,
+ border: theme?.palette?.DialogStyle?.border,
+ width: '700px',
+ maxWidth: '700px',
+ overflowY: 'hidden',
+ height: "600px",
+ maxHeight: "600px",
+ fontFamily: theme?.typography?.fontFamily,
+ zIndex: 1000,
+ '& .MuiDialogContent-root': {
+ padding: '30px',
+ backgroundColor: theme?.palette?.DialogStyle?.backgroundColor,
+ },
+ '& .MuiDialogTitle-root': {
+ backgroundColor: theme?.palette?.DialogStyle?.backgroundColor,
+ },
+ '& .MuiDialogActions-root': {
+ backgroundColor: theme?.palette?.DialogStyle?.backgroundColor,
+ },
+ },
+ }}
+ >
+
+
+
+ Add App Authentication
+
+
+
+
+
+
+
+
+ ) : null;
+
+ return (
+
+ {appModal}
+
+
+
+
setShowAppModal(true)}
+ >
+ Add App Auth
+
+
+ {/*
*/}
+
+
+
+
+
+
+
+ {["Valid", "Label", "App Name", "Workflows", "Fields", "Edited", "Actions", "Distribution"].map((header, index) => (
+
+ ))}
+
+
+ {showAuthenticationLoader
+ ?
+ [...Array(6)].map((_, rowIndex) => (
+
+ {Array(8)
+ .fill()
+ .map((_, colIndex) => (
+
+
+
+ ))}
+
+ ))
+ : authentication?.length === 0 ? (
+
+
+ No authentication found.
+
+
+ ):authentication.map((data, index) => {
+ var bgColor = "#212121";
+ if (index % 2 === 0) {
+ bgColor = "#1A1A1A";
+ }
+
+ //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!",
+ },
+ ];
+ }
+
+ const isDistributed = data.suborg_distributed === true ? true : false;
+ var validIcon =
+ if (data.validation !== null && data.validation !== undefined && data.validation.valid === false) {
+
+ if (data.validation.changed_at === 0) {
+ validIcon = ""
+ } else {
+ validIcon =
+ }
+ }
+ return (
+
+
+
+ {validIcon}
+
+
+ )}
+ style={{ display: "table-cell", verticalAlign: 'middle', minWidth: 60 }}
+ primaryTypographyProps={{
+ style: {
+ padding: "8px 8px 8px 15px",
+ }
+ }}
+ 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")
+ }}
+ />
+
+
+
+ {data?.app?.name?.replaceAll("_", " ")}
+
+
+ }
+ primaryTypographyProps={{
+ style: {
+ padding: 8
+ }
+ }}
+ style={{ marginLeft: 10, display: "table-cell", textAlign: 'center', verticalAlign: 'middle', padding: 8 }}
+ />
+
+ {
+ return data.key;
+ })
+ .join(", ")
+ }
+ primaryTypographyProps={{
+ style: {
+ padding: 8
+ }
+ }}
+ style={{
+ overflow: "auto",
+ display: "table-cell",
+ verticalAlign: 'middle'
+ }}
+ />
+
+
+ {
+ updateAppAuthentication(data);
+ }}
+ disabled={data.org_id !== selectedOrganization.id}
+ >
+
+
+ {data.defined ? (
+
+ {
+ editAuthenticationConfig(data.id);
+ }}
+ >
+
+
+
+ ) : (
+
+ {}}
+ disabled={data.org_id !== selectedOrganization.id}
+ >
+
+
+
+ )}
+ {
+ deleteAuthentication(data);
+ }}
+ >
+
+
+
+
+ {selectedOrganization.id !== undefined && data.org_id !== selectedOrganization.id ?
+
+
+
+ :
+
+ {
+ changeDistribution(data, !isDistributed)
+ }}
+ />
+
+ }
+
+
+ );
+ })}
+
+
+ {editAuthenticationModal}
+ {authenticationView}
+
+
+
App Authentication Groups
+
+ Disabled until further notice . Makes a workflow run replicate across all relevant authentications in an app auth group. Useful when the EXACT same workflow is supposed to run many times from one single input. {" "}
+
+ Learn more about App Authentication Groups
+
+
+
+
+
{
+ if (environments !== undefined && environments !== null && environments.length > 0) {
+ setAppAuthenticationGroupEnvironment(environments[0].Name)
+ }
+
+ setAppAuthenticationGroupModalOpen(true)
+ }}
+ disabled={true}
+ >
+ Add Group
+
+
+
+
+ {["Label", "Environment", "App Auth", "Created At", "Actions"].map((header, index) => (
+
+ ))}
+
+ {showAppAuthGroupLoader ?
+ [...Array(6)].map((_, rowIndex) => (
+
+ {Array(5)
+ .fill()
+ .map((_, colIndex) => (
+
+
+
+ ))}
+
+ ))
+ : appAuthenticationGroups.length === 0 ? (
+
+
+ No authentication groups found.
+
+
+ ): appAuthenticationGroups.map((data, index) => {
+ var bgColor = "#212121";
+ if (index % 2 === 0) {
+ bgColor = "#1A1A1A";
+ }
+
+ if (data.app_auths === undefined || data.app_auths === null) {
+ data.app_auths = []
+ }
+
+ return (
+
+
+
+
+ {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 (
+
+
+
+ )
+ })}
+
+ }
+ style={{ display:'table-cell', verticalAlign: 'middle' }}
+ />
+
+
+ {
+ setAppAuthenticationGroupId(data.id)
+
+ setAppAuthenticationGroupName(data.label)
+ setAppAuthenticationGroupDescription(data.description)
+
+ setAppsForAppAuthGroup(data.app_auths.map((appAuth) => appAuth.id))
+ setAppAuthenticationGroupEnvironment(data.environment)
+ setAppAuthenticationGroupModalOpen(true)
+ }}
+ >
+
+
+ {
+ deleteAppAuthenticationGroup(data.id)
+ }}
+ >
+
+
+
+ }
+ style={{ display:'table-cell', verticalAlign: 'middle' }}
+ />
+
+
+ );
+ }
+ )}
+
+
+
+
+
+
+
+ );
+});
+
+export default AppAuthTab;
+
+
+const SearchBox = ({ currentRefinement, refine, isSearchStalled, searchQuery, setSearchQuery }) => {
+ const handleSearch = (e) => {
+ refine(searchQuery.trim());
+ };
+
+ return (
+
+ );
+};
+
+const Hits = ({
+ hits,
+ insights,
+ setIsAnyAppActivated,
+ searchQuery,
+ isCloud,
+ globalUrl,
+ userdata,
+ getAppAuthentication
+}) => {
+ const [mouseHoverIndex, setMouseHoverIndex] = useState(-1);
+ const [selectedAppData, setSelectedAppData] = useState({})
+ const [appid, setAppId] = useState("")
+ const [selectedAuthentication, setSelectedAuthentication] = useState({})
+ const [authenticationModalOpen, setAuthenticationModalOpen] = useState(false)
+ const [authenticationType, setAuthenticationType] = React.useState("");
+ const [appAuthentication, setAppAuthentication] = useState([]);
+ const [selectedMeta, setSelectedMeta] = useState(undefined);
+ const [selectedAction, setSelectedAction] = useState(
+ {
+ "app_name": selectedAppData.name,
+ "app_id": selectedAppData.id,
+ "app_version": selectedAppData.version,
+ "large_image": selectedAppData.large_image,
+ }
+ )
+ const navigate = useNavigate();
+
+ const normalizedString = (name) => {
+ if (typeof name === 'string') {
+ return name.replace(/_/g, ' ');
+ } else {
+ return name;
+ }
+ };
+
+
+ let workflowDelay = 0;
+ const isHeader = true;
+ const paperStyle = {
+ color: "rgba(241, 241, 241, 1)",
+ padding: isHeader ? null : 15,
+ cursor: "pointer",
+ width: '100%',
+ maxHeight: 96,
+ borderRadius: 4,
+ transition: 'background-color 0.3s ease',
+ };
+
+ const base64_decode = (str) => {
+ return decodeURIComponent(
+ atob(str)
+ .split("")
+ .map(function (c) {
+ return "%" + ("00" + c.charCodeAt(0).toString(16)).slice(-2);
+ })
+ .join("")
+ );
+ };
+
+ const handleAppAuthenticationType = (selectedAppData)=> {
+
+ if (selectedAppData.authentication === undefined || selectedAppData.authentication === null) {
+ setAuthenticationType({
+ type: "",
+ })
+
+ selectedAppData.authentication = {
+ type: "",
+ required: false,
+ }
+ } else {
+ setAuthenticationType(
+ selectedAppData.authentication.type === "oauth2-app" || (selectedAppData.authentication.type === "oauth2" && selectedAppData.authentication.redirect_uri !== undefined && selectedAppData.authentication.redirect_uri !== null) ? {
+ type: selectedAppData.authentication.type,
+ redirect_uri: selectedAppData.authentication.redirect_uri,
+ refresh_uri: selectedAppData.authentication.refresh_uri,
+ token_uri: selectedAppData.authentication.token_uri,
+ scope: selectedAppData.authentication.scope,
+ client_id: selectedAppData.authentication.client_id,
+ client_secret: selectedAppData.authentication.client_secret,
+ grant_type: selectedAppData.authentication.grant_type,
+ } : {
+ type: "",
+ }
+ )
+ }
+ }
+
+ function Heading(props) {
+ const element = React.createElement(
+ `h${props.level}`,
+ { style: { marginTop: 40 } },
+ props.children
+ );
+ return (
+
+ {props.level !== 1 ? (
+
+ ) : null}
+ {element}
+
+ );
+ }
+
+ const getAppDocs = (appname, location, version) => {
+ fetch(`${globalUrl}/api/v1/docs/${appname}?location=${location}&version=${version}`, {
+ headers: {
+ Accept: "application/json",
+ },
+ credentials: "include",
+ })
+ .then((response) => {
+ if (response.status === 200) {
+ //toast("Successfully GOT app "+appId)
+ } else {
+ //toast("Failed getting app");
+ }
+
+ return response.json();
+ })
+ .then((responseJson) => {
+ if (responseJson.success === true) {
+ if (responseJson.meta !== undefined && responseJson.meta !== null && Object.getOwnPropertyNames(responseJson.meta).length > 0) {
+ setSelectedMeta(responseJson.meta)
+ }
+
+ if (responseJson.reason !== undefined && responseJson.reason !== undefined && responseJson.reason.length > 0) {
+ if (!responseJson.reason.includes("404: Not Found") && responseJson.reason.length > 25) {
+ // Translate into markdown ![]()
+ const imgRegex = / ({
+ ...prevState,
+ documentation: newdata,
+ }));
+ }
+ }
+ }
+
+ })
+ .catch((error) => {
+ toast(error.toString());
+ });
+ };
+
+ const handleDecodeOfOpenApiData = (data) => {
+ var appexists = false;
+ var parsedapp = {};
+
+ if (data.app !== undefined && data.app !== null) {
+ var parsedBaseapp = "";
+ try {
+ parsedBaseapp = base64_decode(data.app);
+ } catch (e) {
+ parsedBaseapp = data;
+ }
+
+ parsedapp = JSON.parse(parsedBaseapp);
+ parsedapp.name = parsedapp.name.replaceAll("_", " ");
+
+ appexists =
+ parsedapp.name !== undefined &&
+ parsedapp.name !== null &&
+ parsedapp.name.length !== 0;
+ if(parsedapp?.id.length > 0){
+ setSelectedAppData(parsedapp)
+ handleAppAuthenticationType(parsedapp)
+ const apptype = selectedAppData?.generated === false ? "python" : "openapi"
+ getAppDocs(parsedapp.name, apptype, parsedapp.version);
+ setAuthenticationModalOpen(true);
+ }
+ }
+
+ if (data.openapi === undefined || data.openapi === null) {
+ return;
+ }
+
+ var parsedDecoded = "";
+ try {
+ parsedDecoded = base64_decode(data.openapi);
+ } catch (e) {
+ parsedDecoded = data;
+ }
+
+ parsedapp = JSON.parse(parsedDecoded);
+ data =
+ parsedapp.body === undefined ? parsedapp : JSON.parse(parsedapp.body);
+ };
+
+ const getAppData = (appid) => {
+ if (appid === undefined || appid === null || appid.length === 0) {
+ return;
+ }
+ const url = `${globalUrl}/api/v1/apps/${appid}/config`;
+
+ fetch(url, {
+ credentials: "include",
+ method: "GET",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ })
+ .then((response) => {
+ if (response.status !== 200) {
+ toast.error("Failed to get app data or App doesn't. Please contact support@shuffler.io");
+ return;
+ }
+ return response.json();
+ })
+ .then((responseJson) => {
+ if (responseJson.success === true) {
+ handleDecodeOfOpenApiData(responseJson);
+ } else {
+ toast.error("Failed to get app data or App doesn't exist");
+ }
+ })
+ .catch((error) => {
+ console.error("error for app is :", error);
+ });
+ };
+
+ const UpdateAppAuthentication = (data) => {
+ if (data === undefined || data === null) {
+ return;
+ }
+ const filteredData = data.filter((appAuth) => appAuth?.app?.id === appid);
+ if (filteredData.length === 0) {
+ setAppAuthentication([]);
+ setSelectedAuthentication({});
+ } else {
+ setAppAuthentication(filteredData);
+ setSelectedAuthentication(filteredData[0]);
+ }
+ };
+
+ const HandleAppAuthentication = ()=>{
+
+ const url = `${globalUrl}/api/v1/apps/authentication`;
+
+ fetch(url, {
+ method: "GET",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ credentials: "include",
+ }).then((response) => {
+ if (response.status !== 200) {
+ return;
+ }
+ return response.json();
+ }).then((responseJson) => {
+ if (responseJson.success === true) {
+ UpdateAppAuthentication(responseJson.data);
+ } else {
+ toast.error("Failed to get app authentication data");
+ }
+ }).catch((error) => {
+ console.error("error for app is :", error);
+ });
+ }
+
+ const handleAppAuthenticationNew = (data) => () => {
+
+ const appid = data.objectID;
+ if (appid > 0) {
+ setAppId(appid);
+ }
+ if (appid.length > 0) {
+ toast.info(`Getting authentication for ${data.name}. Please wait...`);
+ getAppData(appid);
+ HandleAppAuthentication();
+ }
+ }
+
+ const AuthenticationData = (props) => {
+ const selectedApp = props.app;
+
+ const [authenticationOption, setAuthenticationOptions] = React.useState({
+ app: JSON.parse(JSON.stringify(selectedApp)),
+ fields: {},
+ label: "",
+ usage: [
+ {
+ // workflow_id: workflow.id,
+ },
+ ],
+ id: uuidv4(),
+ active: true,
+ });
+
+ if (
+ selectedApp.authentication === undefined ||
+ selectedApp.authentication.parameters === null ||
+ selectedApp.authentication.parameters === undefined ||
+ selectedApp.authentication.parameters.length === 0
+ ) {
+ return (
+
+
+ {selectedApp.name} does not require authentication
+
+
+ );
+ }
+
+ authenticationOption.app.actions = [];
+
+ for (let paramkey in selectedApp.authentication.parameters) {
+ if (
+ authenticationOption.fields[
+ selectedApp.authentication.parameters[paramkey].name
+ ] === undefined
+ ) {
+ authenticationOption.fields[
+ selectedApp.authentication.parameters[paramkey].name
+ ] = "";
+ }
+ }
+
+ const setNewAppAuth = (appAuthData, refresh) => {
+ setSelectedAuthentication(appAuthData);
+ var headers = {
+ "Content-Type": "application/json",
+ "Accept": "application/json",
+ }
+
+ headers["Org-Id"] = userdata?.active_org?.id
+
+ fetch(globalUrl + "/api/v1/apps/authentication", {
+ method: "PUT",
+ headers: headers,
+ body: JSON.stringify(appAuthData),
+ credentials: "include",
+ })
+ .then((response) => {
+ if (response.status !== 200) {
+ console.log("Status not 200 for setting app auth :O!");
+
+ if (response.status === 400) {
+ toast.error("Failed setting new auth. Please try again", {
+ "autoClose": true,
+ })
+ }
+ }
+
+ return response.json();
+ })
+ .then((responseJson) => {
+ if (!responseJson.success) {
+ toast.error("Error: " + responseJson.reason, {
+ "autoClose": false,
+ })
+
+ } else {
+ HandleAppAuthentication()
+ setAuthenticationModalOpen(false)
+ getAppAuthentication()
+ }
+ })
+ .catch((error) => {
+ console.log("New auth error: ", error.toString());
+ });
+ };
+
+ const handleSubmitCheck = () => {
+ if (authenticationOption.label.length === 0) {
+ authenticationOption.label = `Auth for ${selectedApp.name}`;
+ }
+ for (let paramkey in selectedApp.authentication.parameters) {
+ if (
+ authenticationOption.fields[
+ selectedApp.authentication.parameters[paramkey].name
+ ].length === 0
+ ) {
+ if (
+ selectedApp.authentication.parameters[paramkey].value !== undefined &&
+ selectedApp.authentication.parameters[paramkey].value !== null &&
+ selectedApp.authentication.parameters[paramkey].value.length > 0
+ ) {
+ authenticationOption.fields[
+ selectedApp.authentication.parameters[paramkey].name
+ ] = selectedApp.authentication.parameters[paramkey].value;
+ } else {
+ if (
+ selectedApp.authentication.parameters[paramkey].schema.type === "bool"
+ ) {
+ authenticationOption.fields[
+ selectedApp.authentication.parameters[paramkey].name
+ ] = "false";
+ } else {
+ toast(
+ "Field " +
+ selectedApp.authentication.parameters[paramkey].name +
+ " can't be empty"
+ );
+ return;
+ }
+ }
+ }
+ }
+
+ var newAuthOption = JSON.parse(JSON.stringify(authenticationOption));
+ var newFields = [];
+ for (let authkey in newAuthOption.fields) {
+ const value = newAuthOption.fields[authkey];
+ newFields.push({
+ "key": authkey,
+ "value": value,
+ });
+ }
+
+ newAuthOption.fields = newFields
+ setNewAppAuth(newAuthOption)
+ }
+
+ if (authenticationOption.label === null || authenticationOption.label === undefined) {
+ authenticationOption.label = selectedApp.name + " authentication";
+ }
+
+ return (
+
+
+
+ Authentication for {selectedApp.name.replaceAll("_", " ", -1)}
+
+
+
+
+ What is app authentication?
+
+
+ These are required fields for authenticating with {selectedApp.name}
+
+ Label for you to remember
+ {
+ authenticationOption.label = event.target.value;
+ }}
+ />
+
+
+ {selectedApp.authentication.parameters.map((data, index) => {
+ if (data.value === "" || data.value === null || data.value === undefined || data.name === "url") {
+ }
+
+
+ return (
+
+
+ {data.name}
+
+ {data.schema !== undefined &&
+ data.schema !== null &&
+ data.schema.type === "bool" ? (
+ {
+ authenticationOption.fields[data.name] = e.target.value;
+ }}
+ style={{
+ backgroundColor: theme.palette.surfaceColor,
+ color: "white",
+ height: 50,
+ }}
+ >
+
+ false
+
+
+ true
+
+
+ ) : (
+ {
+ authenticationOption.fields[data.name] =
+ event.target.value;
+ }}
+ />
+ )}
+
+ );
+ })}
+
+
+ {
+ setAuthenticationModalOpen(false);
+ }}
+ color="secondary"
+ >
+ Cancel
+
+ {
+ setAuthenticationOptions(authenticationOption);
+ handleSubmitCheck()
+ }}
+ color="primary"
+ >
+ Submit
+
+
+
+ );
+ };
+
+ const authenticationModal = authenticationModalOpen ? (
+ {setSelectedMeta(undefined)}}
+ PaperProps={{
+ style: {
+ pointerEvents: "auto",
+ color: "white",
+ minWidth: 1100,
+ minHeight: 700,
+ maxHeight: 700,
+ padding: 15,
+ overflow: "hidden",
+ zIndex: 10012,
+ border: theme.palette.defaultBorder,
+ },
+ }}
+ >
+
+ { selectedAppData.reference_info === undefined ||
+ selectedAppData.reference_info === null ||
+ selectedAppData.reference_info.github_url === undefined ||
+ selectedAppData.reference_info.github_url === null ||
+ selectedAppData.reference_info.github_url.length === 0 ? (
+
+
+
+
+ ) : (
+
+
+
+ )}
+
+
+
+
+
+
+ {
+ setAuthenticationModalOpen(false);
+ }}
+ >
+
+
+
+
+ {authenticationType.type === "oauth2" || authenticationType.type === "oauth2-app" ?
+
+ :
+
+ }
+
+
+ {selectedAppData.documentation === undefined ||
+ selectedAppData.documentation === null ||
+ selectedAppData.documentation.length === 0 ? (
+
+
+
+ {selectedAppData?.description}
+
+
+
+
+
+
+ There is no Shuffle-specific documentation for this app yet outside of the general description above. Documentation is written for each api, and is a community effort. We hope to see your contribution!
+
+ {
+ toast.success("Opening remote Github documentation link. Thanks for contributing!")
+
+ setTimeout(() => {
+ window.open(`https://github.com/Shuffle/openapi-apps/new/master/docs?filename=${selectedAppData.name.toLowerCase()}.md`, "_blank")
+ }, 2500)
+ }}
+ >
+ Create Docs
+
+
+
+
+ Want to help the making of, or improve this app?{" "}
+
+
+ Join the community on Discord!
+
+
+
+
+ Want to help change this app directly?
+
+ {selectedAppData.reference_info === undefined ||
+ selectedAppData.reference_info === null ||
+ selectedAppData.reference_info.github_url === undefined ||
+ selectedAppData.reference_info.github_url === null ||
+ selectedAppData.reference_info.github_url.length === 0 ? (
+
+
+
+ Check it out on Github!
+
+
+
+ ) : (
+
+
+
+ Check it out on Github!
+
+
+
+ )}
+
+ ) : (
+
+ {selectedMeta !== undefined && selectedMeta !== null && Object.getOwnPropertyNames(selectedMeta).length > 0 && selectedMeta.name !== undefined && selectedMeta.name !== null ?
+
+
+ {isMobile ? null : (
+
+
+
+ Edit
+
+
+
+ )}
+ {isMobile ? null : (
+
+ )}
+
+ {selectedMeta.read_time} minute
+ {selectedMeta.read_time === 1 ? "" : "s"} to read
+
+
+
+ {isMobile ||
+ selectedMeta.contributors === undefined ||
+ selectedMeta.contributors === null ? (
+ ""
+ ) : (
+
+ {selectedMeta.contributors.slice(0, 7).map((data, index) => {
+ return (
+
+
+
+
+
+ );
+ })}
+
+ )}
+
+
+ : null}
+
+
+ {selectedAppData.documentation}
+
+
+ )}
+
+
+
+) : null;
+
+ return (
+
+ {authenticationModal}
+ {hits.length === 0 && searchQuery.length >= 0 ? (
+
+
+ No Apps Found
+
+
+ ) : (
+
+
+
+ )}
+
+ );
+
+};
+
+const CustomSearchBox = connectSearchBox(SearchBox);
+const CustomHits = connectHits(Hits);
diff --git a/frontend/src/components/AppGrid.jsx b/frontend/src/components/AppGrid.jsx
index b0577937..46afcb6b 100644
--- a/frontend/src/components/AppGrid.jsx
+++ b/frontend/src/components/AppGrid.jsx
@@ -416,9 +416,13 @@ const AppGrid = (props) => {
>
{hits.map((data, index) => {
const appUrl =
- isCloud
- ? `/apps/${data.objectID}?queryID=${data.__queryID}`
- : `https://shuffler.io/apps/${data.objectID}?queryID=${data.__queryID}`;
+ isCloud === true ?
+ `/apps/${data.id}`
+ : `https://shuffler.io/apps/${data.objectID}`;
+
+ if (data.name === "" && data.id === "") {
+ return null
+ }
return (
{
};
const appUrl =
- isCloud === true
- ? `/apps/${data.id}`
+ isCloud === true ?
+ `/apps/${data.id}`
: `https://shuffler.io/apps/${data.id}`;
- if (data.name === "" && data.id === "") {
- return null
- }
+ if (data.name === "" && data.id === "") {
+ return null
+ }
return (
{
const isCloud =
window.location.host === "localhost:3002" ||
- window.location.host === "shuffler.io" || window.location.host === "localhost:3000"
+ window.location.host === "shuffler.io"
? true
: false;
@@ -574,8 +574,10 @@ const AppModal = ({ open, onClose, app, globalUrl, getApps }) => {
{newAppname}
{
+ const [hovered, setHovered] = useState("");
+
+ //console.log("Date: ", new Date("2019-11-14T08:00:00.000Z"))
+ //var inputdata = keys.data
+ //const inputdata = keys.data === undefined ? [{"key": inputname, "data": keys}] : keys.data
+ const inputdata = keys.data === undefined ? keys : keys.data
+
+ /*
+ series={
+
+ }
+ area={
+ }
+ gradient={
+ ,
+
+ ]}
+ />
+ }
+ />
+ }
+ gridlines={ } />}
+ colorScheme={(colorInput) => {
+ var color = "#f86a3e"
+ //if (colorInput !== undefined && colorInput.length > 0) {
+ // color = colorInput[0].metadata !== undefined && colorInput[0].metadata.color !== undefined ? colorInput[0].metadata.color : color
+ //}
+
+ return color
+ }}
+ />
+ }
+ */
+
+ return (
+
+
+ {inputname}
+
+
} />
+ }
+ />
+ {/*
+
(
+
+ )}
+ />
+ }
+ />
+ }
+ />
+ */}
+
+ )
+}
+
+
+const AppStats = (defaultprops) => {
+ const { globalUrl, appId , workflowId} = defaultprops;
+ const [keys, setKeys] = useState([])
+ const [widgetData, setWidgetData] = useState({});
+ const [searches, setSearches] = useState([]);
+ const [clickData, setClickData] = useState(undefined);
+ const [conversionData, setConversionData] = useState(undefined);
+
+ const handleDataSetting = (inputdata, grouping) => {
+ var newlist = []
+
+ for (var key in inputdata.events) {
+ var newlist = []
+
+ for (var subkey in inputdata.events[key].data) {
+ const subdata = inputdata.events[key].data[subkey]
+ //console.log("Timestamp: ", subdata.key)
+
+ if (grouping === "day") {
+ const daysplit = subdata.key.split("T")[0]
+ //console.log("Grouping by day: ", daysplit)
+
+ const foundIndex = newlist.findIndex(data => data.key === daysplit)
+ if (foundIndex !== undefined && foundIndex !== null && foundIndex >= 0) {
+ newlist[foundIndex].data += 1
+ newlist[foundIndex].y += 1
+ } else {
+ newlist.push({
+ "key": daysplit,
+ "x": daysplit,
+ "data": 1,
+ "y": 1,
+ })
+ }
+ } else {
+ console.log("No grouping set?")
+ try {
+ inputdata.events[key].data[subkey].key = new Date(subdata.key)
+ } catch (e) {
+ console.log("Failed timestamp: ", e)
+ }
+ }
+ }
+
+ // Fixing timestamps after sorting based on day
+ for (var subkey in newlist) {
+ const subdata = newlist[subkey]
+ newlist[subkey].key = new Date(subdata.key)
+ }
+
+ console.log("Inputdata: ", inputdata.events[key])
+ if (inputdata.events[key].key === "click") {
+ setClickData(newlist)
+ } else if (inputdata.events[key].key === "conversion") {
+ setConversionData(newlist)
+ } else {
+ console.log("No handler for ", inputdata.events[key].key)
+ }
+ }
+
+ //new Date('11/22/2019')
+ setWidgetData(inputdata)
+ }
+
+ const getAppStats = (appId) => {
+ fetch(`${globalUrl}/api/v1/apps/${appId}/stats`, {
+ method: "GET",
+ headers: {
+ "Content-Type": "application/json",
+ Accept: "application/json",
+ },
+ credentials: "include",
+ })
+ .then((response) => {
+ if (response.status !== 200) {
+ console.log("Status not 200 for workflows :O!: ", response.status);
+ return;
+ }
+
+ return response.json();
+ })
+ .then((responseJson) => {
+ if (responseJson["success"] === false) {
+ return
+ }
+
+ handleDataSetting(responseJson, "day")
+ })
+ .catch((error) => {
+ console.log("error: ", error)
+ });
+ }
+
+ const getWorkflowStats = (workflowId) => {
+ fetch(`${globalUrl}/api/v1/workflow/${workflowId}/stats`, {
+ method: "GET",
+ headers: {
+ "Content-Type": "application/json",
+ Accept: "application/json",
+ },
+ credentials: "include",
+ })
+ .then((response) => {
+ if (response.status !== 200) {
+ console.log("Status not 200 for workflows :O!: ", response.status);
+ return;
+ }
+
+ return response.json();
+ })
+ .then((responseJson) => {
+ if (responseJson["success"] === false) {
+ return
+ }
+
+ handleDataSetting(responseJson, "day")
+ })
+ .catch((error) => {
+ console.log("error: ", error)
+ });
+ }
+
+
+ useEffect(() => {
+ //setWidgetData(inputdata)
+ getAppStats(appId)
+ getWorkflowStats(workflowId)
+ }, [])
+
+ const paperStyle = {
+ textAlign: "center",
+ padding: 40,
+ margin: 5,
+ backgroundColor: theme.palette.inputColor,
+ }
+
+ console.log("Widget: ", widgetData)
+ const data = (
+
+
+
+
+ {widgetData.orgs}
+
+
+ Orgs
+
+
+
+
+ {widgetData.searches}
+
+
+ Searches
+
+
+ {/*
+
+
+ {widgetData.clicks}
+
+
+ Clicks
+
+
+
+
+ {widgetData.conversions}
+
+
+ Conversions
+
+
+
+
+ {widgetData.forks}
+
+
+ Forks
+
+
+ */}
+
+
+ {clickData === undefined ?
+ null
+ :
+
+ }
+
+
+ {conversionData === undefined ?
+ null
+ :
+
+ }
+
+ )
+
+ const dataWrapper = (
+ {data}
+ );
+
+ return dataWrapper;
+}
+
+export default AppStats;
\ No newline at end of file
diff --git a/frontend/src/components/CloudSyncTab.jsx b/frontend/src/components/CloudSyncTab.jsx
new file mode 100644
index 00000000..08926570
--- /dev/null
+++ b/frontend/src/components/CloudSyncTab.jsx
@@ -0,0 +1,780 @@
+import React, { useEffect, useState, useContext } from "react";
+import {
+ FormControl,
+ Card,
+ Tooltip,
+ Typography,
+ TextField,
+ Button,
+ Grid,
+ ListItem,
+ ListItemText,
+ ListItemAvatar,
+ IconButton,
+ Avatar,
+ Zoom,
+ InputAdornment,
+ Switch,
+ Skeleton
+} from "@mui/material";
+import { ToastContainer, toast } from "react-toastify";
+import {
+ Edit as EditIcon,
+ Polyline as PolylineIcon,
+ CheckCircle as CheckCircleIcon,
+ Close as CloseIcon,
+ Visibility as VisibilityIcon,
+ VisibilityOff as VisibilityOffIcon,
+} from "@mui/icons-material";
+import theme from "../theme.jsx";
+import { styled } from '@mui/styles';
+import { Context } from "../context/ContextApi.jsx";
+
+const CloudSyncTab = (props) => {
+ const {
+ userdata,
+ globalUrl,
+ serverside
+ } = props;
+ const [cloudSyncApikey, setCloudSyncApikey] = useState("");
+ const [loading, setLoading] = useState(false);
+ const [showApiKey, setShowApiKey] = useState(false);
+ const [orgSyncResponse, setOrgSyncResponse] = React.useState("");
+ const [organizationFeatures, setOrganizationFeatures] = React.useState({});
+ const [selectedOrganization, setSelectedOrganization] = React.useState({});
+ const [selectedStatus, setSelectedStatus] = React.useState([]);
+ const [orgRequest, setOrgRequest] = React.useState(true);
+ const [userSettings, setUserSettings] = React.useState({});
+ const [, forceUpdate] = React.useState();
+ const itemColor = "white";
+ const isCloud = window?.location?.host === "localhost:3002" || window?.location?.host === "shuffler.io";
+ useEffect(() => { getSettings(); }, []);
+ 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 ?
+
+ :
+
+
+ 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,
+ };
+ console.log("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)
+ }
+ const handleToggleFeature = (e) => {
+ // Your logic for toggling the feature's active state
+ console.log(`Toggling ${primary}`);
+ if (!isCloud || userdata.support !== true) {
+ return
+ }
+
+ e.preventDefault();
+ e.stopPropagation();
+
+ enableFeature()
+ };
+
+ return (
+
+
+
{
+ setExpanded(prev => !prev);
+ if(showEdit){
+ setShowEdit(false)
+ }
+ }}
+ >
+
+ {primaryIcon}
+
+
+ {isCloud && userdata.support === true ?
+
+ {
+ e.preventDefault();
+ e.stopPropagation();
+ console.log('expanded', expanded)
+ if (expanded){
+ setExpanded(false)
+ }
+ if (showEdit) {
+ setShowEdit(false)
+ return
+ }
+
+ console.log("Edit")
+
+ setShowEdit(true)
+ }}
+ />
+
+ : null}
+ {userdata.support === true ?(
+
+
+
+ ):(
+
+ {
+ if (!isCloud || userdata.support !== true) {
+ return
+ }
+
+ e.preventDefault();
+ e.stopPropagation();
+
+ enableFeature()
+ }}
+ >
+ {secondaryIcon}
+
+
+ )}
+
+
+ {expanded ?
+
+
+ Usage:
+ {props.data.limit === 0 ? (
+ "Unlimited"
+ ) : (
+
+ {props.data.usage} / {props.data.limit === "" ? "Unlimited" : props.data.limit}
+
+ )}
+
+ {/*
+ Data sharing: {props.data.data_collection}
+ */}
+ Description: {secondary}
+
+ : null}
+
+
+ {showEdit ?
+
{
+ console.log("Submit")
+ submitEdit(e)
+ }}>
+
+
+ {
+ setNewValue(event.target.value)
+ }}
+ />
+ {
+ console.log("Submit 2")
+ submitEdit(e)
+ }}
+ sx={{
+ marginTop: 0.5,
+ maarginBottom: 0.5
+ }}
+ >
+ Submit
+
+
+
+ : null}
+
+
+ );
+ };
+ const handleGetOrg = (orgId) => {
+
+ if (serverside !== true && window.location.search !== undefined && window.location.search !== null) {
+ const urlSearchParams = new URLSearchParams(window.location.search);
+ const params = Object.fromEntries(urlSearchParams.entries());
+ const foundorgid = params["org_id"];
+ if (foundorgid !== undefined && foundorgid !== null) {
+ orgId = foundorgid;
+ }
+ }
+
+ 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?
+
+ fetch(`${globalUrl}/api/v1/orgs/${orgId}`, {
+ method: "GET",
+ credentials: "include",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ })
+ .then((response) => {
+ if (response.status === 401) {
+ }
+
+ 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 = {};
+ }
+
+ setSelectedOrganization(responseJson)
+ var lists = {
+ active: {
+ triggers: [],
+ features: [],
+ sync: [],
+ },
+ inactive: {
+ triggers: [],
+ features: [],
+ sync: [],
+ },
+ };
+ setOrganizationFeatures(lists);
+ }
+ })
+ .catch((error) => {
+ console.log("Error getting org: ", error);
+ toast("Error getting current organization");
+ });
+ };
+ 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(); API no longer in use, as it's in handleInfo request
+ 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 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);
+ });
+ };
+ if (
+ selectedOrganization.id === undefined &&
+ userdata !== undefined &&
+ userdata.active_org !== undefined &&
+ orgRequest === true
+ ) {
+ setOrgRequest(false);
+ handleGetOrg(userdata.active_org.id);
+ }
+
+ return (
+
+
+
+ Cloud syncronization
+
+
+ What does cloud sync do? Cloud synchronization is a way of getting more out of Shuffle. Shuffle will ALWAYS make every option open source, but features relying on other users can't be done without a collaborative approach.
+
+
+
+ {isCloud ? (
+
+
+
+ Currently syncronizing:{" "}
+ {selectedOrganization.cloud_sync_active === true
+ ? True
+ : False }
+
+ {selectedOrganization.cloud_sync_active ? (
+
+ Syncronization interval:{" "}
+ {selectedOrganization.sync_config.interval === 0
+ ? "60"
+ : selectedOrganization.sync_config.interval}
+
+ ) : null}
+
+ Your Api key
+
+ {userSettings?.apikey === undefined || userSettings?.apikey === null || userSettings?.apikey?.length <=0 ? (
+
+ ):
+
+
+ {
+ setShowApiKey(!showApiKey)
+ }}
+ >
+ {showApiKey ? : }
+
+
+ )
+ }}
+ required
+ fullWidth={true}
+ disabled={true}
+ autoComplete="cloud apikey"
+ id="apikey_field"
+ margin="normal"
+ placeholder="Cloud Apikey"
+ variant="outlined"
+ value={userSettings?.apikey}
+ defaultValue={userSettings?.apikey}
+ type={!isCloud || showApiKey ? "text" : "password"}
+ />
+ {selectedOrganization.cloud_sync_active ? (
+ {
+ handleStopOrgSync(selectedOrganization.id);
+ }}
+ >
+ Stop Sync
+
+ ) : null}
+
}
+
+
+ ) : (
+
+
+ {
+ setCloudSyncApikey(event.target.value);
+ }}
+ />
+ {
+ setLoading(true);
+ enableCloudSync(
+ cloudSyncApikey,
+ selectedOrganization,
+ selectedOrganization.cloud_sync
+ );
+ }}
+ color="primary"
+ variant={
+ selectedOrganization.cloud_sync === true
+ ? "outlined"
+ : "contained"
+ }
+ >
+ {selectedOrganization.cloud_sync
+ ? "Stop sync"
+ : "Start sync"}
+
+
+ {orgSyncResponse.length > 0 ? (
+
+ Message from Shuffle Cloud: {orgSyncResponse}
+
+ ) : null}
+
+ )}
+
+
+ Features
+
+
+ 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.
+
+
+ {selectedOrganization.sync_features === undefined ||
+ selectedOrganization.sync_features === null
+ ?
+ {[...Array(18)].map((_, i) => (
+
+
+
+
+
+ ))}
+
+ : 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) {
+ return null
+ }
+
+ const newkey = key.replaceAll("_", " ");
+ 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: ,
+ };
+
+ return (
+
+
+
+ );
+ })}
+
+
+ );
+};
+
+export default CloudSyncTab;
diff --git a/frontend/src/components/EditOrgTab.jsx b/frontend/src/components/EditOrgTab.jsx
new file mode 100644
index 00000000..6a5e1662
--- /dev/null
+++ b/frontend/src/components/EditOrgTab.jsx
@@ -0,0 +1,446 @@
+import React, { useEffect, useState, useContext } from 'react';
+import OrgHeaderexpanded from "./OrgHeaderexpandedNew.jsx";
+import OrgHeader from './OrgHeaderNew.jsx';
+import { toast } from "react-toastify";
+import CloudSyncTab from './CloudSyncTab.jsx';
+import {
+ FileCopy as FileCopyIcon,
+} from "@mui/icons-material";
+import {
+ Button,
+ Tooltip,
+ IconButton,
+} from "@mui/material";
+
+const EditOrgTab = (props) => {
+ const {
+ userdata,
+ globalUrl,
+ serverside,
+ selectedOrganization,
+ setSelectedOrganization,
+ handleGetOrg,
+ selectedStatus, setSelectedStatus,
+ handleEditOrg,
+ } = props;
+ const [organizationFeatures, setOrganizationFeatures] = React.useState({});
+ const [users, setUsers] = React.useState([]);
+ const [orgRequest, setOrgRequest] = React.useState(true);
+ const isCloud = (window.location.host === "localhost:3002" || window.location.host === "shuffler.io") ? true : (process.env.IS_SSR === "true");
+ useEffect(() => {
+ if(users.length === 0) {
+ getUsers();
+ }
+ }, []);
+
+ const handleStatusChange = (event) => {
+ const { value } = event.target;
+ setSelectedStatus(value);
+
+ handleEditOrg(
+ selectedOrganization?.name,
+ selectedOrganization?.description,
+ selectedOrganization.id,
+ selectedOrganization.image,
+ {
+ app_download_repo: selectedOrganization?.defaults?.app_download_repo,
+ app_download_branch: selectedOrganization?.defaults?.app_download_branch,
+ workflow_download_repo: selectedOrganization?.defaults?.workflow_download_repo,
+ workflow_download_branch: selectedOrganization?.defaults?.workflow_download_branch,
+ notification_workflow: selectedOrganization?.defaults?.notification_workflow,
+ documentation_reference: selectedOrganization?.defaults?.documentation_reference,
+ workflow_upload_repo: selectedOrganization?.defaults?.workflow_upload_repo,
+ workflow_upload_branch: selectedOrganization?.defaults?.workflow_upload_branch,
+ workflow_upload_username: selectedOrganization?.defaults?.workflow_upload_username,
+ workflow_upload_token: selectedOrganization?.defaults?.workflow_upload_token,
+ newsletter: selectedOrganization?.defaults?.newsletter,
+ weekly_recommendations: selectedOrganization?.defaults?.weekly_recommendations,
+ },
+ {
+ sso_entrypoint: selectedOrganization?.sso_config?.sso_entrypoint,
+ sso_certificate: selectedOrganization?.sso_config?.sso_certificate,
+ client_id: selectedOrganization?.sso_config?.client_id,
+ client_secret: selectedOrganization?.sso_config?.client_secret,
+ openid_authorization: selectedOrganization?.sso_config?.openid_authorization,
+ openid_token: selectedOrganization?.sso_config?.openid_token,
+ SSORequired: selectedOrganization?.sso_config?.SSORequired,
+ auto_provision: selectedOrganization?.sso_config?.auto_provision,
+ },
+ value.length === 0 ? ["none"] : value,
+ );
+ };
+
+
+ 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 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}`
+ }
+
+ if (
+ selectedOrganization.id === undefined &&
+ userdata !== undefined &&
+ userdata.active_org !== undefined &&
+ orgRequest
+ ) {
+ setOrgRequest(false);
+ }
+
+
+ return (
+
+
+
+
+
+
Organization overview
+
+ On this page organization admins can configure organisations, and sub-orgs (MSSP).{" "}
+
+ Learn more
+
+
+
+
+
+
+ {/* {isCloud ?
+
+ {
+ 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}
+
+
+ : null} */}
+
+
+
+
+
+ )
+}
+
+export default EditOrgTab;
\ No newline at end of file
diff --git a/frontend/src/components/EnvironmentTab.jsx b/frontend/src/components/EnvironmentTab.jsx
new file mode 100644
index 00000000..a7fb6bae
--- /dev/null
+++ b/frontend/src/components/EnvironmentTab.jsx
@@ -0,0 +1,1541 @@
+import React, { memo, useContext, useEffect, useState } from 'react';
+import theme 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'
+
+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([]);
+ 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 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
+ ) {
+ 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: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
+ };
+
+ 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} {/* Assuming loginInfo is part of the relevant content */}
+
+
+ setModalOpen(false)}
+ color="primary"
+ >
+ Cancel
+
+ {
+ submitEnvironment(modalUser); // Assuming modalUser is available
+ }}
+ color="primary"
+ >
+ Submit
+
+
+
+ );
+
+
+ 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 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 === "" ? (
+
+ ) : (
+
+ );
+
+ return (
+ handleSelectSubOrg(data.id)}
+ style={{ display: "flex", alignItems: "center" }}
+ >
+
+ {image}
+ {data.name}
+
+ );
+ })}
+
+
+ setShowDistributionPopup(false)}
+ color="primary"
+ >
+ Cancel
+
+ {
+ changeDistribution(selectedEnvironment, selectedSubOrg);
+ }}
+ color="primary"
+ >
+ Submit
+
+
+
+
+ ) : null;
+
+ return (
+
+ {modalView}
+ {EnvironmentDistributionModal}
+
+
+
+
Locations
+
+ Decides which Orborus runtime location to run your workflows in. Previously Environments. If you have scale problems, talk to our team: support@shuffler.io.
+
+ Learn more
+
+
+
+
setModalOpen(true)}
+ >
+ Add Location
+
+
+
+
+
setShowArchived(!showArchived)}
+ />{" "}
+ Show disabled
+ {/* */}
+
+
+
+ {["Type", "Status", "Scale", "Pipeline", "Name", "Type", "Queue", "Actions", "Distribution"].map((header, index) => (
+
+ ))}
+
+ {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 = "#212121";
+ if (index % 2 === 0) {
+ bgColor = "#1A1A1A";
+ }
+
+ // 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",
+ }}
+ />
+
+ IP / label: {environment?.running_ip?.split(":")[0]}. May stay running up to a minute after stopping Orborus.
+ :
+ "Cloud is automatically configured. Reachout to support@shuffler.io if you have any questions."
+ } 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",
+ }}
+ />
+
+
+
+
+
+
+ {
+ e.preventDefault();
+ e.stopPropagation();
+ setDefaultEnvironment(environment)
+ }}
+ color="primary"
+ >
+ Make Default
+
+ {
+ e.preventDefault();
+ e.stopPropagation();
+ deleteEnvironment(environment)
+ }}
+ color="primary"
+ >
+ {environment.archived ? "Activate" : "Disable"}
+
+ {
+ 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"}
+
+
+ {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)
+ }}>
+
+
+
+ }
+
+
+
+
+
+
+
+ Your 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)}
+
+
+
+
+
+ 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
+
+ .
+
+
+
+ {
+ // dismiss -> get envs
+ changeRecommendation(
+ userdata.priorities[foundIndex],
+ "dismiss",
+ );
+ }}
+ >
+ Dismiss
+
+
+
+
+ )}
+ >
+ );
+ })
+ ) }
+
+
+ {/* */}
+
+
+
+
+ )
+});
+
+export default EnvironmentTab;
diff --git a/frontend/src/components/LeftSideBar.jsx b/frontend/src/components/LeftSideBar.jsx
index 60155384..d87fc53c 100644
--- a/frontend/src/components/LeftSideBar.jsx
+++ b/frontend/src/components/LeftSideBar.jsx
@@ -1907,4 +1907,4 @@ useEffect(() => {
);
};
-export default LeftSideBar;
+export default LeftSideBar;
\ No newline at end of file
diff --git a/frontend/src/components/OrgHeaderNew.jsx b/frontend/src/components/OrgHeaderNew.jsx
new file mode 100644
index 00000000..b319f3f3
--- /dev/null
+++ b/frontend/src/components/OrgHeaderNew.jsx
@@ -0,0 +1,520 @@
+import React, { useEffect, useState } from "react";
+import theme from "../theme.jsx";
+import { makeStyles } from "@mui/styles";
+import { toast } from 'react-toastify';
+
+import {
+ Tooltip,
+ TextField,
+ FormControl,
+ InputLabel,
+ OutlinedInput,
+ Checkbox,
+ Select,
+ MenuItem,
+ Button,
+ ListItemText,
+ Dialog,
+ DialogActions,
+ DialogContent,
+ Divider,
+ DialogTitle,
+} from "@mui/material";
+
+import AvatarEditor from "react-avatar-editor";
+
+
+import {
+ AddAPhotoOutlined as AddAPhotoOutlinedIcon,
+ ZoomInOutlined as ZoomInOutlinedIcon,
+ ZoomOutOutlined as ZoomOutOutlinedIcon,
+ Loop as LoopIcon,
+ AddPhotoAlternate as AddPhotoAlternateIcon,
+} from "@mui/icons-material";
+
+import {
+ ExpandLess as ExpandLessIcon,
+ ExpandMore as ExpandMoreIcon,
+ Save as SaveIcon,
+} from "@mui/icons-material";
+
+const useStyles = makeStyles({
+ notchedOutline: {
+ borderColor: "#f85a3e !important",
+ },
+});
+
+const defaultImage =
+ "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAK4AAACuCAYAAACvDDbuAAAgAElEQVR4Xu19e9CvV1Xe3r/vnJOcBEhBSgMEBaoUK9POCOVmAuP0HwcUCNYZSUsh9xv3hGl1qNippQRE20IFEhQoBJiaKVpEgQRnhD+0QHSmRkAsxE4doBZQTs71u/zezruv6/Ksvffvkn/qd8bBfN/3XvZe+1nPevbaa+/XuxX/Tbe6C/ece8qOd5dNk3um9+7Jk/OPds49zE3uyPy4ST5zCr/y4f+sfxO4j16vHsqfmV7gpgm8Yzm/lP9+km1B9+W2zn/zzk2sDeR55ffy3enn1Lf5J/1e3bb42kX43/Do1nt9fUdpLrSbbFt8vvls+idlm/qsaJPWuNK/TfvOLU44577pnPuSc9PvT95/9szu3p9c/IH/c2oVKDbeyB8z/Yz7noNd9zzn3U+5hX+6m9wjnXM7GqXCHpbR0+PnjudGxEtBkzRo02vFtRB83rkAXPqPGD4MnmWGGa3CDqp9+pp4Bwd2dCwPzIXur6D1xaGRXaQz8nf4Kfix1/3joDXtDm0jHVbYspirbZdkj4Npcv/XO/8F59xdfv/o7zz0A1/9yxEAd4E7/by74OCke5Fb+Bvd5J7unDvGHmyCKiMzX40ByW+XQCxvyoSafgGMBT0fgTayXmA/kykr8PI1kS0JIOMPRiSJ7ctOWfuI+mcw4Uj7lO25Y0TQoyHGbKsjJbqXR5HoKijK6nuz3bPhokEX+f4956Z7nXP/8cz+w//bxR/4H00GbgJ3ep170sHC/WxgWecvUJ5AemrLA4NF2cCAa4BDSCbjYVR2RYN2ZXlAOgxZqcXUDLh9toyvSkybopAJ+KBbOpHADOEjoO1EkVYU6LB0HbPUDtaN8N7TzrmPOnfwry9631/8mcW+JnD3Xu+eu3DubZNzT1Xhu8eyxaXw47lWBIzcAW2fGSpoM1WPgzYNWqSRwqjsnSYokgMyvW5JnzWZNrQpC9tC+ZVZW1Ek9GbBQK8ZuS99mrrW1sPh7XXsTeDmqHrvYvKve+j77/8MAi9E1v6t7vnOu7c7555gaM4SuiHGOsajcV8xmeEUMSDFf4yJQLiUmpYNTg906iVCl7b6ltu4gi5lTMs6Z4RpwLQsVMP+IZaNTtYngRoNI1uOzwdYhJxtEhqKAFsdvoI0XHy/m3ZeedH7v/ZxCV7Vir1b3HP8wr3PBC1HDsog5IkJxSdDHAyBlRqBgyHwAANuI3sg3j4G+shSHASQaRlbZtDW8AmiT7b3TLJD8qACrWrwkclYg2l70qACWow5IZvQ9lHQUmeZ7p+8v/Lh7/3z36NDw6w7a9rljvvw5NwPWykOOuEono5YT8IvXQNBK5yhGnwFVoCgZYPYZgvSBtnGMMkJnR2ZrIh3Fga3mGoR9YgpL2xAjbGlBC2YTEGWJu01J3mkrwYGuFP25EF1XMFj9y6Wiyse9p+/9hXl2il78G7n3T9radomMVphpMdGUG9wAJRLYJ4Wz2CZ73QmUpkxa9jlA2fmKvNz0414Fm+Abw6b+U8D4Km+xfsb0l5E99Z+S4mApIHhaEz2bCAPSmMkaNMzW4Av94ac3vtPHdu96TG3f32evFXhuH+ru8J5f4dzrmYPVGiKN7Bfsx/syVj+C07AA3WQmlYBa4TR+bpWnhYCgjyLtN+OBrbezPdAwNcQCjq4EAsaCEAS8BF4PNIZoBKhWdo9PMO0DX5vbmGTRMi41SgyxrQlTcmsVQB+2jt/3UXv/9qdxVLTre5RBwv/m25yz+Qspe2NQWt5ZEQ5x79kMgzaoTC4DM9SuoqDvcEWSqKsNlnJHYvvQ5oWvRutiInrijdwALGBLZ003sEcMl7DSEdHoGTHyoQ4eqTxaqymzdmYuq40wrS1D5A80i+985/1u0de/LAPf+Vb4Y7917uXO+dud84fRcClIOKdVwPGQZRuNBuj6DujMHakCUCgadUsubeMS1A/vhRLBxZEgR6g8juLbUzAE1tWe8wslv9hh9HyQPknZNpEAsUQaJWPDgpttyCjsDxuTMQMwAOiUoTknNv1frrmovfd/wE/1x4cTP4jbuF+3AItwFe81ACGXm0SAC/5UTYGoSBAdwCwmZE9GAqhInmvBr8JPMmAo0zLB7G5YmdMALkTy/fOz5+NonO0VaJl0Bmyh3jDakzL2xL6JnLF8dGjk8xKDBWPtc3euY+dWT7kJX73VveMhfe/4Zy7ONOcgDpnP+a+iC1WLpZJ7RPhArLCPD7snZSVpnmGkgMZTdIrA5BIwOoHBkDLHWs1eRBMZ/Wr2FWGTcFmGQSMZYhjAInAB1C2WQIlrBGUi1JtUaJ8DHrtVIJtwyjZ6bxKjA3Q1n590y8Wl/uDW9wt08LfFgpmCCgx81FroU40KqhUvNLPYu+0NBSYiBUckjDHxtX0eCJJeqBN7cfh2WazKa/FK4MK+wGmHV/t40wbmyoBj8erYDQ5VGYC1VzFmBZpjUzEjGIjhREO5NS2A+f9v/T7t7q7nPc/qZPbYuhZeJdGL6lIMgFQ12gsJUBxwObLxP1KHujO+8nQZT15ANiOsrRcooZVXjDEyyovAzypbDKNQRifdSZiFXTI9m2JQCNBZcAcv+x7w9iF/zEACwlLZEd6ZZiFmWI7vJvu8gevd/dNk/+hPFDa02rwtXOZdZij8axlUoldwnilAYaRDKa1c5ixizxMVqfAEcUIo122RW2OCwv8/aPSotolLn5Y+nBkRWxk4aQuqGu7GOMxE5nPtQcN0DLggvGGYySjRR3L1L77/P6t/tvOuUc0gdvSZb0qL+hxtbSw/tkGma49SB3J2sl4R0SOBqNm+AYbFW9Hz0KgqIPIgWsCgHhzvaZ9rwYsjgKyzZiJMVmNVoiNsi3oWxi3BlkUwshMW7JO35mBu+dc3LmgwNvUfZVNasfH5AHVX/kOvHNBDzbXfX3jZkxnThliwSwtSswcLQDng7haFVUEWb99umAm6+5iyzA0yDbcntDmhQSwo2U5EvPzI3pWOzzDC/OaLtMm0Pm9GbhVpkD0hheX9hZwpxdWPYQ6iiqMweCgUMizB7nBYtXOeKeo0optXC1Mg8WFagMzAtV62mpKwCiC4miBdcCNSRg2aOnQNSu4Amn5MAi2LGyzoF2aaDtLflfCS2kDtBPRtBVf+cr4Dr+XgMs7Tn9CA86uNkDBr4k/AY0jQ0XRsoIdUqV9NXjLUXrywAJ8Yj2LBYrXovsrqMwI1Mhlhn6xUWq/I9tzTKsT1hNO15dNkqV5VFHAE7bTzpGep/6Afi8jXXWMBnBB2CeGXVUeaNAaYV5kD3IYLP5mFpTwlFSkx9VraWv1m+UYNqB46AL2Q6yf2lgr0HRoDX1PBTk1EliEgkO8dPhcLs+Ba9ybTRsuXmVFLI6Cwgoihh7gBcFx4JohKoli8nAJqGhczbIZQIWsynXCSA2mHQ6Don0YuHYY5HvEWsArMKs6by2Wthml9pkzeWwVilwI8KQPrdJEU/rI+xtsa/SfQwKQ1RRlKL1OLwpph6rAtUEbH5z+Xkm3LyFoOMtXwwkLBO1g8XgeSADa7COlzWqASHhKS0RU0rAwqJwyLrOGCUrD4XFKrtqusDRMefEVsfyaVR0ya17AK7GLrWXmsoxGioOKYXB4p0SjCA7ZyqpFNh3KTRG4LbZIf4vMKXa66hYKyh2ZJfNl3Poe6hhW9iCxDGkjBF6nAJwHi5EQT1hwrrBYYatOjkDM5I3FCxrJxmt9OaDs2oP2ZCqMd2jo6osL7SiJbcww1okCfu+WzNK5mXUjXjZafmAc4Lb2ZSzF1IOlafnzqIcaOw9qc0jnKhD6LMByu8Tbx3KhYiAt0ALDZ9uRqUJiAhm9UPagioTS15ZDkrqIEu0U0WBNW5+fbwDABflXjhMiXSxihDZK76x/I4+t7U3ARR1AtbRCR5mxR4TC1myarYhx7RaWcNkoU8t3KrXsMGOsqInoEJqCnA2F7x5Lz88m9m9FOMpu5LqxsksepfBuaquuAsiXYG5QT6vG3SCf1nVCMpSgGTqKHYqN/t4tCxN+sAjcvjopDvnSRiJcLOOW9fkm6LAnjw0sdryxe9Hyqnhetmyr5hSDNqGarLqxsxWkYxhEkydtyX6mz7P2aaerTVwNtENRT/Rfw6mzcJJu8BC4Bf55JEjnGsDV9bRGI1TdQZUgVVdZUUCyeSsKGJ7bOvfALD6viwsVEONOysOvBTxZTxtTekWDC5aSsixcR5weArcxEVt1qw1lxnVAW/ycSRhgU3Xhwmng1haE7BCuV2XSO/0AKn4QyMWKWKQaOYlrhArmVEL3mQOrwR0v7YX4PBlNxwT1nm+ExiwS2gfEVdCOti0afgTYluyR0my9ZVwbtNl17D1szPk6EsSF1FmYg00KuP0TCTVo1SY3GOo7RyL1QFFop2afMmzDrTgEF6eqbGSE93CBdphcT8tlUx/wehuR9V5UBL7i3rfEtPhQPQu08ffBqQoZrFZ7wHGWbGKNAxnfdeUB1dsMuCZoFTC0hGiHQnz4XGaMuN1jLOVVo8YIeMigRT/lk6Tig3bZIHcKW3pwdxZyxnTK0doDBHoJFM6erD1W9kFJwnFNC8HHcMLbB6NIa9LO5EP+obavAheE4HIvCvkpTKk/WUaqupY4ee6cAkSKrqjzOuzTkIkA1HRIw3iFaUHRi9K4lkRoRhEN2jjuKzpkM7cenpWVChnOFKuK1h/L08J0ngU+I11GMWUeu8pAi5eY/d7rYlahT/scEklbsf1JJNSmx3lv1dLyk1usMBqZsFpesEoFhhqcov1aEgJKmrktqQi8KT9Im0UYDMAeBC1tuLFVR/SNs/mcMsQBsV/WCEoTK2GAvvdDPHI6MfGuIVMcR4XvtRY/AHDJAyDTijBYrCZZU8oDYGATOCTE03BOHawJjDpoOERZuUyUORgJ0/Wa0qyGU2ikVU1rH/ckB9bapmT1TUeq8Xpa0L9evW/zFHZDcil5YGtuv5sYV64mSX6tPyPWMxqS5AFlzAIkU37USZJkWsYsEBi54ECwUiy1EA2XbRY7F0zH6NScNlkayYPVJ2J27QGaIOWRk7JrTB5Qs9UdvzpLwPCSbIBroBtEwAa4OVGc/O7rdjiENKAKfnRDRhcXJNs2JmJQG5FoaTKZZFlgIBOMaHFhtYlYn2lTaWIa4dijUdDSvnhwdBMBJyCEUqheAIWWcG25xh85tkAQMSijd4dpw4uMsknO8EPAZTWVZWJiMd78brAiVjyydboMaZxmaTLi1pLgWgcqI00LDGy2O4GvKV309nG9WGNXaWVp0ZY9DeCx72yMgraSTZ2M2hKOTpJUerSXPSi2a0QBIeQr47ZCt4yy/cxBsnUFQJywII/LwxK3k0BJkn8PU2ZEFrTAAx2NL7HGd1ttxEzDmRbd3znzoHS4x2RygYeYKnh5I3wXpkWM1ulXfk2LqBioQI2xAJ2yMwRudhfcvgHgjoR5fGJiZs32ipHWZYpZWvKAGAUyknnvINOG53Mwjy24EJAQh1w53RWEZXw/Zz7iJIp0eIiOfwY5WgbK/AOYRCvGtLT06KZSKSEa7TP6FoELdVE1TDUYQH/v8LkVWXAcfJxpYxtRiDdYkBJWKWiR90MtHs6ibUsmDdqsaRX5dEoTY8pq6Ms2iSdQBDJAKyJclWc0PYpAZrM0cy7xvQoWTcPybe78yMJHHcfQzt3XsslZ+F02Mg+DABQDp4CbeUbx0T71ThF+CM7Sfw5MxkxtJSdj4xOxvjQIoC0YaO5h6xS4w2382C4MtDzSjTOt5i+bWa0l9vAMk6ykEwwtMUOHlMCtoMjvR40IBwNyeTAM9hz00nOLkUswTE1oMnWs07WPn7dzmSx3GZkWnOaNBmx04+Wgpm2A1pJWbZbX8sDnhRTm9YwtCyj4OCAZojUnjY7ZYtGksI65MmwZ2xHg4igqgIvSMyDU9j4SEhpmTsRYgTaUBsVrbSas7IA6hu5DedoRphWar+VQWUsSpxypQNM7MizbNRiQrO02t9soWl1B0zJNrO8L6b1YeCIC5IimTZgR8oU+iLI5AS7RRqWBRgPsExPjBKKlbZTIG89lhtCpPl7XCk3cGNUmwtM7oa1ffGQ7yur3jjK7ZjU88dMTn0wWsdWD28eFBFD4r2RFkCvsYjJt2yELHEmUSsAFxmqlnhrbbTqnqLCiCNvQmAnVqYlHLnDuyBG1LV6xeJMlyeF0s4XgZNPKsZKBN5gsGt1c0RMn8zSS7wk4/DWAWFQ7ePRjNRTLAzftnSWk1l8RG4+QCLQj0kBEa8HA+ccM3OAl5Rqkv4ayBw15AItBUOcMlk+TuYrBhTvyYz/jdn7wR51bHojQxH+EYyl1n4oEzUcO/tGwR79BHEyDb5NBWr+mtsf7hdv/8/vcubtuc9PuWTsPLB6igTtQzBNsO764kAu45nSK5Sj+3GuP6P2IaM/U2l+24WyFG2ItAWttVO73O+7Yle91O0990eCwHl4mLbD/p59zp3/5ajedPSl0KdKk4XeC4AQ7lhdIQhqti0DRATu/333tkbkxMWuzxopYjHV9pi29hsciGZMkaml5EsshcDf2xP0vf86d/vfjwGXkC8d9BXkAIxxzmFD2iIOTd4Fxqw6TtkgPEpqWd6ABWlF7AAGuDCAmibV1fIHhELgPAnAx02YJUse9tzwdyAx8/jU1OeQy29utcEqQSJ1zryHAlbqPAHZM2+QHoMUBJLo7TNsqmjkE7vaAe+ZU4hQJ3Phzn2nR2M6/G1uxY4G1FATJd/PMg4/ANSZEDLhi2bFRLRU6S9JW0bmwUbT1a9ledUpw72LWuL92qHE3gG+QCkHjEuDW2S+oBBiYiJX74TIumT8C0jKPstLj78+95iiXERGsheYz0+Zbw8+t0kSRPVCgLS6MnUWfvmIAfmbcqw6BuwFuHQcuZzRddjk+gR5jWj03wsVE5b0J9LGdArh6GZezcaMAPDNqcgOmh1i8seVBdAoiN+SoUIc5BO4mmA33RuBeo7IKxt438T40EWvIA2Ns65gT/crkCcUc07iUcSVwx8O7jiujx4RWT1dgz6ZCM9igcX/N7TztMB22LoI1cKuuZHMn40gpLoDXWFxIDcd12gmwoSGa7Py51xyb9NfHI413GTCSdnw9ERyR0/un05Ql3LywRJ9XRkM/P152JAH3heuO29/4+2bgnvrla5wLeVxRy5HHQuWjENOOTMK0NIi4MWp4W9F3GaQCB67MHsQKLBTes7vUBunMQ3vHadMxSg4GvXsRvhcbGfcQuOt6YAZumJyhCfpaoMVkFtuoQM9rm+E19Z7cnPk3ALiV4eCsnjKh6NhK1f1EBkDAmx6fvPsQuOvitdxXgBvSYRJUePLMJeGaK2I5IjP8kMhagAdIa5mW78696li5fZWUVepA4MVVmTZLi+pBoxVi5NyDxSHjborcANxfmidnBLhoPqGZMr0abbw0AE8Ij0fa/AcqCfkzGL5TzYzPwB2bSeIwMF7lxfXqaoCvVVxx08cipMOOHEqFtfEbgXttzCpkxh2aiM2vHNluwyf3OLKiOYwBXHLSZwKuqMlU2obYhnQMZgGgxybAs9NNbLHOR0IXgIduJeAeaty1cev2v/x5zrhxQPPsgmjStD8s/H297IG9YVYCV8uDQoxkQcyffdV5BKbjB3WsxpYo84Bmp8ZELK3E1SGa17oX7ryrfnWzydlcDjmBU6bXx8L6d9KZB30KgVGB1chbWD4L3LCz4/a/+N/dqf9wQ5QKa+9a6JwHkV4NU16S3Y0a8GAaMUwVuCZT8swAZNnsp3lCxewUwdj8hhjeo8THqbw4gTsw7q9uJBX2P/NBt//Hdzvnd8TIGlkUGG2MyJGf2FhlzMerBozBKDeWzSmvYvATs3H5fL9w04nvuP2v/qFzB7KemYd4u2BmbAkYnoBEGw1wU5JK8NO4ziXgdlbESKfXYloCbLyF3GbajF713i0A99ydP+v2Pv0O5xdHe6dVJjvSyJEmisBJNTMm52WHKKM+06S7dggyixaF3yR8s6+kCPBmgBR2nyOXdNogB8TGxpWLwIk9ObbY5lRAlgWwgWXtiZ4/+6rz4bkKVe3wF+vMA2ScWtKWjISFuRVmyHYaybQZKNsA7kfe4Pbv+RXnFkcLVzFi6hQSxUhisaI16UhDM3Y2rdKbdGDtzxw0xqwbCSTbjh7ZBByts/0+NgVMxKztU+Raf/aV5xtTMdn5uBrGVEFrIrYFeWB++G5+71wddtV7NpIK5wJw35mAK8v3DEZMA58nG2nRT1yMQBsHqSCxOPTIZKfBnErLcuCpw5hDA0bqaRv73xRiBPhauGD3oolYm2kLb2Hgoh2kwjtaTKM+Kd+fiMUBHWDa7DlzrcLVmwN3LwGXj//IwBrn05rhr2r9qi7QbmM0mPF3+S8l6iEQgN/1o4hk2SAXmAhCS/v2xgC52qo0s3g2IQ2oaTURAMaNg1ZDEjr+xy5xK/dZIR7+nuhF677S1awDF9sB7t3vdG5nlgrFl9WuYXnuQf5ZhypsF8p69SMo48Dg77EkSA4F8f9nabaSQ5Ybx9tGEbhZygsxLRkTFuoXTgBXG0WHGkvTJb3aK5hRo03ytOYZXgBUs8a9es4qvEB57+gvzn3oDW7v0xS4IzWnKGwnPrRCaJa1pX8UGIjtKr9C0LJBRPcLSYePouJba8qLRqSLiL6pPbimRWIK4Ccv46LzglURV2wfAS7WtKU/JhOSgbS+hlhoWDaa7Etqnm0LALU14L7LuZ35bAbLIanhJSAIyw0fgTrCZhq4kTmlHTDo9WmSqG9Ivo20zXiWkofSmQ09mw/0s3aRM8+tkTkBV4NWV/J0BrZ4HfZGrYdEOqkl2hGo5rLGq+/YjHE/8ga3d/e7yuRMM7UuYuZ6UYQy9gCuac1zvBh7WkzbYPSq6dipjkUi9E6DDG1GB093okjxWS/OcEPhHWMny5m0uFDmrVpPE8Dmpb2zrzwe7s8J8OLZTLwAMOa/q3pK5Mn8d7HB/GuN8L2g1rd0KkzONgRukAoIuDKKWBMxzD5kBFJN81geFIXakvJCMgR8kagMSwyoA3o9AVd5bT/Er6tpS1daEzHm0DJn7mepcLzkcfGBxRZoeceK90jW0TMY8DmmTigjMqX+544776o73JF/tInG/VdR44Y8bp6Pyn41Io2haQNwm3laBCokQyR4CJuRd8txi5UFDVnB0D1amogJSZlgRtN8fHD4Q4NpG4sLBULhGXihpwAXAg/qWq6rasPHmTY0rCUNwt+VHha3bB243NDJ8NDvOkxWl2/HQaHfg0Ar5Yd1mLU8twAAiICCk+34Mm6RI+UBtM0GaAcWFyo+xLluJAL7s69IUmHVCvho6UQufdCqrSGmR9qGK4M7/8diS8Cd87ghHYZDIyO2sp9JOxVTVi1Q2E6biRpuhwK7B4ydKWPAG8vTctaMmjL+DoIWEh1dc7E+jRvsGRWWsJ0ixvQLf+YVxye9ImaHbl4sI/aWQaaMQ1oOVG4yrWaZ2Bu5fy3uOTvv6tu3IBVmjXuEgSWHXrL4l3CJQZG7FM0vWUKEdzXiI0X0KMqNkIUI18X266W8eqfLNORBdMrBxQW9/y05CsGOP/OKC+qP2GPqoKHywrU0LdI/NijCWEvA+53tAPeeOR12lEoXcEJ5Z+8ca9s4KKJTgu8tIOcmYwPly0j2YF15kDSz/owtGUcdQUsE8Uke6HYjohqzHwCuwbbprYoBqb6BFpWrYsZkpxo+BwMSP9LginMVtsG4u/e8y/mdo8kxrLZhRwunby9RPW/HCQsJH6nzGHXSjwQFYe4MdvV+zszxMrmZjLRt/tMcbZpRsO7EXUEe1JVXrmmzr4L5hJ6ItTIqFbjaY9TDY/+6wCtQDl8gJ4CPf2D3hzkorBBS+5zEe7fCuD/ndu95p/M7x0S9MAkjFpNNS7d4zJPdzt9/bu3T7KOwLh2Twf6f/r47+F/3haL4pKdY/MrprCpFqGSa3M6jv98decpzNDbRGJEFgtAav3DLb3/d7f3R7zq3v8+iKm3EWPUb719AZ1oNaxaoZ2UbXggmYooI63v8mZtnqWCAkcyelUhuMi0qlgGgbzgLazNcgNiGVJiB++6kcQVmgvUbNRkH++7YpVe48696q3MLq5JKPFP8eOZDv+B2P/HuWiuBpJp1ntbBgTv67Be7C69/i3M7qKa2/e75r/tf+oI7+bYbnIM7IKwNBCgSVPwwnGgnVuWusZVGbXMjEvgzN18IAjzXHqsxLZcG+Ulzco+ZEoKWh7bmh/22xrgWcO0JaiCKg3139NIr3PFNgHvnL7jdT94RMiT1H9J98XcsVG8FuJ93J992o3Nye3oam6YmDY0BTDs3NLCttB+aTFbQVvLV/VdFTnOMJsCt+kM1qK17uW8v2qtweQigHqahEL0zh9RlYMnzrn73hlkFi3E7Kbm57csDd/TSl2wZuEqjJq0IjrNaLt2xZ7/YXXD9bRsw7ufdyV+8MTJu2eWbVYsBPBQVEgCqRGg7fdXUekVMbEXRGj3PgCDjqu81IG+RjYvyIDhiU+y3S//aEwWiBefNktfM6bCf6MdE44qzd/6c25ulwlxkk/91qvZL17YOXA3aktaksixT0/JgY+Dufenz7tTMuEQqzJo2t4RzC2VCDcxqlzHQsu/NjdieXBNwVoFbG1YAaIh8rImTrl0RtPFd8pwyoIdpYXM6V+H8DYF77s7EuAW4/bLG8g2xgz139LJZKrxlfY175791u5+4vTqOsJ0KStSpZscJGndzxp13+YZV2qGUGp4PBWnQ+E6IzbJ56UFKBPAeUhdTgFuS7ubhutb+MKJTVi1NHP4ehJvcNH/KkhLjwm0HuDNwZo3ZTmGpAvCDvenopVf441dvCbggBFcCSY5MkbwVxv2CO/WLN6ZjRkcKgYxJPNS0gnxK20HBzFSSOlrrV/FbF9YK47ZOzGvSONq5gNhyYP9aaKBhGPSpoa1JhdtxVoHurxNGDyYJk2tz7OsAAB/vSURBVLOXuO0CN/afARZOYn18/49sxrh7X4rAnaXCqlvIWfYgTci4IpMMamQOGDABdkQFXH6vP3PTQ8J/K+8ugM3/oTUte6dmjPhIAMbxw/FQnWjacBiAO0/ONtW4beDmEKc02ZaAe+4TdzifsgqqOi9EgTpnrsCYgXvgjv7I5RtJhQxcfSCI1NvAoUJjVpMHHKMD2YPUYVjuGYBL8ixjZ4jFnQsFq+ZMEzeueE1TWiQJIq4pTfWzVNgUuG+Mk7NQq1AdlEwyZ7fzcCKxJeDufuIONy129A5qsxY5te5BAy4GLZNpiWFZDUkGsrqwSJDQ8Dj2CBeSbWuUVlp/ttbpxLhwwhXuIIdDhJeinQtISAPaR3uKoDzQK0nqPIfFgwFcvn08GszQfvPkbAtSYWZc7jjJbnq0+KpjAO4sFd68QTrsCyEdNp09neBmT5BUc3A9bVwJJUTIC2YGACtIkL+3gjkBdyTdRQYxOT37Liz1tMZEJ1xmMnRhWRIfa5gqr5j/ujXgzlIhTc5KDUB+U6PgYzlr3J/eWOMOAldvbJyzChsCN0iFtybgijwuIrIwbO0KrwhbMidQErTnkOnvWjZxh/ZnbnooOLbK8gyZ8mrk7EgDV9K0XAixmWSEU92efv6179pQ477R7d1N0lEJryXlxZxRhLitAfc9fOVMTcZQfncG0NIdffbl7sIb1mfcCNybuufjlqE0vufMCEVEqQqD7jaihHFAVEBe+NM3PZT7AAvd6XjJ1Jj2woIV4kAtLayN0BOx5jljs8bdMnCzCIOaVkaRWeNeNmcVbtsgj/smFxk3Lfm2zqbN6IjyLWUVtgxc5TTVc+OhlpKokFPpjY2McMojUZSv72CA52QWoo8ALvKKGi5tabBd0DIBb0mLLQF3lzJuBgViWhHi5lqFY5sC94MUuEb0YrGWSJeQDtsicK1PmI7kaMsYEaxIgDL7YdByojIiTWLfClzzXIFYexAKpUyPLNICa9NWo8XWaAXaYhTR2ZBVeKc78vQXGDWxCn3qF2c/9PNu9573kHQU0LQgdIdfHextEbg0q5GbifOgpRNbA+7N9HxcMn5pKmIc86kKX6BE6KS8yNhq6du+NwIXApJQPllNYxNGqjkJLAZPdIT7+bEexmFq5wee5fwjH08OZ7bCTz2SiKJ3ef8fuYNvfCWFwLHK+zxgoTrs2T/ljr/830SpACcdDedZLNzZD93mzt39PlEdJvpqRYEHB7iswaY8KExGL+8WzOBa7snn3WDkYf19c/70jQ9TJs8aL8CgtQQsdJ+tS1AY5Cmv6Opy/1WjdmC+YVq6sAuBnQkb+m9W/nPy3zHOh0XSR0wapsktHn6xWzz2B5PBUR/xlp/c14Nv/E+3/NZfiAHtMG0miwLcf7d2OmyenJ18682yHjcCqdQd0H4hYjDkgULViDwgTtuM0s5h4M5LrIVarQHBQr28r/wHuF+xSAVFeS0I0ZwOeEhlK4MwgtR2FNk4pUNJupqW31suD1tn5AilvliGp3aZdz6UMuU2KGoT03VbAu6pt97sptOnnMulIDMXhNEfmYjNrZJVgQZexNYkKAnNRRcWzmOqmDJu2GqjcpmlIVX/GFVEbKwa8kOsL/OcdQvwbEeGNlBse1sbccdCwG3v5I0mJO+WABU/g3DWnp3nMYISgbRtTseFydlmjHvqLTe7aS4kX+Ro58O2myI/ZX/LH7g0iBaZK8wkCyBbrfE1SYZLAdyyKmbStAWKKHrCbU3gSS1Iwm+hQQEMaDgRtoutbNCGx4+cLsNHrOSRi0Pn9nQZNQ0kG8d+2aSbyEGA9F5JBFsE7vLM6Tj0wymvyLQQ3K0QX8IikITMYREG+OnvgXEZ05bWIMq3j/WpTGaFCg7arPO4g7ZFeQQfB20FFAItZkedp+2UNBZ/bLN5LcgxnE+xEWpfo7yQ3r8/55G3wbivcMszJ/mh2tLZGEJr7UHdrNifTEWUA8IJg9o/LkrWCvvTN/4tHuFb+tDwJh5+EXA7tQeQxfSg4vNX03UtUORPcaDySKPgw1xybEUVYTtuWIMIGNMMgnYOyftzHvlFG0uFk295RZQKQ5qWfCTRlGW5Q9LJUZTsEEF6lC5wD1IhAreGUmDgTUsTxWDzAe3sps0mhTXDuWf2RGLdc7zyE2Nbx5jW3Gbf1X2tXcIGAAJwZ8Z902ZZhRWBq+RBq28getfLB2RTmtNoTkrAfXBWxDTLRpiBjX9wCZgP2Nhp19rbY6cNYMBdqsbEwRogoemVUw4yGYvO5YeGNJuBe+nl7sIbtw1c5KTcfpHpLEnIJ2M4cqH79fNaW4n8qRuyVLBZi0646MCYX8UJHQOdRYPY1X0MSPnqmK5p3FvCCARu47yEpr4jjhFFespzG4PYqz0IjRxZrROyaX7trHEvvdw9ZKvARRmAdfO087hlIxGjhgpndu4UlinWCefpUf7UDQ8Hw6+32kTT1Y41WZrkgWvTpcbphIrQcMurka6Vnj7OtDGixfvbkz09sLgQSAMtRxvtbBS4Y5ovtHcrwL3XBY0753HpsRcFEegAv4GJWJJ1Cp+43JWhOyaR+1EPALc/O69s1piIJX1DNWLpCA4zbLUrhwndeStMcaeK9wk2E5OnAiZJAErTin6mgWX61wzvQr4U3bfaEnO5LdZGuWl/PtfhhZsx7hcTcOmBIAC0tfed3djFLiPRmzt3G1MKk9MgcEd1aUp5FbShIzQbbFRGx2LbwRAPswdtXcXDToNVBPhVuGrJAwIKrWn7bJuLnIJ5Z+BetiFwv3SvO3kbyirIxQUEWDGOsW98S1fTmTm4cQTHhBHezKTCUPZglGUB5ff27TfrIpA8qMbralriFONMa7ElYfciLwZmySZwkbYUAyvD7IMGXJny6kc4Kn/gbmHm3Qg/0XZas9p2qcCNoCJ6A7GsxZYogzBSMENYJtCI8ZGQrD5Vz7g8MPeHgfsMI8X+lz9iTZt/S+ZncpZtHB00lt1QOVWk97cB3CIV8p4zuRo2NvunCwt1REc20/Lns3qTKteSmTngE3DlZAwclQ71YZoACRT0vU47gK1pG2F7marDMjHmdhjfHGPNbNQf11qKaCwO0NGJF2CWhsbO1VjxXZbEEoXkz7ncPeTmDbbuBOC+Mi1AiOyBOd5AvxLD1v8cm8TJoaPRsHUclz91wyPA19NX1LTp7XqyMhY+zexB03gLt/P3nuUWj3qiqsfVbNrKO1K1icJY7yw0cr/Z3ngNGlTK3mzQtAhOD8mctnRHvv8fuvN+9PK1tw7tAeAW3+8dElO8mcum0uyhVOBIsQ0niowxf+r6R5Bxrg9KGx7SL+SA6lpauL0dpz/YKALQVrliHqocP15y/rXvcEef8eMEuNZo///6+zknip1tpMczcB8gjFvm1D3QBS9EgEUTTD4PiVegKjIRyTAJhM8czM8gwK1hkdODMRlT4UHOAC2D5t9bjcfF19Qxwgx0seOOX/ef3NGnP39kjA6vARYowD09H3qX9G1vAl1ChwRuf4IZNVej9LExh4mvjRGcABeB1gKenIiN3is7ZkzEWjpwbnzu3CFwN3bGCtwzjT2FGJDG5Nb8nkQsfB2v16WdK6qEOJU/df338DlL+cmQB0ysjeZpqdbtZg6KzMqarzY8d2feObBwx69/xyHjbgDfCNxX15WzFuMJQuGXdspRy8fJwcQut589kF+HahYKcMt9kPHQ/rCkVZjhOnvEkr7hE5X0gEaICsAtuZKUZzwE7gaQjbcG4N726phVYJkYnBExT3TshPiYo1XPjCnDwkpC45beYUkZgNsG7fyEWgSeZ3XQak1RbyWZBzRtQTrZanMI3O0CNwIrwUjOVxBJjeZ4O9+DMCN8axK3cP5klgoDTFsEsjRZR5fm7e9aFyUvszw2r6aEIxNFwcchcB8M4Or9dDwnzjdSDkymmh/1E7KTqoaQ4VBEWHPN/uR1WeOi8BAuzJE6PXcke0BmnKnaB8sDOzyE64s2AitOM3Cvm9Nhh1mFdRFcpEKpDuMTsUi/YvKd5V5Dk4ZLzOq+fvbBXowqkX/yJ697JCBClKcFIOukTnKJGvWkYuSBe+MbQWndfO9iBu7bD4G7Lmqpxj192ihr7JcX2vn7+cSLxlctgSwpMlQhkhBX+hsGLqmuMjVtL9/XqqfV0qJoK5pBiAlxWZqYPPYQuBtANt4aGfc1sB63eeAgmTixRoTBG015oWiL7tWgDXdqxm1sIS/UiXK86+ZpReojhRnItLRO9hC4WwDuH8asQpYKhFBUuktpPYyBsXMVNGj1uXQzYOf98hK48b0EuPZO3Mh86V+Lac2Om/fy4vFinIHdrmFydigVNkHv3hclcOVqGAFYayKW/ja+L1Bo6QCwsXWD3F8O3NIAROP9ukxW6Kws2ikCL4YZAG1oXlqAeMbzNhm7v9H3BuC+ec7jzmWNqxe8yPmKwnY4WD/pXJH2yj+GSrgBTcuW/CvjysM6UFmjLQ/Ce9MXCWHKC6bLonOklEVKIIxuZ5lv3UmMewjcdb1vBu6JN7/aOQjcNN4dps27eHNMZYeENOTFKhMxtO3fn7z2UbF2wUpvNEBX1IN5jb24QCdh8TnDoI2zVb9wFwSpcAjczYD7mnIgSJWE/ZRVwGRvi7qBKRu0AgcN6RmBW3K1eMWEG0auIxuyIkposB+j5gXjQf3W2bJop3ES5uHR3h37x1e6nSc9DZyYWFsMI4BigtbQo0hDrjdf0LlPvLI4cvl9inrm81GbV3inX7j9//1Vd/Y33++m3V3yMMS0+rm5UgtiwyLBzGOQ6FT2IJlE6u48OYvAxVvBe3WZ4ECN0JEeS5cjkcaZFgt/ckynFdIMbVXb2dDUJaQYgOjaZ5UyPvAOfUhfncwa381lDjCSsmShdhC0LaYtDcA2w6fao5SX4SxpTBLjVhasHlQYLzelLAdGXYruyUrHSJUkULMshZWnVYRinPAnAbv2hk8UbdBhHbbWz85Qm8RCrmIQpd0YGOV7UPQiX0EKl4/pUnMi1tKzaexKmGfjI9+7yvjb8oAHRm5L/8C1fycMNWvzgKfKWR4AvIBeAnprGRcydWlw9JcsQWqvOCAAw/L+dfZClft72Q3ECLmKDYCuGYmSqXofCmGDJPaIhed3nCq2obC2crBGiI9OKckKOTtxIIIALC2a516QI/bpe1IxeQFuk+IZ2o3UyUCVVxMU7WqjYSMn8Ofrc8ubmqwYuKG3ufuDY4NybYX8poE2ugwmwfTLRjpKvXtkN25+S2FDnTPXJJADcRFJNfUzuoVcO23SoqLbTdCGHsOT0ZPzBMYtT+zWD4A0Wbm5X0wcL0XhV3upmcxuhbOWntUTxTqQPZbtMVFiI9W0DIyATJzHLhFEaVZiE/Jg9n0OOjfp2qUCipFATx6UXSfCASm0KQYEPNdg2kju1KnyL4gNI3DbIZoc1oAFdy97UHWIBC3SZMjAeBCZRiRM25cGIJxB8LbbV8ZuKC3Ebcfw0vrMKGHbsQOpcd/GisDB+LZ2+7YcepoZE+3kbDOtiqyl/7xt/oFrMuMaHpVutLeQG3oujWr3fFrGJo0Dz1i4lNKlhkXNeiOatqdnRdilrNLdxo21cHhEAqxyNPb8GqWKmjOjnLBLKnipcon0A4YHfn/zIG1jPMpjIRl25YFm2hTNBJE7/8A1F4Mu0A5IQU4eMTMNt3rNPLCnji4uoHc1WI9IgzHACjYKN422TTNZ6/zWGu/sKBW/I0b/YYek8qACo00YuHjfdnjZEnQKOLtGyLISfQygWcUy7D50OqchXwFwA0OFlTyac1Ne29XDuUkjmtZyDgO0gnrW0sPBwAi0CaCNMBj+1DrzoTJibmlRS6FH6SMh0NnYSNZJWOWHAflCSkohm7dCvLkiJhwFMKpdq4JLE3larvv8aMv0XgFcoi9N3TaWdomdGGWzkYJlwnjJ8Guddg21rGA9hShp1JEMALGlpDOlaS0mlCmvEdnTOenSiJDFX/KsnaVIUfvqPsfqazjXrKKKkIfx7zrKS7MFvihZBSoVClgNBoThgQ9q1rRmPa0AhQYf8jwNWjOP3FvNCr1HTsWYLDElCPOwQF4OrL4vPBDmaVV/p/q1PLptCjGtHPD4s8nkHabNk2x+f5vh87W4ymtuSv7yH0+X1qBpSB4gYPOhiPMd/oFrHl0vWW3XQnKI2jH+Lsq2yhNDu8eq7PHgFPzVsGzWRZRLWlGAi63ctxrq6945khi326bat+I3xCog4oJBs0CbpKwUoYKQrrfbrH0kUsEAbx+ahAlbpTsZBqwon3CZ+8aBS8IvoujxlNlIyqt2ooLdCpc6TGvQosUP9LyR7MEoy7ZBy9o4mO6i0qrYBQJvhGkHGDqFaO0U6Pn8d7V9KLdv1x4Qh4yRv6TMjHrt1H9JwJVxm2wLBpPuww/3rjbRWSmcEYkC70NhhWkmJA2yyxuhqjCCpemlfBGSKXs/BG26l7e7hFXmzEN9Q9KuHeJrFBqRFlj64AnqGhOxVo2FOPC5sO4sFXDFTnsJN8X69P8MYIwUvMBGE6OX2DdUZKO1aRl4EAkUKAD4WqG2OlSRFJxpUXi0IovczTwAPMFG8Q5iJ9Y/g3zUSeDyvei+egAdj84StMiZAVEwsZoB0d506R+4+jHhNjWGAyFqlcWF+I5sWrAOTVi1yM1STs9BW7A8NBFbLRLQd0cYGA5TjKYHJ7bPp7QXHdoUDpkQBSmv3kocfzfW3C2nJJqRj33oS0yGln/1PzNLmSWmtKsEP9mrY5OovaztXDHKtaJrAO5KoE0fr4olZauBYizMa4/nHYdhVqRTwjX6OPtsuB7T9pwWOFkBfH6HtbgA5AH/1YA2Je/v34s0ewMUzKnKvRGz4X87KS9hG9i+YizM5vYnFeq7/YnEuIzyjeR69bh89Xi9KvQ6jWRwYLAd4hArVO2G5Mto+G2cd9UDbSt7oPoL5AGyidLrnAVjnwf6BhcXkHTRNsfFMgAHUB6OMm0FZjUDllYAuI0kN9NDI9mD+NLciLi1jRReIBC0PHY4hIa2ZV+p+B5e7QM6jMVUNLC19oCRgAJd/Kt50Am7WbeD6gI1uAz0oI2t/rfunQtmwuG2vGetZVyTafPOX4O02gxdHaACN4dHFiq0J8cHj6SV8ApOWEk2NFQ1DDvSPw50oy6Cecfgd3vRztFWlRt/B2A8xbTZkJIJQS2tKrkUoAPSpUaw/kdCYqTkJMJY2nDKONaWdLFTXuxxQ5q2UevLQnW1iwYu8vgm06bOKQ8ywhnybK4j9P63XmV/eaZRFyFOHIyjIcGxWlqIRBE3rZCnpWGgAKNhu+gwDWnAUaIK3MOjW5NYk2mj45knJrIP9RrSQmLJkEH1cEN6Q9t5/YmrHwu+uiPYsgkMrEEboSK1jmuXOKDohHNDwCsmGK+LoObJA6sCjXo+b0cBbm8ZV9iO2iW+UzKm1nTr1tKaac6OHi5jAYGW7Fz+xp2q2pHay1pcWD0K5LEDwK0PC40wQWtV9MuVFOQ5PARV+wCQ9lJe4eYx6aJZdmQSRtpKhXMALP+bFX5nPZt7phzaYrz0ewpaBQqTLfO4rTYWNYokDLDnr1lLC48oWC+K0MDsT1z1WOFXtdHRUKOgiIPIoz7SR5hRIOP1QmgIg6PtE4zZ+kq3ofmyt+McrdCDpEMZuNU2I1VeiI0szTkie9S9Kl1YzK1OJWqDlvWLhbNGtBRjq0jAZPT4TAHcCrT6XCN7QAaXhnkOQAlcBdry0eJ8XzwkZORbEtlCxooYawgIZ0O6DxveXFxQepO3DYfudkQaZOiMnWiU1m7cFkuPTMSgZpbRBxEWQfSwZscROHQ2Mi6XB/E06YZm7IGicYJNNGxRKoMrdmhw12daXR2FmMwA7cBELKJI1tKO1THTnQv9KKSjSO5bm0C0/MEZlb48yPKI+cNgLTeTiL3oKivEMnDDfeXMA6QbLS8aKQLnTJvDAjVu/O9+agfX0iaF0tPDvS8agvBU5QHcQp6YTh4XhTTtWN9wcXznKKs0dmZtc4NlC49AsJGJWBgg4SiWxlcgrHUvhpSs8tp4pjzbzp+46pKI2fIyYxm3AQoVzpBGJBetVYeL9SwEDpqE5ZOyNYvJAeHRh8qg/KHoGvSkM4O6gzwQLUZpRCBcoM2lT8jHkE+PMyZDY8FqU8K9ot5hlGll9snQtC1p0LBLQbOqW/BBKuy5yR+J/VsNtNqx2qmdzLRUv+dQAyoqRE7SOqwjhb2WdjM/oEKBZ0mDOWkhC2aUVmeZjbYu5WE6a1JoS0r3xWjCscKNqO2N+UUB7np1B2U+wirR1gCt6p98hlkhtu9PXPW4b7tpekRT0zKkNcIF8h7GtMjIIzNsJF0QYPXzYzgxjFqkifH3/pFIOfIV4Cq2Y4wn2iekCTNfb3m6PHcctKSxkSda8kA8H0bVAjwLtLy/MdLSVrD5jviDPMaUveM7/q+vvOSPvVs8BbKgYXTaibJo0GC8ONlDE6yG0Zk3GsU8LO6DZ/U+WWSyVXw5XhEjg0HkFQcFmg+g/sdnKUD1loDTuJgLEx0mGzpdxgSu7EefaSuMUKQyMJBsC4nATX/iT1x5ya9PbvFPOHA1C+Iw3xggAWRFxpss4w5M4rqrRg0mDm1dhuR9Z39ZjAR9aQAijZglF/t3J5irTGKN6KgGo69p4fg39ojRgvaIJp8OLGQCGy9RJ80OAR/Gxt3lT1z5fbdObnqzc24nGs8GYx98yKMQ23aYdorZ3FW2tzOSaR7rn9+NBrWeeaAjEGZaFZTW3tHRtn3tHxofdG/pXyX0FGH4OKI8eLxFRgK2JG9Fq1aUUwDSUWggihwsJ/cv/Hdf/vhnOr/8qHPu4hgf5ek0cjWsYaTc01KUk79LSJiLe2m6gwLeAm1nwNLIdpmWHLNJwVls2tuNG2yEKrySXTqSiebM6/uxw4PsCGcuTjRCI0rHRPZr11PX4VxNHkA93LJLrv6DbMGwMV/5zQO38yL/zZf+gwuPH/nrDzvnf0Ks1zIaV1oDNYTpkrZ2yTgrnzRkz1uvYGbsXIDarvzKIJUhYC0WHFlcwJqe+DYZprYmDu3s1SIb1VosxFsSofzekBalpfZWm6ipetucgGRKztfbfl9OsHHut/z+4qdDS7/78u99qXP+Pc65Yxz0sSNDoE254AACMRELv1tb044zLawuY6Gbgza2Nf8z9ogpLSCWcY1CccaWBBiQjdQ7BIBSWM6/tccDkYVle1lPq+/lZx5YoENLzOJawzGi5VEtLopA4Xf7y+Xy2ks+8bvvCz+duO5Jj5x2z/5X5/xl5WEEwbbXElAVFEiwDyx1lo6tybRVV/FwaUx0FPHMxhs6gC5OxBjghx3SIAEGWgC8BFrYZnBvNUBiP4tljfcqp8g4MBh/nJCAY8C5iARt/dk7/wd+37/w4k996i/Lb7/7su/7p5N3t3vnLmBbnLmO0uHNMF69cN08rel1LCjEMArYoLPSZ8sDFOLn5wNduwJo+8BDfYjRqzoKsom+rxBNC7Qj42bZlkYpJg9J9Oo45CDgaQXb6YMDd+3jPnnPh1iPv37dYy64cPfYrzjnXtY3MtZk6r6B1I5Re4DBSIuByASQIVmFbhQCE2u2CmaE5t460ypjaYdpzrAb4Xc8T8ulU462SkJpQJQQr1ROpwA/v6OpaUHfls5/cLFz/vWP+djH5s9gcjF64p8/8UnLxXJG9FOZZGANbzMhCzWtwWnJgx6TldoKCUo7zJSIlw6fy8u4ZmgMI1KlQR2gxrI29SCyvq4Y07Jnjm6TONpARj0hy1jfeCERkU60YCZlqFuTqdBhrJlRJVlvgSk0hETGYXKMfb/3YOFe8rjfuvvPBKdXi//VlY9/rp+m9zq3eEL4bRO03GOLPQeAZzItrgnlH97oz7Dh+n3oymD2II64XFxQg5mAgR1GsfTAwgkFhXKqBuCH5IHB0uVe5gHtmmgOPBSBgYQhwBVREnhqtqn/2r7zV37vb3/qM/Qe6FLffdkTnz/56e1uchG81OMZmEX6I4xU6TDxdjmwjVpaajz53sJIwp8kI4msBgPQQD1tbAKo9OpJnzSaYwdN48kKH1Bh+x7wFI3JzIEGUxxO2pb26TJqeMzic/7MzLgQ8AJT5B33T37nlZd8/JMfl3axYoH7q5c94TneTb/knH8qWk0bK020JhNrFIH3wowxqNUIMXOg9JsaCSYPkr2xpmfGFO9XA9Rhy1iW2JM+OQbWsynCYyGTCdACeaGYlhCDciBgf72ShhyDE5mKIsz+DOz3Huy4Wx73sbt/DzmzCdz54u++/Ik/sJzcG72bLnfOX0B2Log1/E7Kiw1ae7WmNrKChXYWAg+AgodplO4iRi73V2nQ1qVigIDTMFA0QFsYT7Fley5RSQp981fWHuhJWPwNjZhGuE+AVc0bmoQlO1kH62HQnnaT//WlX77pkt/+9FdwBMKFnOzasLK2OPmCaXKvnpz/Ye/cUcbspm6JhRWZIzbZiavDGQqhIPTmy1bMHnBjDUzGoESAQNDnma37OabmMmmZiCX/1cDlQ29p1M7ignbIzBfRhKqQSIwRd/hdN/nPueX0Tnfs+G/k7MHawM03PvDSv/uofeee57z/Se+mp03O/W3n/E63AJywmQIE8wAGxmKAsXwfN0h5ZW8ixt4PFhd65YUEsCoSaIrSS+jDs3YOvPCuXi0t6xvhD1R0bk2m4a6KyqIqOrIBbh8TmoB94Jz/lp/856Zp+V+Wx93vXPLRT3/bAiv9fVMqoAcEBvYP/NByWlzm3OJZzk1Pds5f7Nx0kXPuCF7qNHbiio6q9yWDKr0IBkXei2sPLI9HW25GmXbFLfmFiRIALBsA+VG6rYEGvrWAn6/8ySxNRCwtnmmtpqVFk8i4lZC8c3uTcw+45fQN5xdfnvzyD5bTkc8e3Xf3XfypT50aAWy+5v8BUrIHNHvQF7oAAAAASUVORK5CYII=";
+
+const OrgHeader = (props) => {
+ const {
+ userdata,
+ selectedOrganization,
+ setSelectedOrganization,
+ globalUrl,
+ isCloud,
+ adminTab,
+ handleEditOrg,
+ isEditOrgTab,
+ serverside,
+ } = props;
+
+ const classes = useStyles();
+
+ var upload = "";
+ const defaultBranch = "master";
+ const [orgName, setOrgName] = React.useState(selectedOrganization.name);
+ const [orgDescription, setOrgDescription] = React.useState(
+ selectedOrganization.description
+ );
+
+
+ const [file, setFile] = React.useState("");
+ const [fileBase64, setFileBase64] = React.useState(
+ selectedOrganization.image
+ );
+ useEffect(() => {
+ if (selectedOrganization.image !== undefined && selectedOrganization.image !== null && selectedOrganization.image.length > 0) {
+ setFileBase64(selectedOrganization.image);
+ setFile(selectedOrganization.image);
+ }
+ }, [selectedOrganization]);
+
+ const removeImage = () => {
+ setFile("");
+ setFileBase64("");
+ setCroppedData(defaultImage);
+ handleEditOrg(
+ orgName,
+ orgDescription,
+ selectedOrganization.id,
+ defaultImage,
+ {
+ app_download_repo: selectedOrganization?.defaults?.app_download_repo,
+ app_download_branch: selectedOrganization?.defaults?.app_download_branch,
+ workflow_download_repo: selectedOrganization?.defaults?.workflow_download_repo,
+ workflow_download_branch: selectedOrganization?.defaults?.workflow_download_branch,
+ notification_workflow: selectedOrganization?.defaults?.notification_workflow,
+ documentation_reference: selectedOrganization?.defaults?.documentation_reference,
+ workflow_upload_repo: selectedOrganization?.defaults?.workflow_upload_repo,
+ workflow_upload_branch: selectedOrganization?.defaults?.workflow_upload_branch,
+ workflow_upload_username: selectedOrganization?.defaults?.workflow_upload_username,
+ workflow_upload_token: selectedOrganization?.defaults?.workflow_upload_token,
+ newsletter: selectedOrganization?.defaults?.newsletter,
+ weekly_recommendations: selectedOrganization?.defaults?.weekly_recommendations,
+ },
+ {
+ sso_entrypoint: selectedOrganization?.sso_config?.sso_entrypoint,
+ sso_certificate: selectedOrganization?.sso_config?.sso_certificate,
+ client_id: selectedOrganization?.sso_config?.client_id,
+ client_secret: selectedOrganization?.sso_config?.client_secret,
+ openid_authorization: selectedOrganization?.sso_config?.openid_authorization,
+ openid_token: selectedOrganization?.sso_config?.openid_token,
+ SSORequired: selectedOrganization?.sso_config?.SSORequired,
+ auto_provision: selectedOrganization?.sso_config?.auto_provision,
+ },
+ [],
+ )
+ };
+
+ const surfaceColor = "#27292D";
+ const inputColor = "#383B40";
+
+ const bodyDivStyle = {
+ margin: "auto",
+ width: "900px",
+ };
+
+ const appIconStyle = {
+ marginLeft: "5px",
+ };
+
+ const dividerStyle = {
+ marginBottom: "10px",
+ marginTop: "10px",
+ height: "1px",
+ width: "100%",
+ backgroundColor: "grey",
+ };
+
+ if (file !== "") {
+ const img = document.getElementById("logo");
+ var canvas = document.createElement("canvas");
+ canvas.width = 174;
+ canvas.height = 174;
+ var ctx = canvas.getContext("2d");
+
+ img.onload = function () {
+ // img, x, y, width, height
+ //ctx.drawImage(img, 174, 174)
+ //console.log("IMG natural: ", img.naturalWidth, img.naturalHeight)
+ //ctx.drawImage(img, 0, 0, 174, 174)
+ ctx.drawImage(
+ img,
+ 0,
+ 0,
+ img.width,
+ img.height,
+ 0,
+ 0,
+ canvas.width,
+ canvas.height
+ );
+
+ const canvasUrl = canvas.toDataURL();
+ if (canvasUrl !== fileBase64) {
+ setFileBase64(canvasUrl);
+ selectedOrganization.image = canvasUrl;
+ setSelectedOrganization(selectedOrganization);
+ }
+ };
+ }
+
+
+ var image = "";
+ const editHeaderImage = (event) => {
+ const file = event.target.value;
+ const actualFile = event.target.files[0];
+ const fileObject = URL.createObjectURL(actualFile);
+ setFile(fileObject);
+ };
+
+ //console.log("USER: ", userdata)
+ const orgSaveButton = (
+
+
+ handleEditOrg(
+ orgName,
+ orgDescription,
+ selectedOrganization.id,
+ selectedOrganization.image,
+ {},
+ {},
+ [],
+ )
+ }
+ >
+
+
+
+ );
+
+ const [imageUploadError, setImageUploadError] = React.useState("");
+ const [openImageModal, setOpenImageModal] = React.useState(false);
+ const [scale, setScale] = React.useState(1);
+ const [rotate, setRotation] = React.useState(0);
+ const [disableImageUpload, setDisableImageUpload] = React.useState(true);
+ const [croppedData, setCroppedData] = React.useState(defaultImage);
+ const [imageData, setImageData] = useState(selectedOrganization?.image?.lenth > 0 ? selectedOrganization?.image : defaultImage)
+
+ React.useEffect(() => {
+ if (file.length > 0) {
+ setCroppedData(file);
+ } else if (fileBase64 !== undefined && fileBase64 !== null && fileBase64.length > 0) {
+ setCroppedData(fileBase64);
+ } else {
+ setCroppedData(defaultImage);
+ }
+
+ if((imageData !== selectedOrganization?.image) && selectedOrganization?.image?.length > 0){
+ setImageData(selectedOrganization?.image)
+ }
+ }, [selectedOrganization, file]);
+
+
+ const alternateImg = (
+ {
+ upload.click();
+ }}
+ />
+ );
+
+ const zoomIn = () => {
+ setScale(scale + 0.1);
+ };
+
+ const zoomOut = () => {
+ setScale(scale - 0.1);
+ };
+
+ const rotation = () => {
+ setRotation(rotate + 10);
+ };
+
+ const onPositionChange = () => {
+ setDisableImageUpload(false);
+ };
+
+ const onCancelSaveAppIcon = () => {
+ setOpenImageModal(false);
+ setImageUploadError("");
+ };
+
+ let editor;
+ const setEditorRef = (imgEditor) => {
+ editor = imgEditor;
+ };
+
+
+ const onSaveAppIcon = () => {
+ const canvas = editor.getImageScaledToCanvas();
+ const newImageData = canvas.toDataURL();
+ setCroppedData(newImageData); // Update croppedData with the new image data
+ setOpenImageModal(false);
+ setDisableImageUpload(true);
+ handleEditOrg(
+ orgName,
+ orgDescription,
+ selectedOrganization.id,
+ newImageData,
+ {
+ app_download_repo: selectedOrganization?.defaults?.app_download_repo,
+ app_download_branch: selectedOrganization?.defaults?.app_download_branch,
+ workflow_download_repo: selectedOrganization?.defaults?.workflow_download_repo,
+ workflow_download_branch: selectedOrganization?.defaults?.workflow_download_branch,
+ notification_workflow: selectedOrganization?.defaults?.notification_workflow,
+ documentation_reference: selectedOrganization?.defaults?.documentation_reference,
+ workflow_upload_repo: selectedOrganization?.defaults?.workflow_upload_repo,
+ workflow_upload_branch: selectedOrganization?.defaults?.workflow_upload_branch,
+ workflow_upload_username: selectedOrganization?.defaults?.workflow_upload_username,
+ workflow_upload_token: selectedOrganization?.defaults?.workflow_upload_token,
+ newsletter: selectedOrganization?.defaults?.newsletter,
+ weekly_recommendations: selectedOrganization?.defaults?.weekly_recommendations,
+ },
+ {
+ sso_entrypoint: selectedOrganization?.sso_config?.sso_entrypoint,
+ sso_certificate: selectedOrganization?.sso_config?.sso_certificate,
+ client_id: selectedOrganization?.sso_config?.client_id,
+ client_secret: selectedOrganization?.sso_config?.client_secret,
+ openid_authorization: selectedOrganization?.sso_config?.openid_authorization,
+ openid_token: selectedOrganization?.sso_config?.openid_token,
+ SSORequired: selectedOrganization?.sso_config?.SSORequired,
+ auto_provision: selectedOrganization?.sso_config?.auto_provision,
+ },
+ [],
+ )
+ };
+
+ const imageInfo = (
+
+ );
+
+ const errorText = imageUploadError.length > 0 ? (
+ Error: {imageUploadError}
+ ) : null;
+
+
+ const imageUploadModalView = openImageModal ? (
+
+
+
+ Upload Organization Image
+
+ {errorText}
+
+ setRotation(0)}
+ />
+
+
+
+ { upload.click(); }}
+ >
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Cancel
+
+
+ Upload
+
+
+
+
+ ) : null;
+
+
+ return (
+
+
+
+ {
+ setOpenImageModal(true);
+ }}
+ >
+ (upload = ref)}
+ onChange={(e) => {
+ const reader = new FileReader();
+ reader.onload = (event) => {
+ setCroppedData(event.target.result);
+ };
+ reader.readAsDataURL(e.target.files[0]);
+ }}
+ />
+ {imageInfo}
+
+ {imageUploadModalView}
+
+
+
+ {
+ setOpenImageModal(true);
+ }}
+ >
+ {file === null ? "Upload" : "Update"}
+
+
+
+ removeImage()}
+ >
+ Remove
+
+
+
+
+
+ );
+};
+
+export default OrgHeader;
diff --git a/frontend/src/components/OrgHeaderexpandedNew.jsx b/frontend/src/components/OrgHeaderexpandedNew.jsx
new file mode 100644
index 00000000..e7ba725f
--- /dev/null
+++ b/frontend/src/components/OrgHeaderexpandedNew.jsx
@@ -0,0 +1,1158 @@
+import React, { memo, useEffect, useState } from "react";
+
+import { makeStyles } from "@mui/styles";
+import { toast } from "react-toastify"
+import theme from '../theme.jsx';
+//import { useAlert
+
+import {
+ FormControl,
+ InputLabel,
+ Paper,
+ OutlinedInput,
+ Checkbox,
+ Card,
+ Tooltip,
+ FormControlLabel,
+ Chip,
+ Link,
+ Typography,
+ Switch,
+ Select,
+ MenuItem,
+ Divider,
+ ListItemText,
+ TextField,
+ Button,
+ Tabs,
+ Tab,
+ Grid,
+ Autocomplete,
+} from "@mui/material";
+
+import {
+ Icon as IconButton,
+ ExpandLess as ExpandLessIcon,
+ ExpandMore as ExpandMoreIcon,
+ Save as SaveIcon,
+ CookieSharp,
+} from "@mui/icons-material";
+import CloudSyncTab from "./CloudSyncTab.jsx";
+
+const useStyles = makeStyles({
+ notchedOutline: {
+ borderColor: "#f85a3e !important",
+ },
+});
+
+const OrgHeaderexpandedNew = (props) => {
+ const {
+ userdata,
+ selectedOrganization,
+ setSelectedOrganization,
+ globalUrl,
+ isCloud,
+ adminTab,
+ selectedStatus,
+ setSelectedStatus,
+ isEditOrgTab
+ } = props;
+
+ const classes = useStyles();
+ const defaultBranch = "main";
+ const ITEM_HEIGHT = 48;
+ const ITEM_PADDING_TOP = 8;
+ const MenuProps = {
+ PaperProps: {
+ style: {
+ maxHeight: ITEM_HEIGHT * 4.5 + ITEM_PADDING_TOP,
+ width: 300,
+ borderRadius: 20,
+ overflowY: "scroll",
+ },
+ },
+ getContentAnchorEl: () => null,
+ };
+
+const [orgName, setOrgName] = useState(selectedOrganization?.name);
+ const [orgDescription, setOrgDescription] = React.useState(
+ selectedOrganization.description
+ );
+
+ const [appDownloadUrl, setAppDownloadUrl] = React.useState(
+ selectedOrganization.defaults === undefined
+ ? "https://github.com/frikky/shuffle-apps"
+ : selectedOrganization.defaults.app_download_repo === undefined ||
+ selectedOrganization.defaults.app_download_repo.length === 0
+ ? "https://github.com/frikky/shuffle-apps"
+ : selectedOrganization.defaults.app_download_repo
+ );
+
+ const [openNotification, setOpenNotification] = React.useState(false);
+
+ const handleStatusChange = (event) => {
+ const { value } = event.target;
+ handleEditOrg(
+ orgName,
+ orgDescription,
+ selectedOrganization.id,
+ selectedOrganization?.image,
+ {
+ app_download_repo: selectedOrganization?.defaults?.app_download_repo,
+ app_download_branch: selectedOrganization?.defaults?.app_download_branch,
+ workflow_download_repo: selectedOrganization?.defaults?.workflow_download_repo,
+ workflow_download_branch: selectedOrganization?.defaults?.workflow_download_branch,
+ notification_workflow: selectedOrganization?.defaults?.notification_workflow,
+ documentation_reference: selectedOrganization?.defaults?.documentation_reference,
+ workflow_upload_repo: selectedOrganization?.defaults?.workflow_upload_repo,
+ workflow_upload_branch: selectedOrganization?.defaults?.workflow_upload_branch,
+ workflow_upload_username: selectedOrganization?.defaults?.workflow_upload_username,
+ workflow_upload_token: selectedOrganization?.defaults?.workflow_upload_token,
+ newsletter: selectedOrganization?.defaults?.newsletter,
+ weekly_recommendations: selectedOrganization?.defaults?.weekly_recommendations,
+ },
+ {
+ sso_entrypoint: selectedOrganization?.sso_config?.sso_entrypoint,
+ sso_certificate: selectedOrganization?.sso_config?.sso_certificate,
+ client_id: selectedOrganization?.sso_config?.client_id,
+ client_secret: selectedOrganization?.sso_config?.client_secret,
+ openid_authorization: selectedOrganization?.sso_config?.openid_authorization,
+ openid_token: selectedOrganization?.sso_config?.openid_token,
+ SSORequired: selectedOrganization?.sso_config?.SSORequired,
+ auto_provision: selectedOrganization?.sso_config?.auto_provision,
+ },
+ value.length === 0 ? ["none"] : value,
+ )
+ }
+ const [appDownloadBranch, setAppDownloadBranch] = React.useState(
+ selectedOrganization.defaults === undefined
+ ? defaultBranch
+ : selectedOrganization.defaults.app_download_branch === undefined ||
+ selectedOrganization.defaults.app_download_branch.length === 0
+ ? defaultBranch
+ : selectedOrganization.defaults.app_download_branch
+ );
+ const [workflowDownloadUrl, setWorkflowDownloadUrl] = React.useState(
+ selectedOrganization.defaults === undefined
+ ? "https://github.com/frikky/shuffle-apps"
+ : selectedOrganization.defaults.workflow_download_repo === undefined ||
+ selectedOrganization.defaults.workflow_download_repo.length === 0
+ ? "https://github.com/frikky/shuffle-workflows"
+ : selectedOrganization.defaults.workflow_download_repo
+ );
+ const [workflowDownloadBranch, setWorkflowDownloadBranch] = React.useState(
+ selectedOrganization.defaults === undefined
+ ? defaultBranch
+ : selectedOrganization.defaults.workflow_download_branch === undefined ||
+ selectedOrganization.defaults.workflow_download_branch.length === 0
+ ? defaultBranch
+ : selectedOrganization.defaults.workflow_download_branch
+ );
+
+ const [documentationReference, setDocumentationReference] = React.useState(
+ selectedOrganization.defaults === undefined
+ ? ""
+ : selectedOrganization.defaults.documentation_reference === undefined ||
+ selectedOrganization.defaults.documentation_reference.length === 0
+ ? ""
+ : selectedOrganization.defaults.documentation_reference
+ );
+ const [newsletter, setNewsletter] = React.useState(
+ selectedOrganization.defaults === undefined
+ ? true
+ : selectedOrganization.defaults.newsletter === undefined ||
+ selectedOrganization.defaults.newsletter.length === 0
+ ? true
+ : !selectedOrganization.defaults.newsletter
+ )
+
+ const [weeklyRecommendations, setWeeklyRecommendations] = React.useState(
+ selectedOrganization.defaults === undefined
+ ? true
+ : selectedOrganization.defaults.weekly_recommendations === undefined ||
+ selectedOrganization.defaults.weekly_recommendations.length === 0
+ ? true
+ : !selectedOrganization.defaults.weekly_recommendations
+ )
+
+ const [uploadRepo, setUploadRepo] = React.useState(selectedOrganization.defaults === undefined ? "" : selectedOrganization.defaults.workflow_upload_repo === undefined || selectedOrganization.defaults.workflow_upload_repo.length === 0 ? "" : selectedOrganization.defaults.workflow_upload_repo)
+ const [uploadBranch, setUploadBranch] = React.useState(selectedOrganization.defaults === undefined ? defaultBranch : selectedOrganization.defaults.workflow_upload_branch === undefined || selectedOrganization.defaults.workflow_upload_branch.length === 0 ? defaultBranch : selectedOrganization.defaults.workflow_upload_branch)
+ const [uploadUsername, setUploadUsername] = React.useState(selectedOrganization.defaults === undefined ? "" : selectedOrganization.defaults.workflow_upload_username === undefined || selectedOrganization.defaults.workflow_upload_username.length === 0 ? "" : selectedOrganization.defaults.workflow_upload_username)
+ const [uploadToken, setUploadToken] = React.useState(selectedOrganization.defaults === undefined ? "" : selectedOrganization.defaults.workflow_upload_token === undefined || selectedOrganization.defaults.workflow_upload_token.length === 0 ? "" : selectedOrganization.defaults.workflow_upload_token)
+ const [regionStatus, setRegionStatus] = useState();
+
+ useEffect(() => {
+
+ if (documentationReference !== selectedOrganization?.defaults?.documentation_reference) {
+ setDocumentationReference(selectedOrganization?.defaults?.documentation_reference)
+ }
+
+ if (uploadRepo !== selectedOrganization?.defaults?.workflow_upload_repo){
+ setUploadRepo(selectedOrganization?.defaults?.workflow_upload_repo)
+ }
+
+ if (uploadBranch !== selectedOrganization?.defaults?.workflow_upload_branch){
+ setUploadBranch(selectedOrganization?.defaults?.workflow_upload_branch)
+ }
+
+ if (uploadUsername !== selectedOrganization?.defaults?.workflow_upload_username){
+ setUploadUsername(selectedOrganization?.defaults?.workflow_upload_username)
+ }
+
+ if (uploadToken !== selectedOrganization?.defaults?.workflow_upload_token){
+ setUploadToken(selectedOrganization?.defaults?.workflow_upload_token)
+ }
+ }, [selectedOrganization])
+
+ useEffect(() => {
+ if (selectedOrganization !== undefined && selectedOrganization !== null) {
+ if((orgName === undefined || orgName === null || orgName.length === 0) && selectedOrganization?.name !== orgName) {
+ setOrgName(selectedOrganization?.name)
+ }
+ if((orgDescription === undefined || orgDescription === null || orgDescription.length === 0) && selectedOrganization?.description !== orgDescription) {
+ setOrgDescription(selectedOrganization?.description)
+ }
+ }
+ }, [selectedOrganization])
+
+ 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,
+ mfa_required: selectedOrganization?.mfa_required,
+ Billing: selectedOrganization?.Billing,
+ };
+
+ 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 handleSendChangeRegionMail = (region)=> {
+ if(selectedOrganization === undefined || selectedOrganization === null){
+ toast.error("Failed to send request for changing region. Please contact support@shuffler.io.")
+ return
+ }
+ let destinationRegion = region;
+ if (region === "US") {
+ destinationRegion = "us-west2";
+ } else if (region === "EU") {
+ destinationRegion = "europe-west3";
+ } else if (region === "CA") {
+ destinationRegion = "northamerica-northeast1";
+ } else if (region === "UK") {
+ destinationRegion = "europe-west2";
+ }
+
+ var data = {
+ dst_region: destinationRegion
+ }
+
+ toast.info("Sending request for changing region to " + region)
+
+ fetch(`${globalUrl}/api/v1/orgs/${selectedOrganization?.id}/change/region/request`,{
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ Accept: "application/json",
+ },
+ credentials: "include",
+ body: JSON.stringify(data),
+ }).then((response)=>{
+ if(response.status !== 200){
+ toast.error("Failed to send request for changing region. Please contact support@shuffler.io.")
+ }else{
+ toast.success("successfully send request for changing region. We will contact you shortly.")
+ }
+ }).catch((err)=>{
+ console.log(err)
+ toast.error("Failed to send request for changing region. Please contact support@shuffler.io.")
+ })
+ }
+
+ const setSelectedRegion = (region) => {
+
+ // send a POST request to /api/v1/orgs/{org_id}/region with the region as the body
+ if(region === "US") {
+ region = "us-west2"
+ } else if(region === "EU") {
+ region = "europe-west3"
+ } else if(region === "CA") {
+ region = "northamerica-northeast1"
+ } else if(region === "UK") {
+ region = "europe-west2"
+ }
+
+ var data = {
+ dst_region: region
+ }
+
+ toast.info("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 orgSaveButton = (
+
+
+ handleEditOrg(
+ orgName,
+ orgDescription,
+ selectedOrganization.id,
+ selectedOrganization.image,
+ {
+ app_download_repo: appDownloadUrl,
+ app_download_branch: appDownloadBranch,
+ workflow_download_repo: workflowDownloadUrl,
+ workflow_download_branch: workflowDownloadBranch,
+ notification_workflow: selectedOrganization?.defaults?.notification_workflow,
+ documentation_reference: documentationReference,
+ workflow_upload_repo: uploadRepo,
+ workflow_upload_branch: uploadBranch,
+ workflow_upload_username: uploadUsername,
+ workflow_upload_token: uploadToken,
+
+ newsletter: !newsletter,
+ weekly_recommendations: !weeklyRecommendations,
+ },
+ {
+ sso_entrypoint: selectedOrganization?.sso_config?.sso_entrypoint,
+ sso_certificate: selectedOrganization?.sso_config?.sso_certificate,
+ client_id: selectedOrganization?.sso_config?.client_id,
+ client_secret: selectedOrganization?.sso_config?.client_secret,
+ openid_authorization: selectedOrganization?.sso_config?.openid_authorization,
+ openid_token: selectedOrganization?.sso_config?.openid_token,
+ SSORequired: selectedOrganization?.sso_config?.SSORequired,
+ auto_provision: selectedOrganization?.sso_config?.auto_provision,
+ }
+ )
+ }
+ >
+ Save Changes
+ {/* */}
+
+
+ );
+
+ return (
+
+
+ {/*
+
+
+
+ Email settings
+
+
+ Enable or disable email notifications for your organization.
+
+
+
+
+ {
+ setNewsletter(e.target.checked)
+ }}
+ disabled={!isCloud}
+ />
+
+
+ {
+ setWeeklyRecommendations(e.target.checked)
+ }}
+ disabled={!isCloud}
+ />
+
+
+
+ */}
+
+
+
+
+
+
+
+
+
+ Name
+ {
+ if((orgName !== selectedOrganization?.name) && (orgName !== "")) {
+ handleEditOrg(
+ orgName,
+ orgDescription,
+ selectedOrganization.id,
+ selectedOrganization.image,
+ {
+ app_download_repo: selectedOrganization?.defaults?.app_download_repo,
+ app_download_branch: selectedOrganization?.defaults?.app_download_branch,
+ workflow_download_repo: selectedOrganization?.defaults?.workflow_download_repo,
+ workflow_download_branch: selectedOrganization?.defaults?.workflow_download_branch,
+ notification_workflow: selectedOrganization?.defaults?.notification_workflow,
+ documentation_reference: selectedOrganization?.defaults?.documentation_reference,
+ workflow_upload_repo: selectedOrganization?.defaults?.workflow_upload_repo,
+ workflow_upload_branch: selectedOrganization?.defaults?.workflow_upload_branch,
+ workflow_upload_username: selectedOrganization?.defaults?.workflow_upload_username,
+ workflow_upload_token: selectedOrganization?.defaults?.workflow_upload_token,
+ newsletter: !newsletter,
+ weekly_recommendations: !weeklyRecommendations,
+ },
+ {
+ sso_entrypoint: selectedOrganization?.sso_config?.sso_entrypoint,
+ sso_certificate: selectedOrganization?.sso_config?.sso_certificate,
+ client_id: selectedOrganization?.sso_config?.client_id,
+ client_secret: selectedOrganization?.sso_config?.client_secret,
+ openid_authorization: selectedOrganization?.sso_config?.openid_authorization,
+ openid_token: selectedOrganization?.sso_config?.openid_token,
+ SSORequired: selectedOrganization?.sso_config?.SSORequired,
+ auto_provision: selectedOrganization?.sso_config?.auto_provision,
+ }
+ )
+ }}}
+ onChange={(e) => {
+ if (e.target.value.length > 100) {
+ toast("Choose a shorter name.");
+ return;
+ }
+
+ setOrgName(e.target.value);
+ }}
+ color="primary"
+ InputProps={{
+ style: {
+ color: "white",
+ height: "35px",
+ fontSize: "1em",
+ borderRadius: 4,
+ },
+ classes: {
+ notchedOutline: isEditOrgTab ? null : classes.notchedOutline,
+ },
+ }}
+ />
+
+ {userdata?.support ? (
+
+
Status
+
+ {handleStatusChange(event);setSelectedStatus(event.target.value)}}
+ input={ }
+ renderValue={(selected) => selected.join(', ')}
+ MenuProps={MenuProps}
+ >
+ {["contacted", "lead", "demo done", "pov", "customer", "open source", "student", "internal", "creator", "tech partner", "old customer", "old lead"].map((name) => (
+
+ -1} />
+
+
+ ))}
+
+
+
+ ): null}
+
+
+ {isCloud ? (
+
+ Region
+
+
+ ): null}
+
+
+ About
+
+ {if((orgDescription !== selectedOrganization?.description) && (orgDescription !== "")) {
+ handleEditOrg(
+ orgName,
+ orgDescription,
+ selectedOrganization.id,
+ selectedOrganization.image,
+ {
+ app_download_repo: selectedOrganization?.defaults?.app_download_repo,
+ app_download_branch: selectedOrganization?.defaults?.app_download_branch,
+ workflow_download_repo: selectedOrganization?.defaults?.workflow_download_repo,
+ workflow_download_branch: selectedOrganization?.defaults?.workflow_download_branch,
+ notification_workflow: selectedOrganization?.defaults?.notification_workflow,
+ documentation_reference: selectedOrganization?.defaults?.documentation_reference,
+ workflow_upload_repo: selectedOrganization?.defaults?.workflow_upload_repo,
+ workflow_upload_branch: selectedOrganization?.defaults?.workflow_upload_branch,
+ workflow_upload_username: selectedOrganization?.defaults?.workflow_upload_username,
+ workflow_upload_token: selectedOrganization?.defaults?.workflow_upload_token,
+ newsletter: !newsletter,
+ weekly_recommendations: !weeklyRecommendations,
+ },
+ {
+ sso_entrypoint: selectedOrganization?.sso_config?.sso_entrypoint,
+ sso_certificate: selectedOrganization?.sso_config?.sso_certificate,
+ client_id: selectedOrganization?.sso_config?.client_id,
+ client_secret: selectedOrganization?.sso_config?.client_secret,
+ openid_authorization: selectedOrganization?.sso_config?.openid_authorization,
+ openid_token: selectedOrganization?.sso_config?.openid_token,
+ SSORequired: selectedOrganization?.sso_config?.SSORequired,
+ auto_provision: selectedOrganization?.sso_config?.auto_provision,
+ }
+ )
+ }}}
+ onChange={(e) => {
+ setOrgDescription(e.target.value);
+ }}
+ InputProps={{
+ classes: {
+ notchedOutline: isEditOrgTab ? null : classes.notchedOutline,
+ },
+ style: {
+ color: "white",
+ height: 89,
+ borderRadius: 4,
+ },
+ }}
+ />
+
+
+
+
+
+
+ Preferences
+
+
+ {/*isCloud ?
+
+ : null*/}
+
+ {/*
+
+ Add a Workflow that receives notifications from Shuffle when an error occurs in one of your workflows
+
+ */}
+
+
+
+
+ Org Documentation reference
+
+
+ Add a URL that is added as a link, pointing to any external documentation page you want.
+
+
+ {
+ if(documentationReference !== selectedOrganization?.defaults?.documentation_reference) {
+ handleEditOrg(
+ orgName,
+ orgDescription,
+ selectedOrganization.id,
+ selectedOrganization.image,
+ {
+ app_download_repo: selectedOrganization?.defaults?.app_download_repo,
+ app_download_branch: selectedOrganization?.defaults?.app_download_branch,
+ workflow_download_repo: selectedOrganization?.defaults?.workflow_download_repo,
+ workflow_download_branch: selectedOrganization?.defaults?.workflow_download_branch,
+ notification_workflow: selectedOrganization?.defaults?.notification_workflow,
+ documentation_reference: documentationReference,
+ workflow_upload_repo: selectedOrganization?.defaults?.workflow_upload_repo,
+ workflow_upload_branch: selectedOrganization?.defaults?.workflow_upload_branch,
+ workflow_upload_username: selectedOrganization?.defaults?.workflow_upload_username,
+ workflow_upload_token: selectedOrganization?.defaults?.workflow_upload_token,
+ newsletter: !newsletter,
+ weekly_recommendations: !weeklyRecommendations,
+ },
+ {
+ sso_entrypoint: selectedOrganization?.sso_config?.sso_entrypoint,
+ sso_certificate: selectedOrganization?.sso_config?.sso_certificate,
+ client_id: selectedOrganization?.sso_config?.client_id,
+ client_secret: selectedOrganization?.sso_config?.client_secret,
+ openid_authorization: selectedOrganization?.sso_config?.openid_authorization,
+ openid_token: selectedOrganization?.sso_config?.openid_token,
+ SSORequired: selectedOrganization?.sso_config?.SSORequired,
+ auto_provision: selectedOrganization?.sso_config?.auto_provision,
+ }
+ )
+ }
+ }}
+ onChange={(e) => {
+ setDocumentationReference(e.target.value);
+ }}
+ InputProps={{
+ classes: {
+ notchedOutline: isEditOrgTab ? null : classes.notchedOutline,
+ },
+ style: {
+ color: "white",
+
+ fontWeight: 400,
+ fontSize: 16,
+ borderRadius: 4,
+ height: 35
+ },
+ }}
+ />
+
+
+
+
+ Workflow Backup Repository
+
+ Decide where workflows are backed up in a Git repository. Will create logs and notifications if upload fails. The repository and branch must already have been initialized. Files will show up in the repo root in the /orgId/workflow-status/workflowId.json format. MSSP: If suborg exists, this will automatically be applied for them as well (not retroactive). Credentials are encrypted.
+
+
+
+
+ Repository for workflow backup
+ {
+ setUploadRepo(e.target.value);
+ }}
+ InputProps={{
+ classes: {
+ notchedOutline: isEditOrgTab ? null : classes.notchedOutline,
+ },
+ style: {
+ color: "white",
+
+ fontWeight: 400,
+ fontSize: 16,
+ borderRadius: 4,
+ height: 35,
+ },
+ }}
+ />
+
+
+
+
+ Branch
+ {
+ setUploadBranch(e.target.value);
+ }}
+ InputProps={{
+ classes: {
+ notchedOutline: isEditOrgTab ? null : classes.notchedOutline,
+ },
+ style: {
+ color: "white",
+
+ fontWeight: 400,
+ fontSize: 16,
+ borderRadius: 4,
+ height: 35,
+ },
+ }}
+ />
+
+
+
+
+
+
+ Username for backup of workflows
+ {
+ setUploadUsername(e.target.value);
+ }}
+ InputProps={{
+ classes: {
+ notchedOutline: isEditOrgTab ? null : classes.notchedOutline,
+ },
+ style: {
+ color: "white",
+
+ fontWeight: 400,
+ fontSize: 16,
+ borderRadius: 4,
+ height: 35,
+ },
+ }}
+ />
+
+
+
+
+ Git token/password
+ {
+ setUploadToken(e.target.value);
+ }}
+ InputProps={{
+ classes: {
+ notchedOutline: isEditOrgTab ? null : classes.notchedOutline,
+ },
+ style: {
+ color: "white",
+
+ fontWeight: 400,
+ fontSize: 16,
+ borderRadius: 4,
+ height: 35,
+ },
+ }}
+ type="password"
+ />
+
+
+
+
+ {/*isCloud ? null : */}
+
+ {isCloud ? null : (
+
+
+ App Download URL
+ {
+ setAppDownloadUrl(e.target.value);
+ }}
+ InputProps={{
+ classes: {
+ notchedOutline: isEditOrgTab ? null : classes.notchedOutline,
+ },
+ style: {
+ color: "white",
+
+ fontWeight: 400,
+ fontSize: 16,
+ borderRadius: 4,
+ height: 35,
+ },
+ }}
+ />
+
+
+ )}
+ {isCloud ? null : (
+
+
+ App Download Branch
+ {
+ setAppDownloadBranch(e.target.value);
+ }}
+ InputProps={{
+ classes: {
+ notchedOutline: isEditOrgTab ? null : classes.notchedOutline,
+ },
+ style: {
+ color: "white",
+
+ fontWeight: 400,
+ fontSize: 16,
+ borderRadius: 4,
+ height: 35,
+ },
+ }}
+ />
+
+
+ )}
+ {isCloud ? null : (
+
+
+ Workflow Download URL
+ {
+ setWorkflowDownloadUrl(e.target.value);
+ }}
+ InputProps={{
+ classes: {
+ notchedOutline: isEditOrgTab ? null : classes.notchedOutline,
+ },
+ style: {
+ color: "white",
+
+ fontWeight: 400,
+ fontSize: 16,
+ borderRadius: 4,
+ height: 35,
+ },
+ }}
+ />
+
+
+ )}
+ {isCloud ? null : (
+
+
+ Workflow Download Branch
+ {
+ setWorkflowDownloadBranch(e.target.value);
+ }}
+ InputProps={{
+ classes: {
+ notchedOutline: isEditOrgTab ? null : classes.notchedOutline,
+ },
+ style: {
+ color: "white",
+
+ fontWeight: 400,
+ fontSize: 16,
+ borderRadius: 4,
+ height: 35,
+ },
+ }}
+ />
+
+
+ )}
+ {/*
+
+ {expanded ?
+
+ :
+
+ }
+
+ */}
+
+
+ {orgSaveButton}
+
+
+ )
+}
+
+export default OrgHeaderexpandedNew;
+
+const RegionChangeModal = memo(({selectedOrganization, setSelectedRegion, userdata, handleSendChangeRegionMail}) => {
+ // Show from options: "us-west2", "europe-west2", "europe-west3", "northamerica-northeast1"
+ // var regions = ["us-west2", "europe-west2", "europe-west3", "northamerica-northeast1"]
+ const regionMapping = {
+ "US": "us",
+ "EU-2": "eu",
+ "CA": "ca",
+ "EU": "eu"
+ };
+
+ //let regiontag = "UK";
+ let regiontag = "EU";
+ let regionCode = "gb";
+
+ const regionsplit = selectedOrganization?.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";
+ regionCode = "us";
+ } else if (regiontag === "frankfurt") {
+ regiontag = "EU";
+ regionCode = "eu";
+ } else if (regiontag === "ca") {
+ regiontag = "CA";
+ regionCode = "ca";
+ }
+ }
+return (
+
+ {/* Region */}
+ {
+ if(userdata?.support){
+ setSelectedRegion(e.target.value)
+ }else{
+ handleSendChangeRegionMail(e.target.value)
+ }
+ }}
+ >
+ {Object.keys(regionMapping).map((region, index) => {
+ const regionImageCode = regionMapping[region];
+ // Set the default region if selectedOrganization.region is not set
+ if (selectedOrganization.region === undefined) {
+ 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 (
+
+ {/* show region image through cdn */}
+
+ {region}
+
+ );
+ } else {
+ return
+
+ {region}
+ ;
+ }
+ })}
+
+
+);
+})
diff --git a/frontend/src/components/OrganizationTab.jsx b/frontend/src/components/OrganizationTab.jsx
new file mode 100644
index 00000000..01d39f09
--- /dev/null
+++ b/frontend/src/components/OrganizationTab.jsx
@@ -0,0 +1,222 @@
+import React, { useEffect, useState, useCallback } from 'react';
+import { Link, useNavigate, useLocation } from "react-router-dom";
+import Billing from "../components/Billing.jsx";
+import Priorities from "../components/Priorities.jsx";
+import Branding from "../components/Branding.jsx";
+// import AnalyticsTab from '../components/AnalyticsTab.jsx';
+import EditOrgTab from '../components/EditOrgTab.jsx';
+import CloudSyncTab from '../components/CloudSyncTab.jsx';
+import SSOTab from "../components/ssoTab.jsx"
+import { ToastContainer, toast } from "react-toastify";
+import { Button, Tooltip } from '@mui/material';
+
+const OrganizationTab = (props) => {
+ const location = useLocation();
+ const navigate = useNavigate();
+ const {
+ userdata,
+ globalUrl,
+ serverside,
+ isCloud,
+ checkLogin,
+ notifications,
+ setNotifications,
+ stripeKey, setSelectedOrganization,
+ selectedStatus, setSelectedStatus,
+ selectedOrganization, handleGetOrg,
+ handleStatusChange, handleEditOrg,
+ isLoaded,
+ removeCookie
+ } = props;
+
+ const [selectedTab, setSelectedTab] = useState('org_config');
+ const [organizationFeatures, setOrganizationFeatures] = useState({});
+ const [billingInfo, setBillingInfo] = useState({});
+ const [orgRequest, setOrgRequest] = React.useState(true);
+ const [curIndex, setCurIndex] = React.useState(0);
+ const [unreadNotifications, setUnreadNotifications] = React.useState(
+ notifications?.filter((notification) => notification.read === false)?.length
+ );
+
+ useEffect(() => {
+ const queryParams = new URLSearchParams(location.search);
+ const tabName = queryParams.get('admin_tab');
+ if (tabName) {
+ const decodedTabName = decodeURIComponent(tabName);
+ setSelectedTab(decodedTabName);
+ if (decodedTabName === 'org_config') {
+ setCurIndex(0);
+ } else if(decodedTabName === 'sso'){
+ setCurIndex(1)
+ }else if (decodedTabName === 'notifications' || decodedTabName === 'priorities') {
+ setCurIndex(2);
+ } else if (decodedTabName === 'billingstats' || decodedTabName === 'billing') {
+ setCurIndex(3);
+ } else if (decodedTabName === 'branding(beta)') {
+ setCurIndex(4);
+ }
+ // else if (decodedTabName === 'analytics') {
+ // setCurIndex(5);
+ // }
+ }
+ }, [location.search]);
+
+ const handleTabClick = (tabName) => {
+ const formattedTabName = tabName.toLowerCase().replace(/[\s&]+/g, '');
+ const encodedTabName = encodeURIComponent(formattedTabName);
+ setSelectedTab(formattedTabName);
+ document.title = `Shuffle - admin - ${formattedTabName}`;
+ navigate(`?admin_tab=${encodedTabName}`);
+ };
+
+ const handleNotifications = useCallback(() => {
+ const unreadCount = notifications?.filter((notification) => notification.read === false).length;
+ setUnreadNotifications(unreadCount);
+ },[unreadNotifications,notifications]);
+
+ useEffect(() => {
+ if ((unreadNotifications !== notifications?.filter((notification) => notification.read === false).length) !== unreadNotifications) {
+ handleNotifications();
+ }
+ }, [notifications]);
+
+ const renderContent = () => {
+ switch (selectedTab) {
+ case 'org_config':
+ return ;
+ case 'sso':
+ return
+ case `notifications`:
+ case `priorities`:
+ return (
+
+ );
+ case 'billingstats' :
+ case 'billing' :
+ return (
+
+ );
+ case 'branding(beta)':
+ return ;
+ // case 'analytics':
+ // return ;
+ default:
+ return ;
+ }
+ };
+
+ return (
+
+
+ {['Org Configuration', "sso", "Notifications", 'Billing & Stats', 'Branding (Beta)'].map((tabName, index) => (
+
+
+
{
+ console.log("index", index);
+ setCurIndex(index);
+ handleTabClick(index === 0 ? "org_config" : tabName.toLowerCase().replace(/[\s&]+/g, ''));
+ }}
+ variant="text"
+ sx={{
+ "&.MuiButton-root": {
+ padding: '28px 0',
+ borderBottom: index === curIndex ? '2px solid #FF8444' : 'none',
+ cursor: 'pointer',
+ fontWeight: index === curIndex ? 'bold' : 'normal',
+ color: index === curIndex ? "#FF8444" : "#FFFFFF",
+ textTransform: 'none',
+ fontSize: 16,
+ width: "100%",
+ height: "100%",
+ borderRadius: 0,
+ },
+ "&: hover": {
+ backgroundColor: "#323232"
+ },
+ "&.Mui-disabled": {
+ color: "#6F6F6F",
+ },
+ }}
+ disabled={
+ ((tabName === "sso")) && !(userdata?.support || userdata?.active_org?.role === "admin")
+ }
+ >
+ {index === 2 && unreadNotifications > 0 ? (
+
+
+ {unreadNotifications}
+
+ {tabName}
+
+ ) : (
+ <>{index === 1 ? "SSO" : tabName}>
+ )}
+
+
+
+ ))}
+
+
+ {renderContent()}
+
+
+ );
+};
+
+export default OrganizationTab;
diff --git a/frontend/src/components/SchedulesTab.jsx b/frontend/src/components/SchedulesTab.jsx
new file mode 100644
index 00000000..7ca85f0d
--- /dev/null
+++ b/frontend/src/components/SchedulesTab.jsx
@@ -0,0 +1,1097 @@
+import React, { forwardRef, memo, useContext, useEffect } from 'react';
+import theme from "../theme.jsx";
+import { toast } from "react-toastify" ;
+import {
+ Divider,
+ List,
+ ListItem,
+ ListItemText,
+ Button,
+ Tooltip,
+ IconButton,
+ Dialog,
+ DialogTitle,
+ DialogContent,
+ DialogActions,
+ TextField,
+} from '@mui/material';
+
+import {
+ FileCopy as FileCopyIcon,
+ OpenInNew as OpenInNewIcon,
+ Padding,
+} from "@mui/icons-material"
+import { Box, Skeleton, Typography } from '@mui/material';
+import { Context } from '../context/ContextApi.jsx';
+
+const SchedulesTab = memo((props) => {
+ const {globalUrl, users, } = props;
+ const [webHooks, setWebHooks] = React.useState([]);
+ const [allSchedules, setAllSchedules] = React.useState([]);
+ const [pipelines, setPipelines] = React.useState([]);
+ const [showLoader, setShowLoader] = React.useState(true);
+ const [pipelineModalOpen, setPipelineModalOpen] = React.useState(false);
+ const [newPipelineValue, setNewPipelineValue] = React.useState("export | sigma /tmp/sigma_rules | to SHUFFLE_WEBHOOK");
+
+ useEffect(() => {
+ if (allSchedules.length === 0 && webHooks.length === 0 && pipelines.length === 0) {
+ handleGetAllTriggers()
+ }
+ }, [])
+
+ const textColor = "#9E9E9E !important";
+
+ 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()
+ }, 10000)
+
+ 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());
+ })
+ }
+
+ const submitPipelineWrapper = (pipelineValue) => {
+ submitPipeline(pipelineValue)
+ }
+
+ const NewPipelineView = (
+ {
+ setPipelineModalOpen(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,
+ },
+ },
+ }}
+ >
+
+
+ Run a Tenzir pipeline
+
+
+ Alpha feature. Deploys to the first available Orborus location. Explore Tenzir Pipelines . The example below exports everything in the Tenzir database, runs Sigma rules on it, and forwards the results to a Shuffle webhook.
+
+
+
+
+
+ setNewPipelineValue(event.target.value)
+ }
+ />
+
+
+
+ {
+ setPipelineModalOpen(false)
+ }}
+ color="primary"
+ >
+ Cancel
+
+ {
+ submitPipelineWrapper(newPipelineValue)
+ }}
+ color="primary"
+ >
+ Submit
+
+
+
+ )
+
+ const submitPipeline = (pipeline, environment) => {
+ var pipelineConfig = {
+ command: pipeline,
+ name: pipeline,
+ type: "create",
+ environment: "",
+
+ workflow_id: "",
+ trigger_id: "",
+ start_node: "",
+ }
+
+ if (environment !== undefined && environment !== "") {
+ pipelineConfig.environment = environment
+ }
+
+ const url = `${globalUrl}/api/v1/triggers/pipeline`;
+ fetch(url, {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ Accept: "application/json",
+ },
+ body: JSON.stringify(pipelineConfig),
+ 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 && pipelineConfig.type !== "delete") {
+ toast("Failed to set pipeline: " + responseJson.reason);
+ } else {
+ if (pipelineConfig.type === "create") {
+ toast("Pipeline will be created: " + responseJson.reason)
+ setPipelineModalOpen(false)
+
+ } else if (pipelineConfig.type === "stop") {
+ toast("Pipeline will be stopped: " + responseJson.reason)
+ setPipelineModalOpen(false)
+
+ } else {
+ toast("Unknown pipeline type: " + pipelineConfig.type)
+ }
+ }
+
+ setTimeout(() => {
+ handleGetAllTriggers()
+ }, 5000)
+
+ setTimeout(() => {
+ handleGetAllTriggers()
+ }, 10000)
+
+ setTimeout(() => {
+ handleGetAllTriggers()
+ }, 15000)
+
+ setTimeout(() => {
+ handleGetAllTriggers()
+ }, 20000)
+ })
+ .catch((error) => {
+ console.log("Get pipeline error: ", 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;
+ fetch(url, {
+ method: "DELETE",
+ credentials: "include",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ })
+ .then((response) =>
+ response.json().then((responseJson) => {
+ if (responseJson["success"] === false) {
+ toast("Failed stopping schedule");
+ } else {
+ setTimeout(() => {
+ handleGetAllTriggers();
+ }, 1500);
+ //toast("Successfully stopped schedule!")
+ }
+ })
+ )
+ .catch((error) => {
+ console.log("Error in userdata: ", error);
+ });
+ };
+
+ 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 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 || []);
+ setAllSchedules(responseJson.schedules || []);
+ setPipelines(responseJson.pipelines || []);
+ setShowLoader(false);
+ })
+ .catch((error) => {
+ // toast(error.toString());
+ });
+ };
+
+ 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 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,
+ };
+
+
+ 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());
+ });
+ };
+
+ return (
+
+
+ {NewPipelineView}
+
+
+
+ Triggers
+
+
+ Triggers are Automatic Workflow starters. Status: Schedules ({allSchedules.length}), Webhooks ({webHooks.length}), Pipelines ({pipelines.length})
+
+
+
+
+ Schedules
+
+
+ Schedules used in Workflows. Makes locating and control easier.{" "}
+
+ Learn more
+
+
+
+
+
+
+
+ {["Name", "Interval", "Environment", "Workflow", "Argument", "Action"].map((header, index) => (
+
+ ))}
+
+ {showLoader ? (
+ [...Array(6)].map((_, rowIndex) => (
+
+ {Array(6)
+ .fill()
+ .map((_, colIndex) => (
+
+
+
+ ))}
+
+ ))
+ ):(
+ allSchedules?.length === 0 ? (
+
+ No schedules found
+
+ ):(
+ allSchedules.map((schedule, index) => {
+ var bgColor = "#212121"
+ if (index % 2 === 0) {
+ bgColor = "#1a1a1a";
+ }
+
+ return (
+
+
+ 0 ? (
+ schedule.frequency
+ ) : (
+ {schedule.seconds} seconds
+ )
+ }
+ />
+
+
+
+
+
+
+
+
+
+
+ }
+ />
+
+
+ {
+ if (schedule.status === "running") {
+ deleteSchedule(schedule);
+ } else startSchedule(schedule);
+ }}
+ >
+ {schedule.status === "running" ? "Stop Schedule" : "Start Schedule"}
+
+
+ )}
+ />
+
+
+ );
+ })
+ )
+ )}
+
+
+
+
+
+
+
+ {["Name", "Environment", "Workflow", "URL", "Action"].map((header, index) => (
+
+ ))}
+
+ {showLoader ? (
+ [...Array(6)].map((_, rowIndex) => (
+
+ {Array(5)
+ .fill()
+ .map((_, colIndex) => (
+
+
+
+ ))}
+
+ ))
+ ):(
+ webHooks?.length === 0 ? (
+
+ No webhooks found
+
+ ):(
+ webHooks.map((webhook, index) => {
+ var bgColor = "#212121"
+ if (index % 2 === 0) {
+ bgColor = "#1a1a1a";
+ }
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+ }
+ />
+
+
+ {
+ const copyText = navigator?.clipboard?.writeText(webhook.info.url);
+ if(copyText){
+ toast.success("URL copied to clipboard");
+ }else{
+ toast.error("Failed to copy URL");
+ }
+ }}
+ >
+
+
+
+ )
+ }
+ />
+
+
+ {
+ if (webhook.status === "running") {
+ deleteWebhook(webhook);
+ } else startWebHook(webhook);
+ }}
+ >
+ {webhook.status === "running" ? "Stop webhook" : "Start Webhook"}
+
+
+ )}
+ />
+
+
+ );
+ })
+ )
+ )}
+
+
+
+
Pipelines
+
+
+ Controls Pipelines on your Orborus Runners, e.g. for Log Ingestion, MQ subscriptions or Sigma Detections.{" "}
+
+ Learn more
+
+
+
+
+
+
setPipelineModalOpen(true)}
+ >
+ Deploy New Pipeline
+
+
+
+
+
+
+ {["Command", "Environment", "Total Runs", "Actions"].map((header, index) => (
+
+ ))}
+
+ {showLoader ? (
+ [...Array(6)].map((_, rowIndex) => {
+ return (
+
+ {Array(5)
+ .fill()
+ .map((_, colIndex) => {
+ return (
+
+
+
+ )
+ })}
+
+ )
+ }
+ )
+
+ ): (
+ pipelines?.length === 0 ? (
+
+ No pipeline trigger found
+
+
+ ):(
+ pipelines.map((pipeline, index) => {
+ var bgColor = "#212121"
+ if (index % 2 === 0) {
+ bgColor = "#1a1a1a";
+ }
+
+ return (
+
+
+
+
+
+ {
+ changePipelineState(pipeline, "stop");
+ /*
+ if (pipeline.status === "running") {
+ changePipelineState(pipeline, "stop");
+ } else changePipelineState(pipeline, "start");
+ */
+ }}
+ >
+ Stop Pipeline
+
+
+ )}
+ />
+
+ );
+ })
+ )
+ )}
+
+
+
+
+
+ );
+});
+
+export default SchedulesTab;
+
diff --git a/frontend/src/components/TenantsTab.jsx b/frontend/src/components/TenantsTab.jsx
new file mode 100644
index 00000000..8ee2f6f2
--- /dev/null
+++ b/frontend/src/components/TenantsTab.jsx
@@ -0,0 +1,1592 @@
+import React, { memo, useContext, useEffect, useState } from 'react';
+import theme from "../theme.jsx";
+import {
+ FormControl,
+ Card,
+ Tooltip,
+ Typography,
+ Switch,
+ Divider,
+ TextField,
+ Button,
+ Grid,
+ List,
+ ListItem,
+ ListItemText,
+ ListItemAvatar,
+ Avatar,
+ Dialog,
+ DialogTitle,
+ DialogActions,
+ DialogContent,
+ Skeleton,
+ } from "@mui/material";
+
+ import {
+ Edit as EditIcon,
+ Polyline as PolylineIcon,
+ CheckCircle as CheckCircleIcon,
+ Close as CloseIcon,
+ Apps as AppsIcon,
+ Business as BusinessIcon,
+ Flag,
+ } from "@mui/icons-material";
+
+import { toast } from 'react-toastify';
+
+const TenantsTab = memo((props) => {
+ const {
+ globalUrl,
+ isCloud,
+ userdata,
+ selectedOrganization,
+ setSelectedOrganization,
+ checkLogin
+ } = props;
+ const imageStyle = { width: 50, height: 50 };
+ // const [selectedOrganization, setSelectedOrganization] = React.useState({});
+ const [subOrgs, setSubOrgs] = useState([]);
+ const [cloudSyncApikey, setCloudSyncApikey] = React.useState("");
+ const [orgName, setOrgName] = React.useState("");
+ const [orgSyncResponse, setOrgSyncResponse] = React.useState("");
+ const [loading, setLoading] = React.useState(false);
+ // const [loginInfo, setLoginInfo] = React.useState("");
+ const [cloudSyncModalOpen, setCloudSyncModalOpen] = React.useState(false);
+ const [modalOpen, setModalOpen] = React.useState(false);
+ const [parentOrg, setParentOrg] = React.useState(null);
+ const [parentOrgFlag, setParentOrgFlag] = React.useState("eu");
+ const [loadOrgs, setLoadOrgs] = React.useState(true);
+ const [, forceUpdate] = React.useState();
+ const itemColor = "black";
+
+ useEffect(() => {
+ if(parentOrgFlag === null) {
+ let regiontag = "EU";
+ let regionCode = "eu";
+
+ if (parentOrg?.region_url?.length > 0) {
+ const regionsplit = parentOrg?.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";
+ regionCode = "us";
+ } else if (regiontag === "frankfurt") {
+ regiontag = "EU-2";
+ regionCode = "eu";
+ } else if (regiontag === "ca") {
+ regiontag = "CA";
+ regionCode = "ca";
+ }
+ }
+ console.log("did i reach here, region code is: ", regionCode);
+ setParentOrgFlag(regionCode);
+ }
+ }
+ }, [parentOrg]);
+
+ var syncList = [
+ {
+ primary: "Workflows",
+ secondary: "",
+ active: true,
+ icon: ,
+ },
+ {
+ primary: "Apps",
+ secondary: "",
+ active: true,
+ icon: ,
+ },
+ {
+ primary: "Organization",
+ secondary: "",
+ active: true,
+ icon: ,
+ },
+ ];
+
+ 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]);
+
+ 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) {
+ setLoadOrgs(false)
+ //toast("Failed getting your org. If this persists, please contact support.");
+ } else {
+ const { subOrgs, parentOrg } = responseJson;
+ setLoadOrgs(false)
+ setSubOrgs(subOrgs);
+ setParentOrg(parentOrg);
+
+ let regiontag = "EU";
+ let regionCode = "eu";
+
+ if (parentOrg?.region_url?.length > 0) {
+ const regionsplit = parentOrg?.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";
+ regionCode = "us";
+ } else if (regiontag === "frankfurt") {
+ regiontag = "EU-2";
+ regionCode = "eu";
+ } else if (regiontag === "ca") {
+ regiontag = "CA";
+ regionCode = "ca";
+ }
+ }
+ setParentOrgFlag(regionCode);
+ }
+ }
+ })
+ .catch((error) => {
+ console.log("Error getting sub orgs: ", error);
+ //toast("Error getting sub organizations");
+ setLoadOrgs(false)
+ });
+ };
+
+ 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 ? (
+
+ ) : (
+
+ );
+
+ 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 (
+
+
+ {
+ setExpanded(!expanded);
+ }}
+ >
+
+ {primaryIcon}
+
+
+ {isCloud && userdata.support === true ? (
+
+ {
+ e.preventDefault();
+ e.stopPropagation();
+
+ if (showEdit) {
+ setShowEdit(false);
+ return;
+ }
+
+ console.log("Edit");
+
+ setShowEdit(true);
+ }}
+ />
+
+ ) : null}
+
+ {
+ if (!isCloud || userdata.support !== true) {
+ return;
+ }
+
+ e.preventDefault();
+ e.stopPropagation();
+
+ enableFeature();
+ }}
+ >
+ {secondaryIcon}
+
+
+
+ {expanded ? (
+
+
+ Usage:
+ {props.data.limit === 0 ? (
+ "Unlimited"
+ ) : (
+
+ {props.data.usage} /{" "}
+ {props.data.limit === "" ? "Unlimited" : props.data.limit}
+
+ )}
+
+ {/*
+ Data sharing: {props.data.data_collection}
+ */}
+
+ Description: {secondary}
+
+
+ ) : null}
+
+ {showEdit ? (
+ {
+ console.log("Submit");
+ submitEdit(e);
+ }}
+ >
+
+ {
+ setNewValue(event.target.value);
+ }}
+ />
+ {
+ console.log("Submit 2");
+ submitEdit(e);
+ }}
+ >
+ Submit
+
+
+
+ ) : null}
+
+
+ );
+ };
+
+ 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 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 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.setItem("apps", [])
+ }
+
+ 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
+ }
+
+ checkLogin()
+ setTimeout(() => {
+ window.location.reload();
+ }, 3000);
+ toast("Successfully changed active organization - refreshing!");
+ } else {
+ toast("Failed changing org: " + responseJson.reason);
+ }
+ })
+ .catch((error) => {
+ console.log("error changing: ", error);
+ //removeCookie("session_token", {path: "/"})
+ });
+ };
+
+ 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 Sub-Organization
+
+
+
+ Name
+ {
+ setOrgName(event.target.value);
+ }}
+ />
+
+ {/* {loginInfo} */}
+
+
+ setModalOpen(false)}
+ color="primary"
+ >
+ Cancel
+
+ {
+ createSubOrg(selectedOrganization.id, orgName);
+ }}
+ color="primary"
+ >
+ Submit
+
+
+
+ );
+
+ const cloudSyncModal = (
+ {
+ setCloudSyncModalOpen(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,
+ },
+ },
+ }}
+ >
+
+ Enable cloud features
+
+
+ What does{" "}
+
+ cloud sync
+ {" "}
+ do?
+
+ {
+ setCloudSyncApikey(event.target.value);
+ }}
+ />
+ {
+ setLoading(true);
+ enableCloudSync(
+ cloudSyncApikey,
+ selectedOrganization,
+ selectedOrganization.cloud_sync,
+ );
+ }}
+ color="primary"
+ variant={
+ selectedOrganization.cloud_sync === true
+ ? "outlined"
+ : "contained"
+ }
+ >
+ {selectedOrganization.cloud_sync ? "Stop sync" : "Start sync"}
+
+
+ {orgSyncResponse.length > 0 ? (
+
+ Error: {orgSyncResponse}
+
+ ) : null}
+
+ {syncList.map((data, index) => {
+ return ;
+ })}
+
+ * New triggers (userinput, hotmail realtime)
+
+ * Execute in the cloud rather than onprem
+
+ * Apps can be built in the cloud
+
+ * Easily share apps and workflows
+
* Access to powerful cloud search
+
+
+ );
+
+
+ const textColor = "#9E9E9E !important";
+
+ return (
+
+ {modalView}
+ {cloudSyncModal}
+
+
+
+
Organizations
+
+ 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."
+ : ''}
+
+ Learn more
+
+
+
+
+
{
+ setModalOpen(true);
+ }}
+ >
+ Add suborganization
+
+
+
+
+
+
+ Your Parent Organization
+
+
+
+
+
+ {/*
*/}
+
+
+
+
+
+
+
+ {isCloud && (
+
+ )}
+
+
+
+
+ {loadOrgs ? (
+ [...Array(3)].map((_, rowIndex) => (
+
+ {[
+ { width: 100, minWidth: 100, maxWidth: 100 },
+ { width: 250, minWidth: 50, maxWidth: 250 },
+ { width: 400, minWidth: 400, maxWidth: 400 },
+ { width: "28%", minWidth: "28%" },
+ { width: 400, minWidth: 400, maxWidth: 400 },
+ ].map((style, colIndex) => (
+
+
+
+ ))}
+
+ ))
+ ) : parentOrg?.id?.length > 0 ? (
+
+
+ }
+ style={{
+ width: 100,
+ minWidth: 100,
+ maxWidth: 100,
+ display: "table-cell",
+ padding: "8px 8px 8px 20px",
+ textAlign: "center",
+ }}
+ />
+
+ {isCloud && (
+
+
+
+
+ }
+ style={{ display: "table-cell", padding: 8, verticalAlign: "middle" }}
+ />
+ )}
+
+
+
+ {
+ handleClickChangeOrg(parentOrg?.id);
+ }}
+ >
+ Switch to Parent
+
+
+
+ }
+ style={{ display: "table-cell", verticalAlign: "middle" }}
+ />
+
+ ): (
+
+ {Array(5).fill().map((_, index) => (
+
+ ))}
+
+ )}
+
+
+
+
+ {subOrgs.length > 0 && (
+
+
+
+
+
+ Sub Organizations of the Current Organization ({subOrgs.length})
+
+
+
+ {/*
*/}
+
+
+
+
+
+
+ {isCloud && (
+
+ )}
+
+
+
+ {subOrgs.map((data, index) => (
+
+ } style={{ width: 100,
+ minWidth: 100,
+ maxWidth: 100,
+ display: "table-cell",
+ padding: "8px 8px 8px 20px",
+ textAlign: "center", }} />
+
+ {isCloud && (
+
+
+
+
+ }
+ style={{ display: "table-cell", padding: 8, verticalAlign: "middle" }}
+ />
+ )}
+
+
+
+ {
+ handleClickChangeOrg(data.id);
+ }}
+ sx={{
+ boxShadow:"none", textTransform:"capitalize",
+ fontSize:16,
+ '&.Mui-disabled': {
+ backgroundColor: 'rgba(200, 200, 200, 0.5)',
+ color: '#A9A9A9',
+ },
+ }}
+ >
+ Change Active Org
+
+
+ }
+ style={{ display: "table-cell", verticalAlign: "middle" }}
+ />
+
+ ))}
+
+
+
+ )}
+
+
+
+
+
+ All Tenants
+
+
+
+ {/*
*/}
+
+
+
+
+
+ {isCloud && (
+
+ )}
+
+
+
+
+ {userdata?.orgs?.length <= 0 ? (
+ [...Array(6)].map((_, rowIndex) => (
+
+ {Array(7)
+ .fill()
+ .map((_, colIndex) => (
+
+
+
+ ))}
+
+ ))
+ ) : (
+ userdata?.orgs?.length > 0 &&
+ userdata.orgs.map((data, index) => {
+ let regiontag = "EU";
+ let regionCode = "eu";
+
+ if (data.region_url?.length > 0) {
+ const regionsplit = data.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";
+ regionCode = "us";
+ } else if (regiontag === "frankfurt") {
+ regiontag = "EU-2";
+ regionCode = "eu";
+ } else if (regiontag === "ca") {
+ regiontag = "CA";
+ regionCode = "ca";
+ }
+ }
+ }
+
+ return (
+
+
+ }
+ style={{
+ width: 100,
+ minWidth: 100,
+ maxWidth: 100,
+ display: "table-cell",
+ padding: "8px 8px 8px 20px",
+ textAlign: "center",
+ }}
+ />
+
+ {isCloud ? (
+
+
+
+
+
+ }
+ style={{
+ display: "table-cell",
+ padding: 8,
+ verticalAlign: "middle",
+ }}
+ >
+ ) : null}
+
+
{
+ handleClickChangeOrg(data?.id);
+ }}
+ >
+ Change Active Org
+
+ }
+ style={{
+ display: "table-cell",
+ padding: 8,
+ verticalAlign: "middle",
+ }}
+ >
+
+ );
+ })
+ )}
+
+
+
+
+
+
+ );
+});
+
+export default TenantsTab;
diff --git a/frontend/src/components/UserManagmentTab.jsx b/frontend/src/components/UserManagmentTab.jsx
new file mode 100644
index 00000000..0dfda66a
--- /dev/null
+++ b/frontend/src/components/UserManagmentTab.jsx
@@ -0,0 +1,1670 @@
+import React, { useState, useEffect, useContext, memo } from "react";
+import { toast } from 'react-toastify';
+
+import {
+ FormControl,
+ InputLabel,
+ OutlinedInput,
+ Checkbox,
+ Tooltip,
+ Typography,
+ Select,
+ MenuItem,
+ Divider,
+ TextField,
+ Button,
+ List,
+ ListItem,
+ ListItemText,
+ IconButton,
+ Dialog,
+ DialogTitle,
+ DialogActions,
+ DialogContent,
+ CircularProgress,
+ Skeleton,
+ Switch,
+ Box,
+} from "@mui/material";
+
+import {
+ Cached as CachedIcon,
+ Edit as EditIcon,
+ Style,
+} from "@mui/icons-material";
+import ModeEditOutlineOutlinedIcon from '@mui/icons-material/ModeEditOutlineOutlined';
+import ContentCopyOutlinedIcon from '@mui/icons-material/ContentCopyOutlined';
+
+import theme from "../theme.jsx";
+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 logsViewModal = false;
+const userdata = "";
+
+const UserManagmentTab = memo((props) => {
+ const { userdata, isCloud, globalUrl, selectedOrganization, handleEditOrg} = props;
+ const [modalOpen, setModalOpen] = React.useState(false);
+ const [loginInfo, setLoginInfo] = React.useState("");
+ const [modalUser, setModalUser] = React.useState({});
+ const [selectedUser, setSelectedUser] = React.useState({});
+ const [matchingOrganizations, setMatchingOrganizations] = React.useState([]);
+ const [selectedUserModalOpen, setSelectedUserModalOpen] = React.useState(false);
+ const [image2FA, setImage2FA] = React.useState("");
+ const [secret2FA, setSecret2FA] = React.useState("");
+ const [value2FA, setValue2FA] = React.useState("");
+ const [newUsername, setNewUsername] = React.useState("");
+ const [newPassword, setNewPassword] = React.useState("");
+ const [show2faSetup, setShow2faSetup] = useState(false);
+ const [showDeleteAccountTextbox, setShowDeleteAccountTextbox] = React.useState(false);
+ const [MFARequired, setMFARequired] = React.useState(selectedOrganization.mfa_required === undefined ? false : selectedOrganization.mfa_required);
+ const [deleteAccountText, setDeleteAccountText] = React.useState("");
+ const [users, setUsers] = React.useState([]);
+ const [showLoader, setShowLoader] = useState(true);
+ const [logsLoading, setLogsLoading] = React.useState(true);
+ const [logs, setLogs] = React.useState([]);
+ const [logsViewModal, setLogsViewModal] = React.useState(false);
+ const [ipSelected, setIpSelected] = React.useState("");
+ const [userLogViewing, setUserLogViewing] = React.useState({});
+
+ useEffect(() => {
+ if (selectedOrganization?.mfa_required !== MFARequired) {
+ setMFARequired(selectedOrganization?.mfa_required);
+ }
+ }, [selectedOrganization]);
+ useEffect(() => { if(users?.length === 0){
+ getUsers();
+ } }, []);
+
+ const changeModalData = (field, value) => {
+ modalUser[field] = value;
+ };
+
+ 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("");
+ toast.success("User added successfully. They will show up in the list when they have accepted the invite.");
+ setModalOpen(false);
+ setTimeout(() => {
+ getUsers();
+ }, 1000);
+ }
+ })
+ )
+ .catch((error) => {
+ console.log("Error in userdata: ", error);
+ });
+ };
+
+ 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 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 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 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 ? (
+
+
+ Accessible Sub-Organizations (
+ {selectedUser?.orgs ? selectedUser?.orgs?.length - 1 : 0})
+
+ }
+ renderValue={(selected) => {
+ return selected.join(", ");
+ }}
+ MenuProps={MenuProps}
+ >
+ {selectedOrganization.child_orgs.map((org, index) => (
+
+ -1} />
+
+
+ ))}
+
+
+ ) : null;
+
+ 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);
+ setShowLoader(false)
+ })
+ .catch((error) => {
+ toast(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 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 handleVerify2FA = (userId, code) => {
+ const data = {
+ code: code,
+ user_id: userId,
+ };
+
+ 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) {
+ toast("Successfully enabled 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 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 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 UpdateMFAInUserOrg = (org_id) => {
+
+ handleEditOrg(
+ selectedOrganization?.name,
+ selectedOrganization?.description,
+ selectedOrganization?.id,
+ selectedOrganization?.image,
+ {
+ app_download_repo: selectedOrganization?.defaults?.app_download_repo,
+ app_download_branch: selectedOrganization?.defaults?.app_download_branch,
+ workflow_download_repo: selectedOrganization?.defaults?.workflow_download_repo,
+ workflow_download_branch: selectedOrganization?.defaults?.workflow_download_branch,
+ notification_workflow: selectedOrganization?.defaults?.notification_workflow,
+ documentation_reference: selectedOrganization?.defaults?.documentation_reference,
+ workflow_upload_repo: selectedOrganization?.defaults?.workflow_upload_repo,
+ workflow_upload_branch: selectedOrganization?.defaults?.workflow_upload_branch,
+ workflow_upload_username: selectedOrganization?.defaults?.workflow_upload_username,
+ workflow_upload_token: selectedOrganization?.defaults?.workflow_upload_token,
+ newsletter: selectedOrganization?.defaults?.newsletter,
+ weekly_recommendations: selectedOrganization?.defaults?.weekly_recommendations,
+ },
+ {
+ sso_entrypoint: selectedOrganization?.sso_config?.sso_entrypoint,
+ sso_certificate: selectedOrganization?.sso_config?.sso_certificate,
+ client_id: selectedOrganization?.sso_config?.client_id,
+ client_secret: selectedOrganization?.sso_config?.client_secret,
+ openid_authorization: selectedOrganization?.sso_config?.openid_authorization,
+ openid_token: selectedOrganization?.sso_config?.openid_token,
+ SSORequired: selectedOrganization?.sso_config?.SSORequired,
+ auto_provision: selectedOrganization?.sso_config?.auto_provision,
+ },
+ [],
+ {
+ mfa_required: !MFARequired
+ }
+ );
+ setMFARequired((prev)=> !prev)
+ }
+
+ const modalView = (
+ {
+ setModalOpen(false);
+ }}
+ PaperProps={{
+ sx: {
+ borderRadius: theme?.palette?.DialogStyle?.borderRadius,
+ border: theme?.palette?.DialogStyle?.border,
+ minWidth: '440px',
+ 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,
+ },
+ }
+ }}
+ >
+
+
+ Add user
+
+
+
+
+ We will send an email to invite them to your organization.
+
+
+ Username
+ {
+ if(e.key === "Enter"){
+ if (isCloud) {
+ inviteUser(modalUser);
+ } else {
+ submitUser(modalUser);
+ }
+ }
+ }}
+ onChange={(event) =>
+ changeModalData("Username", event.target.value)
+ }
+ />
+ {isCloud ? null : (
+
+ Password
+ {
+ if(e.key === "enter"){
+ if (isCloud) {
+ inviteUser(modalUser);
+ } else {
+ submitUser(modalUser);
+ }
+ }
+ }}
+ onChange={(event) =>
+ changeModalData("Password", event.target.value)
+ }
+ />
+
+ )}
+
+ {loginInfo}
+
+
+ setModalOpen(false)}
+ color="primary"
+ >
+ Cancel
+
+ {
+ if (isCloud) {
+ inviteUser(modalUser);
+ } else {
+ submitUser(modalUser);
+ }
+ }}
+ color="primary"
+ >
+ Submit
+
+
+
+ );
+
+ const run2FASetup = (data) => {
+ if (!show2faSetup) {
+ get2faCode(data.id);
+ } else {
+ // Should remove?
+ setImage2FA("");
+ setSecret2FA("");
+ }
+
+ setShow2faSetup(!show2faSetup);
+ //setShow2faSetup(true);
+ };
+
+ const editUserModal = (
+ {
+ setSelectedUserModalOpen(false);
+ setImage2FA("");
+ setValue2FA("");
+ setSecret2FA("");
+ setShow2faSetup(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: "800px",
+ minHeight: "320px",
+ overflow: "hidden",
+ '& .MuiDialogContent-root': {
+ backgroundColor: theme?.palette?.DialogStyle?.backgroundColor,
+ },
+ '& .MuiDialogTitle-root': {
+ backgroundColor: theme?.palette?.DialogStyle?.backgroundColor,
+ },
+ }
+ }}
+ >
+
+
+ Editing {selectedUser.username}
+
+
+
+ {isCloud ? null : (
+
+ {
+ setNewUsername(e.target.value);
+ }}
+ />
+ {
+ setUser(selectedUser.id, "username", newUsername);
+ }}
+ >
+ Submit
+
+
+ )}
+
+ {isCloud ? null : (
+
+ setNewPassword(e.target.value)}
+ />
+ onPasswordChange()}
+ >
+ Submit
+
+
+ )}
+
+ {userOrgEdit}
+
+
+ {
+ deleteUser(selectedUser);
+ setSelectedUserModalOpen(false);
+ }}
+ >
+ {selectedUser.active ? "Delete from org" : "Delete from org"}
+
+ generateApikey(selectedUser)}
+ >
+ Renew API-key
+
+ {
+ run2FASetup(userdata);
+ }}
+ disabled={
+ selectedUser.role === "admin" &&
+ selectedUser.username !== userdata.username
+ }
+ variant="outlined"
+ color="primary"
+ style={{textTransform: 'none', fontSize: 16}}
+ >
+ {selectedUser.mfa_info !== undefined &&
+ selectedUser.mfa_info !== null &&
+ selectedUser.mfa_info.active === true
+ ? "Disable 2FA"
+ : "Enable 2FA"}
+
+
+ {isCloud && userdata.support && selectedUser.id !== userdata.id ? (
+ 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
+ (support users - confirmation needed)
+
+ ) : null}
+
+ {showDeleteAccountTextbox ? (
+ {
+ setDeleteAccountText(e.target.value);
+ }}
+ />
+ ) : null}
+
+ {show2faSetup ? (
+
+ {/*
*/}
+
+ {secret2FA !== undefined &&
+ secret2FA !== null &&
+ secret2FA?.length > 0 ? (
+
+
+ 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.
+
+
+ ) : null}
+ {image2FA !== undefined &&
+ image2FA !== null &&
+ image2FA?.length > 0 ? (
+
+ ) : (
+
+ )}
+
+
+ After scanning the QR code image, the app will display a code that
+ you can enter below.
+
+
+ {
+ if (event.target.value.length > 6) {
+ return;
+ }
+
+ setValue2FA(event.target.value);
+ }}
+ />
+ {
+ handleVerify2FA(userdata.id, value2FA);
+ }}
+ color="primary"
+ >
+ Submit
+
+
+
+ ) : 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 logview = logsViewModal ? (
+ {
+ setLogsViewModal(false);
+ }}
+ PaperProps={{
+ sx: {
+ borderRadius: theme?.palette?.DialogStyle?.borderRadius,
+ border: theme?.palette?.DialogStyle?.border,
+ minWidth: "1200px",
+ 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,
+ },
+ },
+ }}
+ >
+
+ User Logs
+
+
+ {/* ask user for which IP they want to see logs for by iterating of user.login_info */}
+
+
+ User IP
+
+
+ {
+ 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 (
+
+ {data.ip}
+
+ );
+ });
+ })()}
+
+
+
+ {logsLoading && ipSelected.length !== 0 ? (
+
+
+ Loading logs
+
+ ) : null}
+
+
+
+
+
+
+
+ {logs.map((data, index) => {
+ //console.log("LOG: ", data)
+
+ return (
+ // redirect user to logs
+ // using request id or trace id
+
+
+
+
+
+ )})}
+
+
+
+ ) : null
+
+ return (
+
+ {modalView}
+ {editUserModal}
+ {logview}
+
+
+
+
+
+
+
setModalOpen(true)}
+ >
+ Add user
+
+
getUsers()}
+ >
+
+
+
+
+ MFA Required
+ {
+ UpdateMFAInUserOrg(selectedOrganization.id);
+ }}
+ />
+
+
+
+
+
+ {["Username", "API Key", "Role", "Active", "Type", "MFA", ...(selectedOrganization?.child_orgs?.length > 0 ? ["Suborgs"]: []), "Actions", "Last Login"].map((header, index) => (
+
+ ))}
+
+ {showLoader ? (
+ [...Array(6)].map((_, rowIndex) => (
+
+ {Array(9)
+ .fill()
+ .map((_, colIndex) => (
+
+
+
+ ))}
+
+ ))
+ ): users === 0 ? null
+ : users?.map((data, index) => {
+ var bgColor = "#212121";
+ if (index % 2 === 0) {
+ bgColor = "#1A1A1A";
+ }
+
+ 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 = (
+ {
+ 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}
+
+ );
+ }
+
+ return (
+
+
+ {userData || 'No username'}
+
+ )}
+ primaryTypographyProps={{
+ style: {
+ maxWidth: 150,
+ minWidth: 100,
+ width: 'auto',
+ color: "#FF8444",
+ textOverflow: "ellipsis",
+ whiteSpace: "nowrap",
+ overflow: "hidden",
+ padding: "8px 8px 8px 15px",
+ },
+ }}
+ style={{display:'table-cell', verticalAlign: 'middle' }}
+ />
+
+ {
+ navigator.clipboard.writeText(data.apikey);
+ toast.success("Apikey copied to clipboard");
+ }}
+ >
+
+
+
+ )
+ }
+ />
+ {
+ console.log("VALUE: ", e.target.value);
+ setUser(data.id, "role", e.target.value);
+ }}
+ sx={{
+ backgroundColor: "#1A1A1A",
+ color: "white",
+ height: "50px",
+ borderRadius: "4px",
+ marginTop: "8px",
+ marginBottom: "8px",
+ padding: "8px",
+ }}
+ MenuProps={{
+ PaperProps: {
+ sx: {
+ "& .MuiList-root": {
+ padding: 0,
+ },
+ },
+ },
+ }}
+ >
+
+ Org Admin
+
+
+ Org User
+
+
+ Org Reader
+
+
+ }
+ style={{ display:'table-cell', verticalAlign: 'middle' }}
+ />
+
+
+
+ {selectedOrganization?.child_orgs !== undefined &&
+ selectedOrganization?.child_orgs !== null &&
+ selectedOrganization?.child_orgs?.length > 0 ? (
+
+ ) : null}
+
+ {
+ 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);
+ }
+ }}
+ >
+
+
+ {/* {
+ generateApikey(data)
+ }}
+ disabled={data.role === "admin" && data.username !== userdata.username}
+ variant="outlined"
+ color="primary"
+ >
+ New apikey
+ */}
+
+
+
+
+ );
+ })}
+
+
+
+
+
+ );
+})
+
+export default UserManagmentTab;
diff --git a/frontend/src/components/ssoTab.jsx b/frontend/src/components/ssoTab.jsx
new file mode 100644
index 00000000..8f39a296
--- /dev/null
+++ b/frontend/src/components/ssoTab.jsx
@@ -0,0 +1,649 @@
+import { useEffect } from "react";
+import React from "react";
+import {
+ Typography,
+ Switch,
+ Button,
+ Tooltip,
+ TextField,
+ Grid,
+ Skeleton,
+ Dialog,
+ DialogTitle,
+ DialogContent,
+ Box,
+} from "@mui/material";
+import { makeStyles } from "@mui/styles";
+import { Link } from "react-router-dom";
+import theme from "../theme.jsx";
+import { toast } from "react-toastify";
+
+const useStyles = makeStyles({
+ notchedOutline: {
+ borderColor: "#f85a3e !important",
+ },
+});
+
+
+const SSOTab = ({selectedOrganization, userdata, isEditOrgTab, globalUrl, handleEditOrg})=>{
+
+ const classes = useStyles();
+ const [show2faSetup, setShow2faSetup] = React.useState(false);
+ const [autoPrivision, setAutoProvision] = React.useState(selectedOrganization?.sso_config?.auto_provision)
+ const [ssoEntrypoint, setSsoEntrypoint] = React.useState(
+ selectedOrganization.sso_config === undefined
+ ? ""
+ : selectedOrganization.sso_config.sso_entrypoint === undefined ||
+ selectedOrganization.sso_config.sso_entrypoint.length === 0
+ ? ""
+ : selectedOrganization.sso_config.sso_entrypoint
+ );
+ const [SSORequired, setSSORequired] = React.useState(selectedOrganization.sso_config === undefined
+ ? false
+ : selectedOrganization.sso_config.SSORequired === undefined
+ ? false
+ : selectedOrganization.sso_config.SSORequired);
+
+ const [ssoCertificate, setSsoCertificate] = React.useState(
+ selectedOrganization.sso_config === undefined
+ ? ""
+ : selectedOrganization.sso_config.sso_certificate === undefined ||
+ selectedOrganization.sso_config.sso_certificate.length === 0
+ ? ""
+ : selectedOrganization.sso_config.sso_certificate
+ );
+
+ const [openidClientId, setOpenidClientId] = React.useState(
+ selectedOrganization.sso_config === undefined
+ ? ""
+ : selectedOrganization.sso_config.client_id === undefined ||
+ selectedOrganization.sso_config.client_id.length === 0
+ ? ""
+ : selectedOrganization.sso_config.client_id
+ );
+ const [openidClientSecret, setOpenidClientSecret] = React.useState(
+ selectedOrganization.sso_config === undefined
+ ? ""
+ : selectedOrganization.sso_config.client_secret === undefined ||
+ selectedOrganization.sso_config.client_secret.length === 0
+ ? ""
+ : selectedOrganization.sso_config.client_secret
+ );
+ const [openidAuthorization, setOpenidAuthorization] = React.useState(
+ selectedOrganization.sso_config === undefined
+ ? ""
+ : selectedOrganization.sso_config.openid_authorization === undefined ||
+ selectedOrganization.sso_config.openid_authorization.length === 0
+ ? ""
+ : selectedOrganization.sso_config.openid_authorization
+ );
+ const [openidToken, setOpenidToken] = React.useState(
+ selectedOrganization.sso_config === undefined
+ ? ""
+ : selectedOrganization.sso_config.openid_token === undefined ||
+ selectedOrganization.sso_config.openid_token.length === 0
+ ? ""
+ : selectedOrganization.sso_config.openid_token
+ )
+
+ useEffect(()=>{
+
+ if (openidClientSecret !== selectedOrganization?.sso_config?.client_secret) {
+ setOpenidClientSecret(selectedOrganization?.sso_config?.client_secret)
+ }
+
+ if (openidClientId !== selectedOrganization?.sso_config?.client_id) {
+ setOpenidClientId(selectedOrganization?.sso_config?.client_id)
+ }
+
+ if (openidAuthorization !== selectedOrganization?.sso_config?.openid_authorization) {
+ setOpenidAuthorization(selectedOrganization?.sso_config?.openid_authorization)
+ }
+
+ if (openidToken !== selectedOrganization?.sso_config?.openid_token) {
+ setOpenidToken(selectedOrganization?.sso_config?.openid_token)
+ }
+
+ if (ssoCertificate !== selectedOrganization?.sso_config?.sso_certificate) {
+ setSsoCertificate(selectedOrganization?.sso_config?.sso_certificate)
+ }
+
+ if (ssoEntrypoint !== selectedOrganization?.sso_config?.sso_entrypoint) {
+ setSsoEntrypoint(selectedOrganization?.sso_config?.sso_entrypoint)
+ }
+
+ if (SSORequired !== selectedOrganization?.sso_config?.SSORequired) {
+ setSSORequired(selectedOrganization?.sso_config?.SSORequired)
+ }
+ if (autoPrivision !== selectedOrganization?.sso_config?.auto_provision) {
+ setAutoProvision(selectedOrganization?.sso_config?.auto_provision)
+ }
+ },[selectedOrganization])
+
+ const orgSaveButton = (
+
+
+ handleEditOrg(
+ selectedOrganization?.name,
+ selectedOrganization?.description,
+ selectedOrganization.id,
+ selectedOrganization.image,
+ {
+ app_download_repo: selectedOrganization?.defaults?.app_download_repo,
+ app_download_branch: selectedOrganization?.defaults?.app_download_branch,
+ workflow_download_repo: selectedOrganization?.defaults?.workflow_download_repo,
+ workflow_download_branch: selectedOrganization?.defaults?.workflow_download_branch,
+ notification_workflow: selectedOrganization?.defaults?.notification_workflow,
+ documentation_reference: selectedOrganization?.defaults?.documentation_reference,
+ workflow_upload_repo: selectedOrganization?.defaults?.workflow_upload_repo,
+ workflow_upload_branch: selectedOrganization?.defaults?.workflow_upload_branch,
+ workflow_upload_username: selectedOrganization?.defaults?.workflow_upload_username,
+ workflow_upload_token: selectedOrganization?.defaults?.workflow_upload_token,
+ newsletter: !selectedOrganization?.defaults?.newsletter,
+ weekly_recommendations: selectedOrganization?.defaults?.weekly_recommendations,
+ },
+ {
+ sso_entrypoint: ssoEntrypoint,
+ sso_certificate: ssoCertificate,
+ client_id: openidClientId,
+ client_secret: openidClientSecret,
+ openid_authorization: openidAuthorization,
+ openid_token: openidToken,
+ SSORequired: SSORequired,
+ auto_provision: autoPrivision,
+ }
+ )
+ }
+ >
+ Save Changes
+ {/* */}
+
+
+ );
+
+ const toggleBetweenRequiredOrOptional = (event) => {
+ if (
+ ssoEntrypoint === "" &&
+ openidAuthorization === "" &&
+ openidToken === ""
+ ) {
+ if (!SSORequired) {
+ toast.error(
+ "Please fill in fields for either OpenID connect or SSO before continuing. "
+ );
+ return;
+ }
+ } else {
+ toast.info("Toggled SSO. Remember to save.");
+ }
+
+ setSSORequired(event.target.checked);
+ };
+
+ const handleChangeAutoProvision = (event) => {
+
+ if (
+ ssoEntrypoint === "" &&
+ openidAuthorization === "" &&
+ openidToken === ""
+ ) {
+ if (!autoPrivision) {
+ toast.error(
+ "Please fill in fields for either OpenID connect or SSO before continuing. "
+ );
+ return;
+ }
+ } else {
+ setAutoProvision((prev)=> !prev);
+ toast.info("Toggled Auto Provisioning. Remember to save.");
+ }
+ };
+
+ const HandleTestSSO = () => {
+ const url = `${globalUrl}/api/v1/orgs/${selectedOrganization?.id}/change`;
+ const data = {
+ org_id: selectedOrganization?.id,
+ sso_test: true,
+ };
+
+ fetch(url, {
+ mode: "cors",
+ credentials: "include",
+ crossDomain: true,
+ method: "POST",
+ body: JSON.stringify(data),
+ withCredentials: true,
+ headers: {
+ "Content-Type": "application/json; charset=utf-8",
+ },
+ })
+ .then((response) => {
+ if (response.status !== 200) {
+ toast.error(
+ "Failed to test SSO. Please try again later or contact support@shuffler.io if issue persists.",
+ { duration: 3000 }
+ );
+ return null;
+ }
+ return response.json();
+ })
+ .then((responjson) => {
+ if (!responjson) return;
+
+ if (responjson["reason"] === "SSO_REDIRECT") {
+ toast.info(
+ "Redirecting to SSO login page as SSO is required for this organization.",
+ {
+ duration: 3000,
+ onClose: () => {
+ window.location.href = responjson["url"];
+ }
+ }
+ );
+ } else {
+ toast.error(
+ "No SSO found for this org. Please set up SSO for this org.",
+ { duration: 3000 }
+ );
+ }
+ })
+ .catch((error) => {
+ console.error("Error for SSO test:", error);
+ toast.error(
+ "An error occurred while testing SSO. Please try again.",
+ { duration: 3000 }
+ );
+ });
+ };
+
+ return (
+
+
+
+
+ SSO Configuration
+
+
+
+ Make SAML SSO or OpenID Authentication Required or Optional for Your Organization.
+
+
+
+ {SSORequired ? "Required" : "Optional"}
+
+
+
+ {/* auto privisiong in sso */}
+
+
+ Auto-provisioning of users in SSO. By default, users are auto-provisioned in SSO when they login. If you enable this, no new user will be added in your organization when they login via SSO.
+
+
+
+
+
+
+
+
+
+ You can test your SSO configuration by clicking the button below.
+ Before testing, ensure you have set Open ID Connect or SAML SSO
+ credentials.
+
+ 0 ||
+ ssoCertificate?.length > 0 ||
+ openidAuthorization?.length > 0 ||
+ openidClientId?.length > 0
+ )
+ ? "Please ensure all SSO credentials are set before testing."
+ : ""
+ }
+ >
+
+ 0 ||
+ ssoCertificate?.length > 0 ||
+ openidAuthorization?.length > 0 ||
+ openidClientId?.length > 0
+ )
+ }
+ onClick={HandleTestSSO}
+ >
+ Test SSO
+
+
+
+
+
+
+ OpenID connect
+
+ Configure and Authorize SAML / SSO or OpenID connect. {" "}
+
+ Learn more
+
+
+
+
+ IdP URL for Shuffle OpenID: {`${globalUrl}/api/v1/login_openid`}
+
+
+
+
+ Client ID
+ {
+ setOpenidClientId(e.target.value);
+ }}
+ InputProps={{
+ classes: {
+ notchedOutline: isEditOrgTab ? null : classes.notchedOutline,
+ },
+ style: {
+ color: "white",
+ fontFamily: "var(--zds-typography-base,Inter,Helvetica,arial,sans-serif)",
+ fontWeight: 400,
+ fontSize: 16,
+ borderRadius: 4,
+ },
+ }}
+ />
+
+
+
+
+ Client Secret
+ {
+ setOpenidClientSecret(e.target.value);
+ }}
+ InputProps={{
+ classes: {
+ notchedOutline: isEditOrgTab ? null : classes.notchedOutline,
+ },
+ style: {
+ color: "white",
+ fontFamily: "var(--zds-typography-base,Inter,Helvetica,arial,sans-serif)",
+ fontWeight: 400,
+ fontSize: 16,
+ borderRadius: 4,
+ },
+ }}
+ />
+
+
+
+
+
+
+ Authorization URL
+ {
+ setOpenidAuthorization(e.target.value)
+ }}
+ InputProps={{
+ classes: {
+ notchedOutline: isEditOrgTab ? null : classes.notchedOutline,
+ },
+ style: {
+ color: "white",
+ fontFamily: "var(--zds-typography-base,Inter,Helvetica,arial,sans-serif)",
+ fontWeight: 400,
+ fontSize: 16,
+ borderRadius: 4,
+ },
+ }}
+ />
+
+
+
+
+ Token URL
+ {
+ setOpenidToken(e.target.value)
+ }}
+ InputProps={{
+ classes: {
+ notchedOutline: isEditOrgTab ? null : classes.notchedOutline,
+ },
+ style: {
+ color: "white",
+ fontFamily: "var(--zds-typography-base,Inter,Helvetica,arial,sans-serif)",
+ fontWeight: 400,
+ fontSize: 16,
+ borderRadius: 4,
+ },
+ }}
+ />
+
+
+
+
+ {/**/}
+ {/*isCloud ? null : */}
+
+ SAML SSO (v1.1)
+
+ IdP URL for Shuffle SAML/SSO: {`${globalUrl}/api/v1/login_sso`}
+
+
+
+
+ SSO Entrypoint (IdP)
+ {
+ setSsoEntrypoint(e.target.value);
+ }}
+ InputProps={{
+ classes: {
+ notchedOutline: isEditOrgTab ? null : classes.notchedOutline,
+ },
+ style: {
+ color: "white",
+ fontFamily: "var(--zds-typography-base,Inter,Helvetica,arial,sans-serif)",
+ fontWeight: 400,
+ fontSize: 16,
+ borderRadius: 4,
+ },
+ }}
+ />
+
+
+
+
+ SSO Certificate (X509)
+ {
+ setSsoCertificate(e.target.value);
+ }}
+ InputProps={{
+ classes: {
+ notchedOutline: isEditOrgTab ? null : classes.notchedOutline,
+ },
+ style: {
+ color: "white",
+ fontFamily: "var(--zds-typography-base,Inter,Helvetica,arial,sans-serif)",
+ fontWeight: 400,
+ fontSize: 16,
+ borderRadius: 4,
+ },
+ }}
+ />
+
+
+
+
+
+ {orgSaveButton}
+
+
+
+
+ )
+}
+
+export default SSOTab
\ No newline at end of file
diff --git a/frontend/src/views/Admin2.jsx b/frontend/src/views/Admin2.jsx
index 7650e491..45d933b9 100644
--- a/frontend/src/views/Admin2.jsx
+++ b/frontend/src/views/Admin2.jsx
@@ -318,7 +318,7 @@ const Admin2 = (props) => {
return (
);
};
diff --git a/frontend/src/views/AngularWorkflow.jsx b/frontend/src/views/AngularWorkflow.jsx
index b60d3171..4e2bf780 100755
--- a/frontend/src/views/AngularWorkflow.jsx
+++ b/frontend/src/views/AngularWorkflow.jsx
@@ -19116,7 +19116,7 @@ const AngularWorkflow = (defaultprops) => {
const defaultImage =
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAJYAAACOCAMAAADkWgEmAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAAWlBMVEX4Wj69TDgmKCvkVTwlJyskJiokJikkJSkjJSn4Ykf+6+f5h3L////8xLr5alH/9fT7nYz4Wz/919H5cVn/+vr8qpv4XUL94d35e2X//v38t6v4YUbkVDy8SzcVIzHLAAAAAWJLR0QMgbNRYwAAAAlwSFlzAAARsAAAEbAByCf1VAAAAAd0SU1FB+QGGgsvBZ/GkmwAAAFKSURBVHja7dlrTgMxDEXhFgpTiukL2vLc/zbZQH5N7MmReu4KPmlGN4m9WgGzfhgtaOZxM1rQztNoQDvPowHtTKMB7WxHA2TJkiVLlixIZMmSRYgsWbIIkSVLFiGyZMkiRNZirBcma/eKZEW87ZGsOBxPRFbE+R3Jio/LlciKuH0iWfH1/UNkRSR3RRYruSvyWKldkcjK7IpUVl5X5LLSuiKbldQV6aycrihgZXRFCau/K2pY3V1RxersijJWX1cUsnq6opLV0RW1rNldUc2a2RXlrHldsQBrTlfcLwv5EZm/PLIgkHXKPHyQRzXzYoO8BjIvzcgnBvJBxny+Ih/7zNEIcpDEHLshh5TIkS5zAI5cFzCXK8hVFHNxh1xzQpfC0BV6XWTJkkWILFmyCJElSxYhsmTJIkSWLFmEyJIlixBZsmQB8stk/U3/Yb49pVcDMg4AAAAldEVYdGRhdGU6Y3JlYXRlADIwMjAtMDYtMjZUMTE6NDc6MDUrMDI6MDD8QCPmAAAAJXRFWHRkYXRlOm1vZGlmeQAyMDIwLTA2LTI2VDExOjQ3OjA1KzAyOjAwjR2bWgAAAABJRU5ErkJggg=="
- const size = 40;
+ const size = isCloud ? 40 : 35;
const borderRadius = 5
if (execution.execution_source === undefined || execution.execution_source === null || execution.execution_source.length === 0) {
return (
diff --git a/frontend/src/views/AppCreator.jsx b/frontend/src/views/AppCreator.jsx
index 6e6dda22..94a3d136 100755
--- a/frontend/src/views/AppCreator.jsx
+++ b/frontend/src/views/AppCreator.jsx
@@ -6243,7 +6243,7 @@ const AppCreator = (defaultprops) => {
{testView}
*/}
-
+
{appDownloadData.length > 0 ?
diff --git a/frontend/src/views/AppExplorer.jsx b/frontend/src/views/AppExplorer.jsx
new file mode 100644
index 00000000..e746f75e
--- /dev/null
+++ b/frontend/src/views/AppExplorer.jsx
@@ -0,0 +1,4623 @@
+import React, { useState, useEffect, useContext } from "react";
+
+import theme from "../theme.jsx";
+import ReactGA from "react-ga4";
+import Markdown from "react-markdown";
+import algoliasearch from "algoliasearch/lite";
+import ReactJson from "react-json-view-ssr";
+//import { useAlert
+import { ToastContainer, toast } from "react-toastify"
+import { makeStyles, createStyles } from "@mui/styles";
+import { useParams, useNavigate, Link } from "react-router-dom";
+
+import {
+ Autocomplete,
+ Box,
+ Zoom,
+ Card,
+ CardActionArea,
+ Fade,
+ Tabs,
+ Tab,
+ CircularProgress,
+ DialogActions,
+ Dialog,
+ DialogTitle,
+ DialogContent,
+ Tooltip,
+ IconButton,
+ Menu,
+ Paper,
+ Button,
+ Typography,
+ Divider,
+ MenuItem,
+ Avatar,
+ TextField,
+ Breadcrumbs,
+ Checkbox,
+ Chip,
+ Select,
+} from "@mui/material";
+
+import {
+ Business as BusinessIcon,
+ Edit as EditIcon,
+ CloudDownload as CloudDownloadIcon,
+ Warning as WarningIcon,
+ VerifiedUser as VerifiedUserIcon,
+ Close as CloseIcon,
+ LockOpen as LockOpenIcon,
+ PlayArrow as PlayArrowIcon,
+ GetApp as GetAppIcon,
+ Apps as AppsIcon,
+ Description as DescriptionIcon,
+ ShowChart as ShowChartIcon,
+ Person as PersonIcon,
+ Polyline as PolylineIcon,
+ OpenInNew as OpenInNewIcon,
+} from "@mui/icons-material";
+
+import ForkRightIcon from '@mui/icons-material/ForkRight';
+
+import Alert from "@mui/material/Alert";
+import { Context } from "../context/ContextApi.jsx";
+
+import {
+ SearchBox,
+ StaticRefinementList,
+ RefinementList,
+ InstantSearch,
+ connectSearchBox,
+ connectHits,
+ Index,
+} from "react-instantsearch-dom";
+import AppStats from "../components/AppStats.jsx";
+import ParsedAction from "../components/ParsedAction.jsx";
+import { validateJson, GetIconInfo } from "../views/Workflows.jsx";
+import { base64_decode, appCategories } from "../views/AppCreator.jsx";
+import { triggers as workflowTriggers } from "../views/AngularWorkflow.jsx";
+import AuthenticationOauth2 from "../components/Oauth2Auth.jsx";
+import AuthenticationWindow from "../components/AuthenticationWindow.jsx";
+import { CodeHandler, Img, OuterLink, CopyToClipboard, } from "../views/Docs.jsx";
+import { useStyles, } from "../components/ParsedAction.jsx";
+import { sortByKey } from "../views/AngularWorkflow.jsx";
+
+import { v4 as uuidv4 } from "uuid";
+import aa from "search-insights";
+
+const surfaceColor = "#27292D";
+const inputColor = "#383B40";
+
+const chipStyle = {
+ marginTop: 5,
+ backgroundColor: "#3d3f43",
+ height: 30,
+ marginRight: 5,
+ paddingLeft: 5,
+ paddingRight: 5,
+ height: 28,
+ cursor: "pointer",
+ borderColor: "#3d3f43",
+ color: "white",
+};
+
+const actionListStyle = {
+ paddingLeft: 10,
+ paddingRight: 10,
+ paddingTop: 10,
+ marginTop: 5,
+ backgroundColor: inputColor,
+ display: "flex",
+ color: "white",
+ maxWidth: 350,
+ minWidth: 350,
+ maxHeight: 54,
+ overflow: "hidden",
+};
+
+const boxStyle = {
+ color: "white",
+ flex: "3",
+ margin: 10,
+ paddingLeft: 30,
+ paddingRight: 30,
+ paddingBottom: 30,
+ paddingTop: 30,
+ display: "flex",
+ flexDirection: "column",
+ position: "relative",
+ maxHeight: 180,
+ overflow: "hidden",
+};
+
+const buttonBackground = "linear-gradient(to right, #f86a3e, #f34079)";
+
+
+// AppTypes:
+// 0 = OpenAPI (VALID)
+// 1 = Normal app (Python)
+// 2 = OpenAPI (Invalid)
+const searchClient = algoliasearch(
+ "JNSS5CFDZZ",
+ "db08e40265e2941b9a7d8f644b6e5240"
+)
+
+const AppExplorer = (props) => {
+ const {
+ globalUrl,
+ userdata,
+ setUserData,
+ checkLogin,
+ isLoaded,
+ selectedApp,
+ serverside,
+ isMobile,
+ isLoggedIn,
+ selectedDoc,
+ secondApp,
+ } = props;
+
+ //const alert = useAlert();
+ const classes = useStyles()
+ let navigate = useNavigate()
+
+ const { leftSideBarOpenByClick, } = useContext(Context);
+
+ const params = useParams();
+ //var props = JSON.parse(JSON.stringify(defaultprops))
+ //props.match = {}
+ //params = params
+
+ const bodyDivStyle = {
+ margin: "auto",
+ maxWidth: isMobile ? "100%" : 1350,
+ scrollX: "hidden",
+ overflowX: "hidden",
+ };
+
+ var upload = "";
+ const actionNonBodyRequest = ["GET", "HEAD", "DELETE", "CONNECT"];
+ const authenticationOptions = [
+ "No authentication",
+ "API key",
+ "Bearer auth",
+ "Basic auth",
+ ];
+ const apikeySelection = ["Header", "Query"];
+
+ const [app, setApp] = useState({});
+
+ const [openapi, setOpenapi] = useState({});
+ const [name, setName] = useState("");
+ const [appId, setAppId] = useState("");
+ const [contact, setContact] = useState("");
+ const [file] = useState("");
+ const [fileBase64, setFileBase64] = useState("");
+ const [isAppLoaded, setIsAppLoaded] = useState(false);
+ const [, setDescription] = useState("");
+ const [, setBaseUrl] = useState("");
+ const [, setAuthenticationRequired] = useState(false);
+ const [, setAuthenticationOption] = useState(authenticationOptions[0]);
+ const [newWorkflowTags, setNewWorkflowTags] = React.useState([]);
+ const [, setParameterName] = useState("");
+ const [, setParameterLocation] = useState(
+ apikeySelection.length > 0 ? apikeySelection[0] : ""
+ );
+ const [, setUrlPath] = useState("");
+ const [urlPathQueries, setUrlPathQueries] = useState([]);
+ const [, setBasedata] = React.useState({});
+ const [actions, setActions] = useState([]);
+ const [errorCode] = useState("");
+ const [reloadUrl, setReloadUrl] = React.useState(
+ serverside === true ? "" : window.location.href
+ );
+ const [relatedWorkflows, setRelatedWorkflows] = useState(0);
+ const [relatedApps, setRelatedApps] = useState(0);
+ const [appAuthentication, setAppAuthentication] = React.useState([]);
+ const [authLoaded, setAuthLoaded] = useState(false);
+ const baseResult = "The execution result will show up here";
+ const [anchorElAvatar, setAnchorElAvatar] = React.useState(null);
+ const [executionResult, setExecutionResult] = useState({
+ valid: false,
+ result: baseResult,
+ });
+ const [executing, setExecuting] = useState(false);
+ const [anchorEl, setAnchorEl] = React.useState(null);
+ const [creatorProfile, setCreatorProfile] = React.useState({});
+ const [selectedTab, setSelectedTab] = React.useState(0);
+ const defaultDocs = "\n\n## No Shuffle-specific app documentation is available yet.\n\n## Need more information about the app? [Contact us](/contact) and [Join the Community](https://discord.gg/B2CBzUm) and find others using this app."
+ const [sharingConfiguration, setSharingConfiguration] = React.useState("you");
+ const [appdata, setAppData] = React.useState({});
+ const [appDocumentation, setAppDocumentation] = useState(defaultDocs)
+ const [secondaryApp, setSecondaryApp] = useState({});
+ const [firstRequest, setFirstRequest] = useState(true);
+ const [publishModalOpen, setPublishModalOpen] = React.useState(false);
+
+ const [categories, setCategories] = useState(appCategories)
+ const [newWorkflowCategories, setNewWorkflowCategories] = React.useState([]);
+ const [update, setUpdate] = useState("");
+ const [triggers, setTriggers] = useState([])
+ const [selectedOrganization, setSelectedOrganization] = React.useState(undefined)
+ const [selectedValidationAction, setSelectedValidationAction] = React.useState({})
+
+ const [selectedMeta, setSelectedMeta] = React.useState({
+ link: "https://github.com/Shuffle/openapi-apps/new/master/docs",
+ read_time: 1,
+ })
+
+ const isCloud = (window.location.host === "localhost:3002" || window.location.host === "shuffler.io") ? true : (process.env.IS_SSR === "true");
+
+
+ // FIXME: This is used, as useEffect() creates an issue with apps not loading at all
+ var to_be_copied = "";
+
+ // 0 = VALID OpenAPI, 1 = Python, 2 = INVALID OpenAPI
+ const [appType, setAppType] = React.useState(0);
+
+ function handleClick(event) {
+ setAnchorEl(event.currentTarget);
+ }
+
+ function handleClose() {
+ setAnchorEl(null);
+ }
+
+
+ const loadOrganization = (orgId) => {
+ fetch(`${globalUrl}/api/v1/orgs/${orgId}`, {
+ method: "GET",
+ credentials: "include",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ })
+ .then((response) => {
+ if (response.status === 401) {
+ } else {
+ }
+
+ return response.json();
+ })
+ .then((responseJson) => {
+ if (responseJson.success === false) {
+
+ } else {
+ setSelectedOrganization(responseJson)
+ }
+ })
+ .catch((error) => {
+ console.log("Error in fetching organization: ", error)
+ })
+ }
+
+
+ useEffect(() => {
+ if (selectedApp !== undefined && selectedApp !== null && Object.getOwnPropertyNames(selectedApp).length > 0) {
+ //console.log("Firstrequest!!!")
+ } else {
+ if (serverside) {
+ console.log("Not getting app because serverside.");
+ } else {
+ if (params.appid.length === 32 || params.appid.length === 36) {
+ handleEditApp(params.appid);
+ runAlgoliaAppSearch(params.appid, false, true);
+ } else {
+ runAlgoliaAppSearch(params.appid);
+
+ //handleEditApp()
+ }
+ }
+ //parseIncomingOpenapiData(YAML.parse(data))
+ }
+
+ if (serverside !== true) {
+ const urlSearchParams = new URLSearchParams(window.location.search);
+ const queries = Object.fromEntries(urlSearchParams.entries());
+ const foundTab = queries["tab"];
+ console.log("PROPS: ", queries);
+
+ if (params.integrationid !== undefined) {
+ console.log(
+ "Should search for connection integration with ",
+ params.integrationid
+ );
+ setSelectedTab(3);
+
+ runAlgoliaAppSearch(params.integrationid, false);
+ } else if (foundTab !== null && foundTab !== undefined) {
+ if (foundTab === "stats") {
+ setSelectedTab(2);
+ } else if (foundTab === "run") {
+ setSelectedTab(1);
+ } else if (foundTab === "docs" || foundTab === "documentation") {
+ setSelectedTab(0);
+ }
+ } else {
+ //setSelectedTab(1);
+ }
+
+ }
+ }, []);
+
+ if (serverside === false && firstRequest && isLoggedIn === true && selectedOrganization === undefined && userdata !== undefined && userdata.active_org !== undefined && userdata.active_org !== null && userdata.active_org.id !== undefined && userdata.active_org.id !== null) {
+ loadOrganization(userdata.active_org.id)
+ }
+
+ var activateButton = (
+
+ {
+ ReactGA.event({
+ category: "Appexplorer",
+ action: "app_build",
+ label: params.appid,
+ });
+ toast("INTERESTED!");
+ }}
+ style={{
+ padding: 15,
+ marginTop: 15,
+ borderRadius: 25,
+ height: 50,
+ margin: "15px 0px 15px 0px",
+ fontSize: 14,
+ color: "white",
+ backgroundImage: buttonBackground,
+ width: "200px",
+ }}
+ >
+ I'm interested
+
+
+ );
+
+ const Heading = (props) => {
+ const element = React.createElement(`h${props.level}`,{ style: { marginTop: props.level === 1 ? 20 : 50 } },props.children);
+
+ const [hover, setHover] = useState(false);
+
+ var extraInfo = "";
+ if (props.level === 1) {
+ extraInfo = (
+
+
+
+
+ {isMobile === true ||
+ selectedMeta.contributors === undefined ||
+ selectedMeta.contributors === null ? (
+ ""
+ ) : (
+
+ {selectedMeta.contributors.slice(0, 7).map((data, index) => {
+ return (
+
+
+
+
+
+ );
+ })}
+
+ )}
+
+
+ );
+ }
+
+ // Still inside Heading
+
+ // Find appCategory in appCategories
+ const appCategory = newWorkflowCategories.length === 0 ? "" : (newWorkflowCategories[0]).toLowerCase();
+ const foundCategory = appCategories.find((category) => {
+ if (category.name.toLowerCase() === appCategory) {
+ return category
+ }
+ })
+ const bgColor = foundCategory === undefined ? "" : foundCategory.color
+
+ const getAllLabels = () => {
+ const actionLabels = actions.filter((input_action) => {
+ return input_action.action_label !== undefined && input_action.action_label !== null && input_action.action_label !== "No Label"
+ })
+
+ var labels = []
+ actionLabels.forEach((action) => {
+ labels.push(action.action_label)
+ })
+
+ return labels
+ }
+
+ const allLabels = getAllLabels()
+
+ const findRelevantAction = (action_label) => {
+ const foundAction = actions.find((input_action) => {
+ if (input_action.action_label.toLowerCase() === action_label.toLowerCase()) {
+ return input_action
+ }
+ })
+
+ if (foundAction !== undefined) {
+ setCurrentAction(foundAction)
+ setCurrentActionMethod(foundAction.method);
+ setSelectedTab(1);
+ } else {
+ console.log("Could not find action with label: ", action_label)
+ }
+ }
+
+ // Parses out extra category info and such for the app
+ const extraAppInfo = props.level === 1 ?
+
+ {/*
+
+
+ Category:
+
+
+
+ */}
+ {triggers.length === 0 ? null :
+
+ {triggers.map((trigger, index) => {
+
+ return (
+
{
+ console.log("Clicked: ", trigger.name)
+ }}
+ style={{
+ cursor: "pointer",
+ color: "white",
+ borderRadius: 40,
+ minWidth: 80,
+ marginRight: 10,
+ marginTop: 2,
+ fontSize: 14,
+ }}
+ avatar={ }
+ label={trigger.name}
+ />
+ )
+ })}
+
+ }
+
+ {serverside === false && foundCategory !== undefined && foundCategory !== null && foundCategory.action_labels.length > 0 ?
+
+ {foundCategory.action_labels.slice(0,5).map((action_label, index) => {
+ const included = allLabels.includes(action_label)
+ const iconInfo = GetIconInfo({ name: action_label });
+ const useIcon = iconInfo.originalIcon;
+
+ return (
+ {
+ findRelevantAction(action_label)
+ }}
+ disabled={included === false}
+ style={{
+ cursor: included ? "pointer" : "default",
+ color: "white",
+ borderRadius: 40,
+ minWidth: 80,
+ marginRight: 10,
+ marginTop: 2,
+ fontSize: 14,
+ textDecoration: included ? "none" : "line-through",
+ }}
+ avatar={useIcon}
+ label={action_label}
+ />
+ )
+ })}
+
+ : null}
+
+ : null
+
+ return (
+ {
+ setHover(true);
+ }}
+ >
+ {props.level !== 1 ? (
+
+ ) : null}
+ {element}
+ {extraAppInfo}
+ {extraInfo}
+
+ );
+ };
+
+ const [, setCurrentActionMethod] = useState(actionNonBodyRequest[0]);
+
+ // Selectedaction = Shuffle style action
+ // Currentaction = OpenAPI style
+ const [selectedAction, setSelectedAction] = useState({});
+ const [currentAction, setCurrentAction] = useState({
+ name: "",
+ description: "",
+ url: "",
+ headers: "",
+ paths: [],
+ queries: [],
+ body: "",
+ errors: [],
+ method: actionNonBodyRequest[0],
+ });
+
+ if (params.appid === "new") {
+ return null;
+ }
+
+ const WorkflowHits = ({ hits }) => {
+ //console.log("WORKFLOWS: ", hits)
+
+ setRelatedWorkflows(hits.length);
+ return hits.length;
+ };
+
+ const AppHits = ({ hits }) => {
+ if (hits.length >= 1) {
+ setRelatedApps(hits.length - 1);
+
+ return hits.length - 1;
+ } else {
+ setRelatedApps(0);
+ return 0;
+ }
+ };
+
+ const getUserProfile = (username) => {
+ if (serverside === true) {
+ return;
+ }
+
+ fetch(`${globalUrl}/api/v1/users/creators/${username}`, {
+ method: "GET",
+ headers: {
+ "Content-Type": "application/json",
+ Accept: "application/json",
+ },
+ credentials: "include",
+ })
+ .then((response) => {
+ if (response.status !== 200) {
+ console.log("Status not 200 for WORKFLOW EXECUTION :O!");
+ }
+
+ return response.json();
+ })
+ .then((responseJson) => {
+ if (responseJson.success !== false) {
+ setCreatorProfile(responseJson);
+ }
+ })
+ .catch((error) => {
+ console.log(error);
+ });
+ };
+
+ const SearchBox = ({ currentRefinement, refine, isSearchStalled }) => {
+ return null;
+ };
+
+ const CustomWorkflowHits = connectHits(WorkflowHits);
+ const CustomAppHits = connectHits(AppHits);
+ const CustomSearchBox = connectSearchBox(SearchBox);
+
+ const HandleJsonCopy = (base, copy, base_node_name) => {
+ console.log("COPY: ", copy);
+ var newitem = JSON.parse(base);
+ to_be_copied = "$" + base_node_name;
+ for (var key in copy.namespace) {
+ if (copy.namespace[key].includes("Results for")) {
+ continue;
+ }
+
+ if (newitem !== undefined && newitem !== null) {
+ newitem = newitem[copy.namespace[key]];
+ if (!isNaN(copy.namespace[key])) {
+ to_be_copied += ".#";
+ } else {
+ to_be_copied += "." + copy.namespace[key];
+ }
+ }
+ }
+ };
+
+ const handleReactJsonClipboard = (copy) => {
+ console.log("COPY: ", copy);
+
+ const elementName = "copy_element_shuffle";
+ var copyText = document.getElementById(elementName);
+ if (copyText !== null && copyText !== undefined) {
+ navigator.clipboard.writeText(JSON.stringify(copy));
+ copyText.select();
+ copyText.setSelectionRange(0, 99999); /* For mobile devices */
+
+ /* Copy the text inside the text field */
+ document.execCommand("copy");
+ toast("Copied data");
+ }
+ };
+
+ const activateApp = () => {
+ if (serverside === true) {
+ return;
+ }
+
+ const appExists = userdata.active_apps !== undefined && userdata.active_apps !== null && userdata.active_apps.includes(appId)
+ const url = appExists ? `${globalUrl}/api/v1/apps/${appId}/deactivate` : `${globalUrl}/api/v1/apps/${appId}/activate`
+ fetch(url, {
+ method: "GET",
+ headers: {
+ "Content-Type": "application/json",
+ Accept: "application/json",
+ },
+ credentials: "include",
+ })
+ .then((response) => {
+ if (response.status !== 200) {
+ console.log("Failed to activate");
+ }
+
+ return response.json();
+ })
+ .then((responseJson) => {
+ if (responseJson.success === false) {
+ if (responseJson.reason !== undefined) {
+ toast("Failed to activate the app: "+responseJson.reason);
+ } else {
+ toast("Failed to activate the app");
+ }
+ } else {
+ if (checkLogin !== undefined && checkLogin !== null) {
+ checkLogin()
+ }
+
+ if (appExists) {
+ toast("App deactivated for your organization! Existing workflows with the app will continue to work.")
+ } else {
+ toast("App activated for your organization!")
+ }
+ }
+ })
+ .catch((error) => {
+ toast(error.toString());
+ });
+ };
+
+ const handleEditApp = (appid) => {
+ if (serverside === true) {
+ return;
+ }
+
+ setAppId(appid)
+
+
+ fetch(`${globalUrl}/api/v1/apps/${appid}/config`, {
+ method: "GET",
+ headers: {
+ "Content-Type": "application/json",
+ Accept: "application/json",
+ },
+ credentials: "include",
+ })
+ .then((response) => {
+ if (response.status !== 200) {
+ //console.log("App doesn't exist or isn't available to you.")
+ //toast("Something went wrong - this app is not available to you. Redirecting you back to search.")
+ ReactGA.event({
+ category: "appexplorer",
+ action: `app_not_found`,
+ label: appid,
+ });
+ } else {
+ ReactGA.event({
+ category: "appexplorer",
+ action: `app_found`,
+ label: appid,
+ });
+ }
+
+ return response.json();
+ })
+ .then((responseJson) => {
+ if (
+ responseJson.success === false ||
+ responseJson.success === undefined
+ ) {
+ toast("Failed to get the app")
+ setIsAppLoaded(true)
+ // setTimeout(() => {
+ // navigate("/search")
+ // }, 1000);
+ return;
+ } else {
+ parseIncomingOpenapiData(responseJson);
+ }
+ })
+ .catch((error) => {
+ toast("Error in app fetch: " + error.toString());
+ });
+ };
+
+
+ const parseIncomingAppdata = (data, openapiExists) => {
+ document.title = data.name + " App - OpenAPI and API";
+
+
+
+ setExecutionResult({
+ valid: false,
+ result: baseResult,
+ });
+
+ setName((data.name.charAt(0).toUpperCase() + data.name.substring(1)).replaceAll("_"," "));
+
+ setDescription(data.description);
+ setFileBase64(data.large_image);
+ setContact(data.contact_info);
+
+ if (data.categories !== undefined && data.categories !== null) {
+ setNewWorkflowCategories(data.categories);
+ }
+ setAppType(1);
+
+ if (data.owner !== undefined && data.owner !== null) {
+ getUserProfile(data.owner);
+ //console.log("DATA: ", data)
+ }
+
+ setAppData(data)
+ if (data.reference_info.triggers !== undefined && data.reference_info.triggers !== null && data.reference_info.triggers.length > 0) {
+ var parsedtriggers = []
+ for (var key in data.reference_info.triggers) {
+ const curtrigger = data.reference_info.triggers[key]
+
+ const foundTrigger = workflowTriggers.find((trigger) => trigger.name.toLowerCase() === curtrigger.toLowerCase())
+ if (foundTrigger !== undefined && foundTrigger !== null) {
+ parsedtriggers.push(foundTrigger)
+ }
+ }
+
+ setTriggers(parsedtriggers)
+ }
+
+ var newactions = [];
+ if (!openapiExists) {
+ console.log("Skipping openapi");
+ for (var key in data.actions) {
+ const action = data.actions[key];
+ newactions.push({
+ name: action.name,
+ description: action.description,
+ url: "",
+ headers: "",
+ paths: [],
+ queries: [],
+ body: "",
+ errors: [],
+ method: "CUSTOM",
+ });
+ }
+ }
+
+
+ if (newactions.length > 0) {
+ setCurrentAction(newactions[0]);
+
+ if (data.actions !== undefined) {
+ //var methodName = `${data.method}_${data.name}`.toLowerCase()
+ //if (data.name.toLowerCase().startsWith(data.method.toLowerCase())) {
+ // methodName = data.name.toLowerCase()
+ //}
+ //var newselectedaction = data.actions.find(item => item.name.toLowerCase() === methodName)
+ //if (newselectedaction === undefined || newselectedaction === null) {
+ // toast(`Name ${methodName} not found. Please contact us.`)
+ // return
+ //}
+
+ //var newselectedaction = data.actions.find(item => item.name.toLowerCase() === )
+ const newselectedaction = data.actions[0];
+ newselectedaction.app_id = data.id;
+ newselectedaction.app_name = data.name;
+ newselectedaction.app_version = data.app_version;
+
+ newselectedaction.authentication = selectedAction.authentication;
+
+ newselectedaction.authentication_id = selectedAction.authentication_id;
+ newselectedaction.selectedAuthentication = selectedAction.selectedAuthentication;
+
+ if (
+ data.authentication.required &&
+ newselectedaction.authentication_id !== undefined &&
+ newselectedaction.authentication_id !== null &&
+ newselectedaction.authentication_id.length === 0
+ ) {
+ const tmpParams = selectedAction.parameters;
+ selectedAction.parameters = [];
+
+ for (let paramkey in data.authentication.parameters) {
+ var item = data.authentication.parameters[paramkey];
+ console.log("PARAM1: ", item)
+ item.configuration = true;
+
+ const found = selectedAction.parameters.find((param) => param.name === item.name);
+
+ if (found === null || found === undefined) {
+ selectedAction.parameters.push(item);
+ }
+ }
+
+ for (let paramkey in tmpParams) {
+ var item = tmpParams[paramkey];
+ console.log("PARAM2: ", item)
+ //item.configuration = true
+ const found = selectedAction.parameters.find((param) => param.name === item.name);
+
+ if (found === null || found === undefined) {
+ selectedAction.parameters.push(item);
+ }
+ }
+ }
+
+ setSelectedAction(newselectedaction);
+ }
+
+
+ const firstActions = newactions.filter(data => data.action_label !== undefined && data.action_label !== null && data.action_label !== "No Label")
+ //console.log("First actions: ", firstActions)
+ const secondActions = newactions.filter(data => data.action_label === undefined || data.action_label === null || data.action_label === "No Label")
+ const newActions = firstActions.concat(secondActions)
+ setActions(newActions);
+ }
+
+ setIsAppLoaded(true);
+ };
+
+ // Sets the data up as it should be at later points
+ // This is the data FROM the database, not what's being saved
+ const parseIncomingOpenapiData = (data) => {
+ var appexists = false;
+ var nameExists = false;
+ var parsedapp = {};
+ if (data.app !== undefined && data.app !== null) {
+ // Should basically always be true if openapi exists too
+ var parsedBaseapp = ""
+ try {
+ parsedBaseapp = base64_decode(data.app)
+ } catch (e) {
+ console.log("Failed JSON parsing: ", e)
+ parsedBaseapp = data
+ }
+
+ parsedapp = JSON.parse(parsedBaseapp)
+ parsedapp.name = parsedapp.name.replaceAll("_", " ");
+
+ setAppDocumentation("# "+parsedapp.name+defaultDocs);
+ setApp(parsedapp);
+ setSharingConfiguration(parsedapp.sharing === true ? "public" : "you")
+
+ appexists =
+ parsedapp.name !== undefined &&
+ parsedapp.name !== null &&
+ parsedapp.name.length !== 0;
+
+ if (appexists) {
+ getAppDocs(parsedapp.name, "python", parsedapp.app_version);
+ }
+
+ if (data.openapi === undefined || data.openapi === null) {
+ console.log("Parsed app: ", parsedapp)
+ parseIncomingAppdata(parsedapp, false);
+ } else {
+ parseIncomingAppdata(parsedapp, true);
+ }
+ }
+
+ if (data.openapi === undefined || data.openapi === null) {
+ return;
+ }
+
+ var parsedDecoded = ""
+ try {
+ parsedDecoded = base64_decode(data.openapi)
+ } catch (e) {
+ console.log("Failed JSON parsing: ", e)
+ parsedDecoded = data
+ }
+
+ setAppType(0);
+ parsedapp = JSON.parse(parsedDecoded);
+ data = parsedapp.body === undefined ? parsedapp : JSON.parse(parsedapp.body);
+ setOpenapi(data);
+
+ getAppDocs(data.info.title, "openapi", data.app_version);
+
+ setBasedata(data);
+ if (!appexists) {
+ setName(
+ (
+ data.info.title.charAt(0).toUpperCase() + data.info.title.substring(1)
+ ).replaceAll("_", " ")
+ );
+ setDescription(data.info.description);
+
+ console.log("Found name: ", data.info.title);
+ }
+
+ if (serverside !== true) {
+ var doctitle = "Shuffle App for " + data.info.title
+ if (!data.info.title.toLowerCase().includes("api")) {
+ doctitle += " API"
+ }
+
+ document.title = doctitle
+ }
+
+ if (data.info !== null && data.info !== undefined) {
+ if (data.info["x-logo"] !== undefined) {
+ setFileBase64(data.info["x-logo"]);
+ }
+
+ if (data.info.contact !== undefined) {
+ setContact(data.info.contact);
+ }
+
+ if (data.info["x-categories"] !== undefined && data.info["x-categories"].length > 0) {
+ setNewWorkflowCategories(data.info["x-categories"]);
+ }
+ }
+
+ if (data.tags !== undefined && data.tags.length > 0) {
+ for (var key in data.tags) {
+ newWorkflowTags.push(data.tags[key].name);
+ }
+
+ setNewWorkflowTags(newWorkflowTags);
+ }
+
+ // This is annoying (:
+ var securitySchemes = data.components.securityDefinitions;
+ if (securitySchemes === undefined) {
+ securitySchemes = data.securitySchemes;
+ }
+
+ if (securitySchemes === undefined) {
+ securitySchemes = data.components.securitySchemes;
+ }
+
+ const allowedfunctions = [
+ "GET",
+ "CONNECT",
+ "HEAD",
+ "DELETE",
+ "POST",
+ "PATCH",
+ "PUT",
+ ];
+
+ // FIXME - headers?
+ var newActions = [];
+ var wordlist = {};
+ if (data.paths !== null && data.paths !== undefined) {
+ for (let [path, pathvalue] of Object.entries(data.paths)) {
+ if (path === "tmp0") {
+ setAppType(2);
+ }
+
+ for (let [method, methodvalue] of Object.entries(pathvalue)) {
+ if (methodvalue === null) {
+ toast("Skipped method " + method);
+ continue;
+ }
+
+ if (!allowedfunctions.includes(method.toUpperCase())) {
+ continue;
+ }
+
+ var tmpname = methodvalue.summary;
+ if (
+ methodvalue.operationId !== undefined &&
+ methodvalue.operationId !== null &&
+ methodvalue.operationId.length > 0
+ ) {
+ tmpname = methodvalue.operationId;
+ }
+
+ var newaction = {
+ name: tmpname,
+ description: methodvalue.description,
+ url: path,
+ method: method.toUpperCase(),
+ headers: "",
+ queries: [],
+ paths: [],
+ body: "",
+ errors: [],
+ example_response: "",
+ action_label: "No Label",
+ required_bodyfields: [],
+ }
+
+ // Related to Label Management
+ if (methodvalue["x-label"] !== undefined && methodvalue["x-label"] !== null) {
+ // Check if there are commas in it then loop and find the correct one
+ // Should ignore 'No Label' and 'No label'
+ var correctlabel = ""
+ const labels = methodvalue["x-label"].split(",")
+ for (let labelkey in labels) {
+ var label = labels[labelkey].trim()
+ if (label.toLowerCase() === "no label") {
+ continue
+ }
+
+ // Remove quotes and escapes
+ label = label.replace(/['"]+/g, '')
+ label = label.replace(/\\/g, '')
+
+ //label = label.replace("_", " ", -1)
+ //label = label.charAt(0).toUpperCase() + label.slice(1)
+
+ correctlabel = label
+ break
+ }
+
+ // FIX: Map labels only if they're actually in the category list
+ newaction.action_label = correctlabel
+ }
+
+ if (methodvalue["x-required-fields"] !== undefined && methodvalue["x-required-fields"] !== null) {
+ newaction.required_bodyfields = methodvalue["x-required-fields"]
+ }
+
+ for (key in methodvalue.parameters) {
+ const parameter = methodvalue.parameters[key];
+ if (parameter.in === "query") {
+ var tmpaction = {
+ description: parameter.description,
+ name: parameter.name,
+ required: parameter.required,
+ in: "query",
+ };
+
+ if (parameter.required === undefined) {
+ tmpaction.required = false;
+ }
+
+ newaction.queries.push(tmpaction);
+ } else if (parameter.in === "path") {
+ // FIXME - parse this to the URL too
+ newaction.paths.push(parameter.name);
+
+ // FIXME: This doesn't follow OpenAPI3 exactly.
+ // https://swagger.io/docs/specification/describing-request-body/
+ // https://swagger.io/docs/specification/describing-parameters/
+ // Need to split the data.
+ } else if (parameter.in === "body") {
+ // FIXME: Add tracking for components
+ // E.G: https://raw.githubusercontent.com/owentl/Shuffle/master/gosecure.yaml
+ if (parameter.example !== undefined) {
+ newaction.body = parameter.example;
+ }
+ } else if (parameter.in === "header") {
+ newaction.headers += `${parameter.name}=${parameter.example}\n`;
+ }
+ }
+
+ if (newaction.name === "" || newaction.name === undefined) {
+ // Find a unique part of the string
+ // FIXME: Looks for length between /, find the one where they differ
+ // Should find others with the same START to their path
+ // Make a list of reserved names? Aka things that show up only once
+ if (Object.getOwnPropertyNames(wordlist).length === 0) {
+ for (let [newpath] of Object.entries(data.paths)) {
+ const newpathsplit = newpath.split("/");
+ for (key in newpathsplit) {
+ const pathitem = newpathsplit[key].toLowerCase();
+ if (wordlist[pathitem] === undefined) {
+ wordlist[pathitem] = 1;
+ } else {
+ wordlist[pathitem] += 1;
+ }
+ }
+ }
+ }
+
+ //console.log("WORDLIST: ", wordlist)
+
+ // Remove underscores and make it normal with upper case etc
+ const urlsplit = path.split("/");
+ if (urlsplit.length > 0) {
+ var curname = "";
+ for (key in urlsplit) {
+ var subpath = urlsplit[key];
+ if (wordlist[subpath] > 2 || subpath.length < 1) {
+ continue;
+ }
+
+ curname = subpath;
+ break;
+ }
+
+ // FIXME: If name exists,
+ // FIXME: Check if first part of parsedname is verb, otherwise use method
+ const parsedname = curname
+ .split("_")
+ .join(" ")
+ .split("-")
+ .join(" ")
+ .split("{")
+ .join(" ")
+ .split("}")
+ .join(" ")
+ .trim();
+ if (parsedname.length === 0) {
+ newaction.errors.push("Missing name");
+ } else {
+ const newname =
+ method.charAt(0).toUpperCase() +
+ method.slice(1) +
+ " " +
+ parsedname;
+ const searchactions = newActions.find(
+ (data) => data.name === newname
+ );
+ //console.log("SEARCH: ", searchactions);
+ if (searchactions !== undefined) {
+ newaction.errors.push("Missing name");
+ } else {
+ newaction.name = newname;
+ }
+ }
+ } else {
+ newaction.errors.push("Missing name");
+ }
+ }
+ newActions.push(newaction);
+ }
+
+ if (data.servers !== undefined && data.servers.length > 0) {
+ var firstUrl = data.servers[0].url;
+ if (
+ firstUrl.includes("{") &&
+ firstUrl.includes("}") &&
+ data.servers[0].variables !== undefined
+ ) {
+ const regex = /{\w+}/g;
+ const found = firstUrl.match(regex);
+ if (found !== null) {
+ for (key in found) {
+ const item = found[key].slice(1, found[key].length - 1);
+ const foundVar = data.servers[0].variables[item];
+ if (foundVar["default"] !== undefined) {
+ firstUrl = firstUrl.replaceAll(
+ found[key],
+ foundVar["default"]
+ );
+ }
+ }
+ }
+ }
+
+ if (firstUrl.endsWith("/")) {
+ setBaseUrl(firstUrl.slice(0, firstUrl.length - 1));
+ } else {
+ setBaseUrl(firstUrl);
+ }
+ }
+ }
+ }
+
+ // FIXME: Have multiple authentication options?
+ if (securitySchemes !== undefined) {
+ for (const [, value] of Object.entries(securitySchemes)) {
+ if (value.scheme === "bearer") {
+ setAuthenticationOption("Bearer auth");
+ setAuthenticationRequired(true);
+ break;
+ } else if (value.type === "apiKey") {
+ setAuthenticationOption("API key");
+
+ value.in = value.in.charAt(0).toUpperCase() + value.in.slice(1);
+ setParameterLocation(value.in);
+ if (!apikeySelection.includes(value.in)) {
+ //console.log("APIKEY SELECT: ", apikeySelection)
+ toast("Might be error in setting up API key authentication");
+ }
+
+ //console.log("PARAM NAME: ", value.name)
+ setParameterName(value.name);
+ setAuthenticationRequired(true);
+ break;
+ } else if (value.scheme === "basic") {
+ setAuthenticationOption("Basic auth");
+ setAuthenticationRequired(true);
+ break;
+ }
+ }
+ }
+
+ const firstActions = newActions.filter(data => data.action_label !== undefined && data.action_label !== null && data.action_label !== "No Label")
+ //console.log("First actions: ", firstActions)
+ const secondActions = newActions.filter(data => data.action_label === undefined || data.action_label === null || data.action_label === "No Label")
+ newActions = firstActions.concat(secondActions)
+ setActions(newActions);
+ setIsAppLoaded(true);
+
+ if (newActions.length > 0) {
+ setCurrentAction(newActions[0]);
+ setCurrentActionMethod(newActions[0].method);
+ setUrlPathQueries(newActions[0].queries);
+ setUrlPath(newActions[0].url);
+ //setActionsModalOpen(true)
+ }
+ };
+
+ const getAppDocs = (appname, location, version) => {
+ if (serverside === true) {
+ return;
+ }
+
+ fetch(
+ `${globalUrl}/api/v1/docs/${appname}?location=${location}&version=${version}`,
+ {
+ headers: {
+ Accept: "application/json",
+ },
+ credentials: "include",
+ }
+ )
+ .then((response) => {
+ if (response.status === 200) {
+ //toast("Successfully GOT app "+appId)
+ } else {
+ //toast("Failed getting app");
+ }
+
+ return response.json();
+ })
+ .then((responseJson) => {
+ var setMeta = false
+ if (responseJson.success === true) {
+ if (responseJson.reason !== undefined && responseJson.reason !== undefined && responseJson.reason.length > 0) {
+
+ if (!responseJson.reason.includes("404: Not Found") && responseJson.reason.length > 25) {
+ const imgRegex = / {
+ toast("Error in doc loading: " + error.toString());
+ });
+ };
+
+ const runAlgoliaAppSearch = (appname, isOriginal, triggerOnly) => {
+ const index = searchClient.initIndex("appsearch");
+
+ console.log("Running appsearch for: ", appname);
+
+
+ index
+ .search(appname)
+ .then(({ hits }) => {
+ const appsearchname = appname.replaceAll("_", " ").toLowerCase();
+ var found = false
+
+ if (hits !== undefined && hits !== null && hits.length === 1) {
+ found = true
+ if (isOriginal !== false) {
+ handleEditApp(hits[0].objectID)
+ } else {
+ setSecondaryApp(hits[0])
+ }
+ }
+
+ for (var key in hits) {
+ const hit = hits[key];
+
+ if (hit["name"] === null || hit["name"] === undefined) {
+ continue;
+ }
+
+ if (hit["name"].replaceAll("_", " ").toLowerCase().includes(appsearchname) || hit["objectID"] === appname) {
+ /*
+ if (hit.triggers !== undefined && hit.triggers !== null && hit.triggers.length > 0) {
+ var parsedtriggers = []
+ for (var key in hit.triggers) {
+ const curtrigger = hit.triggers[key].toLowerCase()
+
+ const foundTrigger = workflowTriggers.find((trigger) => trigger.name.toLowerCase() === curtrigger)
+ if (foundTrigger !== undefined && foundTrigger !== null) {
+ parsedtriggers.push(foundTrigger)
+ }
+ }
+
+ setTriggers(parsedtriggers)
+ }
+ */
+
+ if (triggerOnly === true) {
+
+ } else {
+ if (isOriginal !== false) {
+ found = true
+ handleEditApp(hit.objectID);
+ } else {
+ console.log("Found second app: ", hit);
+ hit.name = hit.name.charAt(0).toUpperCase() + hit.name.slice(1);
+ setSecondaryApp(hit);
+ }
+ }
+
+ break;
+ }
+ }
+
+ if (!found) {
+ if (hits.length > 0) {
+ if (isOriginal !== false) {
+ handleEditApp(hits[0].objectID)
+ } else {
+ setSecondaryApp(hits[0]);
+ }
+
+ return
+ }
+
+ //navigate("/search?message=App not found&q=" + appname + "&tab=apps")
+ //toast("App not found. Please contact support@shuffler.io if you believe this is an error.")
+ return
+ }
+ })
+ .catch((err) => {
+ console.log(err);
+ });
+ };
+
+
+
+
+ if (serverside === true && firstRequest) {
+ setFirstRequest(false);
+ if (
+ selectedApp !== undefined &&
+ selectedApp !== null &&
+ Object.getOwnPropertyNames(selectedApp).length > 0
+ ) {
+ parseIncomingOpenapiData(selectedApp);
+ }
+
+ if (
+ selectedDoc !== undefined &&
+ selectedDoc !== null &&
+ Object.getOwnPropertyNames(selectedDoc).length > 0
+ ) {
+ setAppDocumentation(selectedDoc.reason);
+ setSelectedTab(0);
+ }
+
+ if (
+ secondApp !== undefined &&
+ secondApp !== null &&
+ Object.getOwnPropertyNames(secondApp).length > 0
+ ) {
+ setSelectedTab(3);
+ setSecondaryApp(secondApp);
+ }
+ }
+
+ //, [])
+
+ if (serverside !== true && window.location.href !== reloadUrl) {
+ setReloadUrl(window.location.href);
+ setAppDocumentation(defaultDocs);
+ setTriggers([])
+ //handleEditApp(params.appid)
+
+ if (params.appid.length === 32 || params.appid.length === 36) {
+ handleEditApp(params.appid);
+ runAlgoliaAppSearch(params.appid, false, true);
+ } else {
+ runAlgoliaAppSearch(params.appid);
+ }
+ }
+
+ const loopQueries =
+ urlPathQueries === undefined ||
+ urlPathQueries === null ||
+ urlPathQueries.length === 0 ? null : (
+
+
+ Queries
+ {urlPathQueries.map((data, index) => {
+ return (
+
+
+ {
+ //urlPathQueries[index].name = e.target.value
+ //setUrlPathQueries(urlPathQueries)
+ }}
+ InputProps={{
+ style: {
+ color: "white",
+ },
+ }}
+ />
+
+
+ );
+ })}
+
+
+ );
+
+ const executeSingleAction = (appid, thisaction) => {
+ if (serverside === true) {
+ return;
+ }
+
+ if (isCloud) {
+ thisaction.environment = "Cloud"
+ } else {
+ thisaction.environment = "Shuffle"
+ }
+
+ setExecutionResult({
+ valid: false,
+ result: baseResult,
+ });
+
+ setExecuting(true);
+
+ fetch(globalUrl + "/api/v1/apps/" + appid + "/run", {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ Accept: "application/json",
+ },
+ body: JSON.stringify(thisaction),
+ credentials: "include",
+ })
+ .then((response) => {
+ if (response.status !== 200) {
+ console.log("Status not 200 for stream results :O!");
+ }
+
+ return response.json();
+ })
+ .then((responseJson) => {
+ //console.log("RESPONSE: ", responseJson)
+ if (
+ responseJson.success === true &&
+ responseJson.result !== null &&
+ responseJson.result !== undefined &&
+ responseJson.result.length > 0
+ ) {
+ const result = responseJson.result.slice(0, 50) + "...";
+ //toast("SUCCESS: "+result)
+
+ const validate = validateJson(responseJson.result);
+ setExecutionResult(validate);
+ } else if (
+ responseJson.success === false &&
+ responseJson.reason !== undefined &&
+ responseJson.reason !== null
+ ) {
+ toast(responseJson.reason);
+ setExecutionResult({ valid: false, result: responseJson.reason });
+ } else if (responseJson.success === true) {
+ setExecutionResult({
+ valid: false,
+ result:
+ "Couldn't finish execution. Please fill all the required fields, and retry the execution.",
+ });
+ } else {
+ setExecutionResult({
+ valid: false,
+ result:
+ "Couldn't finish execution (2). Please fill all the required fields, and validate the execution.",
+ });
+ }
+
+ setExecuting(false);
+ })
+ .catch((error) => {
+ toast("Execution error: " + error.toString());
+ setExecuting(false);
+ });
+ };
+
+ const MethodWrapper = (props) => {
+ const { data } = props;
+
+ var bgColor = "#61afee";
+ if (data.method === "POST") {
+ bgColor = "#49cc90";
+ } else if (data.method === "PUT") {
+ bgColor = "#fca130";
+ } else if (data.method === "PATCH") {
+ bgColor = "#50e3c2";
+ } else if (data.method === "DELETE") {
+ bgColor = "#f93e3e";
+ } else if (data.method === "HEAD") {
+ bgColor = "#9012fe";
+ }
+
+ return (
+
+ );
+ };
+
+ const parseName = (name, length) => {
+ if (name === undefined || name === null) {
+ return ""
+ }
+
+ var parsedName = name.charAt(0).toUpperCase() + name.slice(1);
+ parsedName = parsedName.replaceAll("_", " ");
+ if (
+ length !== undefined &&
+ length !== null &&
+ length > 3 &&
+ length < parsedName.length
+ ) {
+ parsedName = parsedName.slice(0, length) + "..";
+ }
+
+ return parsedName;
+ };
+
+ const SubAction = (props) => {
+ const { data, selected, hovered, index } = props;
+
+ const [hoveredItem, setHoveredItem] = useState(true);
+
+ var urlPath = data.url !== undefined && data.url !== null && data.url.length > 0 ? data.url : ""
+ var wrappedStyle = JSON.parse(JSON.stringify(actionListStyle));
+ wrappedStyle.backgroundColor = selected || hovered ? theme.palette.platformColor : theme.palette.inputColor;
+ wrappedStyle.paddingBottom = urlPath.length > 0 ? 0 : 10
+ wrappedStyle.border = selected || hovered ? "1px solid rgba(255,255,255,0.3)" : ""
+
+ var methodName = `${data.method}_${data.name}`;
+ if ((data.name !== undefined && data.name !== null && data.method !== undefined && data.method !== null ) && (data.method.toLowerCase() === "custom" || data.name.toLowerCase().startsWith(data.method.toLowerCase()))) {
+ methodName = data.name;
+ }
+
+ const invalid_keys = [".", "(", ")", "'", ",", "[", "]"];
+ methodName = methodName.toLowerCase().replaceAll(" ", "_").replaceAll("-", "_");
+
+ for (var key in invalid_keys) {
+ methodName = methodName.replaceAll(invalid_keys[key], "");
+ }
+
+ const parsedName = parseName(data.name, 35);
+ if (parsedName.length === 0) {
+ return null
+ }
+
+ const actionLabels = foundCategory !== undefined && foundCategory !== null && foundCategory.name !== "Other" && foundCategory.action_labels.length > 0 ? ["No Label"].concat(foundCategory.action_labels) : []
+
+ var bgColor = "#61afee";
+ if (data.method === "POST") {
+ bgColor = "#49cc90";
+ } else if (data.method === "PUT") {
+ bgColor = "#fca130";
+ } else if (data.method === "PATCH") {
+ bgColor = "#50e3c2";
+ } else if (data.method === "DELETE") {
+ bgColor = "#f93e3e";
+ } else if (data.method === "HEAD") {
+ bgColor = "#9012fe";
+ }
+
+ //console.log("Category: ", foundCategory, actionLabels)
+
+ return (
+ {
+ setHoveredItem(true);
+ }}
+ onMouseOut={() => {
+ setHoveredItem(false);
+ }}
+ >
+ {parsedName} {data.method} {urlPath} {data.description}
+
+ }
+ placement="left"
+ >
+ {
+ if (app.actions !== undefined) {
+ var newselectedaction = app.actions.find((item) => item.name.toLowerCase().replaceAll(" ", "_").replaceAll(".", "").replaceAll("(", "").replaceAll(")", "") === methodName)
+
+ if (newselectedaction === undefined || newselectedaction === null) {
+ newselectedaction = app.actions.find((item) => item.name.toLowerCase().replaceAll(" ", "_").replaceAll(".", "").replaceAll("(", "").replaceAll(")", "") === data.name.toLowerCase().replaceAll(" ", "_").replaceAll(".", "").replaceAll("(", "").replaceAll(")", ""))
+ if (newselectedaction === undefined || newselectedaction === null) {
+ for (var key in app.actions) {
+ console.log(methodName, app.actions[key].name.toLowerCase().replaceAll(" ", "_"));
+ }
+
+ toast(`Name ${methodName} not found. Please contact us.`);
+ return;
+ }
+ }
+
+ newselectedaction.app_id = app.id;
+ newselectedaction.app_name = app.name;
+ newselectedaction.app_version = app.app_version;
+
+ newselectedaction.authentication = selectedAction.authentication;
+ newselectedaction.authentication_id = selectedAction.authentication_id;
+ newselectedaction.selectedAuthentication = selectedAction.selectedAuthentication;
+
+ if (
+ app.authentication.required &&
+ newselectedaction.authentication_id !== undefined &&
+ newselectedaction.authentication_id !== null &&
+ newselectedaction.authentication_id.length === 0
+ ) {
+ const tmpParams = selectedAction.parameters;
+ selectedAction.parameters = [];
+
+ for (var paramkey in app.authentication.parameters) {
+ var item = app.authentication.parameters[paramkey];
+ item.configuration = true;
+
+ const found = selectedAction.parameters.find(
+ (param) => param.name === item.name
+ );
+ if (found === null || found === undefined) {
+ selectedAction.parameters.push(item);
+ }
+ }
+
+ for (var paramkey in tmpParams) {
+ var item = tmpParams[paramkey];
+ //item.configuration = true
+
+ const found = selectedAction.parameters.find(
+ (param) => param.name === item.name
+ );
+ if (found === null || found === undefined) {
+ selectedAction.parameters.push(item);
+ }
+ }
+ }
+
+ setSelectedAction(newselectedaction);
+ }
+
+ setCurrentAction(data);
+ setCurrentActionMethod(data.method);
+ setUrlPathQueries(data.queries);
+ setUrlPath(data.url);
+
+ /*
+ if (selectedTab !== 1) {
+ setSelectedTab(1);
+ }
+ */
+ }}
+ >
+
+
+
+ 0 ? "auto" : 3,
+ marginBottom: "auto",
+ textAlign: "left",
+ overflow: "hidden",
+ maxHeight: 27,
+ maxWidth: 175,
+ }}
+ >
+ {parsedName}
+
+ {urlPath.length > 0 ?
+
+ {urlPath}
+
+ : null}
+
+ {actionLabels.length > 0 && newWorkflowCategories !== undefined && newWorkflowCategories !== null && newWorkflowCategories.length > 0 && categories.length > 0 ?
+
{
+ console.log("Should change: ", e.target.value, " Index: ", index)
+
+ const foundIndex = actions.findIndex((action) => action.name === data.name)
+ console.log("Found index: ", foundIndex)
+ if (foundIndex !== undefined && foundIndex !== null && foundIndex >= 0) {
+ actions[foundIndex].action_label = e.target.value
+ setActions(actions)
+ setUpdate(Math.random())
+
+ // Should send recommendations to the owner
+
+ if (creatorProfile.github_username !== undefined && creatorProfile.github_username !== null && creatorProfile.github_username.length > 0) {
+ const labelData = {
+ "app_id": app.id,
+ "action_name": data.name,
+ "label": e.target.value
+ }
+
+ // Should send recommendations to the owner
+ var url = `${globalUrl}/api/v1/apps/label`;
+ fetch(url, {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ "Accept": "application/json",
+ },
+ body: JSON.stringify(labelData),
+ credentials: "include",
+ })
+ .then((response) => response.json())
+ .then((responseJson) => {
+ if (responseJson.reason !== undefined && responseJson.reason !== null && responseJson.reason.length > 0) {
+ toast(responseJson.reason)
+ }
+ })
+ .catch((error) => {
+ console.log("Error: ", error)
+ });
+
+ }
+ }
+ }}
+ value={data.action_label.toLowerCase().replaceAll(" ", "_")}
+ style={{
+ border: data.action_label === undefined || data.action_label === "No Label" ? "" : `2px solid ${bgColor}`,
+ borderRadius: theme.shape.borderRadius,
+ backgroundColor: inputColor,
+ paddingLeft: 10,
+ color: "white",
+ height: 30,
+ maxWidth: 35,
+ marginLeft: 10,
+ marginRight: 10,
+ overflow: "hidden",
+ }}
+ inputProps={{
+ name: "Method",
+ id: "method-option",
+ }}
+ >
+ {actionLabels.map((label, labelindex) => {
+ //label = label.toLowerCase().replaceAll(" ", "_")
+ if (label === undefined || label === null) {
+ return null
+ }
+
+ return (
+
+ {label}
+
+ )
+ })}
+
+
+ App Validation
+
+
+ : null}
+
+
+
+
+ );
+ };
+
+ const foundCategory = newWorkflowCategories !== undefined && newWorkflowCategories !== null && newWorkflowCategories.length > 0 ? categories.find((x) => x.name === newWorkflowCategories[0]) : undefined
+ const LoopActions = (props) => {
+ const { actions } = props;
+
+ //const [activeActions] = useState(actions === undefined ? [] : actions);
+
+ if (actions === undefined || actions === null || actions.length === 0) {
+ return null;
+ }
+
+ return (
+
+ {actions.map((data, index) => {
+ if (data.action_label === undefined || data.action_label === null || data.action_label.length === 0) {
+ data.action_label = "No Label"
+ }
+
+ return (
+
+ );
+ })}
+
+ );
+ //
+ };
+
+ const ParsedActionHandler = () => {
+ const passedOrg = { id: "", name: "" };
+ const owner = "";
+ const passedTags = ["single test"];
+
+ const [, setUpdate] = useState();
+ const [authenticationModalOpen, setAuthenticationModalOpen] = useState(false);
+ const [selectedApp, setSelectedApp] = useState({
+ versions: [
+ {
+ id: selectedAction.app_id,
+ version: selectedAction.app_version,
+ },
+ ],
+ loop_versions: [selectedAction.app_version],
+ id: selectedAction.app_id,
+ name: selectedAction.app_name,
+ version: selectedAction.app_version,
+ });
+
+ const [requiresAuthentication, setRequiresAuthentication] = useState(
+ app.authentication.required &&
+ app.authentication.parameters !== undefined &&
+ app.authentication.parameters !== null
+ );
+ const [workflow, setWorkflow] = useState({
+ name: "",
+ description: "",
+ actions: [selectedAction],
+ start: selectedAction.id,
+ tags: passedTags,
+ execution_org: passedOrg,
+ org_id: passedOrg.id,
+ id: uuidv4(),
+ isValid: true,
+ owner: owner,
+ created: Date.now(),
+ });
+
+ const EndpointData = () => {
+ const [tmpVar, setTmpVar] = React.useState("");
+
+ return (
+
+ The API endpoint to use (URL) - predefined in the app
+ {
+ setTmpVar(event.target.value);
+ }}
+ onBlur={() => {
+ selectedApp.link = tmpVar;
+ console.log("LINK: ", selectedApp.link);
+ setSelectedApp(selectedApp);
+ }}
+ />
+
+ );
+ };
+
+ const setAppActionAuthentication = (newauth) => {
+ if (app.authentication.required) {
+ var findAuthId = "";
+ if (
+ selectedAction.authentication_id !== null &&
+ selectedAction.authentication_id !== undefined &&
+ selectedAction.authentication_id.length > 0
+ ) {
+ findAuthId = selectedAction.authentication_id;
+ }
+
+ var baseAuthOptions = [];
+ for (var key in newauth) {
+ var item = newauth[key];
+
+ const newfields = {};
+ for (var filterkey in item.fields) {
+ newfields[item.fields[filterkey].key] = item.fields[filterkey].value;
+ }
+
+ item.fields = newfields;
+ if (item.app.name === app.name) {
+ baseAuthOptions.push(item);
+
+ if (item.id === findAuthId) {
+ selectedAction.selectedAuthentication = item;
+ }
+ }
+ }
+
+ selectedAction.authentication = baseAuthOptions;
+ //console.log("Authentication: ", authenticationOptions)
+ if (
+ selectedAction.selectedAuthentication === null ||
+ selectedAction.selectedAuthentication === undefined ||
+ selectedAction.selectedAuthentication.length === ""
+ ) {
+ selectedAction.selectedAuthentication = {};
+ }
+ } else {
+ selectedAction.authentication = [];
+ selectedAction.authentication_id = "";
+ selectedAction.selectedAuthentication = {};
+ }
+
+ setSelectedAction(selectedAction);
+ console.log("Action: ", selectedAction);
+ };
+
+ //{selectedAction.authentication !== undefined && selectedAction.authentication.length > 0 ?
+ const getAppAuthentication = () => {
+ if (serverside === true) {
+ return;
+ }
+
+ 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 app auth :O!");
+ }
+
+ return response.json();
+ })
+ .then((responseJson) => {
+ if (responseJson.success && responseJson.data !== undefined && responseJson.data !== null && responseJson.data.length !== 0) {
+ var newauth = [];
+
+ //console.log("Got auth. Trying to map to selectedAction appname: ", selectedAction)
+ var authUpdate = false;
+ const appname = selectedAction.app_name.toLowerCase().replaceAll(" ", "_")
+ selectedAction.authentication = []
+
+ for (var key in responseJson.data) {
+ if (responseJson.data[key].defined === false) {
+ continue;
+ }
+
+ if (responseJson.data[key].active === false) {
+ continue;
+ }
+
+ newauth.push(responseJson.data[key]);
+
+ if (responseJson.data[key].app === undefined || responseJson.data[key].app === null) {
+ continue
+ }
+
+ if (responseJson.data[key].app.name.toLowerCase().replaceAll(" ", "_") === appname) {
+ console.log("Found matching app name: ", responseJson.data[key].app.name)
+ selectedAction.authentication.push(responseJson.data[key])
+ selectedAction.authentication_id = responseJson.data[key].id
+ selectedAction.selectedAuthentication = responseJson.data[key]
+ authUpdate = true;
+ }
+}
+
+ console.log("New auth: ", newauth)
+ if (authUpdate === true) {
+ setSelectedAction(selectedAction)
+ }
+
+ //setUpdate(Math.random())
+ setAppAuthentication(newauth);
+ setAppActionAuthentication(newauth);
+ } else {
+ if (app.authentication.required) {
+ const tmpParams = selectedAction.parameters;
+ selectedAction.parameters = [];
+
+ for (var paramkey in app.authentication.parameters) {
+ var item = app.authentication.parameters[paramkey];
+ item.configuration = true;
+
+ const found = selectedAction.parameters.find(
+ (param) => param.name === item.name
+ );
+ if (found === null || found === undefined) {
+ selectedAction.parameters.push(item);
+ }
+ }
+
+ for (var paramkey in tmpParams) {
+ var item = tmpParams[paramkey];
+ //item.configuration = true
+
+ const found = selectedAction.parameters.find((param) => param.name === item.name);
+
+ if (found === null || found === undefined) {
+ selectedAction.parameters.push(item);
+ }
+ }
+
+ setSelectedAction(selectedAction);
+ }
+
+ //toast("Failed getting authentications")
+ }
+ })
+ .catch((error) => {
+ toast("Auth loading error: " + error.toString());
+ });
+ };
+
+ if (!authLoaded && appAuthentication.length === 0 && selectedAction.id !== undefined) {
+ setAuthLoaded(true);
+ getAppAuthentication();
+ } else if (
+ selectedAction.id === undefined &&
+ currentAction.name !== undefined &&
+ currentAction.name !== null &&
+ currentAction.name.length > 0
+ ) {
+ var methodName = `${currentAction.method}_${currentAction.name}`;
+ if (
+ currentAction.method.toLowerCase() === "custom" ||
+ currentAction.name
+ .toLowerCase()
+ .startsWith(currentAction.method.toLowerCase())
+ ) {
+ methodName = currentAction.name;
+ }
+
+ methodName = methodName.toLowerCase().replaceAll(" ", "_");
+ if (app.actions !== null && app.actions !== undefined) {
+ var newselectedaction = app.actions.find(
+ (item) => item.name.toLowerCase().replaceAll(" ", "_") === methodName
+ );
+ if (newselectedaction !== undefined && newselectedaction !== null) {
+ newselectedaction.app_id = app.id;
+ newselectedaction.app_name = app.name;
+ newselectedaction.app_version = app.app_version;
+ newselectedaction.authentication = [];
+ newselectedaction.authentication_id = "";
+ newselectedaction.selectedAuthentication = {};
+ setSelectedAction(newselectedaction);
+ }
+ }
+ }
+
+ const setNewAppAuth = (appAuthData) => {
+ if (serverside === true) {
+ return;
+ }
+
+ //console.log("DAta: ", appAuthData)
+ fetch(globalUrl + "/api/v1/apps/authentication", {
+ method: "PUT",
+ headers: {
+ "Content-Type": "application/json",
+ Accept: "application/json",
+ },
+ body: JSON.stringify(appAuthData),
+ credentials: "include",
+ })
+ .then((response) => {
+ if (response.status !== 200) {
+ console.log("Status not 200 for setting app auth :O!");
+ }
+
+ return response.json();
+ })
+ .then((responseJson) => {
+ if (!responseJson.success) {
+ toast("Failed to set app auth: " + responseJson.reason);
+ } else {
+ getAppAuthentication(true);
+ setAuthenticationModalOpen(false);
+
+ // Needs a refresh with the new authentication..
+ //toast("Successfully saved new app auth")
+ }
+ })
+ .catch((error) => {
+ toast("Auth error: ", error.toString());
+ });
+ };
+
+ const AuthenticationData = (props) => {
+ const selectedApp = props.app;
+
+ const [authenticationOption, setAuthenticationOptions] = React.useState({
+ app: JSON.parse(JSON.stringify(selectedApp)),
+ fields: {},
+ label: "",
+ usage: [
+ {
+ workflow_id: workflow.id,
+ },
+ ],
+ id: uuidv4(),
+ active: true,
+ });
+
+ if (selectedApp.authentication === undefined) {
+ return null;
+ }
+
+ if (
+ selectedApp.authentication.parameters === null ||
+ selectedApp.authentication.parameters === undefined ||
+ selectedApp.authentication.parameters.length === 0
+ ) {
+ return null;
+ }
+
+ authenticationOption.app.actions = [];
+
+ for (var key in selectedApp.authentication.parameters) {
+ if (
+ authenticationOption.fields[
+ selectedApp.authentication.parameters[key].name
+ ] === undefined
+ ) {
+ authenticationOption.fields[
+ selectedApp.authentication.parameters[key].name
+ ] = "";
+ }
+ }
+
+ const handleSubmitCheck = () => {
+ if (authenticationOption.label.length === 0) {
+ authenticationOption.label = `Auth for ${selectedApp.name}`;
+ //toast("Label can't be empty")
+ //return
+ }
+
+ for (var key in selectedApp.authentication.parameters) {
+ if (
+ authenticationOption.fields[
+ selectedApp.authentication.parameters[key].name
+ ].length === 0
+ ) {
+ toast(
+ "Field " +
+ selectedApp.authentication.parameters[key].name +
+ " can't be empty"
+ );
+ return;
+ }
+ }
+
+ console.log("Action: ", selectedAction);
+ selectedAction.authentication_id = authenticationOption.id;
+ selectedAction.selectedAuthentication = authenticationOption;
+ if (
+ selectedAction.authentication === undefined ||
+ selectedAction.authentication === null
+ ) {
+ selectedAction.authentication = [authenticationOption];
+ } else {
+ selectedAction.authentication.push(authenticationOption);
+ }
+
+ setSelectedAction(selectedAction);
+
+ var newAuthOption = JSON.parse(JSON.stringify(authenticationOption));
+ var newFields = [];
+ for (const key in newAuthOption.fields) {
+ const value = newAuthOption.fields[key];
+ newFields.push({
+ key: key,
+ value: value,
+ });
+ }
+
+ console.log("FIELDS: ", newFields);
+ newAuthOption.fields = newFields;
+ setNewAppAuth(newAuthOption);
+ //appAuthentication.push(newAuthOption)
+ //setAppAuthentication(appAuthentication)
+ //
+
+ setUpdate(authenticationOption.id);
+
+ /*
+ {selectedAction.authentication.map(data => (
+
+ */
+ };
+
+ if (
+ authenticationOption.label === null ||
+ authenticationOption.label === undefined
+ ) {
+ authenticationOption.label = selectedApp.name + " authentication";
+ }
+
+ return (
+
+
+
+ What is this?
+
+
+ These are required fields for authenticating with {selectedApp.name}
+
+ Name - what is this used for?
+ {
+ authenticationOption.label = event.target.value;
+ }}
+ />
+ {selectedApp.link.length > 0 ? (
+
+
+
+ ) : null}
+
+
+ {selectedApp.authentication.parameters !== undefined &&
+ selectedApp.authentication.parameters !== null
+ ? selectedApp.authentication.parameters.map((data, index) => {
+ return (
+
+
+ {data.name}
+ {
+ authenticationOption.fields[data.name] =
+ event.target.value;
+ }}
+ />
+
+ );
+ })
+ : null}
+
+
+ {
+ setAuthenticationModalOpen(false);
+ }}
+ color="primary"
+ >
+ Cancel
+
+ {
+ handleSubmitCheck();
+ }}
+ color="primary"
+ >
+ Submit
+
+
+
+ );
+ };
+
+ const authenticationModal = authenticationModalOpen ? (
+ {
+ //setAuthenticationModalOpen(false)
+ }}
+ PaperProps={{
+ style: {
+ backgroundColor: surfaceColor,
+ color: "white",
+ minWidth: 600,
+ padding: 15,
+ },
+ }}
+ >
+ {
+ setAuthenticationModalOpen(false);
+ }}
+ >
+
+
+
+
+ Authentication for {selectedApp.name}
+
+
+
+ {/* */}
+
+ {app.authentication.type === "oauth2" || app.authentication.type === "oauth2-app" ?
+
+ :
+
+ }
+
+ ) : null;
+
+ const selectedNameChange = (event) => {
+ if (event.target === undefined || event.target === null || event.target.value === undefined || event.target.value === null || event.target.value.length === 0) {
+ return
+ }
+
+
+ //console.log("OLDNAME: ", selectedAction.name)
+ event.target.value = event.target.value.replaceAll("(", "");
+ event.target.value = event.target.value.replaceAll(")", "");
+ event.target.value = event.target.value.replaceAll("$", "");
+ event.target.value = event.target.value.replaceAll("#", "");
+ event.target.value = event.target.value.replaceAll(".", "");
+ event.target.value = event.target.value.replaceAll(",", "");
+ event.target.value = event.target.value.replaceAll(" ", "_");
+ selectedAction.label = event.target.value;
+ setSelectedAction(selectedAction);
+ };
+
+ const actionStyling = {
+ width: "100%",
+ };
+
+ //backgroundColor: "#1F2023",
+ const appApiViewStyle = {
+ display: "flex",
+ flexDirection: "column",
+ color: "white",
+ minHeight: "100%",
+ zIndex: 1000,
+ resize: "vertical",
+ overflowY: "auto",
+ overflowX: "hidden",
+ maxHeight: 680,
+ paddingRight: 7,
+ };
+ // maxWidth: 420,
+
+ return (
+
+ {authenticationModal}
+
+ {selectedAction.id !== undefined ? (
+
+ ) : null}
+
+ );
+ };
+
+ const SelectedActionView = (props) => {
+ const { action } = props;
+ //console.log("Parsedaction: ", selectedAction)
+ const parsedName = parseName(action.name);
+ const splitHeaders = action.headers === undefined || action.headers === null ? [] : action.headers.split("\n");
+
+ return (
+
+
+
+
+ {parsedName}
+
+
+
+
+
+ );
+ };
+
+
+ const AppDetails = (props) => {
+ const { title, inputTitle } = props
+
+ const [details, setDetails] = useState("")
+
+
+ return (
+
+ {
+ setDetails(event.target.value)
+ }}
+ onBlur={(event) => {
+ submitAppDetails(title.toLowerCase().replaceAll(" ", "_"), details)
+ }}
+ />
+
+ )
+ }
+
+ const imageStyle = {
+ borderRadius: theme.palette?.borderRadius,
+ border: "1px solid rgba(255,255,255,0.6)",
+ minWidth: 100,
+ maxWidth: 100,
+ minhHight: 100,
+ maxHeight: 100,
+ };
+
+ const textStyle = {
+ marginLeft: 15,
+ marginTop: 15,
+ };
+
+ const submitAppDetails = (field, value) => {
+ console.log("To submit. Skipping if value is empty: ", field, value)
+
+ if (value === undefined || value === null || (value.length === 0 && field !== "triggers")) {
+ return
+ }
+
+ //toast("Submitting details for field", field)
+ const data = {
+ field: field,
+ value: value,
+ app_id: app.id,
+ }
+
+ fetch(globalUrl + "/api/v1/apps/label", {
+ 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 === false) {
+ if (responseJson.reason === undefined) {
+ toast("Failed to submit details for field ", field, " because ", responseJson.reason)
+ } else {
+ toast("Failed to submit details for field ", field, " because ", responseJson.reason)
+ }
+ } else {
+ toast("Successfully submitted details for field ", field)
+ }
+ })
+ .catch((error) => {
+ console.log("Error: ", error)
+ toast("Error submitting details for field ", field)
+ })
+ }
+
+ const editTrigger = (triggerName) => {
+ console.log("Editing trigger: ", triggerName)
+
+ var found = false
+ for (var i = 0; i < triggers.length; i++) {
+ if (triggers[i].name === triggerName) {
+ found = true
+ break
+ }
+ }
+
+ var newtriggers = JSON.parse(JSON.stringify(triggers))
+ if (!found) {
+ // Find the trigger with the same name in imported workflowTriggers
+ var importedTrigger = undefined
+ for (var i = 0; i < workflowTriggers.length; i++) {
+ if (workflowTriggers[i].name.toLowerCase() === triggerName.toLowerCase()) {
+ newtriggers.push(workflowTriggers[i])
+ break
+ }
+ }
+
+ } else {
+ // Remove
+ newtriggers = newtriggers.filter((trigger) => trigger.name !== triggerName)
+ }
+
+ console.log("Triggers: ", newtriggers)
+ setTriggers(newtriggers)
+
+ var parsedtriggers = []
+ for (var i = 0; i < newtriggers.length; i++) {
+ parsedtriggers.push(newtriggers[i].name)
+ }
+
+ submitAppDetails("triggers", parsedtriggers.join(","))
+ }
+
+ const removeAppFromSearchEngine = (appID) => {
+ toast(`Removing app ${appId} from search engine`)
+
+ const field = "public"
+ const data = {
+ field: field,
+ value: "false",
+ app_id: appID,
+ }
+
+ fetch(globalUrl + "/api/v1/apps/label", {
+ 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 === false) {
+ if (responseJson.reason === undefined) {
+ toast.error("Failed removing app from search engine.")
+ } else {
+ toast.error("Failed to remove app from search engine because: " + responseJson.reason)
+ }
+ } else {
+ toast.info("Successfully unpublished app. It can still be accessed by direct link.")
+ }
+ })
+ .catch((error) => {
+ console.log("Error: ", error)
+ toast.info("Error when removing app from search engine.")
+ })
+ }
+
+ const deduplicateByName = (array) => {
+ const uniqueNames = {};
+ return array.filter(item => {
+ if (!item.hasOwnProperty('name') || !item.name.length) {
+ return true
+ }
+ if (!uniqueNames[item.name]) {
+ uniqueNames[item.name] = true
+ return true
+ }
+ return false
+ })
+ }
+
+ const ActionSelectOption = (actionprops) => {
+ const { option, newActionname, newActiondescription, useIcon, extraDescription, } = actionprops;
+ const [hover, setHover] = React.useState(false);
+
+ return (
+
+ setHover(true)} onMouseLeave={() => setHover(false)}
+ onClick={(event) => {
+ console.log("Clicked on action: ", option)
+
+
+ if (option !== undefined && option !== null) {
+ setSelectedValidationAction(option)
+
+ const labelData = {
+ "app_id": app.id,
+ "action_name": option.name,
+ "label": "app_validation",
+ }
+
+ // Should send recommendations to the owner
+ var url = `${globalUrl}/api/v1/apps/label`;
+ fetch(url, {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ "Accept": "application/json",
+ },
+ body: JSON.stringify(labelData),
+ credentials: "include",
+ })
+ .then((response) => response.json())
+ .then((responseJson) => {
+ if (responseJson.reason !== undefined && responseJson.reason !== null && responseJson.reason.length > 0) {
+ toast(responseJson.reason)
+ }
+ })
+ .catch((error) => {
+ console.log("Error: ", error)
+ })
+
+ // Update the app itself?
+ /*
+ setNewSelectedAction({
+ target: {
+ value: option.name
+ }
+ });
+ */
+ }
+ }}
+ >
+
+
+ {useIcon}
+
+ {newActionname}
+
+ {extraDescription.length > 0 ?
+
+ {extraDescription}
+
+ : null}
+
+
+ )
+ }
+
+ const userRoles = ["you", "public"];
+
+ const updateAppField = (app_id, fieldname, fieldvalue) => {
+ const data = {};
+ data[fieldname] = fieldvalue;
+
+
+ console.log("DATA: ", data);
+
+ fetch(globalUrl + "/api/v1/apps/" + app_id, {
+ method: "PATCH",
+ headers: {
+ Accept: "application/json",
+ },
+ body: JSON.stringify(data),
+ credentials: "include",
+ })
+ .then((response) => {
+ //setAppSearchLoading(false)
+ return response.json();
+ })
+ .then((responseJson) => {
+ //console.log(responseJson)
+ //toast(responseJson)
+ if (responseJson.success) {
+ toast("Successfully updated app configuration");
+ } else {
+ if (responseJson.reason !== undefined && responseJson.reason !== null) {
+ toast("Error: "+responseJson.reason);
+ } else {
+ toast("Error updating app configuration. Are you the owner of this app?");
+ }
+ }
+ })
+ .catch((error) => {
+ toast(error.toString());
+ });
+ };
+
+ const sortByCategoryLabel = (a, b) => {
+ const aHasCategoryLabel = a.category_label !== undefined && a.category_label !== null && a.category_label.length > 0
+ const bHasCategoryLabel = b.category_label !== undefined && b.category_label !== null && b.category_label.length > 0
+
+ // Sort by existence and length of "category_label"
+ if (aHasCategoryLabel && !bHasCategoryLabel) {
+ return -1
+ } else if (!aHasCategoryLabel && bHasCategoryLabel) {
+ return 1
+ } else {
+ return 0
+ }
+ }
+
+ const getDownloadUrl = () => {
+
+ return `curl -X POST ${globalUrl}/api/v1/download_docker_image \\\n -H \"Authorization: Bearer ${userdata?.apikey}" \\\n -d '{"name": "frikky/shuffle:${app?.name.toLowerCase().replaceAll(' ', '_')}_${app.app_version}"}' \\\n -o image.zip; \\\n docker load -i image.zip`
+ }
+
+ const renderedActionOptions = deduplicateByName((
+ actions === undefined || actions === null ? [] :
+ actions.filter((a) =>
+ a.category_label !== undefined && a.category_label !== null && a.category_label.length > 0).concat(sortByKey(actions, "label"))
+ ).sort(sortByCategoryLabel))
+
+ const actionView =
+ actions === undefined || actions === null ? null : (
+
+
+
+ {isMobile || appType === 0 || appType === 2 ? null : (
+
+
+
+
+
+ )}
+
+
+ {
+ if (newValue === 1 && appType === 0 || appType === 2) {
+ window.open(`/apis/${app.id}`, "_blank")
+ } else {
+ setSelectedTab(newValue)
+ }
+ }}
+ style={{ marginBottom: 0, marginLeft: 0, marginRight: 0, minWidth: 800, maxWidth: 800, margin: "auto", }}
+ aria-label="disabled tabs example"
+ >
+ } label="Docs" />
+ : } label={appType === 0 || appType === 2 ? "Explore the API" : "Try it out"} />
+
+ } label="Stats" />
+ } disabled label="Integrations" />
+ } disabled={userdata.support !== true} label="Creator" value={4} />
+
+
+ {selectedTab === 1 && app.skipped_build == false ? (
+
+
+
+
+
+ {currentAction.description !== undefined && currentAction.description !== null && currentAction.description !== "" ?
+
+
+ Action Description
+
+
+ {currentAction.description}
+
+
+ : app.description !== undefined &&
+ app.description !== null &&
+ app.description.length > 0 ?
+
+
+ App Description
+
+
+ {app.description}
+
+
+ : null}
+
+
+ Result
+
+
{
+ executeSingleAction(
+ selectedAction.app_id,
+ selectedAction
+ );
+ }}
+ >
+
+
+
+
+
+ {executing ? (
+
+ {serverside === true ? null :
+
+ }
+
+ ) : (
+
+ {executionResult.valid ? (
+ {
+ handleReactJsonClipboard(copy);
+ }}
+ onSelect={(select) => {
+ HandleJsonCopy(
+ executionResult.result,
+ select,
+ "exec"
+ );
+ console.log("SELECTED!: ", select);
+ }}
+ name={"Result"}
+ />
+ ) : (
+
+ {executionResult.result}
+
+ )}
+
+ )}
+
+
+ ) : selectedTab === 2 && app.id !== undefined ? (
+
+
+
+ Use the App onprem
+
+
+ Due to using docker containers with private containers, we had to use a custom registry. Use the command below to download the image to the server if it fails to run.
+
+ PS: This does NOT work for ARM containers.
+
+
+
+
+ {getDownloadUrl()}
+
+
+
+
+
+
+ ) : selectedTab === 0 ? (
+
+
+ {appDocumentation}
+
+
+ ) : selectedTab === 3 && secondaryApp.objectID !== undefined && app.name !== undefined ? (
+
+
+ Connect {app.name.replaceAll("_", " ")} and{" "}
+ {secondaryApp.name.replaceAll("_", " ")}
+
+
+ Using Shuffle, you can connect{" "}
+ {app.name.replaceAll("_", " ")} and{" "}
+ {secondaryApp.name.replaceAll("_", " ")} with no code.
+
+
+
+
+
+ {app.name.replaceAll("_", " ")}
+
+
+
+
+
+ {secondaryApp.name.replaceAll("_", " ")}
+
+
+
+
+
+
+ {
+ const allapps = [app.name, secondaryApp.name].sort()
+
+ ReactGA.event({
+ category: "Appexplorer",
+ action: "app_connect_click",
+ label: `${allapps[0]}_and_${allapps[1]}`,
+ })
+ }}
+ style={{
+ padding: 15,
+ marginTop: 15,
+ borderRadius: 25,
+ height: 50,
+ margin: "15px 0px 15px 0px",
+ fontSize: 14,
+ color: "white",
+ backgroundImage: buttonBackground,
+ marginRight: 10,
+ }}
+ >
+ Connect {app.name.replaceAll("_", " ")} and{" "}
+ {secondaryApp.name.replaceAll("_", " ")}
+
+
+
+
+
+
+
+
+ {appDocumentation}
+
+
+
+ ) :
+ selectedTab === 4 ?
+
+
+ App Details
+
+
+ Add more details about your app here. This is to help both the Shuffle team, and the public get easier access to this information. Data from these will be used to track app "completeness" for recommendation systems.
+
+
+
+ Validation Action
+
+
+ The validation action is the action that is used to validate the app. This is used both when a user wants to validate their auth, as well as when Shuffle runs automatic tests of the app. It is recommended that the action should be a GET request. Validation is decided based on whether the action is ran successfully in a workflow.
+
+
+
{
+ // Most popular
+ // Is categorized
+ // Uncategorized
+ return option.category_label !== undefined && option.category_label !== null && option.category_label.length > 0 ? "Most used" : "All Actions";
+ }}
+ renderGroup={(params) => {
+
+ return (
+
+ {params.group}
+ {params.children}
+
+ )
+ }}
+ options={renderedActionOptions}
+ ListboxProps={{
+ style: {
+ backgroundColor: theme.palette.surfaceColor,
+ color: "white",
+ },
+ }}
+ filterOptions={(options, { inputValue }) => {
+ const lowercaseValue = inputValue === null ? "" : inputValue.toLowerCase()
+ options = options.filter((x) => {
+ if (x.name === undefined || x.name === null) {
+ x.name = ""
+ }
+
+ if (x.description === undefined || x.description === null) {
+ x.description = ""
+ }
+
+ if (x.method !== "GET") {
+ return null
+ }
+
+ return x.name.replaceAll("_", " ").toLowerCase().includes(lowercaseValue) || x.description.toLowerCase().includes(lowercaseValue)
+ })
+
+ return options
+ }}
+ getOptionLabel={(option) => {
+ if (option === undefined || option === null || option.name === undefined || option.name === null ) {
+ return null;
+ }
+
+ const newname = (
+ option.name.charAt(0).toUpperCase() + option.name.substring(1)
+ ).replaceAll("_", " ");
+
+ return newname;
+ }}
+ fullWidth
+ style={{
+ backgroundColor: theme.palette.inputColor,
+ height: 50,
+ borderRadius: theme.palette?.borderRadius,
+ }}
+ onChange={(event, newValue) => {
+ // Workaround with event lol
+ console.log("Changed to: ", event, newValue)
+ toast("Changed validation action")
+ if (newValue !== undefined && newValue !== null) {
+ /*
+ setNewSelectedAction({
+ target: {
+ value: newValue.name
+ }
+ })
+ */
+ }
+ }}
+ renderOption={(props, option, state) => {
+ var newActionname = option.name;
+ if (option.label !== undefined && option.label !== null && option.label.length > 0) {
+ newActionname = option.label;
+ }
+
+ var newActiondescription = option.description;
+ //console.log("DESC: ", newActiondescription)
+ if (option.description === undefined || option.description === null) {
+ newActiondescription = "Description: No description defined for this action"
+ } else {
+ newActiondescription = "Description: "+newActiondescription
+ }
+
+ const iconInfo = GetIconInfo({ name: option.name });
+ const useIcon = iconInfo.originalIcon;
+
+ if (newActionname === undefined || newActionname === null) {
+ newActionname = "No name"
+ option.name = "No name"
+ option.label = "No name"
+ }
+
+ newActionname = (newActionname.charAt(0).toUpperCase() + newActionname.substring(1)).replaceAll("_", " ");
+
+ var method = ""
+ var extraDescription = ""
+ if (option.name.includes("get_")) {
+ method = "GET"
+ } else if (option.name.includes("post_")) {
+ method = "POST"
+ } else if (option.name.includes("put_")) {
+ method = "PUT"
+ } else if (option.name.includes("patch_")) {
+ method = "PATCH"
+ } else if (option.name.includes("delete_")) {
+ method = "DELETE"
+ } else if (option.name.includes("options_")) {
+ method = "OPTIONS"
+ } else if (option.name.includes("connect_")) {
+ method = "CONNECT"
+ }
+
+ // FIXME: Should it require a base URL?
+ if (method.length > 0 && option.description !== undefined && option.description !== null && option.description.includes("http")) {
+ var extraUrl = ""
+ const descSplit = option.description.split("\n")
+ // Last line of descSplit
+ if (descSplit.length > 0) {
+ extraUrl = descSplit[descSplit.length-1]
+ }
+
+ //for (let [line,lineval] in Object.entries(descSplit)) {
+ // if (descSplit[line].includes("http") && descSplit[line].includes("://")) {
+ // const urlsplit = descSplit[line].split("/")
+ // try {
+ // extraUrl = "/"+urlsplit.slice(3, urlsplit.length).join("/")
+ // } catch (e) {
+ // //console.log("Failed - running with -1")
+ // extraUrl = "/"+urlsplit.slice(3, urlsplit.length-1).join("/")
+ // }
+
+
+ // //console.log("NO BASEURL TOO!! Why missing last one in certain scenarios (sevco)?", extraUrl, urlsplit, descSplit[line])
+ // //break
+ // }
+ //}
+
+ if (extraUrl.length > 0) {
+ if (extraUrl.includes(" ")) {
+ extraUrl = extraUrl.split(" ")[0]
+ }
+
+ if (extraUrl.includes("#")) {
+ extraUrl = extraUrl.split("#")[0]
+ }
+
+ extraDescription = `${method} ${extraUrl}`
+ } else {
+ //console.log("No url found. Check again :)")
+ }
+ }
+
+ return (
+
+ );
+ }}
+ renderInput={(params) => {
+ if (params.inputProps?.value) {
+ const prefixes = ["Post", "Put", "Patch"];
+ for (let prefix of prefixes) {
+ if (params.inputProps.value.startsWith(prefix)) {
+ let newValue = params.inputProps.value.replace(prefix + " ", "");
+ if (newValue.length > 1) {
+ newValue = newValue.charAt(0).toUpperCase() + newValue.substring(1);
+ }
+ // Set the new value without mutating inputProps
+ params = { ...params, inputProps: { ...params.inputProps, value: newValue } };
+ break;
+ }
+ }
+ // Check if it starts with "Get List" and method is "Get"
+ if (params.inputProps.value.startsWith("Get List")) {
+ console.log("Get List")
+ }
+ }
+
+ const actionDescription = ""
+ const isIntegration = false
+
+ return (
+
+
+
+ )
+ }}
+ />
+
+
+
+ Triggers
+
+
+
+ Schedule:
+
+ trigger.name === "Schedule") !== undefined}
+ label="Schedule"
+ onChange={(e) => {
+ editTrigger("Schedule")
+
+ }}
+ />
+
+ Webhook:
+
+ trigger.name === "Webhook") !== undefined}
+ label="Webhook"
+ onChange={() => {
+ editTrigger("Webhook")
+
+ }}
+ />
+
+ {triggers === undefined || triggers === null || triggers.find(trigger => trigger.name === "Webhook") === undefined ? null :
+ {
+ submitAppDetails("extra_value", e.target.value)
+ }}
+ />
+ }
+
+
+ External info
+
+
+
+
+
+
+ Partner Details
+
+
+
+
+
+
+
+ Public Status
+
+ {
+ removeAppFromSearchEngine(app.id)
+ }}
+ >
+ Unpublish App from Search Engine
+
+
+
+ :
+ (
+
+
+ This app is currently in Beta, but is usable. Interested in using this app? Click the button below or contact us.
+
+ {activateButton}
+
+
+
+ {app.description !== undefined &&
+ app.description !== null &&
+ app.description !== "" ?
+
+
+ More about the app
+
+
+ {app.description}
+
+
+ : null}
+
+ )}
+
+
+
+
+ );
+
+ // Random names for type & autoComplete. Didn't research :^)
+ const imageData = file.length > 0 ? file : fileBase64;
+ const height = 100;
+ const imageInfo = (
+
+ );
+
+ const publishModal = publishModalOpen ? (
+ {
+ setPublishModalOpen(false);
+ }}
+ PaperProps={{
+ sx: {
+ borderRadius: theme?.palette?.DialogStyle?.borderRadius,
+ border: theme?.palette?.DialogStyle?.border,
+ minWidth: '500px',
+ 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,
+ },
+ }
+ }}
+ >
+
+
+ Are you sure you want to PUBLISH this app?
+
+
+
+
+
+ Before publishing, make sure to sanitize the App for anything you don't want public.
+
+
+ The published App is yours, and you can always change your public Apps after they are released.
+
+
+ {
+ updateAppField(selectedApp.id, "sharing", true);
+ setPublishModalOpen(false);
+ }}
+ color="primary"
+ >
+ Yes
+
+ {
+ setPublishModalOpen(false);
+ }}
+ color="primary"
+ >
+ No
+
+
+
+ ) : null;
+
+ console.log("App: ", app);
+
+ console.log("userdata is: ", userdata);
+
+ const landingpageDataBrowser = (
+
+ {publishModal}
+
+ {isMobile ? null : (
+
+
+
+
+ Apps
+
+
+
+ {name}
+
+
+ )}
+
+ {app.documentation_download_url !== undefined &&
+ app.documentation_download_url !== null &&
+ app.documentation_download_url.length > 0 ? (
+
{
+ const data = openapi;
+
+ let linkElement = document.createElement("a");
+ linkElement.setAttribute("target", "_blank");
+ linkElement.setAttribute(
+ "href",
+ app.documentation_download_url
+ );
+ linkElement.setAttribute(
+ "download",
+ app.documentation_download_url
+ );
+ linkElement.click();
+ }}
+ >
+
+
+
+
+ ) : null}
+ {appType === 0 || appType === 2 ? (
+
{
+ const data = openapi;
+ let exportFileDefaultName = name + ".json";
+
+ let dataStr = JSON.stringify(data);
+ let dataUri =
+ "data:application/json;charset=utf-8," +
+ encodeURIComponent(dataStr);
+ let linkElement = document.createElement("a");
+ linkElement.setAttribute("href", dataUri);
+ linkElement.setAttribute("download", exportFileDefaultName);
+ linkElement.click();
+
+ const tmpurl = new URL(window.location.href);
+ const searchParams = tmpurl.searchParams;
+ const queryID = searchParams.get("queryID");
+
+ if (queryID !== undefined && queryID !== null) {
+ aa("init", {
+ appId: "JNSS5CFDZZ",
+ apiKey: "db08e40265e2941b9a7d8f644b6e5240",
+ });
+
+ const timestamp = new Date().getTime();
+ aa("sendEvents", [
+ {
+ eventType: "conversion",
+ eventName: "Public App Downloaded",
+ index: "appsearch",
+ objectIDs: [app.id],
+ timestamp: timestamp,
+ queryID: queryID,
+ userToken:
+ userdata === undefined ||
+ userdata === null ||
+ userdata.id === undefined
+ ? "unauthenticated"
+ : userdata.id,
+ },
+ ]);
+ } else {
+ console.log("No query to handle when downloading");
+ }
+ }}
+ >
+
+
+
+
+ ) : null}
+
+ {selectedOrganization !== undefined && selectedOrganization !== null && selectedOrganization.id !== undefined && userdata.support === true ?
+ // Iconbutton for authentication with just an icon. Link to /apps/authentication?app_id=app.id
+
{
+ window.open(`/appauth?app_id=${app.id}&auth=${selectedOrganization.org_auth.token}`, "_blank")
+ }}
+ >
+
+
+
+
+ : null}
+
+ {isMobile ? null : (
+
{
+ if (!isLoggedIn) {
+ //navigate("/login?message=You must be logged in to activate this app&view=/apps/" + params.appid);
+ toast("You must be logged in to activate apps! Go to /login first.")
+ return;
+ }
+
+ const tmpurl = new URL(window.location.href);
+ const searchParams = tmpurl.searchParams;
+ const queryID = searchParams.get("queryID");
+
+ if (queryID !== undefined && queryID !== null) {
+ aa("init", {
+ appId: "JNSS5CFDZZ",
+ apiKey: "db08e40265e2941b9a7d8f644b6e5240",
+ });
+
+ const timestamp = new Date().getTime();
+ aa("sendEvents", [
+ {
+ eventType: "conversion",
+ eventName: "Public App Activated",
+ index: "appsearch",
+ objectIDs: [app.id],
+ timestamp: timestamp,
+ queryID: queryID,
+ userToken:
+ userdata === undefined ||
+ userdata === null ||
+ userdata.id === undefined
+ ? "unauthenticated"
+ : userdata.id,
+ },
+ ]);
+ } else {
+ console.log("No query to handle when activating");
+ }
+
+ activateApp();
+ }}
+ style={{ height: 40, marginTop: 5 }}
+ >
+ {userdata.active_apps !== undefined && userdata.active_apps !== null && userdata.active_apps.includes(appId) ?
+ "Deactivate App"
+ :
+ "Activate App"
+ }
+
+ )}
+ {appType === 1 ? null :
+ creatorProfile.self === true || userdata.support === true ?
+
+ {
+ navigate(`/apps/edit/${appId}`)
+ }}
+ style={{ height: 40, marginTop: 5, marginLeft: 5, }}
+ >
+ Edit
+
+
+ : isMobile || !isLoggedIn ? null :
+
+ {
+ navigate(`/apps/new?id=${appId}`)
+ }}
+ style={{ height: 40, marginTop: 5, marginLeft: 5, }}
+ >
+ Fork
+
+
+ }
+
+ {appType === 0 || appType === 2 ? (
+
+
+
+
+ Explore API
+
+
+ {(appdata?.owner === userdata?.id) ? (
+
{
+ if ((appdata.owner !== userdata.id) && (!userdata.support)) {
+ toast("You're not the owner of this app. You can't update it.");
+ return;
+ }
+ setSharingConfiguration(event.target.value);
+ if (event.target.value === "you") {
+ toast("Changing sharing to " + event.target.value);
+ updateAppField(appdata.id, "sharing", false);
+ } else if (
+ event.target.value === "everyone" ||
+ event.target.value === "public"
+ ) {
+
+ if (!isCloud) {
+ setPublishModalOpen(true)
+ } else {
+ updateAppField(appdata.id, "sharing", true);
+ }
+ } else {
+ console.log(
+ "Can't handle value for sharing: ",
+ event.target.value
+ );
+ }
+ }}
+ style={{
+ width: 150,
+ backgroundColor: theme.palette.surfaceColor,
+ color: "white",
+ height: 40,
+ marginTop: 5
+ }}
+ SelectDisplayProps={{
+ style: {
+ marginLeft: 10,
+ display: 'flex'
+ },
+ }}
+ >
+ {userRoles.map((data) => {
+ return (
+
+ {data}
+
+ );
+ })}
+
+ ): null }
+
+ ) : (
+
+
+
+ )}
+
+
+
+
+
+
{
+ upload.click();
+ }}
+ >
+ {imageInfo}
+
+
+
+
+ {name}
+
+
+ {(app.public || appType === 0) && app.skipped_build == false ? (
+
+
+
+ ) : (
+
+
+
+ )}
+
+
+ {/* Handles category changing, but looks like shit. Should probably work as a suggestion */}
+ {/*
+ {
+ console.log("Changed: ", e.target.value)
+ setNewWorkflowCategories([e.target.value]);
+ }}
+ value={newWorkflowCategories.length === 0 ? "Select a category" : newWorkflowCategories[0]}
+ style={{
+ flexAlign: "right",
+ color: "white",
+ height: 40,
+ width: 150,
+ borderRadius: 20,
+ marginLeft: 200,
+ }}
+ >
+ {categories.map((category, categoryIndex) => {
+ if (category === undefined || category === null || category === "") {
+ return null
+ }
+
+ return (
+
+ {category.name}
+
+ )
+ })}
+
+ */}
+
+
+
+ Version - {app?.app_version}
+
+
+
+ {Object.getOwnPropertyNames(creatorProfile).length !== 0 &&
+ creatorProfile.github_avatar !== undefined &&
+ creatorProfile.github_avatar !== null ? (
+
+
{
+ setAnchorElAvatar(event.currentTarget);
+ }}
+ >
+
+
+
+
+
+ Shared by{" "}
+
+ {creatorProfile.github_username}
+
+ {
+ ReactGA.event({
+ category: "hiring_button",
+ action: `${creatorProfile.github_username}_app_click`,
+ label: name,
+ });
+
+ if (window.drift !== undefined) {
+ window.drift.api.startInteraction({
+ interactionId: 342345,
+ });
+ } else {
+ console.log(
+ "Couldn't find drift in window.drift and not .drift-open-chat with querySelector: ",
+ window.drift
+ );
+ toast(
+ `Thanks for showing interest in getting help from ${creatorProfile.github_username}`
+ );
+ }
+ }}
+ >
+ Get help
+
+ {contact.name !== undefined &&
+ contact.name !== null &&
+ !contact.name.includes("frikky") &&
+ contact.name.length > 0 &&
+ contact.name.toLowerCase() !==
+ creatorProfile.github_username.toLowerCase() &&
+ !(
+ contact.name.toLowerCase().includes("anon") &&
+ creatorProfile.github_username.length > 0
+ ) ? (
+
+ {" "}
+ • Created by {contact.name}
+
+ ) : (
+ ""
+ )}
+
+
+ ) : contact.name !== undefined &&
+ contact.name !== null &&
+ contact.name.length > 0 ? (
+
+ Created by {contact.name}
+
+ ) : null}
+
+ {newWorkflowTags.map((tag, index) => {
+ return (
+
+ );
+ })}
+
+
+
+
+ {isMobile || serverside ? null : (
+
+
+
+
+ {relatedWorkflows !== 0 ? (
+ relatedWorkflows
+ ) : (
+
+
+
+
+ )}
+
+
+ Workflows
+
+
+
+
+
+ )}
+ {app.video !== undefined &&
+ app.video !== null &&
+ app.video.includes("http") ? (
+
+ {app.video.includes("loom.com/share") &&
+ app.video.split("/").length > 4 ? (
+
+
+
+ ) : app.video.includes("youtube.com") &&
+ app.video.split("/").length > 3 &&
+ app.video.includes("v=") ? (
+
+ VIDEO
+
+ ) : (
+
{app.video}
+ )}
+
+ ) : isMobile || serverside ? null : (
+
+
+
+
+ {relatedApps !== 0 ? (
+ relatedApps
+ ) : (
+
+
+
+
+ )}
+
+
+ Related apps
+
+
+
+
+
+ )}
+
+
+
{actionView}
+ {errorCode.length > 0 ? `Error: ${errorCode}` : null}
+
+
+ );
+ const loadedCheck =
+ (isLoaded && isAppLoaded) || serverside === true ? (
+
+ {serverside === true ? (
+
{landingpageDataBrowser}
+ ) : (
+
+ {landingpageDataBrowser}
+
+ )}
+
+
+ ) : (
+
+ {serverside === true ? null :
+
+
+
+ Loading App
+
+
+ }
+
+ );
+
+ return {loadedCheck}
;
+};
+export default AppExplorer;
diff --git a/frontend/src/views/Usecases2.jsx b/frontend/src/views/Usecases2.jsx
index 3e64f723..9648881e 100644
--- a/frontend/src/views/Usecases2.jsx
+++ b/frontend/src/views/Usecases2.jsx
@@ -1221,8 +1221,7 @@ const Usecases2 = (props) => {
) : null
const data =
-