From dfe0314d0405c193570f37f70b3ed467362deeb2 Mon Sep 17 00:00:00 2001 From: Frikky Date: Mon, 3 Mar 2025 12:01:29 +0100 Subject: [PATCH 01/18] Fixed dashboardchart issue --- frontend/src/views/DashboardViews.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/views/DashboardViews.jsx b/frontend/src/views/DashboardViews.jsx index 4bf532a6..7905a21a 100644 --- a/frontend/src/views/DashboardViews.jsx +++ b/frontend/src/views/DashboardViews.jsx @@ -8,7 +8,7 @@ import { useNavigate, Link, useParams } from "react-router-dom"; import { ToastContainer, toast } from "react-toastify" import Draggable from "react-draggable"; -// import DashboardBarchart, { LoadStats } from '../components/DashboardBarchart.jsx'; +import DashboardBarchart, { LoadStats } from '../components/DashboardBarchart.jsx'; import { Autocomplete, From 91268ca3b0d675ffc556346fa3de3710f211ec04 Mon Sep 17 00:00:00 2001 From: Frikky Date: Mon, 3 Mar 2025 12:02:27 +0100 Subject: [PATCH 02/18] Fixed go.mod bumps --- backend/go-app/go.mod | 2 +- functions/onprem/orborus/go.mod | 2 +- functions/onprem/worker/go.mod | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/backend/go-app/go.mod b/backend/go-app/go.mod index f9094264..d554503d 100644 --- a/backend/go-app/go.mod +++ b/backend/go-app/go.mod @@ -18,7 +18,7 @@ require ( github.com/gorilla/mux v1.8.1 github.com/h2non/filetype v1.1.3 github.com/satori/go.uuid v1.2.0 - github.com/shuffle/shuffle-shared v0.8.3 + github.com/shuffle/shuffle-shared v0.8.4 golang.org/x/crypto v0.32.0 google.golang.org/api v0.176.1 google.golang.org/grpc v1.68.1 diff --git a/functions/onprem/orborus/go.mod b/functions/onprem/orborus/go.mod index b7f9b684..f81e87a4 100644 --- a/functions/onprem/orborus/go.mod +++ b/functions/onprem/orborus/go.mod @@ -9,7 +9,7 @@ require ( github.com/docker/docker v27.5.0+incompatible github.com/docker/go-connections v0.5.0 github.com/satori/go.uuid v1.2.0 - github.com/shuffle/shuffle-shared v0.8.3 + github.com/shuffle/shuffle-shared v0.8.4 k8s.io/api v0.30.2 k8s.io/apimachinery v0.30.2 ) diff --git a/functions/onprem/worker/go.mod b/functions/onprem/worker/go.mod index 64929892..dd892f78 100644 --- a/functions/onprem/worker/go.mod +++ b/functions/onprem/worker/go.mod @@ -8,7 +8,7 @@ require ( github.com/docker/docker v27.5.0+incompatible github.com/gorilla/mux v1.8.1 github.com/satori/go.uuid v1.2.0 - github.com/shuffle/shuffle-shared v0.8.3 + github.com/shuffle/shuffle-shared v0.8.4 k8s.io/api v0.30.2 k8s.io/apimachinery v0.30.2 k8s.io/client-go v0.30.2 From 43e28c355f36399f9f8ea43681c6323ebf6caa38 Mon Sep 17 00:00:00 2001 From: Frikky Date: Mon, 3 Mar 2025 12:04:16 +0100 Subject: [PATCH 03/18] Fixed issues with nightly/latest and versions --- docker-compose.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index b3c4dbdb..adf6b533 100755 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,6 +1,6 @@ services: frontend: - image: ghcr.io/shuffle/shuffle-frontend:nightly + image: ghcr.io/shuffle/shuffle-frontend:latest container_name: shuffle-frontend hostname: shuffle-frontend ports: @@ -14,7 +14,7 @@ services: depends_on: - backend backend: - image: ghcr.io/shuffle/shuffle-backend:nightly + image: ghcr.io/shuffle/shuffle-backend:latest container_name: shuffle-backend hostname: ${BACKEND_HOSTNAME} # Here for debugging: @@ -33,7 +33,7 @@ services: - SHUFFLE_FILE_LOCATION=/shuffle-files restart: unless-stopped orborus: - image: ghcr.io/shuffle/shuffle-orborus:nightly + image: ghcr.io/shuffle/shuffle-orborus:latest container_name: shuffle-orborus hostname: shuffle-orborus networks: @@ -55,13 +55,13 @@ services: - SHUFFLE_STATS_DISABLED=true - SHUFFLE_LOGS_DISABLED=true - SHUFFLE_SWARM_CONFIG=run - - SHUFFLE_WORKER_IMAGE=ghcr.io/shuffle/shuffle-worker:nightly + - SHUFFLE_WORKER_IMAGE=ghcr.io/shuffle/shuffle-worker:latest env_file: .env restart: unless-stopped security_opt: - seccomp:unconfined opensearch: - image: opensearchproject/opensearch:2.14.0 + image: opensearchproject/opensearch:2.19.1 hostname: shuffle-opensearch container_name: shuffle-opensearch environment: From 2e73f772e60990cd494a99e4335abe5294d88456 Mon Sep 17 00:00:00 2001 From: Frikky Date: Mon, 3 Mar 2025 16:38:33 +0100 Subject: [PATCH 04/18] Migrated a bunch of pages --- backend/go-app/go.mod | 2 +- backend/go-app/go.sum | 4 + backend/go-app/main.go | 22 +- frontend/src/App.jsx | 26 +- frontend/src/components/AppAuthTab.jsx | 38 +- frontend/src/components/AppCreationModal.jsx | 3 +- frontend/src/components/EnvironmentTab.jsx | 2 +- frontend/src/components/TenantsTab.jsx | 6 +- frontend/src/components/UserManagmentTab.jsx | 4 +- frontend/src/views/AngularWorkflow.jsx | 4 +- frontend/src/views/AppCreator.jsx | 8 +- frontend/src/views/AppExplorer.jsx | 9 +- frontend/src/views/Apps2.jsx | 8 +- frontend/src/views/LoginPage.jsx | 1404 +++++++++++------- frontend/src/views/LoginPageOld.jsx | 546 +++++++ 15 files changed, 1514 insertions(+), 572 deletions(-) create mode 100755 frontend/src/views/LoginPageOld.jsx diff --git a/backend/go-app/go.mod b/backend/go-app/go.mod index d554503d..37174871 100644 --- a/backend/go-app/go.mod +++ b/backend/go-app/go.mod @@ -18,7 +18,7 @@ require ( github.com/gorilla/mux v1.8.1 github.com/h2non/filetype v1.1.3 github.com/satori/go.uuid v1.2.0 - github.com/shuffle/shuffle-shared v0.8.4 + github.com/shuffle/shuffle-shared v0.8.5 golang.org/x/crypto v0.32.0 google.golang.org/api v0.176.1 google.golang.org/grpc v1.68.1 diff --git a/backend/go-app/go.sum b/backend/go-app/go.sum index 5135bcff..8e367a5a 100644 --- a/backend/go-app/go.sum +++ b/backend/go-app/go.sum @@ -350,6 +350,10 @@ github.com/sendgrid/sendgrid-go v3.14.0+incompatible/go.mod h1:QRQt+LX/NmgVEvmdR github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8= github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= +github.com/shuffle/shuffle-shared v0.8.4 h1:R/A62IzHJXnhVNG1PqFXN1YtRgdiiSIL5q4YJEef5P8= +github.com/shuffle/shuffle-shared v0.8.4/go.mod h1:bBXhEsPKjxln0mFnSeri7gIJ3tL/636Sh5NyTyNrvIQ= +github.com/shuffle/shuffle-shared v0.8.5 h1:c98AMOzIrDXmgQrKyf1HJ9wZwAwB02LPRvf9+yy05OQ= +github.com/shuffle/shuffle-shared v0.8.5/go.mod h1:bBXhEsPKjxln0mFnSeri7gIJ3tL/636Sh5NyTyNrvIQ= github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= diff --git a/backend/go-app/main.go b/backend/go-app/main.go index dfdce781..42b58256 100755 --- a/backend/go-app/main.go +++ b/backend/go-app/main.go @@ -3295,6 +3295,8 @@ func buildSwaggerApp(resp http.ResponseWriter, body []byte, user shuffle.User, s api.Contributors = append(api.Contributors, user.Id) } + shuffle.SetAppRevision(ctx, api) + log.Printf("[INFO] API LENGTH FOR %s: %d, ID: %s", api.Name, len(parsed.Body), newmd5) // FIXME: Might cause versioning issues if we re-use the same!! // FIXME: Need a way to track different versions of the same app properly. @@ -3365,11 +3367,27 @@ func buildSwaggerApp(resp http.ResponseWriter, body []byte, user shuffle.User, s } } + log.Printf("[DEBUG] Successfully built app %s (%s)", api.Name, api.ID) if len(user.Id) > 0 { resp.WriteHeader(200) resp.Write([]byte(fmt.Sprintf(`{"success": true, "id": "%s"}`, api.ID))) } + + org, err := shuffle.GetOrg(ctx, user.ActiveOrg.Id) + if err != nil { + log.Printf("[ERROR] Failed getting org during image build (%s): %s", user.ActiveOrg.Id, err) + } else { + imagenames := []string{ + fmt.Sprintf("%s_%s", api.Name, api.AppVersion), + fmt.Sprintf("%s_%s", api.Name, api.ID), + } + + err = shuffle.DistributeAppToEnvironments(ctx, *org, imagenames) + if err != nil { + log.Printf("[ERROR] Failed distributing app to environments: %s", err) + } + } } // Creates an app from the app builder @@ -4359,8 +4377,8 @@ func runInitEs(ctx context.Context) { } if os.Getenv("SHUFFLE_HEALTHCHECK_DISABLED") != "true" { - healthcheckInterval := 30 - log.Printf("[INFO] Starting healthcheck job every %d minute. Stats available on /api/v1/health/stats. Disable with SHUFFLE_HEALTHCHECK_DISABLED=true", healthcheckInterval) + healthcheckInterval := 60 + log.Printf("[INFO] Starting healthcheck job every %d minute. Stats available on /api/v1/health/stats, and dashboard on /health. Disable with SHUFFLE_HEALTHCHECK_DISABLED=true", healthcheckInterval) job := func() { // Prepare a fake http.responsewriter resp := httptest.NewRecorder() diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index b6f3e6b0..9fc34790 100755 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -34,9 +34,10 @@ import RunWorkflow from "./views/RunWorkflow.jsx"; import Admin2 from "./views/Admin2.jsx"; import LoginPage from "./views/LoginPage.jsx"; +//import LoginPage from "./views/LoginPage.jsx"; + import SettingsPage from "./views/SettingsPage.jsx"; import KeepAlive from "./views/KeepAlive.jsx"; - import { ThemeProvider } from "@mui/material/styles"; import CssBaseline from '@mui/material/CssBaseline'; @@ -236,6 +237,7 @@ const App = (message, props) => {
*/} + { { /> } /> + + + } + /> + {
- + + {isCloud ? + + : null} {/* { {editAuthenticationModal} {authenticationView}
+ + {/*

App Authentication Groups

@@ -1415,13 +1420,14 @@ const AppAuthTab = memo((props) => { ); } )} - -
- -
- - - + + + + + */} + + + ); }); diff --git a/frontend/src/components/AppCreationModal.jsx b/frontend/src/components/AppCreationModal.jsx index f5a25169..80cf1711 100644 --- a/frontend/src/components/AppCreationModal.jsx +++ b/frontend/src/components/AppCreationModal.jsx @@ -59,6 +59,7 @@ const AppCreationModal = ({ open, onClose, theme, globalUrl, isCloud }) => { : "1px solid rgba(255,255,255,0.3)", borderImage: makeFancy ? "linear-gradient(to right, #ff8544 0%, #ec517c 50%, #9c5af2 100%) 1" : "none", transition: 'all 0.2s ease-in-out', + paddingBottom: isCloud ? 0 : 175, } return ( @@ -416,7 +417,7 @@ const AppCreationModal = ({ open, onClose, theme, globalUrl, isCloud }) => { -
+
{ diff --git a/frontend/src/components/EnvironmentTab.jsx b/frontend/src/components/EnvironmentTab.jsx index cd0a9416..15efd2fc 100644 --- a/frontend/src/components/EnvironmentTab.jsx +++ b/frontend/src/components/EnvironmentTab.jsx @@ -1420,7 +1420,7 @@ const EnvironmentTab = memo((props) => { -
+
Self-Hosted Orborus instance diff --git a/frontend/src/components/TenantsTab.jsx b/frontend/src/components/TenantsTab.jsx index 0d8c3df2..afeb9563 100644 --- a/frontend/src/components/TenantsTab.jsx +++ b/frontend/src/components/TenantsTab.jsx @@ -497,7 +497,9 @@ const TenantsTab = memo((props) => { response.json().then((responseJson) => { if (responseJson["success"] === false) { if (responseJson.reason !== undefined) { - toast(responseJson.reason); + toast.error(responseJson.reason, { + autoClose: 5000, + }) } else { toast("Failed creating suborg. Please try again"); } @@ -781,7 +783,7 @@ const TenantsTab = memo((props) => {
-

Organizations

+

Tenants

Control sub organizations (tenants)! {" "} {isCloud diff --git a/frontend/src/components/UserManagmentTab.jsx b/frontend/src/components/UserManagmentTab.jsx index 2bb924b0..8aff24a5 100644 --- a/frontend/src/components/UserManagmentTab.jsx +++ b/frontend/src/components/UserManagmentTab.jsx @@ -1557,10 +1557,12 @@ const UserManagmentTab = memo((props) => { style={{ display:'table-cell', verticalAlign: 'middle' }} /> + {/* + */} { style={{ display:'table-cell',verticalAlign: 'middle', padding: "8px", }} /> - {/* { } style={{ display:'table-cell', verticalAlign: 'middle',padding: "8px", color: data.mfa_info.active ? "#02CB70" : "#F53434" }} /> - */} {selectedOrganization?.child_orgs !== undefined && selectedOrganization?.child_orgs !== null && diff --git a/frontend/src/views/AngularWorkflow.jsx b/frontend/src/views/AngularWorkflow.jsx index 16d5e9d0..51812543 100755 --- a/frontend/src/views/AngularWorkflow.jsx +++ b/frontend/src/views/AngularWorkflow.jsx @@ -11446,7 +11446,7 @@ const AngularWorkflow = (defaultprops) => { var type = "app" const baseImage = return ( -
+
{hits.length === 0 ? @@ -11736,7 +11736,7 @@ const AngularWorkflow = (defaultprops) => { }} > - Click one of the relevant public apps below to Activate it for your organization. + Click one of the public apps below to Activate it for your organization. { console.log("CLICKED") diff --git a/frontend/src/views/AppCreator.jsx b/frontend/src/views/AppCreator.jsx index 2d1bbb32..50106831 100755 --- a/frontend/src/views/AppCreator.jsx +++ b/frontend/src/views/AppCreator.jsx @@ -2657,12 +2657,12 @@ const AppCreator = (defaultprops) => { if (responseJson.reason !== undefined) { setErrorCode(responseJson.reason); - if (responseJson.extra === undefined && responseJson.extra === null) { - toast("Failed to verify: " + responseJson.reason); - } + toast.error("Failed to build: " + responseJson.reason, { + autoClose: 10000 + }) } } else { - toast("Successfully uploaded openapi"); + toast.success("Successfully built openapi app! Added job to rebuild it in your hybrid runtime locations (Orborus)."); if (window.location.pathname.includes("/new")) { if (responseJson.id !== undefined && responseJson.id !== null) { window.location = `/apps/edit/${responseJson.id}`; diff --git a/frontend/src/views/AppExplorer.jsx b/frontend/src/views/AppExplorer.jsx index b3cb0d99..d37709ee 100644 --- a/frontend/src/views/AppExplorer.jsx +++ b/frontend/src/views/AppExplorer.jsx @@ -3866,13 +3866,13 @@ const AppExplorer = (props) => { } }} > - +
Are you sure you want to PUBLISH this app?
@@ -4120,7 +4120,7 @@ const AppExplorer = (props) => { )} {appType === 1 ? null : - creatorProfile.self === true || userdata.support === true ? + userdata?.support || userdata?.id === app?.owner || (userdata?.admin === "true" && userdata?.active_org?.id === app?.reference_org) || app?.contributors?.includes(userdata?.id) || creatorProfile?.self === true ? - {(appdata?.owner === userdata?.id) ? ( - ): null }
) : ( - {hoverEffect === index && isCloud ? ( + {hoverEffect === index ? (
{data.tags && ( { { }} /> { { - const { - globalUrl, - isLoaded, - isLoggedIn, - setIsLoggedIn, - setCookie, - register, - checkLogin, - } = props; - let navigate = useNavigate(); - const classes = useStyles(); +const FreePlanCard = ({ classes }) => { + const features = [ + "Access to All Apps", + "10,000 App Runs", + "Monthly Runs Refresh", + "Unlimited Users & Workflows", + "Multi-Tenancy & -Region", + "Support & Discord Access" + ]; - const [username, setUsername] = useState(""); - const [password, setPassword] = useState(""); - const [firstRequest, setFirstRequest] = useState(true); - const [loginLoading, setLoginLoading] = useState(false); - const [loginViewLoading, setLoginViewLoading] = useState(false); - const [ssoUrl, setSSOUrl] = useState(""); + return ( +
+

+ The free plan includes: +

- const [MFAField, setMFAField] = useState(false); - const [MFAValue, setMFAValue] = useState(""); - - - // Used to swap from login to register. True = login, false = register - - useEffect(() => { - checkAdmin() - }, [loginViewLoading]) - - // Error messages etc - const [loginInfo, setLoginInfo] = useState(""); - - const handleValidateForm = () => { - return username.length > 1 && password.length > 1; - }; - - if (isLoggedIn === true) { - //window.location.pathname = "/workflows"; - navigate("/workflows") - } - - const checkAdmin = () => { - const url = globalUrl + "/api/v1/checkusers"; - fetch(url, { - method: "GET", - headers: { - "Content-Type": "application/json", - }, - }) - .then((response) => - response.json().then((responseJson) => { - if (responseJson["success"] === false) { - setLoginInfo(responseJson["reason"]); - } else { - - if (responseJson.sso_url !== undefined && responseJson.sso_url !== null) { - setSSOUrl(responseJson.sso_url); - } - - if (loginViewLoading) { - setLoginViewLoading(false); - checkLogin(); - stop(); - - if ( - responseJson.reason !== undefined && - responseJson.reason !== null - ) { - setLoginInfo(responseJson.reason); - } - } - - if (responseJson.reason === "stay") { - navigate("/adminsetup") - } - } - }) - ) - .catch((error) => { - if (!loginViewLoading) { - setLoginViewLoading(true); - start(); - } - }); - }; - - const { start, stop } = useInterval({ - duration: 3000, - startImmediate: false, - callback: () => { - checkAdmin(); - }, - }); - - if (firstRequest) { - setFirstRequest(false); - checkAdmin(); - } - - const onSubmit = (e) => { - setLoginLoading(true); - e.preventDefault(); - setLoginInfo(""); - // FIXME - add some check here ROFL - - // Just use this one? - var data = { username: username, password: password }; - if (MFAValue !== undefined && MFAValue !== null && MFAValue.length > 0) { - data["mfa_code"] = MFAValue; - } - - var baseurl = globalUrl; - if (register) { - var url = baseurl + "/api/v1/login"; - 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) => { - setLoginLoading(false); - if (responseJson["success"] === false) { - setLoginInfo(responseJson["reason"]); - } else { - if (responseJson["reason"] === "MFA_REDIRECT") { - setLoginInfo( - "MFA required. Please enter the 6-digit code from your authenticator" - ); - setMFAField(true); - return; - } else if (responseJson["reason"] === "MFA_SETUP") { - window.location.href = `/login/${responseJson.url}/mfa-setup`; - return; - } - - setLoginInfo("Successful login, rerouting"); - for (var key in responseJson["cookies"]) { - setCookie( - responseJson["cookies"][key].key, - responseJson["cookies"][key].value, - { path: "/" } - ); - } - - if (responseJson.tutorials === undefined || responseJson.tutorials === null || !responseJson.tutorials.includes("welcome")) { - console.log("RUN Welcome!!") - setTimeout(() => { - navigate("/welcome?tab=2") - },200) - // window.location.pathname = "" - return - } - - const tmpView = new URLSearchParams(window.location.search).get("view") - if (tmpView !== undefined && tmpView !== null) { - //const newUrl = `/${tmpView}${decodeURIComponent(window.location.search)}` - const newUrl = `/${tmpView}` - window.location.pathname = newUrl - } else { - window.location.pathname = "/workflows" - } - - setIsLoggedIn(true); - } - }) - ) - .catch((error) => { - setLoginLoading(false); - setLoginInfo("Error logging in: " + error); - }); - } else { - url = baseurl + "/api/v1/users/register"; - fetch(url, { - method: "POST", - body: JSON.stringify(data), - headers: { - "Content-Type": "application/json", - }, - }) - .then((response) => - response.json().then((responseJson) => { - if (responseJson["success"] === false) { - setLoginInfo(responseJson["reason"]); - } else { - setLoginInfo("Successful register!"); - } - }) - ) - .catch((error) => { - setLoginInfo("Error in from backend: ", error); - }); - } - }; - - const onChangeUser = (e) => { - setUsername(e.target.value); - }; - - const onChangePass = (e) => { - setPassword(e.target.value); - }; - - //const onClickRegister = () => { - // if (props.location.pathname === "/login") { - // window.location.pathname = "/register" - // } else { - // window.location.pathname = "/login" - // } - - // setLoginCheck(!register) - //} - - //var loginChange = register ? (

Want to register? Click here.

) : (

Go back to login? Click here.

); - var formtitle = register ?
Login
:
Register
; - const imgsize = 100; - const basedata = ( -
- -
- -
- - {loginViewLoading ? ( -
- - Waiting for the Shuffle database to become available. This may - take up to a minute. - - - {loginInfo === undefined || - loginInfo === null || - loginInfo.length === 0 ? null : ( -
Database Response: {loginInfo}
- )} - - - - - - Are you sure Shuffle is{" "} -
- installed correctly - - ? - - - - 1. Make sure shuffle-database folder has correct access, and that you have a minimum of 2Gb of RAM available:{" "} -
-
- sudo chown -R 1000:1000 shuffle-database -
- - 2. Disable memory swap on the host: -
-
- sudo swapoff -a -
- - 3. Restart the database: -
-
- sudo docker restart shuffle-opensearch -
- - - Need help?{" "} - - Join the Discord! - - -
- ) : ( -
-

{formtitle}

- Username -
- -
- Password -
- -
- {MFAField === true ? ( -
- 2-factor code - { - setMFAValue(event.target.value); - }} - /> -
- ) : null} -
- -
-
{loginInfo}
- {ssoUrl !== undefined && ssoUrl !== null && ssoUrl.length > 0 ? ( -
- Or -
- -
-
- ) : null} -
- )} - -
- ); - - const loadedCheck = isLoaded ?
{basedata}
:
; - - useEffect(() => { - setTimeout(() => { - if (ssoUrl !== undefined && ssoUrl !== null && ssoUrl.length > 0) { - //id="sso_button" - const ssoBtn = document.getElementById("sso_button"); - if (ssoBtn !== undefined && ssoBtn !== null) { - //console.log("SSO BTN: ", ssoBtn) - const cursearch = typeof window === "undefined" || window.location === undefined ? "" : window.location.search; - var tmpView = new URLSearchParams(cursearch).get("autologin"); - if (tmpView !== undefined && tmpView !== null) { - if (tmpView === "true") { - console.log("Tmp: ", tmpView) - ssoBtn.click() - } - } - } - } - }, 200); - }, [ssoUrl]) - - return
{loadedCheck}
; + {features.map((feature, index) => ( +
+ + {feature} +
+ ))} +
+ ); }; -export default LoginDialog; +const MarketplaceCard = ({ classes }) => { + const marketplaceOptions = [ + { + name: "Amazon Web Services", + logo: "https://cdn.cdnlogo.com/logos/a/19/aws.svg", + tooltipText: "Coming soon to AWS Marketplace!", + valid: false, + }, + { + name: "Microsoft Azure", + logo: "https://cdn.cdnlogo.com/logos/a/12/azure.svg", + tooltipText: "Coming soon to Azure Marketplace!", + valid: false, + }, + { + name: "Google Cloud Platform", + logo: "https://cdn.cdnlogo.com/logos/g/75/google-cloud.svg", + tooltipText: "Coming soon to Google Cloud Marketplace!", + valid: false, + } + ]; + + return ( +
+

+ Self Host +

+ + {marketplaceOptions.map((option, index) => ( + + + + ))} + + + +
+ ); +}; + + +const LoginPage = props => { + const { globalUrl, isLoaded, isLoggedIn, setIsLoggedIn, setCookie, inregister, serverside } = props; + let navigate = useNavigate(); + const [username, setUsername] = useState(""); + const [password, setPassword] = useState(""); + const [message, setMessage] = useState(""); + const [loginLoading, setLoginLoading] = useState(false); + + const [MFAField, setMFAField] = useState(false); + const [MFAValue, setMFAValue] = useState(""); + const [register, setRegister] = useState(inregister); + const [checkboxClicked, setCheckboxClicked] = useState(false); + const [loginWithSSO, setLoginWithSSO] = useState(false) + + const isCloud = window.location.host === "localhost:3002" || window.location.host === "shuffler.io" || window.location.host === "migration.shuffler.io"; + const parsedsearch = serverside === true ? "" : window.location.search + if (serverside !== true) { + const tmpMessage = new URLSearchParams(window.location.search).get("message") + if (tmpMessage !== undefined && tmpMessage !== null && message !== tmpMessage) { + setMessage(tmpMessage) + } + } + + if (document !== undefined) { + if (register) { + document.title = "Login to Shuffle SaaS" + } else { + document.title = "Register to Shuffle SaaS" + } + } + + // Just a way to force location loading properly + // Register & login should be split :3 + if (window !== undefined) { + const path = window.location.pathname; + if (path.includes("/login") && register === false) { + console.log("Should register instead of login!") + setRegister(!register) + } else if (path.includes("/register") && register === true) { + if (!isCloud) { + setRegister(true) + navigate("/login") + } + + console.log("Should login instead of register!") + setRegister(!register) + } else { + console.log("Path: " + path, "Register: " + register) + } + } + + const bodyDivStyle = { + marginTop: 100, + width: isMobile ? "100%" : "100%", + maxWidth: "1200px", // Increased max-width to accommodate larger cards + background: "#1A1A1A", + margin: "auto", + display: isMobile ? "block" : "flex", + padding: "40px", + gap: "40px", + overflow: "hidden", + alignItems: "center" + }; + + const boxStyle = { + color: "white", + padding: "40px", + flex: 1, + maxWidth: isMobile ? "100%" : "550px", + background: "#212121", + borderRadius: "12px", + display: "flex", + // flexDirection: "column", + }; + + + + const paperStyleReg = { + width: 300, + height: 350, + background: "#212121", + borderRadius: "8px", + marginLeft: isMobile ? 80 : register ? "455px" : "485px", + marginTop: isMobile ? 10 : 110, + position: isMobile ? "" : "absolute", + + } + const paperStyleLog = { + position: "absolute", + width: isMobile ? "400px" : "532px", + padding: "0px 30px 0px", + // marginTop: "135px", + // background: "#212121", + borderRadius: "8px", + } + const createData = (icon, title) => { + return { + icon, + title, + } + } + + // Used to swap from login to register. True = login, false = register + const activeIcon = + const rows = [ + createData(activeIcon, "Access to All Apps"), + createData(activeIcon, "10,000 App Runs"), + createData(activeIcon, "Monthly Runs Refresh"), + createData(activeIcon, "Unlimited Users & Workflows"), + createData(activeIcon, "Multi-Tenancy & -Region"), + createData(activeIcon, "Support & Discord Access"), + ]; + const classes = useStyles(); + // Error messages etc + const [loginInfo, setLoginInfo] = useState(""); + + const handleValidateForm = (username, password) => { + if (loginWithSSO) { + return username.length > 1 + } + + if (!isCloud) { + return (username.length > 0 && password.length > 0); + } + + return (username.length > 1 && password.length > 8); + } + + if (isLoggedIn === true && serverside !== true) { + const tmpView = new URLSearchParams(window.location.search).get("view") + if (tmpView !== undefined && tmpView !== null && tmpView === "pricing") { + window.location.pathname = "/pricing" + return + } else if (tmpView !== undefined && tmpView !== null) { + window.location.pathname = tmpView + return + } + + window.location.pathname = "/workflows" + } + + const onSubmit = (e) => { + //toast("Testing from login page") + + setMessage("") + setLoginLoading(true) + e.preventDefault() + + // Just use this one? + var data = { "username": username, "password": password } + if (MFAValue !== undefined && MFAValue !== null && MFAValue.length > 0) { + data["mfa_code"] = MFAValue + } + + localStorage.setItem("globalUrl", "") + + var baseurl = globalUrl + if (register) { + var url = baseurl + '/api/v1/login'; + + if (loginWithSSO === true) { + url = baseurl + '/api/v1/login/sso' + setLoginInfo("Logging in with SSO. Please wait while we find a relevant 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) => { + if (response.status !== 200) { + console.log("Status not 200 for login:O!"); + } + + return response.json(); + }) + .then((responseJson) => { + + setLoginLoading(false) + + console.log("Resp from backend: ", responseJson) + + if (responseJson["success"] === false) { + setLoginInfo(responseJson["reason"]) + } + else { + + if (responseJson["reason"] === "MFA_REDIRECT") { + setLoginInfo("Enter the 6-digit MFA code.") + setMFAField(true) + return + + } + else if (responseJson["reason"] === "MFA_SETUP") { + window.location.href = `/login/${responseJson.url}/mfa-setup`; + return; + } + else if (responseJson["reason"] === "SSO_REDIRECT") { + //navigate(responseJson["url"]) + window.location.href = responseJson["url"] + return + + } + else if (responseJson["reason"] !== undefined && responseJson["reason"] !== null && responseJson["reason"].includes("error")) { + setLoginInfo(responseJson["reason"]) + return + } + + + setLoginInfo("Successful login! Redirecting you in 3 seconds...") + for (var key in responseJson["cookies"]) { + setCookie(responseJson["cookies"][key].key, responseJson["cookies"][key].value, { path: "/" }) + } + + const tmpView = new URLSearchParams(window.location.search).get("view") + if (tmpView !== undefined && tmpView !== null) { + //const newUrl = `/${tmpView}${decodeURIComponent(window.location.search)}` + // Check if slash in the url + + var newUrl = `/${tmpView}` + if (tmpView.startsWith("/")) { + newUrl = `${tmpView}` + } + + console.log("Found url: ", newUrl) + + window.location.pathname = newUrl + return + } + + console.log("LOGIN DATA: ", responseJson) + if (responseJson.tutorials !== undefined && responseJson.tutorials !== null) { + // Find welcome in responseJson.tutorials under key name + const welcome = responseJson.tutorials.find(function (element) { + return element.name === "welcome"; + }) + + console.log("Welcome: ", welcome) + if (welcome === undefined || welcome === null) { + console.log("RUN login Welcome!!") + // window.location.pathname = "/welcome?tab=2" + // window.location = "/welcome?tab=2" + window.location.href = "/welcome?tab=2" + return + } + } + + window.location.pathname = "/workflows" + } + }) + .catch(error => { + setLoginInfo("Error from login API: " + error) + setLoginLoading(false) + }); + } else { + url = baseurl + '/api/v1/register'; + fetch(url, { + method: 'POST', + body: JSON.stringify(data), + headers: { + 'Content-Type': 'application/json', + }, + }) + .then(response => + response.json().then(responseJson => { + if (responseJson["success"] === false) { + setLoginInfo(responseJson["reason"]) + } else { + if (responseJson["reason"] === "shuffle_account") { + window.location.href = "/login?message=Please+login+with+your+Shuffle+account" + return + } + + //setLoginInfo("Successful register!") + //var newpath = "/login?message=Successfully signed up. You can now sign in." + //const tmpMessage = new URLSearchParams(window.location.search).get("message") + setLoginInfo("Successful registration! Redirecting in 3 seconds...") + for (var key in responseJson["cookies"]) { + setCookie(responseJson["cookies"][key].key, responseJson["cookies"][key].value, { path: "/" }) + } + + setTimeout(() => { + console.log("LOGIN DATA: ", responseJson) + + const tmpView = new URLSearchParams(window.location.search).get("view") + if (tmpView !== undefined && tmpView !== null) { + //const newUrl = `/${tmpView}${decodeURIComponent(window.location.search)}` + const newUrl = `/${tmpView}` + window.location.pathname = newUrl + return + } + + //if (responseJson.tutorials === undefined || responseJson.tutorials === null || !responseJson.tutorials.includes("welcome")) { + console.log("RUN Welcome!!") + //window.location.pathname = "/welcome?tab=2" + window.location.href = "/welcome" + }, 1500); + } + setLoginLoading(false) + }), + ) + .catch(error => { + setLoginInfo("Error in login. Please try again, or contact support@shuffler.io if the problem persists.") + setLoginLoading(false) + }); + } + } + + const onChangeUser = (e) => { + setUsername(e.target.value) + } + + const onChangePass = (e) => { + setPassword(e.target.value) + } + + const HandleLoginWithSSO = () => { + setPassword("") + setLoginInfo("") + setLoginWithSSO(true) + } + + //const onClickRegister = () => { + // if (props.location.pathname === "/login") { + // window.location.pathname = "/register" + // } else { + // window.location.pathname = "/login" + // } + + // setLoginCheck(!register) + //} + + //var loginChange = register ? (

Want to register? Click here.

) : (

Go back to login? Click here.

); + var formtitle = register ?
Welcome Back!
:
Create your account
+ var formButton = !isCloud ? "" : register ?
Don’t have an account yet?
Register here
: <> +
Already have an account?
Login here
+ + //
Click here to Login
+ + // {formtitle} + + const buttonBackground = "linear-gradient(89.83deg, #FF8444 0.13%, #F2643B 99.84%)" + + const buttonStyle = { borderRadius: 25, height: 50, fontSize: 18, backgroundImage: handleValidateForm(username, password) || loginLoading || (checkboxClicked && register) ? buttonBackground : "grey", color: "white" } + // +
+ +
+
+ {formButton} +
+
+ +
+ {loginInfo} +
+ + + + {isMobile ? null : ( + <> +
+ OR +
+ + + )} +
+ ); + + const loadedCheck = isLoaded ? +
+ {basedata} +
+ : +
+
+ + return ( +
+ {loadedCheck} +
+ ) +} + +export default LoginPage; diff --git a/frontend/src/views/LoginPageOld.jsx b/frontend/src/views/LoginPageOld.jsx new file mode 100755 index 00000000..aaf376cd --- /dev/null +++ b/frontend/src/views/LoginPageOld.jsx @@ -0,0 +1,546 @@ +/* eslint-disable react/no-multi-comp */ +import React, { useState, useEffect } from "react"; +import { makeStyles } from "@mui/styles"; +import { useInterval } from "react-powerhooks"; +import theme from '../theme.jsx'; + +import { + CircularProgress, + TextField, + Button, + Paper, + Typography, +} from "@mui/material"; + +import { useNavigate } from "react-router-dom"; + +const hrefStyle = { + color: "white", + textDecoration: "none", +}; + +const bodyDivStyle = { + margin: "auto", + marginTop: 150, + width: "500px", +}; + +const useStyles = makeStyles({ + notchedOutline: { + borderColor: "#f85a3e !important", + }, +}); + +const LoginDialog = (props) => { + const { + globalUrl, + isLoaded, + isLoggedIn, + setIsLoggedIn, + setCookie, + register, + checkLogin, + } = props; + + let navigate = useNavigate(); + const classes = useStyles(); + + const [username, setUsername] = useState(""); + const [password, setPassword] = useState(""); + const [firstRequest, setFirstRequest] = useState(true); + const [loginLoading, setLoginLoading] = useState(false); + const [loginViewLoading, setLoginViewLoading] = useState(false); + const [ssoUrl, setSSOUrl] = useState(""); + + const [MFAField, setMFAField] = useState(false); + const [MFAValue, setMFAValue] = useState(""); + + + // Used to swap from login to register. True = login, false = register + + useEffect(() => { + checkAdmin() + }, [loginViewLoading]) + + // Error messages etc + const [loginInfo, setLoginInfo] = useState(""); + + const handleValidateForm = () => { + return username.length > 1 && password.length > 1; + }; + + if (isLoggedIn === true) { + //window.location.pathname = "/workflows"; + navigate("/workflows") + } + + const checkAdmin = () => { + const url = globalUrl + "/api/v1/checkusers"; + fetch(url, { + method: "GET", + headers: { + "Content-Type": "application/json", + }, + }) + .then((response) => + response.json().then((responseJson) => { + if (responseJson["success"] === false) { + setLoginInfo(responseJson["reason"]); + } else { + + if (responseJson.sso_url !== undefined && responseJson.sso_url !== null) { + setSSOUrl(responseJson.sso_url); + } + + if (loginViewLoading) { + setLoginViewLoading(false); + checkLogin(); + stop(); + + if ( + responseJson.reason !== undefined && + responseJson.reason !== null + ) { + setLoginInfo(responseJson.reason); + } + } + + if (responseJson.reason === "stay") { + navigate("/adminsetup") + } + } + }) + ) + .catch((error) => { + if (!loginViewLoading) { + setLoginViewLoading(true); + start(); + } + }); + }; + + const { start, stop } = useInterval({ + duration: 3000, + startImmediate: false, + callback: () => { + checkAdmin(); + }, + }); + + if (firstRequest) { + setFirstRequest(false); + checkAdmin(); + } + + const onSubmit = (e) => { + setLoginLoading(true); + e.preventDefault(); + setLoginInfo(""); + // FIXME - add some check here ROFL + + // Just use this one? + var data = { username: username, password: password }; + if (MFAValue !== undefined && MFAValue !== null && MFAValue.length > 0) { + data["mfa_code"] = MFAValue; + } + + var baseurl = globalUrl; + if (register) { + var url = baseurl + "/api/v1/login"; + 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) => { + setLoginLoading(false); + if (responseJson["success"] === false) { + setLoginInfo(responseJson["reason"]); + } else { + if (responseJson["reason"] === "MFA_REDIRECT") { + setLoginInfo( + "MFA required. Please enter the 6-digit code from your authenticator" + ); + setMFAField(true); + return; + } else if (responseJson["reason"] === "MFA_SETUP") { + window.location.href = `/login/${responseJson.url}/mfa-setup`; + return; + } + + setLoginInfo("Successful login, rerouting"); + for (var key in responseJson["cookies"]) { + setCookie( + responseJson["cookies"][key].key, + responseJson["cookies"][key].value, + { path: "/" } + ); + } + + if (responseJson.tutorials === undefined || responseJson.tutorials === null || !responseJson.tutorials.includes("welcome")) { + console.log("RUN Welcome!!") + setTimeout(() => { + navigate("/welcome?tab=2") + },200) + // window.location.pathname = "" + return + } + + const tmpView = new URLSearchParams(window.location.search).get("view") + if (tmpView !== undefined && tmpView !== null) { + //const newUrl = `/${tmpView}${decodeURIComponent(window.location.search)}` + const newUrl = `/${tmpView}` + window.location.pathname = newUrl + } else { + window.location.pathname = "/workflows" + } + + setIsLoggedIn(true); + } + }) + ) + .catch((error) => { + setLoginLoading(false); + setLoginInfo("Error logging in: " + error); + }); + } else { + url = baseurl + "/api/v1/users/register"; + fetch(url, { + method: "POST", + body: JSON.stringify(data), + headers: { + "Content-Type": "application/json", + }, + }) + .then((response) => + response.json().then((responseJson) => { + if (responseJson["success"] === false) { + setLoginInfo(responseJson["reason"]); + } else { + setLoginInfo("Successful register!"); + } + }) + ) + .catch((error) => { + setLoginInfo("Error in from backend: ", error); + }); + } + }; + + const onChangeUser = (e) => { + setUsername(e.target.value); + }; + + const onChangePass = (e) => { + setPassword(e.target.value); + }; + + //const onClickRegister = () => { + // if (props.location.pathname === "/login") { + // window.location.pathname = "/register" + // } else { + // window.location.pathname = "/login" + // } + + // setLoginCheck(!register) + //} + + //var loginChange = register ? (

Want to register? Click here.

) : (

Go back to login? Click here.

); + var formtitle = register ?
Login
:
Register
; + const imgsize = 100; + const basedata = ( +
+ +
+ +
+ + {loginViewLoading ? ( +
+ + Waiting for the Shuffle database to become available. This may + take up to a minute. + + + {loginInfo === undefined || + loginInfo === null || + loginInfo.length === 0 ? null : ( +
Database Response: {loginInfo}
+ )} + + + + + + Are you sure Shuffle is{" "} + + installed correctly + + ? + + + + 1. Make sure shuffle-database folder has correct access, and that you have a minimum of 2Gb of RAM available:{" "} +
+
+ sudo chown -R 1000:1000 shuffle-database +
+ + 2. Disable memory swap on the host: +
+
+ sudo swapoff -a +
+ + 3. Restart the database: +
+
+ sudo docker restart shuffle-opensearch +
+
+ + Need help?{" "} + + Join the Discord! + + +
+ ) : ( +
+

{formtitle}

+ Username +
+ +
+ Password +
+ +
+ {MFAField === true ? ( +
+ 2-factor code + { + setMFAValue(event.target.value); + }} + /> +
+ ) : null} +
+ +
+
{loginInfo}
+ {ssoUrl !== undefined && ssoUrl !== null && ssoUrl.length > 0 ? ( +
+ Or +
+ +
+
+ ) : null} +
+ )} +
+
+ ); + + const loadedCheck = isLoaded ?
{basedata}
:
; + + useEffect(() => { + setTimeout(() => { + if (ssoUrl !== undefined && ssoUrl !== null && ssoUrl.length > 0) { + //id="sso_button" + const ssoBtn = document.getElementById("sso_button"); + if (ssoBtn !== undefined && ssoBtn !== null) { + //console.log("SSO BTN: ", ssoBtn) + const cursearch = typeof window === "undefined" || window.location === undefined ? "" : window.location.search; + var tmpView = new URLSearchParams(cursearch).get("autologin"); + if (tmpView !== undefined && tmpView !== null) { + if (tmpView === "true") { + console.log("Tmp: ", tmpView) + ssoBtn.click() + } + } + } + } + }, 200); + }, [ssoUrl]) + + return
{loadedCheck}
; +}; + +export default LoginDialog; From 4a95979373ed91b0156533030a0ad4f37a53392e Mon Sep 17 00:00:00 2001 From: monilprajapati Date: Tue, 4 Mar 2025 00:13:31 +0530 Subject: [PATCH 05/18] Added New navbar --- .../public/images/ProfessionalServices.svg | 6 + .../public/images/SecurityConsultation.svg | 5 + frontend/public/images/Support.svg | 3 + frontend/public/images/Training.svg | 6 + frontend/public/images/icons/API.svg | 3 + frontend/public/images/icons/about_us.svg | 6 + .../public/images/icons/about_us_hover.svg | 6 + frontend/public/images/icons/articles.svg | 7 + .../public/images/icons/articles_hover.svg | 7 + frontend/public/images/icons/contact_us.svg | 3 + .../public/images/icons/contact_us_hover.svg | 3 + frontend/public/images/icons/discord.svg | 10 + frontend/public/images/icons/docs.svg | 4 + frontend/public/images/icons/docs_hover.svg | 4 + frontend/public/images/icons/faq.svg | 5 + frontend/public/images/icons/faq_hover.svg | 5 + frontend/public/images/icons/github.svg | 11 + frontend/public/images/icons/linkedIn.svg | 10 + frontend/public/images/icons/usecases.svg | 6 + .../public/images/icons/usecases_hover.svg | 6 + frontend/public/images/icons/x.svg | 10 + frontend/public/images/logos/singul.svg | 22 + frontend/src/App.jsx | 16 +- frontend/src/views/LoginPage.jsx | 4 +- frontend/src/views/Navbar.jsx | 2580 +++++++++++++++++ 25 files changed, 2745 insertions(+), 3 deletions(-) create mode 100644 frontend/public/images/ProfessionalServices.svg create mode 100644 frontend/public/images/SecurityConsultation.svg create mode 100644 frontend/public/images/Support.svg create mode 100644 frontend/public/images/Training.svg create mode 100644 frontend/public/images/icons/API.svg create mode 100644 frontend/public/images/icons/about_us.svg create mode 100644 frontend/public/images/icons/about_us_hover.svg create mode 100644 frontend/public/images/icons/articles.svg create mode 100644 frontend/public/images/icons/articles_hover.svg create mode 100644 frontend/public/images/icons/contact_us.svg create mode 100644 frontend/public/images/icons/contact_us_hover.svg create mode 100644 frontend/public/images/icons/discord.svg create mode 100644 frontend/public/images/icons/docs.svg create mode 100644 frontend/public/images/icons/docs_hover.svg create mode 100644 frontend/public/images/icons/faq.svg create mode 100644 frontend/public/images/icons/faq_hover.svg create mode 100644 frontend/public/images/icons/github.svg create mode 100644 frontend/public/images/icons/linkedIn.svg create mode 100644 frontend/public/images/icons/usecases.svg create mode 100644 frontend/public/images/icons/usecases_hover.svg create mode 100644 frontend/public/images/icons/x.svg create mode 100644 frontend/public/images/logos/singul.svg create mode 100644 frontend/src/views/Navbar.jsx diff --git a/frontend/public/images/ProfessionalServices.svg b/frontend/public/images/ProfessionalServices.svg new file mode 100644 index 00000000..164b6917 --- /dev/null +++ b/frontend/public/images/ProfessionalServices.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/frontend/public/images/SecurityConsultation.svg b/frontend/public/images/SecurityConsultation.svg new file mode 100644 index 00000000..05614a4d --- /dev/null +++ b/frontend/public/images/SecurityConsultation.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/frontend/public/images/Support.svg b/frontend/public/images/Support.svg new file mode 100644 index 00000000..83a18f00 --- /dev/null +++ b/frontend/public/images/Support.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/public/images/Training.svg b/frontend/public/images/Training.svg new file mode 100644 index 00000000..b01a9901 --- /dev/null +++ b/frontend/public/images/Training.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/frontend/public/images/icons/API.svg b/frontend/public/images/icons/API.svg new file mode 100644 index 00000000..447b94de --- /dev/null +++ b/frontend/public/images/icons/API.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/public/images/icons/about_us.svg b/frontend/public/images/icons/about_us.svg new file mode 100644 index 00000000..c77e7e8e --- /dev/null +++ b/frontend/public/images/icons/about_us.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/frontend/public/images/icons/about_us_hover.svg b/frontend/public/images/icons/about_us_hover.svg new file mode 100644 index 00000000..2c693ca9 --- /dev/null +++ b/frontend/public/images/icons/about_us_hover.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/frontend/public/images/icons/articles.svg b/frontend/public/images/icons/articles.svg new file mode 100644 index 00000000..763d960f --- /dev/null +++ b/frontend/public/images/icons/articles.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/frontend/public/images/icons/articles_hover.svg b/frontend/public/images/icons/articles_hover.svg new file mode 100644 index 00000000..6b51f294 --- /dev/null +++ b/frontend/public/images/icons/articles_hover.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/frontend/public/images/icons/contact_us.svg b/frontend/public/images/icons/contact_us.svg new file mode 100644 index 00000000..5fbc3568 --- /dev/null +++ b/frontend/public/images/icons/contact_us.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/public/images/icons/contact_us_hover.svg b/frontend/public/images/icons/contact_us_hover.svg new file mode 100644 index 00000000..0da02b7c --- /dev/null +++ b/frontend/public/images/icons/contact_us_hover.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/public/images/icons/discord.svg b/frontend/public/images/icons/discord.svg new file mode 100644 index 00000000..b982260b --- /dev/null +++ b/frontend/public/images/icons/discord.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/frontend/public/images/icons/docs.svg b/frontend/public/images/icons/docs.svg new file mode 100644 index 00000000..a568538d --- /dev/null +++ b/frontend/public/images/icons/docs.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/public/images/icons/docs_hover.svg b/frontend/public/images/icons/docs_hover.svg new file mode 100644 index 00000000..a711ba59 --- /dev/null +++ b/frontend/public/images/icons/docs_hover.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/public/images/icons/faq.svg b/frontend/public/images/icons/faq.svg new file mode 100644 index 00000000..7345040f --- /dev/null +++ b/frontend/public/images/icons/faq.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/frontend/public/images/icons/faq_hover.svg b/frontend/public/images/icons/faq_hover.svg new file mode 100644 index 00000000..4d29215a --- /dev/null +++ b/frontend/public/images/icons/faq_hover.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/frontend/public/images/icons/github.svg b/frontend/public/images/icons/github.svg new file mode 100644 index 00000000..de6c0200 --- /dev/null +++ b/frontend/public/images/icons/github.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/frontend/public/images/icons/linkedIn.svg b/frontend/public/images/icons/linkedIn.svg new file mode 100644 index 00000000..605e7804 --- /dev/null +++ b/frontend/public/images/icons/linkedIn.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/frontend/public/images/icons/usecases.svg b/frontend/public/images/icons/usecases.svg new file mode 100644 index 00000000..cc96e086 --- /dev/null +++ b/frontend/public/images/icons/usecases.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/frontend/public/images/icons/usecases_hover.svg b/frontend/public/images/icons/usecases_hover.svg new file mode 100644 index 00000000..6841b21e --- /dev/null +++ b/frontend/public/images/icons/usecases_hover.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/frontend/public/images/icons/x.svg b/frontend/public/images/icons/x.svg new file mode 100644 index 00000000..815e4e30 --- /dev/null +++ b/frontend/public/images/icons/x.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/frontend/public/images/logos/singul.svg b/frontend/public/images/logos/singul.svg new file mode 100644 index 00000000..5f27d7a3 --- /dev/null +++ b/frontend/public/images/logos/singul.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 9fc34790..cd8d49d5 100755 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -62,6 +62,7 @@ import Drift from "react-driftjs"; import { AppContext } from './context/ContextApi.jsx'; import Workflows2 from "./views/Workflows2.jsx"; import AppExplorer from "./views/AppExplorer.jsx"; +import Navbar from "./views/Navbar.jsx"; // Production - backend proxy forwarding in nginx var globalUrl = window.location.origin; @@ -213,7 +214,20 @@ const App = (message, props) => { { window?.location?.pathname === "/" || window?.location?.pathname === "/training" || !(isLoggedIn && isLoaded) ? (
-
*/} + {
{ + const theme = useTheme(); + return ( + + + + + + + ); +}; + +// Add this new component for mobile menu +const MobileMenu = ({ anchorEl, handleClose, isLoggedIn, navigate, isCloud }) => { + const [openSection, setOpenSection] = useState(null); + const theme = useTheme(); + + // Add useEffect to handle body scroll + useEffect(() => { + if (Boolean(anchorEl)) { + // Disable scroll + document.body.style.overflow = 'hidden'; + } else { + // Re-enable scroll + document.body.style.overflow = 'auto'; + } + + // Cleanup function to ensure scroll is re-enabled when component unmounts + return () => { + document.body.style.overflow = 'auto'; + }; + }, [anchorEl]); + + const handleSectionClick = (section) => { + setOpenSection(openSection === section ? null : section); + }; + + const handleItemClick = (path) => { + handleClose(); + navigate(path); + }; + + const showDesktopToast = () => { + toast.info("Please open on desktop for the full experience"); + handleClose(); + }; + + return ( + + + {Object.keys(menuData).map((section) => ( + + + + + {section === 'Resources' ? ( + + {menuData.Resources.columns.map((column, index) => ( + + + {column.title} + + + {column.social ? ( + <> + + {column.platforms.map((platform) => ( + { + if (isCloud) { + ReactGA.event(platform.gaData); + } + window.open(platform.link, '_blank') + return; + }} + // onClick={() => showDesktopToast()} + > + {platform.name} + + ))} + + + Follow Us + + + {column.followUs.map((platform) => ( + { + if (isCloud) { + ReactGA.event(platform.gaData); + } + window.open(platform.link, '_blank') + return; + }} + > + {platform.name} + + ))} + + + ) : ( + column.items.map((item) => ( + + )) + )} + + ))} + + ) : ( + + {menuData[section].map((item) => ( + + ))} + + )} + + + ))} + + {/* Static menu items */} + + + + {/* Action buttons */} + + + + + + + ); +}; + +const Navbar = (props) => { + const { + globalUrl, + isLoaded, + isLoggedIn, + removeCookie, + homePage, + userdata, + // isMobile, + serverside, + billingInfo, + + notifications, + } = props; + const theme = useTheme(); + const [searchBarModalOpen, setSearchBarModalOpen] = useState(false); + const [pricingModalOpen, setPricingModalOpen] = useState(false); + const isTabletOrMobile = useMediaQuery(theme.breakpoints.down("lg")); + const isMobile = useMediaQuery(theme.breakpoints.down("md")); + const [anchorElNav, setAnchorElNav] = useState(null); + const [openMenu, setOpenMenu] = useState(null); + const [selectedOrganization, setSelectedOrganization] = useState({}); + const navigate = useNavigate(); + const [anchorElUser, setAnchorElUser] = useState(null); + const [anchorElOrg, setAnchorElOrg] = useState(null); + const [isOrgChanging, setIsOrgChanging] = useState(false); + const [isHovered, setIsHovered] = useState(""); + const [orgSelectOpen, setOrgSelectOpen] = useState(false); + const [showTopbar, setShowTopbar] = useState(true) // Set to true to show top bar + const isCloud = + serverside === true || typeof window === "undefined" + ? true + : window.location.host === "localhost:3002" || + window.location.host === "shuffler.io" || + window.location.host === "localhost:5002"; + + + const stripeKey = typeof window === 'undefined' || window.location === undefined ? "" : window.location.origin === "https://shuffler.io" ? "pk_live_51PXYYMEJjT17t98N20qEqItyt1fLQjrnn41lPeG2PjnSlZHTDNKHuisAbW00s4KAn86nGuqB9uSVU4ds8MutbnMU00DPXpZ8ZD" : "pk_test_51PXYYMEJjT17t98NbDkojZ3DRvsFUQBs35LGMx3i436BXwEBVFKB9nCvHt0Q3M4MG3dz4mHheuWvfoYvpaL3GmsG00k1Rb2ksO" + + const topbar_var = "topbar_closed5" + useEffect(() => { + // Manually setShowTopbar(true) to show topbar by default + const topbar = localStorage.getItem(topbar_var) + if (topbar === "true") { + setShowTopbar(false) + } + }, []) + + const pricingModal = + { + setPricingModalOpen(false); + }} + PaperProps={{ + style: { + color: "white", + minWidth: 850, + minHeight: 370, + padding: 20, + backgroundColor: "rgba(0, 0, 0, 1)", + borderRadius: theme.palette?.borderRadius, + }, + }} + > + + + Upgrade your plan + + { + if (isCloud) { + ReactGA.event({ + category: "navbar", + action: "close_upgrade_modal", + label: "navbar_upgrade_modal", + }) + }; + setPricingModalOpen(false); + }} + style={{ + marginLeft: "auto", + position: "absolute", + top: 20, + right: 20, + }} + > + + + +
+ +
+
+ + + const modalView = ( + { + setSearchBarModalOpen(false); + }} + PaperProps={{ + style: { + color: "white", + minWidth: 750, + height: 785, + borderRadius: 16, + border: "1px solid var(--Container-Stroke, #494949)", + background: "var(--Container, #000000)", + boxShadow: "0px 16px 24px 8px rgba(0, 0, 0, 0.25)", + }, + }} + sx={{ + zIndex: 50005, + '& .MuiBackdrop-root': { + backgroundColor: 'rgba(0, 0, 0, 0.8)', + }, + }} + > + + + Search for Docs, Apps, Workflows and more + + setSearchBarModalOpen(false)} + sx={{ + color: 'white', + '&:hover': { + backgroundColor: 'rgba(255, 255, 255, 0.1)' + } + }} + > + + + + + + + + + + + ); + + const handleOpenNavMenu = (event) => { + setAnchorElNav(event.currentTarget); + }; + + const handleCloseNavMenu = () => { + setAnchorElNav(null); + }; + + const handleMenuOpen = (menu) => { + setOpenMenu(menu); + }; + + const handleMenuClose = () => { + setOpenMenu(null); + }; + + const handleOpenUserMenu = (event) => { + setAnchorElUser(event.currentTarget); + }; + + const handleCloseUserMenu = () => { + setAnchorElUser(null); + }; + + const handleOpenOrgMenu = (event) => { + setAnchorElOrg(event.currentTarget); + }; + + const handleCloseOrgMenu = () => { + setAnchorElOrg(null); + }; + + const menuStyles = { + "& .MuiPaper-root": { + backgroundColor: "#212121", + marginTop: 1, + backdropFilter: "blur(10px)", + fontFamily: theme.typography.fontFamily, + }, + }; + + const menuItemBox = { + display: "flex", + alignItems: "flex-start", + padding: 2, + paddingTop: "20px", + paddingBottom: "20px", + paddingLeft: "20px", + gap: 2, + width: "100%", + backgroundColor: "#212121", + transition: "all 0.2s ease-in-out", + borderBottom: "1px solid #494949", + }; + + const buttonStyles = { + fontFamily: theme.typography.fontFamily, + color: "white", + textTransform: "none", + fontSize: "16px", + fontWeight: 400, + padding: "6px 12px", + "& .MuiSvgIcon-root": { + transition: "transform 0.2s ease-in-out", + }, + "&:hover": { + backgroundColor: "transparent", + color: theme.palette.primary.main, + "& .MuiSvgIcon-root": { + transform: "rotate(180deg)", + }, + }, + }; + + // Render menu content based on type + const renderMenuContent = (item, isCloud) => { + if (item === "Resources") { + return ( + + {menuData.Resources.columns.map((column, index) => ( + + + {column.title} + + + {column.social ? ( + <> + + {column.platforms.map((platform) => ( + { + if (isCloud) { + ReactGA.event(platform.gaData); + } + handleMenuClose(); + }} + sx={{ + padding: 0, + width: 48, + height: 48, + "&:hover": { + backgroundColor: "transparent", + opacity: 0.8, + }, + }} + > + {platform.name} + + ))} + + + Follow Us + + + {column.followUs.map((platform) => ( + { + if (isCloud) { + ReactGA.event(platform.gaData); + } + handleMenuClose(); + }} + sx={{ + padding: 0, + width: 48, + height: 48, + "&:hover": { + backgroundColor: "transparent", + opacity: 0.8, + }, + }} + > + {platform.name} + + ))} + + + ) : ( + + {column.items.map((item) => ( + + ))} + + )} + + ))} + + ); + } + + return menuData[item].map((menuItem, index) => ( + { + if (menuItem.title === "Singul") { + window.open(menuItem.path, '_blank'); + return; + } + if(isCloud) { + ReactGA.event(menuItem.gaData); + } + handleCloseNavMenu(); + handleMenuClose(); + if(isCloud) { + navigate(menuItem.path); + } else { + if(menuItem.path.includes("docs")){ + navigate(menuItem.path); + handleMenuClose(); + }else{ + window.open("https://shuffler.io" + menuItem.path, '_blank'); + return; + } + } + }} + sx={{ + padding: 0, + backgroundColor: "#212121", + "&:hover": { + backgroundColor: "#212121", + }, + "&:last-child": { + borderBottomLeftRadius: "4px", + borderBottomRightRadius: "4px", + }, + "&:first-of-type": { + paddingTop: "4px", + }, + }} + > + + + + + + {menuItem.title} + + {menuItem.title === "Singul" && ( + + Coming Soon + + )} + + + {menuItem.description} + + + + + )); + }; + + const renderDesktopMenu = () => ( + + {Object.keys(menuData).map((item) => ( + + + + {/* Custom Dropdown */} + handleMenuOpen(item)} + onMouseLeave={handleMenuClose} + sx={{ + position: "absolute", + top: "130%", + left: + item === "Products" + ? 0 + : item === "Services" + ? -50 + : item === "Resources" + ? isTabletOrMobile ? -330 : -250 + : 0, + backgroundColor: "#212121", + borderTopLeftRadius: 0, + borderTopRightRadius: 0, + borderBottomLeftRadius: "4px", + borderBottomRightRadius: "4px", + boxShadow: + "0 8px 16px rgba(0, 0, 0, 0.25), 0 2px 4px rgba(0, 0, 0, 0.1)", + backdropFilter: "blur(10px)", + zIndex: -20, + opacity: openMenu === item ? 1 : 0, + visibility: openMenu === item ? "visible" : "hidden", + transform: openMenu === item ? "translateY(0)" : "translateY(-8px)", + transition: "transform 0.3s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.3s ease", + pointerEvents: openMenu === item ? "auto" : "none", + ...(item === "Resources" && { + width: "900px", + padding: 3, + }), + }} + > + {renderMenuContent(item, isCloud)} + + + ))} + + + + ); + + const menuItemStyles = { + fontFamily: theme.typography.fontFamily, + color: "white", + "&:hover": { + backgroundColor: "rgba(255, 133, 68, 0.1)", + }, + }; + + const searchButtonStyles = { + fontFamily: theme.typography.fontFamily, + color: "rgba(255, 255, 255, 0.7)", + border: "1px solid rgba(26, 26, 26, 0.8)", + backgroundColor: "#2F2F2F", + textTransform: "none", + borderRadius: "8px", + fontSize: "14px", + padding: "6px 12px", + display: "flex", + alignItems: "center", + height: "42px", + gap: 1, + minWidth: "110px", + justifyContent: "space-between", + "&:hover": { + borderColor: "rgba(255, 255, 255, 0.1)", + }, + }; + + const searchIconButtonStyles = { + color: "rgba(255, 255, 255, 0.7)", + padding: "8px", + minWidth: "unset", + height: "42px", + "&:hover": { + backgroundColor: "#3F3F3F", + }, + }; + + // Add this shared button style + const sharedButtonStyles = { + fontFamily: theme.typography.fontFamily, + textTransform: "none", + fontSize: "16px", + fontWeight: 600, + padding: "6px 24px", + }; + + + useEffect(() => { + Mousetrap.bind(['command+k', 'ctrl+k'], () => { + setSearchBarModalOpen(true); + return false; // Prevent the default action + }); + Mousetrap.bind(['esc'], () => { + setSearchBarModalOpen(false); + return false; // Prevent the default action + }); + + return () => { + Mousetrap.unbind(['command+k', 'ctrl+k']); + }; + }, []); + + useEffect(() => { + if (isLoggedIn && userdata?.active_org?.id?.length > 0) { + handleGetOrg(userdata.active_org.id); + } + }, [isLoggedIn]); + + const handleGetOrg = (orgId) => { + 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) { + console.log("Error getting org: ", responseJson); + } else { + setSelectedOrganization(responseJson); + } + }) + .catch((error) => { + console.log("Error getting org: ", error); + }); + }; + + const handleClickLogout = () => { + console.log("SHOULD LOG OUT"); + + toast.info("Logging out..."); + // Don't really care about the logout + fetch(globalUrl + "/api/v1/logout", { + credentials: "include", + method: "POST", + headers: { + "Content-Type": "application/json", + }, + }) + .then(() => { + // Log out anyway + removeCookie("session_token", { path: "/" }); + removeCookie("session_token", { path: "/" }); + removeCookie("session_token", { path: "/" }); + + removeCookie("__session", { path: "/" }); + removeCookie("__session", { path: "/" }); + removeCookie("__session", { path: "/" }); + + removeCookie("__session", { path: "/" }); + + window.location.pathname = "/"; + + localStorage.setItem("globalUrl", "") + + // Delete userinfo from localstorage + localStorage.removeItem("apps") + localStorage.removeItem("workflows") + localStorage.removeItem("userinfo") + }) + .catch((error) => { + console.log(error); + }); + }; + + const topbarHeight = showTopbar ? 40 : 0 + const topbar = !isCloud || !showTopbar ? null : + curpath === "/" || curpath.includes("/docs") || curpath === "/pricing" || curpath === "/contact" || curpath === "/search" || curpath === "/usecases" || curpath === "/usecases2" || curpath === "/training" || curpath === "/professional-services" ? + +
+ + {/* Shuffle 1.4.0 is out! Read more about  */} + New  + + { + ReactGA.event({ + category: "landingpage", + action: "click_header_training", + label: "", + }) + + navigate("/training") + + }} style={{ cursor: "pointer", textDecoration: "none", fontWeight: 600, color: "rgba(255,255,255,0.9)" }}> + Public Training + + +  Dates Released! + + { + setShowTopbar(false) + + // Set storage that it's clicked + localStorage.setItem(topbar_var, "true") + }}> + + +
+
+ : + null + + const handleClickChangeOrg = (orgId) => { + setIsOrgChanging(true); + const data = { org_id: orgId }; + + localStorage.setItem("globalUrl", ""); + localStorage.setItem("getting_started_sidebar", "open"); + toast.info("Changing organization..."); + fetch(`${globalUrl}/api/v1/orgs/${orgId}/change`, { + mode: "cors", + credentials: "include", + crossDomain: true, + method: "POST", + body: JSON.stringify(data), + withCredentials: true, + headers: { + "Content-Type": "application/json; charset=utf-8", + }, + }) + .then(function (response) { + if (response.status !== 200) { + console.log("Error in response"); + } else { + localStorage.removeItem("apps"); + localStorage.removeItem("workflows"); + localStorage.removeItem("userinfo"); + } + + return response.json(); + }) + .then(function (responseJson) { + if (responseJson.success === true) { + if ( + responseJson.region_url !== undefined && + responseJson.region_url !== null && + responseJson.region_url.length > 0 + ) { + console.log("Region Change: ", responseJson.region_url); + localStorage.setItem("globalUrl", responseJson.region_url); + //globalUrl = responseJson.region_url + } + + if (responseJson["reason"] === "SSO_REDIRECT") { + toast.info("Redirecting to SSO login page as SSO is required for this organization."); + setTimeout(() => { + window.location.href = responseJson["url"]; + }, 2000); + } else { + toast("Successfully changed active organization - refreshing!"); + setTimeout(() => { + window.location.reload(); + }, 2000); + } + } else { + setIsOrgChanging(false); + if ( + responseJson.reason !== undefined && + responseJson.reason !== null && + responseJson.reason.length > 0 + ) { + toast(responseJson.reason); + } else { + toast("Failed changing org. Try again or contact support@shuffler.io if this persists."); + } + } + }) + .catch((error) => { + console.log("error changing: ", error); + setIsOrgChanging(false); + }); + }; + + // Add these helper functions from LeftSideBar + const getRegionTag = (region_url) => { + let regiontag = "EU"; + if (region_url !== undefined && region_url !== null && region_url.length > 0) { + const regionsplit = 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"; + } else if (regiontag === "frankfurt") { + regiontag = "EU-2"; + } else if (regiontag === "ca") { + regiontag = "CA"; + } + } + } + return regiontag; + }; + + const getRegionFlag = (region_url) => { + const regionTag = getRegionTag(region_url); + const regionMapping = { + "US": "us", + "EU": "eu", + "EU-2": "de", + "CA": "ca", + "UK": "gb" + }; + + const region = regionMapping[regionTag] || "eu"; + return `https://flagcdn.com/48x36/${region}.png`; + }; + + // Add this useEffect to prevent scroll locking + useEffect(() => { + // Remove modal classes that cause scroll locking + const removeModalClasses = () => { + document.body.style.overflow = 'auto'; + document.body.style.paddingRight = '0px'; + }; + + // Create observer to watch for class changes + const observer = new MutationObserver((mutations) => { + mutations.forEach((mutation) => { + if (mutation.attributeName === 'class') { + if (document.body.classList.contains('MuiModal-open')) { + removeModalClasses(); + } + } + }); + }); + + // Start observing + observer.observe(document.body, { + attributes: true, + attributeFilter: ['class'], + }); + + // Cleanup + return () => { + observer.disconnect(); + removeModalClasses(); + }; + }, []); + + const handleOrgSelectClick = (event) => { + // Prevent the default Select behavior + event.preventDefault(); + if(isCloud){ + ReactGA.event({ + category: "navbar", + action: "org_dropdown_click", + label: "org_dropdown_click", + }) + } + // Toggle the select open state + setOrgSelectOpen(!orgSelectOpen); + }; + + return ( + + {topbar} + + + + { + if (isCloud) { + ReactGA.event({ + category: "navbar", + action: "home_click", + label: "shuffle_logo", + }); + } + }} + > + shuffle logo + + + + {isMobile ? ( + /* Mobile Navigation Icons */ + + setSearchBarModalOpen(true)} + > + + + { + if(Boolean(anchorElNav)) { + handleCloseNavMenu(); + }else{ + handleOpenNavMenu(e); + } + } + } + sx={{ color: "white" }} + > + {Boolean(anchorElNav) ? ( + + ) : ( + + )} + + + + ) : ( + <> + {modalView} + {pricingModal} + {renderDesktopMenu()} + {/* Right Side Buttons */} + + {!isLoaded ? ( + + ) : isLoggedIn ? ( + <> + {isOrgChanging ? ( + + ) : ( + <> + + + + K +
+ } + placement="bottom" + arrow + componentsProps={{ + tooltip: { + sx: { + backgroundColor: "rgba(33, 33, 33, 1)", + color: "rgba(241, 241, 241, 1)", + fontSize: 12, + border: "1px solid rgba(73, 73, 73, 1)", + fontFamily: theme?.typography?.fontFamily, + } + }, + popper: { + sx: { + zIndex: 1000019, + } + } + }} + > + { + if (isCloud) { + ReactGA.event({ + category: "navbar", + action: "search_icon_click", + label: "search_icon_click", + }) + } + setSearchBarModalOpen(true); + }} + > + + + + {isCloud && (userdata.org_status === undefined || userdata.org_status === null || userdata.org_status.length === 0) ? + + : null} + + + {/* Organization Dropdown */} + {/* */} + + {/* User Avatar Menu */} + { + if (isCloud) { + ReactGA.event({ + category: "navbar", + action: "click_user_menu", + label: "open_user_dropdown" + }); + } + handleOpenUserMenu(e); + }} + sx={{ + padding: 0, + "&:hover": { + backgroundColor: "rgba(47, 47, 47, 0.5)", + }, + }} + > + + + + + + + {/* User Info Section */} + + + + + + {userdata?.username} + + + + + + + + {/* Menu Items */} + { + if (isCloud) { + ReactGA.event({ + category: "navbar", + action: "user_dropdown_click", + label: "go_to_admin" + }); + } + handleCloseUserMenu(); + navigate("/admin"); + }} + onMouseEnter={() => setIsHovered("organization")} + onMouseLeave={() => setIsHovered("")} + sx={{ + py: 1.5, + px: 2, + fontFamily: theme.typography.fontFamily, + transition: "color 0.1s ease", + '&:hover': { + backgroundColor: 'rgba(255, 255, 255, 0.1)', + color: isHovered === "organization" ? "#FF8544" : "white", + }, + }} + > + + + + Organization + + + + + { + if (isCloud) { + ReactGA.event({ + category: "navbar", + action: "user_dropdown_click", + label: "go_to_settings", + }) + } + handleCloseUserMenu(); + navigate("/settings"); + }} + onMouseEnter={() => setIsHovered("settings")} + onMouseLeave={() => setIsHovered("")} + sx={{ + py: 1.5, + px: 2, + fontFamily: theme.typography.fontFamily, + transition: "color 0.1s ease", + '&:hover': { + backgroundColor: 'rgba(255, 255, 255, 0.1)', + color: isHovered === "settings" ? "#FF8544" : "white", + }, + }} + > + + + + Settings + + + + + { + if (isCloud) { + ReactGA.event({ + category: "navbar", + action: "user_dropdown_click", + label: "go_to_notifications", + }) + } + handleCloseUserMenu(); + navigate("/admin?admin_tab=notifications"); + }} + onMouseEnter={() => setIsHovered("notifications")} + onMouseLeave={() => setIsHovered("")} + sx={{ + py: 1.5, + px: 2, + fontFamily: theme.typography.fontFamily, + transition: "color 0.1s ease", + '&:hover': { + backgroundColor: 'rgba(255, 255, 255, 0.1)', + color: isHovered === "notifications" ? "#FF8544" : "white", + }, + }} + > + + + + Notifications ({notifications === undefined || notifications === null ? 0 : + notifications?.filter((notification) => notification.read === false).length}) + + + + + + + + { + if (isCloud) { + ReactGA.event({ + category: "navbar", + action: "user_dropdown_click", + label: "go_to_about", + }) + } + handleCloseUserMenu(); + navigate("/docs/about"); + }} + onMouseEnter={() => setIsHovered("about")} + onMouseLeave={() => setIsHovered("")} + sx={{ + py: 1.5, + px: 2, + fontFamily: theme.typography.fontFamily, + transition: "color 0.1s ease", + '&:hover': { + backgroundColor: 'rgba(255, 255, 255, 0.1)', + color: isHovered === "about" ? "#FF8544" : "white", + }, + }} + > + + + + About + + + + + { + if (isCloud) { + ReactGA.event({ + category: "navbar", + action: "user_dropdown_click", + label: "logout_click", + }) + } + handleCloseUserMenu(); + handleClickLogout(); + }} + onMouseEnter={() => setIsHovered("logout")} + onMouseLeave={() => setIsHovered("")} + sx={{ + py: 1.5, + px: 2, + fontFamily: theme.typography.fontFamily, + '&:hover': { + backgroundColor: 'rgba(255, 255, 255, 0.1)', + color: isHovered === "logout" ? "#FD4C62" : "white", + }, + }} + > + + + + Logout + + + + + + + + + Version 1.4.5 + + + + + )} + + ) : ( + <> + + + + K +
+ } + placement="bottom" + arrow + componentsProps={{ + tooltip: { + sx: { + backgroundColor: "rgba(33, 33, 33, 1)", + color: "rgba(241, 241, 241, 1)", + fontSize: 12, + border: "1px solid rgba(73, 73, 73, 1)", + fontFamily: theme?.typography?.fontFamily, + } + }, + popper: { + sx: { + zIndex: 1000019, + } + } + }} + > + { + if (isCloud) { + ReactGA.event({ + category: "navbar", + action: "search_icon_click", + label: "search_icon_click", + }) + } + setSearchBarModalOpen(true); + }} + > + + + + + + + + + )} + + + )} + + + + ); +}; + +export default Navbar; From ad07379508d1abe7d0ac02baa5a1205e6d9585a1 Mon Sep 17 00:00:00 2001 From: Frikky Date: Mon, 3 Mar 2025 19:52:13 +0100 Subject: [PATCH 06/18] Minor fixes EVERYWHERE --- backend/go-app/go.mod | 2 +- frontend/src/App.jsx | 42 +++++++++++++- frontend/src/components/LeftSideBar.jsx | 41 ++++++++----- frontend/src/components/TenantsTab.jsx | 2 +- frontend/src/views/AdminSetup.jsx | 28 +++++---- frontend/src/views/AngularWorkflow.jsx | 53 +++++++++-------- frontend/src/views/LoginPage.jsx | 76 +++++++++++++++++++------ frontend/src/views/LoginPageOld.jsx | 12 ++-- 8 files changed, 184 insertions(+), 72 deletions(-) diff --git a/backend/go-app/go.mod b/backend/go-app/go.mod index 37174871..477c6f4a 100644 --- a/backend/go-app/go.mod +++ b/backend/go-app/go.mod @@ -2,7 +2,7 @@ module shuffle go 1.22.2 -//replace github.com/shuffle/shuffle-shared => ../../../shuffle-shared +replace github.com/shuffle/shuffle-shared => ../../../shuffle-shared require ( cloud.google.com/go/datastore v1.15.0 diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 9fc34790..a24b4daf 100755 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -34,7 +34,7 @@ import RunWorkflow from "./views/RunWorkflow.jsx"; import Admin2 from "./views/Admin2.jsx"; import LoginPage from "./views/LoginPage.jsx"; -//import LoginPage from "./views/LoginPage.jsx"; +import LoginPageOld from "./views/LoginPageOld.jsx"; import SettingsPage from "./views/SettingsPage.jsx"; import KeepAlive from "./views/KeepAlive.jsx"; @@ -257,6 +257,44 @@ const App = (message, props) => { } /> + + } + /> + + + } + /> + { } /> ) : null} + { /> } /> + { return orgOptions.find((option) => option.name === selectedOrg); }, [selectedOrg, orgOptions]); -//With this code it is opening search bar on google chrome search bar as well which is not required -useEffect(() => { - const handleKeyDown = (event) => { - if ((event.ctrlKey || event.metaKey) && event.key === "k") { - event.preventDefault(); - setSearchBarModalOpen((prev)=> !prev); - } - }; + //With this code it is opening search bar on google chrome search bar as well which is not required + useEffect(() => { + const handleKeyDown = (event) => { + if ((event.ctrlKey || event.metaKey) && event.key === "k") { + event.preventDefault(); + setSearchBarModalOpen((prev)=> !prev); + } + }; - window.addEventListener("keydown", handleKeyDown); + window.addEventListener("keydown", handleKeyDown); - return () => { - window.removeEventListener("keydown", handleKeyDown); - }; -}, [setSearchBarModalOpen]); + return () => { + window.removeEventListener("keydown", handleKeyDown); + }; + }, [setSearchBarModalOpen]); const CustomPopper = (props) => { return ( @@ -155,6 +155,7 @@ useEffect(() => { > {props.children} + { marginLeft: 5, }} > + + {expandLeftNav && + + } { }; export default LeftSideBar; - - const ModalView = memo(({searchBarModalOpen, setSearchBarModalOpen, globalUrl, serverside, userdata}) => { return ( ( diff --git a/frontend/src/components/TenantsTab.jsx b/frontend/src/components/TenantsTab.jsx index afeb9563..907c982e 100644 --- a/frontend/src/components/TenantsTab.jsx +++ b/frontend/src/components/TenantsTab.jsx @@ -785,7 +785,7 @@ const TenantsTab = memo((props) => {

Tenants

- Control sub organizations (tenants)! {" "} + Create, manage and change to 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." : ''} diff --git a/frontend/src/views/AdminSetup.jsx b/frontend/src/views/AdminSetup.jsx index f8f3d747..0365cf2f 100755 --- a/frontend/src/views/AdminSetup.jsx +++ b/frontend/src/views/AdminSetup.jsx @@ -1,6 +1,8 @@ /* eslint-disable react/no-multi-comp */ import React, { useState } from "react"; import { makeStyles } from "@mui/styles"; +import theme from '../theme.jsx'; +import { useNavigate } from "react-router-dom"; import { CircularProgress, @@ -20,11 +22,8 @@ const surfaceColor = "#27292D"; const inputColor = "#383B40"; const boxStyle = { - paddingLeft: "30px", - paddingRight: "30px", - paddingBottom: "30px", - paddingTop: "30px", - backgroundColor: surfaceColor, + padding: 40, + backgroundColor: theme.palette.backgroundColor, }; const useStyles = makeStyles({ @@ -41,8 +40,10 @@ const AdminAccount = (props) => { const [firstRequest, setFirstRequest] = useState(true); const [loginLoading, setLoginLoading] = useState(false); + // Used to swap from login to register. True = login, false = register const register = true; + let navigate = useNavigate(); const classes = useStyles(); // Error messages etc @@ -70,13 +71,16 @@ const AdminAccount = (props) => { setLoginInfo(responseJson["reason"]); } else { if (responseJson.reason === "redirect") { - window.location.pathname = "/login"; + setTimeout(() => { + window.location.pathname = "/login"; + }, 2500) } } }) ) .catch((error) => { - setLoginInfo("Error in userdata: ", error); + setLoginInfo("Error in userdata (1): ", error); + navigate("/loginsetup") }); }; @@ -108,13 +112,17 @@ const AdminAccount = (props) => { setLoginInfo(responseJson["reason"]); } else { setLoginInfo("Successful register :)"); - window.location.pathname = "/login"; + + setTimeout(() => { + window.location.pathname = "/login"; + }, 2500) } }) ) .catch((error) => { - setLoginLoading(false); - setLoginInfo("Error in userdata: ", error); + setLoginInfo("Error in userdata (2): ", error); + setLoginLoading(false) + navigate("/loginsetup") }); }; diff --git a/frontend/src/views/AngularWorkflow.jsx b/frontend/src/views/AngularWorkflow.jsx index 51812543..5f9eadd2 100755 --- a/frontend/src/views/AngularWorkflow.jsx +++ b/frontend/src/views/AngularWorkflow.jsx @@ -2540,18 +2540,21 @@ const AngularWorkflow = (defaultprops) => { } */ + // FIXME: What is this? if (cy !== undefined && cy !== null) { // scale: 0.3, // bg: "#27292d", - const cyImageData = cy.png({ - output: "base64uri", - maxWidth: 480, - maxHeight: 270, - }) + if (cy.png !== undefined && cy.png !== null) { + const cyImageData = cy.png({ + output: "base64uri", + maxWidth: 480, + maxHeight: 270, + }) - if (cyImageData !== undefined && cyImageData !== null && cyImageData.length > 0) { - useworkflow.image = cyImageData - } + if (cyImageData !== undefined && cyImageData !== null && cyImageData.length > 0) { + useworkflow.image = cyImageData + } + } } if (useworkflow.id === undefined || useworkflow.id === null || useworkflow.id.length === 0) { @@ -9785,9 +9788,14 @@ const AngularWorkflow = (defaultprops) => { toast("Successfully stopped schedule"); } - workflow.triggers[triggerindex].status = "stopped"; - trigger.status = "stopped"; - setSelectedTrigger(trigger); + if (triggerindex !== undefined && triggerindex !== null && triggerindex >= 0) { + workflow.triggers[triggerindex].status = "stopped"; + } + + //trigger.status = "stopped"; + //console.log("TRIGGER: ", trigger) + //setSelectedTrigger(trigger); + setWorkflow(workflow); saveWorkflow(workflow) @@ -14637,20 +14645,19 @@ const AngularWorkflow = (defaultprops) => { cy.add(cybranch); } - console.log("Value to be set: ", e.target.value); try { - workflow.triggers[ - selectedTriggerIndex - ].parameters[3].value = e.target.value.id; - } catch { - workflow.triggers[selectedTriggerIndex].parameters[3] = - { - name: "startnode", - value: e.target.value.id, - }; + workflow.triggers[selectedTriggerIndex].parameters[3].value = e.target.value.id + } catch(e) { + console.log("Error: ", e) + workflow.triggers[selectedTriggerIndex].parameters[3] = + { + name: "startnode", + value: e.target.value.id, + }; } - setWorkflow(workflow); + setWorkflow(workflow) + setUpdate(Math.random()) } } @@ -15035,7 +15042,7 @@ const AngularWorkflow = (defaultprops) => { {data.image !== undefined && data.image !== null && data.image.length > 0 ? - {data.name} + {data.name} : null} Choose Subflow '{data.name}' diff --git a/frontend/src/views/LoginPage.jsx b/frontend/src/views/LoginPage.jsx index ece6897c..030781ab 100755 --- a/frontend/src/views/LoginPage.jsx +++ b/frontend/src/views/LoginPage.jsx @@ -1,9 +1,10 @@ /* eslint-disable react/no-multi-comp */ -import React, { useState } from 'react'; +import React, { useState, useEffect, } from 'react'; import { makeStyles } from '@mui/styles'; import { useNavigate, Link, useParams } from "react-router-dom"; import { isMobile } from "react-device-detect"; import { toast } from 'react-toastify'; +import { useInterval } from "react-powerhooks"; import { @@ -259,7 +260,7 @@ const MarketplaceCard = ({ classes }) => { target="_blank" style={{ color: "rgba(255, 132, 68, 1)", textDecoration: "underline" }} > - Read more about self hosting + Learn more about self hosting
@@ -269,7 +270,7 @@ const MarketplaceCard = ({ classes }) => { const LoginPage = props => { - const { globalUrl, isLoaded, isLoggedIn, setIsLoggedIn, setCookie, inregister, serverside } = props; + const { globalUrl, isLoaded, isLoggedIn, setIsLoggedIn, setCookie, inregister, serverside, checkLogin, } = props; let navigate = useNavigate(); const [username, setUsername] = useState(""); const [password, setPassword] = useState(""); @@ -284,6 +285,21 @@ const LoginPage = props => { const isCloud = window.location.host === "localhost:3002" || window.location.host === "shuffler.io" || window.location.host === "migration.shuffler.io"; const parsedsearch = serverside === true ? "" : window.location.search + + useEffect(() => { + if (!isCloud) { + checkAdmin() + } + }, []) + + const { start, stop } = useInterval({ + duration: 3000, + startImmediate: false, + callback: () => { + checkAdmin() + }, + }) + if (serverside !== true) { const tmpMessage = new URLSearchParams(window.location.search).get("message") if (tmpMessage !== undefined && tmpMessage !== null && message !== tmpMessage) { @@ -409,6 +425,38 @@ const LoginPage = props => { window.location.pathname = "/workflows" } + const checkAdmin = () => { + const url = globalUrl + "/api/v1/checkusers"; + fetch(url, { + method: "GET", + headers: { + "Content-Type": "application/json", + }, + }) + .then((response) => + response.json().then((responseJson) => { + if (responseJson["success"] === false) { + setLoginInfo(responseJson["reason"]); + } else { + + // Stay = 0 users + // Redirect = >1 user + if (responseJson.reason === "stay") { + setTimeout(() => { + navigate("/adminsetup") + }, 2500) + } + } + }) + ) + .catch((error) => { + setTimeout(() => { + navigate("/adminsetup") + }, 2500) + }) + }; + + const onSubmit = (e) => { //toast("Testing from login page") @@ -629,12 +677,13 @@ const LoginPage = props => { width: "max-content", }} > - + Shuffle Logo { variant="body2" > {register - ? "Find new ways to automate by discovering usecases made by Shufflers" + ? "Find new ways to automate by discovering usecases Shufflers" : "Please fill in the information to continue to discover the power of Shuffle" } -
+
{isCloud ? "Email" : "Username"}
{
{!loginWithSSO && ( -
+
Password
{ flexDirection: "column", marginTop: "0px" }}> - {register && !loginWithSSO && ( + {isCloud && register && !loginWithSSO && ( { - if (!isCloud) { - toast.warn("Talk to your administrator") - } - }} style={{ ...hrefStyle, alignSelf: isMobile ? "center" : "flex-end" diff --git a/frontend/src/views/LoginPageOld.jsx b/frontend/src/views/LoginPageOld.jsx index aaf376cd..6b34b957 100755 --- a/frontend/src/views/LoginPageOld.jsx +++ b/frontend/src/views/LoginPageOld.jsx @@ -58,9 +58,9 @@ const LoginDialog = (props) => { // Used to swap from login to register. True = login, false = register - useEffect(() => { - checkAdmin() - }, [loginViewLoading]) + useEffect(() => { + checkAdmin() + }, [loginViewLoading]) // Error messages etc const [loginInfo, setLoginInfo] = useState(""); @@ -91,6 +91,8 @@ const LoginDialog = (props) => { if (responseJson.sso_url !== undefined && responseJson.sso_url !== null) { setSSOUrl(responseJson.sso_url); } + + navigate("/login") if (loginViewLoading) { setLoginViewLoading(false); @@ -125,7 +127,7 @@ const LoginDialog = (props) => { callback: () => { checkAdmin(); }, - }); + }) if (firstRequest) { setFirstRequest(false); @@ -294,7 +296,7 @@ const LoginDialog = (props) => { style={{ marginBottom: 20, color: "white" }} > Waiting for the Shuffle database to become available. This may - take up to a minute. + take up to two minutes. {loginInfo === undefined || From 7eaca42a45a5c018297828ddc9098917a2a1a27e Mon Sep 17 00:00:00 2001 From: Frikky Date: Mon, 3 Mar 2025 21:09:15 +0100 Subject: [PATCH 07/18] Minor loginpage / navbar fixes --- frontend/src/views/LoginPage.jsx | 2 +- frontend/src/views/Navbar.jsx | 71 +++++++++++++++++--------------- 2 files changed, 38 insertions(+), 35 deletions(-) diff --git a/frontend/src/views/LoginPage.jsx b/frontend/src/views/LoginPage.jsx index 172c01aa..1c3c3f80 100755 --- a/frontend/src/views/LoginPage.jsx +++ b/frontend/src/views/LoginPage.jsx @@ -256,7 +256,7 @@ const MarketplaceCard = ({ classes }) => { */} diff --git a/frontend/src/views/Navbar.jsx b/frontend/src/views/Navbar.jsx index 933e63a4..9cee6f74 100644 --- a/frontend/src/views/Navbar.jsx +++ b/frontend/src/views/Navbar.jsx @@ -203,7 +203,7 @@ const menuData = { social: true, platforms: [ { name: "Discord", icon: "/images/icons/discord.svg", link: "https://discord.gg/B2CBzUm", gaData: { category: "navbar", action: "social_click", label: "discord_icon_click" } }, - { name: "GitHub", icon: "/images/icons/github.svg", link: "https://github.com/shuffle/shuffle", gaData: { category: "navbar", action: "social_click", label: "github_icon_click" } }, + { name: "GitHub", icon: "/images/icons/github.svg", link: "https://github.com/shuffle/shuffle/blob/main/.github/install-guide.md", gaData: { category: "navbar", action: "social_click", label: "github_icon_click" } }, ], followUs: [ { name: "LinkedIn", icon: "/images/icons/linkedIn.svg", link: "https://www.linkedin.com/company/shuffleio", gaData: { category: "navbar", action: "social_click", label: "linkedin_icon_click" } }, @@ -403,7 +403,7 @@ const MobileMenu = ({ anchorEl, handleClose, isLoggedIn, navigate, isCloud }) => fontSize: '14px', fontFamily: theme.typography.fontFamily, }}> - {column.title} + {column.title} {column.social ? ( @@ -1103,9 +1103,9 @@ const Navbar = (props) => { if (isCloud) { ReactGA.event(item.gaData); handleMenuClose(); - } else if(item.link.includes("docs") || item.link.includes("usecases")){ + } else if (item.link.includes("docs") || item.link.includes("usecases")) { handleMenuClose(); - }else{ + } else { window.open("https://shuffler.io" + item.link, '_blank'); return; } @@ -2509,36 +2509,39 @@ const Navbar = (props) => { > Login - + + {isCloud && + + } - - - {section === 'Resources' ? ( - - {menuData.Resources.columns.map((column, index) => ( - - - {column.title} - - - {column.social ? ( - <> - - {column.platforms.map((platform) => ( - { - if (isCloud) { - ReactGA.event(platform.gaData); - } - window.open(platform.link, '_blank') - return; - }} - // onClick={() => showDesktopToast()} - > - {platform.name} - - ))} - - - Follow Us - - - {column.followUs.map((platform) => ( - { - if (isCloud) { - ReactGA.event(platform.gaData); - } - window.open(platform.link, '_blank') - return; - }} - > - {platform.name} - - ))} - - - ) : ( - column.items.map((item) => ( - - )) - )} - - ))} - - ) : ( - - {menuData[section].map((item) => ( - - ))} - - )} - - - ))} - - {/* Static menu items */} - - - - {/* Action buttons */} - - - - - - - ); -}; - -const Navbar = (props) => { - const { - globalUrl, - isLoaded, - isLoggedIn, - removeCookie, - homePage, - userdata, - // isMobile, - serverside, - billingInfo, - - notifications, - } = props; - const theme = useTheme(); - const [searchBarModalOpen, setSearchBarModalOpen] = useState(false); - const [pricingModalOpen, setPricingModalOpen] = useState(false); - const isTabletOrMobile = useMediaQuery(theme.breakpoints.down("lg")); - const isMobile = useMediaQuery(theme.breakpoints.down("md")); - const [anchorElNav, setAnchorElNav] = useState(null); - const [openMenu, setOpenMenu] = useState(null); - const [selectedOrganization, setSelectedOrganization] = useState({}); - const navigate = useNavigate(); - const [anchorElUser, setAnchorElUser] = useState(null); - const [anchorElOrg, setAnchorElOrg] = useState(null); - const [isOrgChanging, setIsOrgChanging] = useState(false); - const [isHovered, setIsHovered] = useState(""); - const [orgSelectOpen, setOrgSelectOpen] = useState(false); - const [showTopbar, setShowTopbar] = useState(true) // Set to true to show top bar - const isCloud = - serverside === true || typeof window === "undefined" - ? true - : window.location.host === "localhost:3002" || - window.location.host === "shuffler.io" || - window.location.host === "localhost:5002"; - - - const stripeKey = typeof window === 'undefined' || window.location === undefined ? "" : window.location.origin === "https://shuffler.io" ? "pk_live_51PXYYMEJjT17t98N20qEqItyt1fLQjrnn41lPeG2PjnSlZHTDNKHuisAbW00s4KAn86nGuqB9uSVU4ds8MutbnMU00DPXpZ8ZD" : "pk_test_51PXYYMEJjT17t98NbDkojZ3DRvsFUQBs35LGMx3i436BXwEBVFKB9nCvHt0Q3M4MG3dz4mHheuWvfoYvpaL3GmsG00k1Rb2ksO" - - const topbar_var = "topbar_closed5" - useEffect(() => { - // Manually setShowTopbar(true) to show topbar by default - const topbar = localStorage.getItem(topbar_var) - if (topbar === "true") { - setShowTopbar(false) - } - }, []) - - const pricingModal = - { - setPricingModalOpen(false); - }} - PaperProps={{ - style: { - color: "white", - minWidth: 850, - minHeight: 370, - padding: 20, - backgroundColor: "rgba(0, 0, 0, 1)", - borderRadius: theme.palette?.borderRadius, - }, - }} - > - - - Upgrade your plan - - { - if (isCloud) { - ReactGA.event({ - category: "navbar", - action: "close_upgrade_modal", - label: "navbar_upgrade_modal", - }) - }; - setPricingModalOpen(false); - }} - style={{ - marginLeft: "auto", - position: "absolute", - top: 20, - right: 20, - }} - > - - - -
- -
-
- - - const modalView = ( - { - setSearchBarModalOpen(false); - }} - PaperProps={{ - style: { - color: "white", - minWidth: 750, - height: 785, - borderRadius: 16, - border: "1px solid var(--Container-Stroke, #494949)", - background: "var(--Container, #000000)", - boxShadow: "0px 16px 24px 8px rgba(0, 0, 0, 0.25)", - }, - }} - sx={{ - zIndex: 50005, - '& .MuiBackdrop-root': { - backgroundColor: 'rgba(0, 0, 0, 0.8)', - }, - }} - > - - - Search for Docs, Apps, Workflows and more - - setSearchBarModalOpen(false)} - sx={{ - color: 'white', - '&:hover': { - backgroundColor: 'rgba(255, 255, 255, 0.1)' - } - }} - > - - - - - - - - - - - ); - - const handleOpenNavMenu = (event) => { - setAnchorElNav(event.currentTarget); - }; - - const handleCloseNavMenu = () => { - setAnchorElNav(null); - }; - - const handleMenuOpen = (menu) => { - setOpenMenu(menu); - }; - - const handleMenuClose = () => { - setOpenMenu(null); - }; - - const handleOpenUserMenu = (event) => { - setAnchorElUser(event.currentTarget); - }; - - const handleCloseUserMenu = () => { - setAnchorElUser(null); - }; - - const handleOpenOrgMenu = (event) => { - setAnchorElOrg(event.currentTarget); - }; - - const handleCloseOrgMenu = () => { - setAnchorElOrg(null); - }; - - const menuStyles = { - "& .MuiPaper-root": { - backgroundColor: "#212121", - marginTop: 1, - backdropFilter: "blur(10px)", - fontFamily: theme.typography.fontFamily, - }, - }; - - const menuItemBox = { - display: "flex", - alignItems: "flex-start", - padding: 2, - paddingTop: "20px", - paddingBottom: "20px", - paddingLeft: "20px", - gap: 2, - width: "100%", - backgroundColor: "#212121", - transition: "all 0.2s ease-in-out", - borderBottom: "1px solid #494949", - }; - - const buttonStyles = { - fontFamily: theme.typography.fontFamily, - color: "white", - textTransform: "none", - fontSize: "16px", - fontWeight: 400, - padding: "6px 12px", - "& .MuiSvgIcon-root": { - transition: "transform 0.2s ease-in-out", - }, - "&:hover": { - backgroundColor: "transparent", - color: theme.palette.primary.main, - "& .MuiSvgIcon-root": { - transform: "rotate(180deg)", - }, - }, - }; - - // Render menu content based on type - const renderMenuContent = (item, isCloud) => { - if (item === "Resources") { - return ( - - {menuData.Resources.columns.map((column, index) => ( - - - {column.title} - - - {column.social ? ( - <> - - {column.platforms.map((platform) => ( - { - if (isCloud) { - ReactGA.event(platform.gaData); - } - handleMenuClose(); - }} - sx={{ - padding: 0, - width: 48, - height: 48, - "&:hover": { - backgroundColor: "transparent", - opacity: 0.8, - }, - }} - > - {platform.name} - - ))} - - - Follow Us - - - {column.followUs.map((platform) => ( - { - if (isCloud) { - ReactGA.event(platform.gaData); - } - handleMenuClose(); - }} - sx={{ - padding: 0, - width: 48, - height: 48, - "&:hover": { - backgroundColor: "transparent", - opacity: 0.8, - }, - }} - > - {platform.name} - - ))} - - - ) : ( - - {column.items.map((item) => ( - - ))} - - )} - - ))} - - ); - } - - return menuData[item].map((menuItem, index) => ( - { - if (menuItem.title === "Singul") { - window.open(menuItem.path, '_blank'); - return; - } - if(isCloud) { - ReactGA.event(menuItem.gaData); - } - handleCloseNavMenu(); - handleMenuClose(); - if(isCloud) { - navigate(menuItem.path); - } else { - if(menuItem.path.includes("docs")){ - navigate(menuItem.path); - handleMenuClose(); - }else{ - window.open("https://shuffler.io" + menuItem.path, '_blank'); - return; - } - } - }} - sx={{ - padding: 0, - backgroundColor: "#212121", - "&:hover": { - backgroundColor: "#212121", - }, - "&:last-child": { - borderBottomLeftRadius: "4px", - borderBottomRightRadius: "4px", - }, - "&:first-of-type": { - paddingTop: "4px", - }, - }} - > - - - - - - {menuItem.title} - - {menuItem.title === "Singul" && ( - - Coming Soon - - )} - - - {menuItem.description} - - - - - )); - }; - - const renderDesktopMenu = () => ( - - {Object.keys(menuData).map((item) => ( - - - - {/* Custom Dropdown */} - handleMenuOpen(item)} - onMouseLeave={handleMenuClose} - sx={{ - position: "absolute", - top: "130%", - left: - item === "Products" - ? 0 - : item === "Services" - ? -50 - : item === "Resources" - ? isTabletOrMobile ? -330 : -250 - : 0, - backgroundColor: "#212121", - borderTopLeftRadius: 0, - borderTopRightRadius: 0, - borderBottomLeftRadius: "4px", - borderBottomRightRadius: "4px", - boxShadow: - "0 8px 16px rgba(0, 0, 0, 0.25), 0 2px 4px rgba(0, 0, 0, 0.1)", - backdropFilter: "blur(10px)", - zIndex: -20, - opacity: openMenu === item ? 1 : 0, - visibility: openMenu === item ? "visible" : "hidden", - transform: openMenu === item ? "translateY(0)" : "translateY(-8px)", - transition: "transform 0.3s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.3s ease", - pointerEvents: openMenu === item ? "auto" : "none", - ...(item === "Resources" && { - width: "900px", - padding: 3, - }), - }} - > - {renderMenuContent(item, isCloud)} - - - ))} - - - - ); - - const menuItemStyles = { - fontFamily: theme.typography.fontFamily, - color: "white", - "&:hover": { - backgroundColor: "rgba(255, 133, 68, 0.1)", - }, - }; - - const searchButtonStyles = { - fontFamily: theme.typography.fontFamily, - color: "rgba(255, 255, 255, 0.7)", - border: "1px solid rgba(26, 26, 26, 0.8)", - backgroundColor: "#2F2F2F", - textTransform: "none", - borderRadius: "8px", - fontSize: "14px", - padding: "6px 12px", - display: "flex", - alignItems: "center", - height: "42px", - gap: 1, - minWidth: "110px", - justifyContent: "space-between", - "&:hover": { - borderColor: "rgba(255, 255, 255, 0.1)", - }, - }; - - const searchIconButtonStyles = { - color: "rgba(255, 255, 255, 0.7)", - padding: "8px", - minWidth: "unset", - height: "42px", - "&:hover": { - backgroundColor: "#3F3F3F", - }, - }; - - // Add this shared button style - const sharedButtonStyles = { - fontFamily: theme.typography.fontFamily, - textTransform: "none", - fontSize: "16px", - fontWeight: 600, - padding: "6px 24px", - }; - - - useEffect(() => { - Mousetrap.bind(['command+k', 'ctrl+k'], () => { - setSearchBarModalOpen(true); - return false; // Prevent the default action - }); - Mousetrap.bind(['esc'], () => { - setSearchBarModalOpen(false); - return false; // Prevent the default action - }); - - return () => { - Mousetrap.unbind(['command+k', 'ctrl+k']); - }; - }, []); - - useEffect(() => { - if (isLoggedIn && userdata?.active_org?.id?.length > 0) { - handleGetOrg(userdata.active_org.id); - } - }, [isLoggedIn]); - - const handleGetOrg = (orgId) => { - 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) { - console.log("Error getting org: ", responseJson); - } else { - setSelectedOrganization(responseJson); - } - }) - .catch((error) => { - console.log("Error getting org: ", error); - }); - }; - - const handleClickLogout = () => { - console.log("SHOULD LOG OUT"); - - toast.info("Logging out..."); - // Don't really care about the logout - fetch(globalUrl + "/api/v1/logout", { - credentials: "include", - method: "POST", - headers: { - "Content-Type": "application/json", - }, - }) - .then(() => { - // Log out anyway - removeCookie("session_token", { path: "/" }); - removeCookie("session_token", { path: "/" }); - removeCookie("session_token", { path: "/" }); - - removeCookie("__session", { path: "/" }); - removeCookie("__session", { path: "/" }); - removeCookie("__session", { path: "/" }); - - removeCookie("__session", { path: "/" }); - - window.location.pathname = "/"; - - localStorage.setItem("globalUrl", "") - - // Delete userinfo from localstorage - localStorage.removeItem("apps") - localStorage.removeItem("workflows") - localStorage.removeItem("userinfo") - }) - .catch((error) => { - console.log(error); - }); - }; - - const topbarHeight = showTopbar ? 40 : 0 - const topbar = !isCloud || !showTopbar ? null : - curpath === "/" || curpath.includes("/docs") || curpath === "/pricing" || curpath === "/contact" || curpath === "/search" || curpath === "/usecases" || curpath === "/usecases2" || curpath === "/training" || curpath === "/professional-services" ? - -
- - {/* Shuffle 1.4.0 is out! Read more about  */} - New  - - { - ReactGA.event({ - category: "landingpage", - action: "click_header_training", - label: "", - }) - - navigate("/training") - - }} style={{ cursor: "pointer", textDecoration: "none", fontWeight: 600, color: "rgba(255,255,255,0.9)" }}> - Public Training - - -  Dates Released! - - { - setShowTopbar(false) - - // Set storage that it's clicked - localStorage.setItem(topbar_var, "true") - }}> - - -
-
- : - null - - const handleClickChangeOrg = (orgId) => { - setIsOrgChanging(true); - const data = { org_id: orgId }; - - localStorage.setItem("globalUrl", ""); - localStorage.setItem("getting_started_sidebar", "open"); - toast.info("Changing organization..."); - fetch(`${globalUrl}/api/v1/orgs/${orgId}/change`, { - mode: "cors", - credentials: "include", - crossDomain: true, - method: "POST", - body: JSON.stringify(data), - withCredentials: true, - headers: { - "Content-Type": "application/json; charset=utf-8", - }, - }) - .then(function (response) { - if (response.status !== 200) { - console.log("Error in response"); - } else { - localStorage.removeItem("apps"); - localStorage.removeItem("workflows"); - localStorage.removeItem("userinfo"); - } - - return response.json(); - }) - .then(function (responseJson) { - if (responseJson.success === true) { - if ( - responseJson.region_url !== undefined && - responseJson.region_url !== null && - responseJson.region_url.length > 0 - ) { - console.log("Region Change: ", responseJson.region_url); - localStorage.setItem("globalUrl", responseJson.region_url); - //globalUrl = responseJson.region_url - } - - if (responseJson["reason"] === "SSO_REDIRECT") { - toast.info("Redirecting to SSO login page as SSO is required for this organization."); - setTimeout(() => { - window.location.href = responseJson["url"]; - }, 2000); - } else { - toast("Successfully changed active organization - refreshing!"); - setTimeout(() => { - window.location.reload(); - }, 2000); - } - } else { - setIsOrgChanging(false); - if ( - responseJson.reason !== undefined && - responseJson.reason !== null && - responseJson.reason.length > 0 - ) { - toast(responseJson.reason); - } else { - toast("Failed changing org. Try again or contact support@shuffler.io if this persists."); - } - } - }) - .catch((error) => { - console.log("error changing: ", error); - setIsOrgChanging(false); - }); - }; - - // Add these helper functions from LeftSideBar - const getRegionTag = (region_url) => { - let regiontag = "EU"; - if (region_url !== undefined && region_url !== null && region_url.length > 0) { - const regionsplit = 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"; - } else if (regiontag === "frankfurt") { - regiontag = "EU-2"; - } else if (regiontag === "ca") { - regiontag = "CA"; - } - } - } - return regiontag; - }; - - const getRegionFlag = (region_url) => { - const regionTag = getRegionTag(region_url); - const regionMapping = { - "US": "us", - "EU": "eu", - "EU-2": "de", - "CA": "ca", - "UK": "gb" - }; - - const region = regionMapping[regionTag] || "eu"; - return `https://flagcdn.com/48x36/${region}.png`; - }; - - // Add this useEffect to prevent scroll locking - useEffect(() => { - // Remove modal classes that cause scroll locking - const removeModalClasses = () => { - document.body.style.overflow = 'auto'; - document.body.style.paddingRight = '0px'; - }; - - // Create observer to watch for class changes - const observer = new MutationObserver((mutations) => { - mutations.forEach((mutation) => { - if (mutation.attributeName === 'class') { - if (document.body.classList.contains('MuiModal-open')) { - removeModalClasses(); - } - } - }); - }); - - // Start observing - observer.observe(document.body, { - attributes: true, - attributeFilter: ['class'], - }); - - // Cleanup - return () => { - observer.disconnect(); - removeModalClasses(); - }; - }, []); - - const handleOrgSelectClick = (event) => { - // Prevent the default Select behavior - event.preventDefault(); - if(isCloud){ - ReactGA.event({ - category: "navbar", - action: "org_dropdown_click", - label: "org_dropdown_click", - }) - } - // Toggle the select open state - setOrgSelectOpen(!orgSelectOpen); - }; - - return ( - - {topbar} - - - - { - if (isCloud) { - ReactGA.event({ - category: "navbar", - action: "home_click", - label: "shuffle_logo", - }); - } - }} - > - shuffle logo - - - - {isMobile ? ( - /* Mobile Navigation Icons */ - - setSearchBarModalOpen(true)} - > - - - { - if(Boolean(anchorElNav)) { - handleCloseNavMenu(); - }else{ - handleOpenNavMenu(e); - } - } - } - sx={{ color: "white" }} - > - {Boolean(anchorElNav) ? ( - - ) : ( - - )} - - - - ) : ( - <> - {modalView} - {pricingModal} - {renderDesktopMenu()} - {/* Right Side Buttons */} - - {!isLoaded ? ( - - ) : isLoggedIn ? ( - <> - {isOrgChanging ? ( - - ) : ( - <> - - - + K -
- } - placement="bottom" - arrow - componentsProps={{ - tooltip: { - sx: { - backgroundColor: "rgba(33, 33, 33, 1)", - color: "rgba(241, 241, 241, 1)", - fontSize: 12, - border: "1px solid rgba(73, 73, 73, 1)", - fontFamily: theme?.typography?.fontFamily, - } - }, - popper: { - sx: { - zIndex: 1000019, - } - } - }} - > - { - if (isCloud) { - ReactGA.event({ - category: "navbar", - action: "search_icon_click", - label: "search_icon_click", - }) - } - setSearchBarModalOpen(true); - }} - > - - - - {isCloud && (userdata.org_status === undefined || userdata.org_status === null || userdata.org_status.length === 0) ? - - : null} - - - {/* Organization Dropdown */} - {/* */} - - {/* User Avatar Menu */} - { - if (isCloud) { - ReactGA.event({ - category: "navbar", - action: "click_user_menu", - label: "open_user_dropdown" - }); - } - handleOpenUserMenu(e); - }} - sx={{ - padding: 0, - "&:hover": { - backgroundColor: "rgba(47, 47, 47, 0.5)", - }, - }} - > - - - - - - - {/* User Info Section */} - - - - - - {userdata?.username} - - - - - - - - {/* Menu Items */} - { - if (isCloud) { - ReactGA.event({ - category: "navbar", - action: "user_dropdown_click", - label: "go_to_admin" - }); - } - handleCloseUserMenu(); - navigate("/admin"); - }} - onMouseEnter={() => setIsHovered("organization")} - onMouseLeave={() => setIsHovered("")} - sx={{ - py: 1.5, - px: 2, - fontFamily: theme.typography.fontFamily, - transition: "color 0.1s ease", - '&:hover': { - backgroundColor: 'rgba(255, 255, 255, 0.1)', - color: isHovered === "organization" ? "#FF8544" : "white", - }, - }} - > - - - - Organization - - - - - { - if (isCloud) { - ReactGA.event({ - category: "navbar", - action: "user_dropdown_click", - label: "go_to_settings", - }) - } - handleCloseUserMenu(); - navigate("/settings"); - }} - onMouseEnter={() => setIsHovered("settings")} - onMouseLeave={() => setIsHovered("")} - sx={{ - py: 1.5, - px: 2, - fontFamily: theme.typography.fontFamily, - transition: "color 0.1s ease", - '&:hover': { - backgroundColor: 'rgba(255, 255, 255, 0.1)', - color: isHovered === "settings" ? "#FF8544" : "white", - }, - }} - > - - - - Settings - - - - - { - if (isCloud) { - ReactGA.event({ - category: "navbar", - action: "user_dropdown_click", - label: "go_to_notifications", - }) - } - handleCloseUserMenu(); - navigate("/admin?admin_tab=notifications"); - }} - onMouseEnter={() => setIsHovered("notifications")} - onMouseLeave={() => setIsHovered("")} - sx={{ - py: 1.5, - px: 2, - fontFamily: theme.typography.fontFamily, - transition: "color 0.1s ease", - '&:hover': { - backgroundColor: 'rgba(255, 255, 255, 0.1)', - color: isHovered === "notifications" ? "#FF8544" : "white", - }, - }} - > - - - - Notifications ({notifications === undefined || notifications === null ? 0 : - notifications?.filter((notification) => notification.read === false).length}) - - - - - - - - { - if (isCloud) { - ReactGA.event({ - category: "navbar", - action: "user_dropdown_click", - label: "go_to_about", - }) - } - handleCloseUserMenu(); - navigate("/docs/about"); - }} - onMouseEnter={() => setIsHovered("about")} - onMouseLeave={() => setIsHovered("")} - sx={{ - py: 1.5, - px: 2, - fontFamily: theme.typography.fontFamily, - transition: "color 0.1s ease", - '&:hover': { - backgroundColor: 'rgba(255, 255, 255, 0.1)', - color: isHovered === "about" ? "#FF8544" : "white", - }, - }} - > - - - - About - - - - - { - if (isCloud) { - ReactGA.event({ - category: "navbar", - action: "user_dropdown_click", - label: "logout_click", - }) - } - handleCloseUserMenu(); - handleClickLogout(); - }} - onMouseEnter={() => setIsHovered("logout")} - onMouseLeave={() => setIsHovered("")} - sx={{ - py: 1.5, - px: 2, - fontFamily: theme.typography.fontFamily, - '&:hover': { - backgroundColor: 'rgba(255, 255, 255, 0.1)', - color: isHovered === "logout" ? "#FD4C62" : "white", - }, - }} - > - - - - Logout - - - - - - - - - Version 1.4.5 - - - - - )} - - ) : ( - <> - - - + K -
- } - placement="bottom" - arrow - componentsProps={{ - tooltip: { - sx: { - backgroundColor: "rgba(33, 33, 33, 1)", - color: "rgba(241, 241, 241, 1)", - fontSize: 12, - border: "1px solid rgba(73, 73, 73, 1)", - fontFamily: theme?.typography?.fontFamily, - } - }, - popper: { - sx: { - zIndex: 1000019, - } - } - }} - > - { - if (isCloud) { - ReactGA.event({ - category: "navbar", - action: "search_icon_click", - label: "search_icon_click", - }) - } - setSearchBarModalOpen(true); - }} - > - - - - - - - {isCloud && - - } - - - )} - - - )} - - - - ); -}; - -export default Navbar; From ea41d8c79dbb8234733b6f6d219a808931204f8a Mon Sep 17 00:00:00 2001 From: Frikky Date: Mon, 3 Mar 2025 21:38:57 +0100 Subject: [PATCH 10/18] Re-added navbar --- frontend/src/components/Navbar.jsx | 2585 ++++++++++++++++++++++++++++ 1 file changed, 2585 insertions(+) create mode 100644 frontend/src/components/Navbar.jsx diff --git a/frontend/src/components/Navbar.jsx b/frontend/src/components/Navbar.jsx new file mode 100644 index 00000000..509b9b4f --- /dev/null +++ b/frontend/src/components/Navbar.jsx @@ -0,0 +1,2585 @@ +import React, { useEffect, useState } from "react"; +import { Link, useNavigate } from "react-router-dom"; +import { + AppBar, + Box, + Toolbar, + IconButton, + Typography, + Menu, + MenuItem, + Button, + Container, + useTheme, + useMediaQuery, + Divider, + alpha, + Avatar, + Select, + Tooltip, + DialogTitle, + DialogContent, + Dialog, + Slide, + Collapse +} from "@mui/material"; +import MenuIcon from "@mui/icons-material/Menu"; +import { Close as CloseIcon, Folder as FolderIcon, Code as CodeIcon, LibraryBooks as LibraryBooksIcon } from '@mui/icons-material' +import SearchBox from "../components/SearchData.jsx"; +import ReactGA from "react-ga4"; +import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown"; +import KeyboardCommandKeyIcon from '@mui/icons-material/KeyboardCommandKey'; +import SearchIcon from "@mui/icons-material/Search"; +// import SearchField from "../components/Searchfield.jsx"; +import { toast } from "react-toastify"; +import AddIcon from '@mui/icons-material/Add'; +import Mousetrap from "mousetrap"; +import LicencePopup from "../components/LicencePopup.jsx"; + +const curpath = (typeof window !== "undefined" && window.location && typeof window.location.pathname === "string") +? window.location.pathname +: ""; + +// Menu Data Structure +const menuData = { + Products: [ + { + title: "Shuffle", + description: + "The most versatile automation engine with focus on security.", + icon: "images/logos/orange_logo.svg", + path: "/docs", + gaData: { + category: "navbar", + action: "products_click", + label: "shuffle_logo_click" + } + }, + { + title: "Singul", + description: + "Connect and run actions seamlessly between different platforms.", + icon: "/images/logos/singul.svg", + path: "https://singul.io/", + gaData: { + category: "navbar", + action: "products_click", + label: "singul_click" + } + }, + { + title: "API Explorer", + description: + "Explore, run and automate APIs from over 2500 platforms.", + icon: "/images/icons/API.svg", + path: "/apis", + gaData: { + category: "navbar", + action: "products_click", + label: "api_explorer_click" + } + }, + ], + Services: [ + { + title: "Professional Services", + description: + "Professional Services help you solve problems at your convenience.", + icon: "/images/ProfessionalServices.svg", + path: "/professional-services", + gaData: { + category: "navbar", + action: "services_click", + label: "professional_services_click" + } + }, + { + title: "Support", + description: + "Support to help you build automations with confidence.", + icon: "/images/Support.svg", + path: "/contact?category=support", + gaData: { + category: "navbar", + action: "services_click", + label: "support_click" + } + }, + { + title: "Training", + description: + "Tailored to provide hands-on learning of Shuffle for automation mastery.", + icon: "/images/Training.svg", + path: "/training", + gaData: { + category: "navbar", + action: "services_click", + label: "training_click" + } + }, + { + title: "Security Consultation", + description: + "Automate your infrastructure with expert guidance and tailored solutions.", + icon: "/images/SecurityConsultation.svg", + path: "/contact?category=security_consultation", + gaData: { + category: "navbar", + action: "services_click", + label: "security_consultation_click" + } + }, + ], + Resources: { + columns: [ + { + title: "Platform", + items: [ + { + title: "Usecases", + icon: "/images/icons/usecases.svg", + hoverIcon: "/images/icons/usecases_hover.svg", + link: "/usecases", + gaData: { + category: "navbar", + action: "resources_click", + label: "usecases_click" + } + }, + { + title: "Documentation", + icon: "/images/icons/docs.svg", + hoverIcon: "/images/icons/docs_hover.svg", + link: "/docs", + gaData: { + category: "navbar", + action: "resources_click", + label: "documentation_click" + } + }, + { title: "FAQ", icon: "/images/icons/faq.svg", hoverIcon: "/images/icons/faq_hover.svg", link: "/faq", gaData: { category: "navbar", action: "resources_click", label: "faq" } }, + ], + }, + { + title: "Company", + items: [ + { + title: "About us", + icon: "/images/icons/about_us.svg", + hoverIcon: "/images/icons/about_us_hover.svg", + link: "/docs/about", + gaData: { + category: "navbar", + action: "resources_click", + label: "about_us_click" + } + }, + { + title: "Articles", + icon: "/images/icons/articles.svg", + hoverIcon: "/images/icons/articles_hover.svg", + link: "/articles", + gaData: { + category: "navbar", + action: "resources_click", + label: "articles_click" + } + }, + { + title: "Contact Us", + icon: "/images/icons/contact_us.svg", + hoverIcon: "/images/icons/contact_us_hover.svg", + link: "/contact?category=contact", + gaData: { + category: "navbar", + action: "resources_click", + label: "contact_us_click" + } + }, + ], + }, + { + title: "Join the community", + social: true, + platforms: [ + { name: "Discord", icon: "/images/icons/discord.svg", link: "https://discord.gg/B2CBzUm", gaData: { category: "navbar", action: "social_click", label: "discord_icon_click" } }, + { name: "GitHub", icon: "/images/icons/github.svg", link: "https://github.com/shuffle/shuffle/blob/main/.github/install-guide.md", gaData: { category: "navbar", action: "social_click", label: "github_icon_click" } }, + ], + followUs: [ + { name: "LinkedIn", icon: "/images/icons/linkedIn.svg", link: "https://www.linkedin.com/company/shuffleio", gaData: { category: "navbar", action: "social_click", label: "linkedin_icon_click" } }, + { name: "Twitter", icon: "/images/icons/x.svg", link: "https://twitter.com/shuffleio", gaData: { category: "navbar", action: "social_click", label: "twitter_icon_click" } }, + ], + }, + ], + }, +}; + +// Add this new component at the top level of the file, right after the imports +const LoadingSkeleton = () => { + const theme = useTheme(); + return ( + + + + + + + ); +}; + +// Add this new component for mobile menu +const MobileMenu = ({ anchorEl, handleClose, isLoggedIn, navigate, isCloud }) => { + const [openSection, setOpenSection] = useState(null); + const theme = useTheme(); + + // Add useEffect to handle body scroll + useEffect(() => { + if (Boolean(anchorEl)) { + // Disable scroll + document.body.style.overflow = 'hidden'; + } else { + // Re-enable scroll + document.body.style.overflow = 'auto'; + } + + // Cleanup function to ensure scroll is re-enabled when component unmounts + return () => { + document.body.style.overflow = 'auto'; + }; + }, [anchorEl]); + + const handleSectionClick = (section) => { + setOpenSection(openSection === section ? null : section); + }; + + const handleItemClick = (path) => { + handleClose(); + navigate(path); + }; + + const showDesktopToast = () => { + toast.info("Please open on desktop for the full experience"); + handleClose(); + }; + + return ( + + + {Object.keys(menuData).map((section) => ( + + + + + {section === 'Resources' ? ( + + {menuData.Resources.columns.map((column, index) => ( + + + {column.title} + + + {column.social ? ( + <> + + {column.platforms.map((platform) => ( + { + if (isCloud) { + ReactGA.event(platform.gaData); + } + window.open(platform.link, '_blank') + return; + }} + // onClick={() => showDesktopToast()} + > + {platform.name} + + ))} + + + Follow Us + + + {column.followUs.map((platform) => ( + { + if (isCloud) { + ReactGA.event(platform.gaData); + } + window.open(platform.link, '_blank') + return; + }} + > + {platform.name} + + ))} + + + ) : ( + column.items.map((item) => ( + + )) + )} + + ))} + + ) : ( + + {menuData[section].map((item) => ( + + ))} + + )} + + + ))} + + {/* Static menu items */} + + + + {/* Action buttons */} + + + + + + + ); +}; + +const Navbar = (props) => { + const { + globalUrl, + isLoaded, + isLoggedIn, + removeCookie, + homePage, + userdata, + // isMobile, + serverside, + billingInfo, + + notifications, + } = props; + + const topbar_var = "topbar_closed10" + + const theme = useTheme(); + const [searchBarModalOpen, setSearchBarModalOpen] = useState(false); + const [pricingModalOpen, setPricingModalOpen] = useState(false); + const isTabletOrMobile = useMediaQuery(theme.breakpoints.down("lg")); + const isMobile = useMediaQuery(theme.breakpoints.down("md")); + const [anchorElNav, setAnchorElNav] = useState(null); + const [openMenu, setOpenMenu] = useState(null); + const [selectedOrganization, setSelectedOrganization] = useState({}); + const navigate = useNavigate(); + const [anchorElUser, setAnchorElUser] = useState(null); + const [anchorElOrg, setAnchorElOrg] = useState(null); + const [isOrgChanging, setIsOrgChanging] = useState(false); + const [isHovered, setIsHovered] = useState(""); + const [orgSelectOpen, setOrgSelectOpen] = useState(false); + const [showTopbar, setShowTopbar] = useState(true) // Set to true to show top bar + const isCloud = + serverside === true || typeof window === "undefined" + ? true + : window.location.host === "localhost:3002" || + window.location.host === "shuffler.io" || + window.location.host === "localhost:5002"; + + + const stripeKey = typeof window === 'undefined' || window.location === undefined ? "" : window.location.origin === "https://shuffler.io" ? "pk_live_51PXYYMEJjT17t98N20qEqItyt1fLQjrnn41lPeG2PjnSlZHTDNKHuisAbW00s4KAn86nGuqB9uSVU4ds8MutbnMU00DPXpZ8ZD" : "pk_test_51PXYYMEJjT17t98NbDkojZ3DRvsFUQBs35LGMx3i436BXwEBVFKB9nCvHt0Q3M4MG3dz4mHheuWvfoYvpaL3GmsG00k1Rb2ksO" + + useEffect(() => { + // Manually setShowTopbar(true) to show topbar by default + const topbar = localStorage.getItem(topbar_var) + if (topbar === "true") { + setShowTopbar(false) + } + }, []) + + const pricingModal = + { + setPricingModalOpen(false); + }} + PaperProps={{ + style: { + color: "white", + minWidth: 850, + minHeight: 370, + padding: 20, + backgroundColor: "rgba(0, 0, 0, 1)", + borderRadius: theme.palette?.borderRadius, + }, + }} + > + + + Upgrade your plan + + { + if (isCloud) { + ReactGA.event({ + category: "navbar", + action: "close_upgrade_modal", + label: "navbar_upgrade_modal", + }) + }; + setPricingModalOpen(false); + }} + style={{ + marginLeft: "auto", + position: "absolute", + top: 20, + right: 20, + }} + > + + + +
+ +
+
+ + + const modalView = ( + { + setSearchBarModalOpen(false); + }} + PaperProps={{ + style: { + color: "white", + minWidth: 750, + height: 785, + borderRadius: 16, + border: "1px solid var(--Container-Stroke, #494949)", + background: "var(--Container, #000000)", + boxShadow: "0px 16px 24px 8px rgba(0, 0, 0, 0.25)", + }, + }} + sx={{ + zIndex: 50005, + '& .MuiBackdrop-root': { + backgroundColor: 'rgba(0, 0, 0, 0.8)', + }, + }} + > + + + Search for Docs, Apps, Workflows and more + + setSearchBarModalOpen(false)} + sx={{ + color: 'white', + '&:hover': { + backgroundColor: 'rgba(255, 255, 255, 0.1)' + } + }} + > + + + + + + + + + + + ); + + const handleOpenNavMenu = (event) => { + setAnchorElNav(event.currentTarget); + }; + + const handleCloseNavMenu = () => { + setAnchorElNav(null); + }; + + const handleMenuOpen = (menu) => { + setOpenMenu(menu); + }; + + const handleMenuClose = () => { + setOpenMenu(null); + }; + + const handleOpenUserMenu = (event) => { + setAnchorElUser(event.currentTarget); + }; + + const handleCloseUserMenu = () => { + setAnchorElUser(null); + }; + + const handleOpenOrgMenu = (event) => { + setAnchorElOrg(event.currentTarget); + }; + + const handleCloseOrgMenu = () => { + setAnchorElOrg(null); + }; + + const menuStyles = { + "& .MuiPaper-root": { + backgroundColor: "#212121", + marginTop: 1, + backdropFilter: "blur(10px)", + fontFamily: theme.typography.fontFamily, + }, + }; + + const menuItemBox = { + display: "flex", + alignItems: "flex-start", + padding: 2, + paddingTop: "20px", + paddingBottom: "20px", + paddingLeft: "20px", + gap: 2, + width: "100%", + backgroundColor: "#212121", + transition: "all 0.2s ease-in-out", + borderBottom: "1px solid #494949", + }; + + const buttonStyles = { + fontFamily: theme.typography.fontFamily, + color: "white", + textTransform: "none", + fontSize: "16px", + fontWeight: 400, + padding: "6px 12px", + "& .MuiSvgIcon-root": { + transition: "transform 0.2s ease-in-out", + }, + "&:hover": { + backgroundColor: "transparent", + color: theme.palette.primary.main, + "& .MuiSvgIcon-root": { + transform: "rotate(180deg)", + }, + }, + }; + + // Render menu content based on type + const renderMenuContent = (item, isCloud) => { + if (item === "Resources") { + return ( + + {menuData.Resources.columns.map((column, index) => ( + + + {column.title} + + + {column.social ? ( + <> + + {column.platforms.map((platform) => ( + { + if (isCloud) { + ReactGA.event(platform.gaData); + } + handleMenuClose(); + }} + sx={{ + padding: 0, + width: 48, + height: 48, + "&:hover": { + backgroundColor: "transparent", + opacity: 0.8, + }, + }} + > + {platform.name} + + ))} + + + Follow Us + + + {column.followUs.map((platform) => ( + { + if (isCloud) { + ReactGA.event(platform.gaData); + } + handleMenuClose(); + }} + sx={{ + padding: 0, + width: 48, + height: 48, + "&:hover": { + backgroundColor: "transparent", + opacity: 0.8, + }, + }} + > + {platform.name} + + ))} + + + ) : ( + + {column.items.map((item) => ( + + ))} + + )} + + ))} + + ); + } + + return menuData[item].map((menuItem, index) => ( + { + if (menuItem.title === "Singul") { + window.open(menuItem.path, '_blank'); + return; + } + if(isCloud) { + ReactGA.event(menuItem.gaData); + } + handleCloseNavMenu(); + handleMenuClose(); + if(isCloud) { + navigate(menuItem.path); + } else { + if(menuItem.path.includes("docs")){ + navigate(menuItem.path); + handleMenuClose(); + }else{ + window.open("https://shuffler.io" + menuItem.path, '_blank'); + return; + } + } + }} + sx={{ + padding: 0, + backgroundColor: "#212121", + "&:hover": { + backgroundColor: "#212121", + }, + "&:last-child": { + borderBottomLeftRadius: "4px", + borderBottomRightRadius: "4px", + }, + "&:first-of-type": { + paddingTop: "4px", + }, + }} + > + + + + + + {menuItem.title} + + {menuItem.title === "Singul" && ( + + Coming Soon + + )} + + + {menuItem.description} + + + + + )); + }; + + const renderDesktopMenu = () => ( + + {Object.keys(menuData).map((item) => ( + + + + {/* Custom Dropdown */} + handleMenuOpen(item)} + onMouseLeave={handleMenuClose} + sx={{ + position: "absolute", + top: "130%", + left: + item === "Products" + ? 0 + : item === "Services" + ? -50 + : item === "Resources" + ? isTabletOrMobile ? -330 : -250 + : 0, + backgroundColor: "#212121", + borderTopLeftRadius: 0, + borderTopRightRadius: 0, + borderBottomLeftRadius: "4px", + borderBottomRightRadius: "4px", + boxShadow: + "0 8px 16px rgba(0, 0, 0, 0.25), 0 2px 4px rgba(0, 0, 0, 0.1)", + backdropFilter: "blur(10px)", + zIndex: -20, + opacity: openMenu === item ? 1 : 0, + visibility: openMenu === item ? "visible" : "hidden", + transform: openMenu === item ? "translateY(0)" : "translateY(-8px)", + transition: "transform 0.3s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.3s ease", + pointerEvents: openMenu === item ? "auto" : "none", + ...(item === "Resources" && { + width: "900px", + padding: 3, + }), + }} + > + {renderMenuContent(item, isCloud)} + + + ))} + + + + ); + + const menuItemStyles = { + fontFamily: theme.typography.fontFamily, + color: "white", + "&:hover": { + backgroundColor: "rgba(255, 133, 68, 0.1)", + }, + }; + + const searchButtonStyles = { + fontFamily: theme.typography.fontFamily, + color: "rgba(255, 255, 255, 0.7)", + border: "1px solid rgba(26, 26, 26, 0.8)", + backgroundColor: "#2F2F2F", + textTransform: "none", + borderRadius: "8px", + fontSize: "14px", + padding: "6px 12px", + display: "flex", + alignItems: "center", + height: "42px", + gap: 1, + minWidth: "110px", + justifyContent: "space-between", + "&:hover": { + borderColor: "rgba(255, 255, 255, 0.1)", + }, + }; + + const searchIconButtonStyles = { + color: "rgba(255, 255, 255, 0.7)", + padding: "8px", + minWidth: "unset", + height: "42px", + "&:hover": { + backgroundColor: "#3F3F3F", + }, + }; + + // Add this shared button style + const sharedButtonStyles = { + fontFamily: theme.typography.fontFamily, + textTransform: "none", + fontSize: "16px", + fontWeight: 600, + padding: "6px 24px", + }; + + + useEffect(() => { + Mousetrap.bind(['command+k', 'ctrl+k'], () => { + setSearchBarModalOpen(true); + return false; // Prevent the default action + }); + Mousetrap.bind(['esc'], () => { + setSearchBarModalOpen(false); + return false; // Prevent the default action + }); + + return () => { + Mousetrap.unbind(['command+k', 'ctrl+k']); + }; + }, []); + + useEffect(() => { + if (isLoggedIn && userdata?.active_org?.id?.length > 0) { + handleGetOrg(userdata.active_org.id); + } + }, [isLoggedIn]); + + const handleGetOrg = (orgId) => { + 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) { + console.log("Error getting org: ", responseJson); + } else { + setSelectedOrganization(responseJson); + } + }) + .catch((error) => { + console.log("Error getting org: ", error); + }); + }; + + const handleClickLogout = () => { + console.log("SHOULD LOG OUT"); + + toast.info("Logging out..."); + // Don't really care about the logout + fetch(globalUrl + "/api/v1/logout", { + credentials: "include", + method: "POST", + headers: { + "Content-Type": "application/json", + }, + }) + .then(() => { + // Log out anyway + removeCookie("session_token", { path: "/" }); + removeCookie("session_token", { path: "/" }); + removeCookie("session_token", { path: "/" }); + + removeCookie("__session", { path: "/" }); + removeCookie("__session", { path: "/" }); + removeCookie("__session", { path: "/" }); + + removeCookie("__session", { path: "/" }); + + window.location.pathname = "/"; + + localStorage.setItem("globalUrl", "") + + // Delete userinfo from localstorage + localStorage.removeItem("apps") + localStorage.removeItem("workflows") + localStorage.removeItem("userinfo") + }) + .catch((error) => { + console.log(error); + }); + }; + + const topbarHeight = showTopbar ? 40 : 0 + const topbar = !isCloud || !showTopbar ? null : + curpath === "/" || curpath.includes("/docs") || curpath === "/pricing" || curpath === "/contact" || curpath === "/search" || curpath === "/usecases" || curpath === "/training" || curpath === "/professional-services" ? + +
+ + {/* Shuffle 1.4.0 is out! Read more about  */} + Shuffle 2.0.0 is out now! + + { + ReactGA.event({ + category: "landingpage", + action: "click_header_training", + label: "", + }) + + navigate("/articles/2.0_release") + + }} style={{ cursor: "pointer", textDecoration: "none", fontWeight: 600, color: "rgba(255,255,255,0.9)" }}> +  Read about it here. + + + + { + setShowTopbar(false) + + // Set storage that it's clicked + localStorage.setItem(topbar_var, "true") + }}> + + +
+
+ : + null + + const handleClickChangeOrg = (orgId) => { + setIsOrgChanging(true); + const data = { org_id: orgId }; + + localStorage.setItem("globalUrl", ""); + localStorage.setItem("getting_started_sidebar", "open"); + toast.info("Changing organization..."); + fetch(`${globalUrl}/api/v1/orgs/${orgId}/change`, { + mode: "cors", + credentials: "include", + crossDomain: true, + method: "POST", + body: JSON.stringify(data), + withCredentials: true, + headers: { + "Content-Type": "application/json; charset=utf-8", + }, + }) + .then(function (response) { + if (response.status !== 200) { + console.log("Error in response"); + } else { + localStorage.removeItem("apps"); + localStorage.removeItem("workflows"); + localStorage.removeItem("userinfo"); + } + + return response.json(); + }) + .then(function (responseJson) { + if (responseJson.success === true) { + if ( + responseJson.region_url !== undefined && + responseJson.region_url !== null && + responseJson.region_url.length > 0 + ) { + console.log("Region Change: ", responseJson.region_url); + localStorage.setItem("globalUrl", responseJson.region_url); + //globalUrl = responseJson.region_url + } + + if (responseJson["reason"] === "SSO_REDIRECT") { + toast.info("Redirecting to SSO login page as SSO is required for this organization."); + setTimeout(() => { + window.location.href = responseJson["url"]; + }, 2000); + } else { + toast("Successfully changed active organization - refreshing!"); + setTimeout(() => { + window.location.reload(); + }, 2000); + } + } else { + setIsOrgChanging(false); + if ( + responseJson.reason !== undefined && + responseJson.reason !== null && + responseJson.reason.length > 0 + ) { + toast(responseJson.reason); + } else { + toast("Failed changing org. Try again or contact support@shuffler.io if this persists."); + } + } + }) + .catch((error) => { + console.log("error changing: ", error); + setIsOrgChanging(false); + }); + }; + + // Add these helper functions from LeftSideBar + const getRegionTag = (region_url) => { + let regiontag = "EU"; + if (region_url !== undefined && region_url !== null && region_url.length > 0) { + const regionsplit = 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"; + } else if (regiontag === "frankfurt") { + regiontag = "EU-2"; + } else if (regiontag === "ca") { + regiontag = "CA"; + } + } + } + return regiontag; + }; + + const getRegionFlag = (region_url) => { + const regionTag = getRegionTag(region_url); + const regionMapping = { + "US": "us", + "EU": "eu", + "EU-2": "de", + "CA": "ca", + "UK": "gb" + }; + + const region = regionMapping[regionTag] || "eu"; + return `https://flagcdn.com/48x36/${region}.png`; + }; + + // Add this useEffect to prevent scroll locking + useEffect(() => { + // Remove modal classes that cause scroll locking + const removeModalClasses = () => { + document.body.style.overflow = 'auto'; + document.body.style.paddingRight = '0px'; + }; + + // Create observer to watch for class changes + const observer = new MutationObserver((mutations) => { + mutations.forEach((mutation) => { + if (mutation.attributeName === 'class') { + if (document.body.classList.contains('MuiModal-open')) { + removeModalClasses(); + } + } + }); + }); + + // Start observing + observer.observe(document.body, { + attributes: true, + attributeFilter: ['class'], + }); + + // Cleanup + return () => { + observer.disconnect(); + removeModalClasses(); + }; + }, []); + + const handleOrgSelectClick = (event) => { + // Prevent the default Select behavior + event.preventDefault(); + if(isCloud){ + ReactGA.event({ + category: "navbar", + action: "org_dropdown_click", + label: "org_dropdown_click", + }) + } + // Toggle the select open state + setOrgSelectOpen(!orgSelectOpen); + }; + + return ( + + {topbar} + + + + { + if (isCloud) { + ReactGA.event({ + category: "navbar", + action: "home_click", + label: "shuffle_logo", + }); + } + }} + > + shuffle logo + + + + {isMobile ? ( + /* Mobile Navigation Icons */ + + setSearchBarModalOpen(true)} + > + + + { + if(Boolean(anchorElNav)) { + handleCloseNavMenu(); + }else{ + handleOpenNavMenu(e); + } + } + } + sx={{ color: "white" }} + > + {Boolean(anchorElNav) ? ( + + ) : ( + + )} + + + + ) : ( + <> + {modalView} + {pricingModal} + {renderDesktopMenu()} + {/* Right Side Buttons */} + + {!isLoaded ? ( + + ) : isLoggedIn ? ( + <> + {isOrgChanging ? ( + + ) : ( + <> + + + + K +
+ } + placement="bottom" + arrow + componentsProps={{ + tooltip: { + sx: { + backgroundColor: "rgba(33, 33, 33, 1)", + color: "rgba(241, 241, 241, 1)", + fontSize: 12, + border: "1px solid rgba(73, 73, 73, 1)", + fontFamily: theme?.typography?.fontFamily, + } + }, + popper: { + sx: { + zIndex: 1000019, + } + } + }} + > + { + if (isCloud) { + ReactGA.event({ + category: "navbar", + action: "search_icon_click", + label: "search_icon_click", + }) + } + setSearchBarModalOpen(true); + }} + > + + + + {isCloud && (userdata.org_status === undefined || userdata.org_status === null || userdata.org_status.length === 0) ? + + : null} + + + {/* Organization Dropdown */} + {/* */} + + {/* User Avatar Menu */} + { + if (isCloud) { + ReactGA.event({ + category: "navbar", + action: "click_user_menu", + label: "open_user_dropdown" + }); + } + handleOpenUserMenu(e); + }} + sx={{ + padding: 0, + "&:hover": { + backgroundColor: "rgba(47, 47, 47, 0.5)", + }, + }} + > + + + + + + + {/* User Info Section */} + + + + + + {userdata?.username} + + + + + + + + {/* Menu Items */} + { + if (isCloud) { + ReactGA.event({ + category: "navbar", + action: "user_dropdown_click", + label: "go_to_admin" + }); + } + handleCloseUserMenu(); + navigate("/admin"); + }} + onMouseEnter={() => setIsHovered("organization")} + onMouseLeave={() => setIsHovered("")} + sx={{ + py: 1.5, + px: 2, + fontFamily: theme.typography.fontFamily, + transition: "color 0.1s ease", + '&:hover': { + backgroundColor: 'rgba(255, 255, 255, 0.1)', + color: isHovered === "organization" ? "#FF8544" : "white", + }, + }} + > + + + + Organization + + + + + { + if (isCloud) { + ReactGA.event({ + category: "navbar", + action: "user_dropdown_click", + label: "go_to_settings", + }) + } + handleCloseUserMenu(); + navigate("/settings"); + }} + onMouseEnter={() => setIsHovered("settings")} + onMouseLeave={() => setIsHovered("")} + sx={{ + py: 1.5, + px: 2, + fontFamily: theme.typography.fontFamily, + transition: "color 0.1s ease", + '&:hover': { + backgroundColor: 'rgba(255, 255, 255, 0.1)', + color: isHovered === "settings" ? "#FF8544" : "white", + }, + }} + > + + + + Settings + + + + + { + if (isCloud) { + ReactGA.event({ + category: "navbar", + action: "user_dropdown_click", + label: "go_to_notifications", + }) + } + handleCloseUserMenu(); + navigate("/admin?admin_tab=notifications"); + }} + onMouseEnter={() => setIsHovered("notifications")} + onMouseLeave={() => setIsHovered("")} + sx={{ + py: 1.5, + px: 2, + fontFamily: theme.typography.fontFamily, + transition: "color 0.1s ease", + '&:hover': { + backgroundColor: 'rgba(255, 255, 255, 0.1)', + color: isHovered === "notifications" ? "#FF8544" : "white", + }, + }} + > + + + + Notifications ({notifications === undefined || notifications === null ? 0 : + notifications?.filter((notification) => notification.read === false).length}) + + + + + + + + { + if (isCloud) { + ReactGA.event({ + category: "navbar", + action: "user_dropdown_click", + label: "go_to_about", + }) + } + handleCloseUserMenu(); + navigate("/docs/about"); + }} + onMouseEnter={() => setIsHovered("about")} + onMouseLeave={() => setIsHovered("")} + sx={{ + py: 1.5, + px: 2, + fontFamily: theme.typography.fontFamily, + transition: "color 0.1s ease", + '&:hover': { + backgroundColor: 'rgba(255, 255, 255, 0.1)', + color: isHovered === "about" ? "#FF8544" : "white", + }, + }} + > + + + + About + + + + + { + if (isCloud) { + ReactGA.event({ + category: "navbar", + action: "user_dropdown_click", + label: "logout_click", + }) + } + handleCloseUserMenu(); + handleClickLogout(); + }} + onMouseEnter={() => setIsHovered("logout")} + onMouseLeave={() => setIsHovered("")} + sx={{ + py: 1.5, + px: 2, + fontFamily: theme.typography.fontFamily, + '&:hover': { + backgroundColor: 'rgba(255, 255, 255, 0.1)', + color: isHovered === "logout" ? "#FD4C62" : "white", + }, + }} + > + + + + Logout + + + + + + + + + Version 1.4.5 + + + + + )} + + ) : ( + <> + + + + K +
+ } + placement="bottom" + arrow + componentsProps={{ + tooltip: { + sx: { + backgroundColor: "rgba(33, 33, 33, 1)", + color: "rgba(241, 241, 241, 1)", + fontSize: 12, + border: "1px solid rgba(73, 73, 73, 1)", + fontFamily: theme?.typography?.fontFamily, + } + }, + popper: { + sx: { + zIndex: 1000019, + } + } + }} + > + { + if (isCloud) { + ReactGA.event({ + category: "navbar", + action: "search_icon_click", + label: "search_icon_click", + }) + } + setSearchBarModalOpen(true); + }} + > + + + + + + + {isCloud && + + } + + + + )} + + + )} + + + + ); +}; + +export default Navbar; From 63ce21fe6072489ab1e6109139db380fb39c5f5d Mon Sep 17 00:00:00 2001 From: Frikky Date: Mon, 3 Mar 2025 22:08:06 +0100 Subject: [PATCH 11/18] Fixed linking problems --- frontend/src/components/Navbar.jsx | 2 +- frontend/src/views/AngularWorkflow.jsx | 57 ++------------------------ 2 files changed, 5 insertions(+), 54 deletions(-) diff --git a/frontend/src/components/Navbar.jsx b/frontend/src/components/Navbar.jsx index 509b9b4f..fd808894 100644 --- a/frontend/src/components/Navbar.jsx +++ b/frontend/src/components/Navbar.jsx @@ -178,7 +178,7 @@ const menuData = { title: "Articles", icon: "/images/icons/articles.svg", hoverIcon: "/images/icons/articles_hover.svg", - link: "/articles", + link: "/articles/2.0_release", gaData: { category: "navbar", action: "resources_click", diff --git a/frontend/src/views/AngularWorkflow.jsx b/frontend/src/views/AngularWorkflow.jsx index 5f9eadd2..79a9d98a 100755 --- a/frontend/src/views/AngularWorkflow.jsx +++ b/frontend/src/views/AngularWorkflow.jsx @@ -1288,56 +1288,6 @@ const AngularWorkflow = (defaultprops) => { return } - //console.log("Failed in trigger selection: ", selectedTriggerIndex, "Trigger: ", selectedTrigger) - - var found = null - try { - for (var key in workflows) { - const curworkflow = workflows[key] - const curtrigger = curworkflow?.triggers[selectedTriggerIndex] - if (curtrigger === undefined || curtrigger === null) { - console.log("Failed in trigger selection (1): ", curworkflow) - continue - } - - if (curtrigger?.parameters === undefined || curtrigger?.parameters === null || curtrigger?.parameters.length === 0) { - console.log("Failed in trigger selection (2): ", curworkflow) - continue - } - - if (curtrigger?.parameters[0] === undefined || curtrigger?.parameters[0] === null || curtrigger?.parameters[0].value === undefined || curtrigger?.parameters[0].value === null) { - console.log("Failed in trigger selection (3): ", curworkflow) - continue - } - - if (curtrigger?.parameters[0].value === selectedTrigger?.parameters[0]?.value) { - found = curworkflow - setSubworkflow(curworkflow) - } - } - - if (found !== null) { - setSubworkflow(found) - } - } catch (e) { - console.log("Failed in trigger selection (4): ", e) - //return - } - - if (found) { - const startNode = found.actions?.find((action) => action.id === workflow?.triggers[selectedTriggerIndex]?.parameters[3]?.value) - setSubworkflowStartnode(startNode) - } - - /* - // Multi-tenant sometimes gives us shit :( - if (selectedTrigger === undefined || selectedTrigger === null || selectedTrigger.id === undefined || selectedTrigger.id === null && selectedTriggerIndex >= 0) { - if (workflow.triggers !== undefined && workflow.triggers !== null && workflow.triggers.length > selectedTriggerIndex) { - setSelectedTrigger(workflow.triggers[selectedTriggerIndex]) - } - } - */ - // Check if the running state is correct or not according to allTriggers if (allTriggers !== undefined && allTriggers !== null) { // Find the active trigger @@ -1592,10 +1542,10 @@ const AngularWorkflow = (defaultprops) => { if (Object.getOwnPropertyNames(baseSubflow).length > 0) { const foundAction = baseSubflow.actions.find(action => action?.id === param.value) if (foundAction !== null && foundAction !== undefined) { - setSubworkflowStartnode(foundAction); + setSubworkflowStartnode(foundAction) } } else { - setSubworkflowStartnode(param.value); + setSubworkflowStartnode(param.value) } } } @@ -3951,7 +3901,7 @@ const AngularWorkflow = (defaultprops) => { apps.push(responseJson.actions[index]); } - console.log("Setting used subflow apps: ", apps) + //console.log("Setting used subflow apps: ", apps) setUsedSubflowApps(apps); return apps @@ -15102,6 +15052,7 @@ const AngularWorkflow = (defaultprops) => { Select the Startnode
+ Date: Tue, 4 Mar 2025 19:37:44 +0100 Subject: [PATCH 12/18] Last builds for 2.0 --- backend/go-app/go.mod | 2 +- frontend/src/components/Navbar.jsx | 10 +- frontend/src/components/RuntimeDebugger.jsx | 2 +- frontend/src/views/Docs.jsx | 329 +++++++++++++++++--- frontend/src/views/Workflows2.jsx | 8 + functions/onprem/orborus/go.mod | 2 +- functions/onprem/worker/go.mod | 2 +- 7 files changed, 298 insertions(+), 57 deletions(-) diff --git a/backend/go-app/go.mod b/backend/go-app/go.mod index 9864b1dc..8af42425 100644 --- a/backend/go-app/go.mod +++ b/backend/go-app/go.mod @@ -18,7 +18,7 @@ require ( github.com/gorilla/mux v1.8.1 github.com/h2non/filetype v1.1.3 github.com/satori/go.uuid v1.2.0 - github.com/shuffle/shuffle-shared v0.8.6 + github.com/shuffle/shuffle-shared v0.8.7 golang.org/x/crypto v0.32.0 google.golang.org/api v0.176.1 google.golang.org/grpc v1.68.1 diff --git a/frontend/src/components/Navbar.jsx b/frontend/src/components/Navbar.jsx index fd808894..ebee1db5 100644 --- a/frontend/src/components/Navbar.jsx +++ b/frontend/src/components/Navbar.jsx @@ -48,7 +48,7 @@ const menuData = { description: "The most versatile automation engine with focus on security.", icon: "images/logos/orange_logo.svg", - path: "/docs", + path: "/docs/about", gaData: { category: "navbar", action: "products_click", @@ -150,7 +150,7 @@ const menuData = { title: "Documentation", icon: "/images/icons/docs.svg", hoverIcon: "/images/icons/docs_hover.svg", - link: "/docs", + link: "/docs/about", gaData: { category: "navbar", action: "resources_click", @@ -1516,7 +1516,7 @@ const Navbar = (props) => {
{/* Shuffle 1.4.0 is out! Read more about  */} - Shuffle 2.0.0 is out now! + Shuffle 2.0.0 is out now!  { ReactGA.event({ @@ -1528,7 +1528,7 @@ const Navbar = (props) => { navigate("/articles/2.0_release") }} style={{ cursor: "pointer", textDecoration: "none", fontWeight: 600, color: "rgba(255,255,255,0.9)" }}> -  Read about it here. + Read about it here. @@ -1701,6 +1701,7 @@ const Navbar = (props) => { { { imageSource = "/images/no_image.png" } }else { - if (userdata.active_org.image?.length > 0){ + if (userdata?.active_org.image?.length > 0){ imageSource = userdata?.active_org?.image }else { imageSource = "/images/no_image.png" diff --git a/frontend/src/views/Docs.jsx b/frontend/src/views/Docs.jsx index 836ada35..7a9c371e 100755 --- a/frontend/src/views/Docs.jsx +++ b/frontend/src/views/Docs.jsx @@ -5,7 +5,7 @@ import theme from '../theme.jsx'; import ReactJson from "react-json-view-ssr"; import { isMobile } from "react-device-detect"; import { BrowserView, MobileView } from "react-device-detect"; -import { useParams, useNavigate, Link } from "react-router-dom"; +import { useParams, useNavigate, Link, useLocation } from "react-router-dom"; import { validateJson, GetIconInfo } from "../views/Workflows.jsx"; import remarkGfm from 'remark-gfm' import { Context } from "../context/ContextApi.jsx"; @@ -30,7 +30,9 @@ import { Edit as EditIcon, KeyboardArrowRight as KeyboardArrowRightIcon, ExpandMore as ExpandMoreIcon, - FileCopy as FileCopyIcon + FileCopy as FileCopyIcon, + ChevronLeft, + ChevronRight } from "@mui/icons-material"; import { fontGrid } from "@mui/material/styles/cssUtils.js"; @@ -51,6 +53,7 @@ const dividerColor = "rgb(225, 228, 232)"; const hrefStyle = { color: "rgba(255, 255, 255, 0.8)", textDecoration: "none", + marginRight: window.location.pathname.includes("/articles/") ? "0.8em" : undefined, }; @@ -152,11 +155,33 @@ export const OuterLink = (props) => { } + export const Img = (props) => { // Find parent container and check width - + const isArticlePage = window.location.pathname.includes("/articles/"); var height = "auto" - var width = 750 + var width = isArticlePage ? 1000 : 750 + + const docsImageStyle = { + border: "1px solid rgba(255,255,255,0.3)", + borderRadius: theme.palette?.borderRadius, + width: width, + maxWidth: width, + margin: "auto", + marginTop: 10, + marginBottom: 10 + } + + const articleImageStyle = { + borderRadius: theme.palette?.borderRadius, + minWidth: 350, + maxWidth: "100%", + textAlign: "center", + margin: "auto", + marginTop: 20, + marginBottom: 20, + } + if (props.height !== undefined && props.height !== null) { height = props.height } @@ -167,7 +192,7 @@ export const Img = (props) => { return( {props.alt} @@ -251,8 +276,12 @@ export const CodeHandler = (props) => { } const Docs = (defaultprops) => { - const { globalUrl, selectedDoc, serverside, serverMobile, isLoggedIn, isLoaded } = defaultprops; + const { globalUrl, selectedDoc, serverside, serverMobile, isLoggedIn, isLoaded, userdata } = defaultprops; + + console.log("\n\nSELECTED DOC\n", selectedDoc, "\n\n") + let navigate = useNavigate(); + const location = useLocation(); // Quickfix for react router 5 -> 6 const params = useParams(); //var props = JSON.parse(JSON.stringify(defaultprops)) @@ -283,8 +312,16 @@ const Docs = (defaultprops) => { serverside === true ? "" : window.location.href ); const [hashRendered, setHashRendered] = React.useState(false) - + const [sidebarOpen, setSidebarOpen] = useState(false); + const [activeSubItem, setActiveSubItem] = useState(false); const headingElementsRef = useRef({}) + var isArticlePage = window.location.pathname.includes("/articles/") || window.location.pathname === "/articles" ? true : false; + + + useEffect(() => { + fetchDocList(); + setSidebarOpen(false); + }, [location]); useEffect(() => { //if (params["key"] === undefined) { @@ -460,8 +497,13 @@ const Docs = (defaultprops) => { } const SidebarPaperStyle = { - backgroundColor: "rgb(26,26,26)", + backgroundColor: isArticlePage ? "transparent" : "rgb(26,26,26)", + border: isArticlePage ? "none" : undefined, + borderRadius: isArticlePage ? "none" : undefined, + boxShadow: isArticlePage ? "none" : undefined, backgroundImage: "none", + width: isArticlePage ? "100%" : undefined, + height: isArticlePage ? "100%" : undefined, overflowX: "hidden", position: "relative", paddingLeft: 15, @@ -527,7 +569,7 @@ const Docs = (defaultprops) => { backgroundColor: theme.palette.inputColor, padding: 15, borderRadius: theme.palette?.borderRadius, - marginBottom: 25, + marginBottom: isArticlePage ? 25 : 30, display: "flex", }} > @@ -620,7 +662,7 @@ const Docs = (defaultprops) => { style={{ width: "90%", marginTop: 60, - marginBottom: 20, + marginBottom: isArticlePage ? 40 : 20, backgroundColor: theme.palette.inputColor, }} /> @@ -630,7 +672,21 @@ const Docs = (defaultprops) => { marginBlock: "0.85em", alignItems: "center" }}> {element} - { + e.preventDefault(); // Prevent default navigation + document.getElementById(id)?.scrollIntoView({ + behavior: 'smooth' + }); + + const url = `${window.location.origin}${window.location.pathname}#${id}`; + navigator.clipboard.writeText(url); + toast("Link copied to clipboard", { + position: "bottom-center", + autoClose: 2000, + }); + }} + style={{ textDecoration: "none", color: "white", paddingLeft: "0.3em", rotate: "-30deg", paddingTop: "0.9em", display: props.level === 1 ? "none" : "block", @@ -639,7 +695,7 @@ const Docs = (defaultprops) => {
- {extraInfo} + {extraInfo && userdata?.support}
) } @@ -647,21 +703,56 @@ const Docs = (defaultprops) => { const SideBar = { width: "17%", + borderRight: !isArticlePage ? "1px solid rgba(255,255,255,0.3)" : undefined, + minWidth: isArticlePage ? "250px" : "17%", + maxWidth: isArticlePage ? "250px" : "17%", + marginLeft: isArticlePage ? sidebarOpen ? 40 : 50 : undefined, position: "sticky", - top: 50, - paddingTop: "0.25em", - minHeight: "95vh", - maxHeight: "95vh", + top: isArticlePage ? 120 : 50, + paddingTop: isArticlePage ? "0.65em" : "0.25em", + paddingRight: isArticlePage ? "1em" : undefined, + minHeight: isArticlePage ? "80vh" : "95vh", + maxHeight: isArticlePage ? "80vh" : "95vh", overflowX: "hidden", overflowY: "auto", zIndex: 1000, - //borderRight: "1px solid rgba(255,255,255,0.3)", + backgroundColor: isArticlePage ? "#212121" : undefined, + borderRadius: isArticlePage ? theme.palette?.borderRadius : undefined, + transition: isArticlePage ? "width 0.3s ease, min-width 0.3s ease" : undefined, + }; + + const sideBarToggleButton = { + position: "absolute", + bottom: sidebarOpen ? 20 : "50%", + right: sidebarOpen ? 30 : 21, + zIndex: 1000, + backgroundColor: "#212121", + border: "1px solid rgba(255,255,255,0.3)", + borderRadius: "50%", + width: 40, + height: 40, + display: "flex", + alignItems: "center", + justifyContent: "center", + cursor: "pointer", + transition: "transform 0.3s ease", + "&:hover": { + backgroundColor: "#2c2c2c", + } + }; + + const collapsedSideBar = { + ...SideBar, + width: "40px", + minWidth: "40px", + maxWidth: "40px", + overflow: "hidden", }; const IndexBar = { alignSelf: "flex-start", position: "sticky", - top: 80, + top: 120, overflowY: "auto", minHeight: "93vh", maxHeight: "93vh", @@ -670,8 +761,9 @@ const Docs = (defaultprops) => { overflow: "hidden", } - const fetchDocList = () => { - fetch(`${globalUrl}/api/v1/docs`, { + const fetchDocList = (resetCache = false) => { + const url = isArticlePage ? `${globalUrl}/api/v1/articles?resetCache=${resetCache}` : `${globalUrl}/api/v1/docs`; + fetch(url, { method: "GET", headers: { "Content-Type": "application/json", @@ -692,7 +784,8 @@ const Docs = (defaultprops) => { }; const fetchDocs = (docId) => { - fetch(`${globalUrl}/api/v1/docs/${docId}`, { + const url = isArticlePage ? `${globalUrl}/api/v1/articles/${docId}` : `${globalUrl}/api/v1/docs/${docId}`; + fetch(url, { method: "GET", headers: { "Content-Type": "application/json", @@ -706,18 +799,32 @@ const Docs = (defaultprops) => { } if (responseJson.success && responseJson.reason !== undefined) { + + if(isArticlePage && window.location.pathname.includes("/articles/")) { + if (responseJson.reason !== undefined && responseJson.reason !== null && responseJson.reason.includes("404: Not Found")) { + setData("# Error\nThis page doesn't exist."); + toast("This page doesn't exist. Redirecting to the latest article...", { + position: "bottom-center", + autoClose: 2000, + }); + setTimeout(() => { + navigate(`/articles/2.0_release`) + }, 2000) + return + } + } // Find tags and translate them into ![]() format const imgRegex = / { .catch((error) => { }); }; + const handleResetCache = () => { + fetchDocList(true); + toast("Cache has been reset"); + } + if (firstrequest) { setFirstrequest(false); if (!serverside) { @@ -769,8 +881,26 @@ const Docs = (defaultprops) => { // return null //} // - if (props.match.params.key === undefined) { - + if (props.match.params.key === undefined && isArticlePage) { + fetch(`${globalUrl}/api/v1/articles`, { + method: "GET", + headers: { + "Content-Type": "application/json", + Accept: "application/json", + }, + }) + .then((response) => response.json()) + .then((responseJson) => { + if (responseJson.success && responseJson.list && responseJson.list.length > 0) { + // Navigate to the latest article + if (responseJson?.list[0]?.name) { + navigate(`/articles/${responseJson?.list[0].name}`); + } + } + }) + .catch((error) => { + console.error("Error fetching articles:", error); + }); } else { console.log("DOCID: ", props.match.params.key) fetchDocs(props.match.params.key) @@ -793,7 +923,7 @@ const Docs = (defaultprops) => { maxWidth: "100%", minWidth: "100%", overflow: "hidden", - fontSize: isMobile ? "1.3rem" : "1rem", + fontSize: isMobile ? "1.3rem" : "1.1rem", }; const alertNote = { @@ -940,14 +1070,38 @@ const Docs = (defaultprops) => { } + const activeHrefStyleToc2 = { + ...hrefStyleToc2, + color: "#f86a3e", + }; + + const activeListItemStyle = { + backgroundColor: "rgba(248, 106, 62, 0.08)", // Slight orange tint + marginRight: window.location.pathname.includes("/articles/") ? "0.8em" : undefined, + borderLeft: "3px solid #f86a3e", + paddingLeft: "13px", // Compensate for the border + }; + + const toggleSidebar = () => { + setSidebarOpen(!sidebarOpen); + }; + + // PostDataBrowser Section const postDataBrowser = list === undefined || list === null ? null : (
-
+
- + {list.map((data, index) => { const item = data.name; if (item === undefined) { @@ -965,23 +1119,62 @@ const Docs = (defaultprops) => { - {newname} - + style={{ + color: itemMatching ? "#f86a3e" : "inherit", + flex: 1, + }} + primary={ + + {newname} + + } + /> + ); })} + { + isArticlePage && ( + + {sidebarOpen ? : } + + ) + }
-
+
{props.match.params.key === undefined ? mainpageInfo : @@ -1003,12 +1196,26 @@ const Docs = (defaultprops) => { }
+ {userdata?.support && isArticlePage && ( + + )} {tocLines.length > 0 ? ( -

Table of Content

+

Table Of Content

) : null} - +
@@ -1083,7 +1297,7 @@ const Docs = (defaultprops) => { color="primary" onClick={handleClick} > -
More docs
+
More {isArticlePage ? "articles" : "docs"}
{ return null; } - const path = "/docs/" + item; + const path = isArticlePage ? "/articles/" + item : "/docs/" + item; const newname = item.charAt(0).toUpperCase() + item.substring(1).split("_").join(" ").split("-").join(" "); @@ -1108,7 +1322,8 @@ const Docs = (defaultprops) => { key={index} style={{ color: "white" }} onClick={() => { - window.location.pathname = path; + navigate(path) + handleClose() }} > {newname} @@ -1140,8 +1355,22 @@ const Docs = (defaultprops) => { { + console.log(`Scrolling to: ${data.id}`); + setTimeout(() => { + const element = document.getElementById(data?.id); + if (element) { + element.scrollIntoView({ behavior: 'smooth' }); + } else { + console.error(`Element with id ${data.id} not found.`); + } + }, 100); // Delay to ensure the content is loaded + handleCloseToc(); + }} > - {data.title} + {data.title} ) })} @@ -1178,7 +1407,7 @@ const Docs = (defaultprops) => { color="primary" onClick={handleClick} > -
More docs
+
More {isArticlePage ? "articles" : "docs"}
); @@ -1198,7 +1427,7 @@ export default Docs; const DocsContent = memo(({postDataBrowser, postDataMobile}) => { return( -
+
{postDataBrowser} {postDataMobile}
@@ -1210,12 +1439,14 @@ const DocsWrapper = memo(({isLoggedIn, isLoaded, children })=>{ return (
{children} diff --git a/frontend/src/views/Workflows2.jsx b/frontend/src/views/Workflows2.jsx index 33f8618f..0b079407 100644 --- a/frontend/src/views/Workflows2.jsx +++ b/frontend/src/views/Workflows2.jsx @@ -1758,9 +1758,17 @@ const Workflows2 = (props) => { let exportFileDefaultName = data.name + ".json"; data["owner"] = ""; + data["updated_by"] = ""; data["org"] = []; data["org_id"] = ""; data["execution_org"] = {}; + data["created"] = 0 + data["due_date"] = 0 + data["edited"] = 0 + + data["validation"] = {}; + data["suborg_distribution"] = [] + data["parentorg_workflow"] = "" // These are backwards.. True = saved before. Very confuse. data["previously_saved"] = false; diff --git a/functions/onprem/orborus/go.mod b/functions/onprem/orborus/go.mod index 6b8018a5..7909c2ee 100644 --- a/functions/onprem/orborus/go.mod +++ b/functions/onprem/orborus/go.mod @@ -9,7 +9,7 @@ require ( github.com/docker/docker v27.5.0+incompatible github.com/docker/go-connections v0.5.0 github.com/satori/go.uuid v1.2.0 - github.com/shuffle/shuffle-shared v0.8.6 + github.com/shuffle/shuffle-shared v0.8.7 k8s.io/api v0.30.2 k8s.io/apimachinery v0.30.2 ) diff --git a/functions/onprem/worker/go.mod b/functions/onprem/worker/go.mod index 01cad7bf..5b8cbd22 100644 --- a/functions/onprem/worker/go.mod +++ b/functions/onprem/worker/go.mod @@ -8,7 +8,7 @@ require ( github.com/docker/docker v27.5.0+incompatible github.com/gorilla/mux v1.8.1 github.com/satori/go.uuid v1.2.0 - github.com/shuffle/shuffle-shared v0.8.6 + github.com/shuffle/shuffle-shared v0.8.7 k8s.io/api v0.30.2 k8s.io/apimachinery v0.30.2 k8s.io/client-go v0.30.2 From 6a2355c6f6b55a8b0b8ad303e69ce95fa06793d2 Mon Sep 17 00:00:00 2001 From: Frikky Date: Tue, 4 Mar 2025 21:56:39 +0100 Subject: [PATCH 13/18] Minor frontend fixes --- frontend/src/components/AppCreationModal.jsx | 40 ++++++++++---------- frontend/src/views/Apps2.jsx | 1 + 2 files changed, 21 insertions(+), 20 deletions(-) diff --git a/frontend/src/components/AppCreationModal.jsx b/frontend/src/components/AppCreationModal.jsx index 80cf1711..a9f8f598 100644 --- a/frontend/src/components/AppCreationModal.jsx +++ b/frontend/src/components/AppCreationModal.jsx @@ -22,7 +22,6 @@ import CreateIcon from '@mui/icons-material/Create' import { toast } from 'react-toastify' import YAML from "yaml"; - const AppCreationModal = ({ open, onClose, theme, globalUrl, isCloud }) => { const [openApiModal, setOpenApiModal] = useState(false) const [generateAppModal, setGenerateAppModal] = useState(false) @@ -257,26 +256,27 @@ const AppCreationModal = ({ open, onClose, theme, globalUrl, isCloud }) => { body: openApidata, credentials: "include", }) - .then((response) => { + .then((response) => { - setValidation(false); - return response.json(); - }) - .then((responseJson) => { - if (responseJson.success) { - setAppValidation(responseJson.id); - } else { - if (responseJson.reason !== undefined) { - setOpenApiError(responseJson.reason); - } - toast("An error occurred in the response"); - } - }) - .catch((error) => { - setValidation(false); - toast(error.toString()); - setOpenApiError(error.toString()); - }); + setValidation(false); + return response.json(); + }) + .then((responseJson) => { + if (responseJson?.success === true) { + setAppValidation(responseJson?.id) + navigate(`/apps/new?id=${responseJson?.id}`) + } else { + if (responseJson.reason !== undefined) { + setOpenApiError(responseJson.reason) + } + toast("An error occurred in the response"); + } + }) + .catch((error) => { + setValidation(false); + toast(error.toString()); + setOpenApiError(error.toString()); + }); }; const redirectOpenApi = () => { diff --git a/frontend/src/views/Apps2.jsx b/frontend/src/views/Apps2.jsx index 87e60f82..c5fb9d38 100644 --- a/frontend/src/views/Apps2.jsx +++ b/frontend/src/views/Apps2.jsx @@ -1,6 +1,7 @@ import React, { useState, useEffect, useContext, useCallback, memo, useMemo, useRef } from "react"; import theme from "../theme.jsx"; import { isMobile } from "react-device-detect"; + import { useLocation, useNavigate } from "react-router-dom"; import { TextField, Button, Typography, MenuItem, Select, Tabs, Tab, Zoom, From bc13fc882a8c11e187d160929057a3327b5605de Mon Sep 17 00:00:00 2001 From: Frikky Date: Wed, 5 Mar 2025 13:49:57 +0100 Subject: [PATCH 14/18] Rebuild with 2.0.0 images for consistency --- .github/workflows/dockerbuild.yaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/dockerbuild.yaml b/.github/workflows/dockerbuild.yaml index 570c21f1..85890765 100644 --- a/.github/workflows/dockerbuild.yaml +++ b/.github/workflows/dockerbuild.yaml @@ -20,19 +20,19 @@ jobs: include: - app: frontend path: frontend - version: latest + version: 2.0.0 experimental: true - app: backend path: backend - version: latest + version: 2.0.0 experimental: true - app: orborus path: functions/onprem/orborus - version: latest + version: 2.0.0 experimental: true - app: worker path: functions/onprem/worker - version: latest + version: 2.0.0 experimental: true steps: - name: Checkout From 6598fe7cfed08121a61f40277059b19d8ab775ea Mon Sep 17 00:00:00 2001 From: Frikky Date: Wed, 5 Mar 2025 14:09:13 +0100 Subject: [PATCH 15/18] Fixed an issue where SSO URL was not being parsed on the login page --- frontend/src/context/ContextApi.jsx | 13 +++++++++---- frontend/src/views/Docs.jsx | 3 +-- frontend/src/views/LoginPage.jsx | 28 ++++++++++++++++++++++++++++ frontend/src/views/LoginPageOld.jsx | 2 +- 4 files changed, 39 insertions(+), 7 deletions(-) diff --git a/frontend/src/context/ContextApi.jsx b/frontend/src/context/ContextApi.jsx index 3932dfd0..4e82ba1e 100644 --- a/frontend/src/context/ContextApi.jsx +++ b/frontend/src/context/ContextApi.jsx @@ -1,14 +1,15 @@ -import { createContext, useState, useEffect } from 'react'; +import React, { createContext, useState, useEffect } from 'react'; export const Context = createContext(); -export const AppContext =(props) => { +export const AppContext = (props) => { + const { serverside } = props - const currentLocation = window?.location?.pathname; + const currentLocation = serverside === true ? "" : window?.location?.pathname; // Left side bar global states const [searchBarModalOpen, setSearchBarModalOpen] = useState(false); const [leftSideBarOpenByClick, setLeftSideBarOpenByClick] = useState(currentLocation?.includes('/workflows/') ? false : true) - const [windowWidth, setWindowWidth] = useState(window.innerWidth); + const [windowWidth, setWindowWidth] = useState(serverside === true ? 100 : window.innerWidth); useEffect(() => { if (currentLocation?.includes('/workflows/') && leftSideBarOpenByClick === true) { @@ -18,6 +19,10 @@ export const AppContext =(props) => { //Calculate window width useEffect(() => { + if (serverside === true) { + return + } + const handleResize = () => { setWindowWidth(window?.innerWidth); }; diff --git a/frontend/src/views/Docs.jsx b/frontend/src/views/Docs.jsx index 7a9c371e..6aa8ec08 100755 --- a/frontend/src/views/Docs.jsx +++ b/frontend/src/views/Docs.jsx @@ -694,8 +694,7 @@ const Docs = (defaultprops) => {
- - {extraInfo && userdata?.support} + {isArticlePage ? (userdata?.support ? extraInfo : "") : extraInfo} ) } diff --git a/frontend/src/views/LoginPage.jsx b/frontend/src/views/LoginPage.jsx index 02a2967f..385a2d8e 100755 --- a/frontend/src/views/LoginPage.jsx +++ b/frontend/src/views/LoginPage.jsx @@ -282,6 +282,8 @@ const LoginPage = props => { const [register, setRegister] = useState(inregister); const [checkboxClicked, setCheckboxClicked] = useState(false); const [loginWithSSO, setLoginWithSSO] = useState(false) + + const [ssoUrl, setSSOUrl] = useState(""); const isCloud = window.location.host === "localhost:3002" || window.location.host === "shuffler.io" || window.location.host === "migration.shuffler.io"; const parsedsearch = serverside === true ? "" : window.location.search @@ -438,6 +440,9 @@ const LoginPage = props => { if (responseJson["success"] === false) { setLoginInfo(responseJson["reason"]); } else { + if (responseJson.sso_url !== undefined && responseJson.sso_url !== null) { + setSSOUrl(responseJson.sso_url); + } // Stay = 0 users // Redirect = >1 user @@ -901,6 +906,29 @@ const LoginPage = props => {
{loginInfo}
+ + {ssoUrl !== undefined && ssoUrl !== null && ssoUrl.length > 0 ? ( +
+ Or +
+ +
+
+ ) : null} diff --git a/frontend/src/views/LoginPageOld.jsx b/frontend/src/views/LoginPageOld.jsx index 6b34b957..541cd903 100755 --- a/frontend/src/views/LoginPageOld.jsx +++ b/frontend/src/views/LoginPageOld.jsx @@ -498,7 +498,7 @@ const LoginDialog = (props) => {
+
{top_text === "Base Cloud Access" && userdata.has_card_available === true ? { { }} @@ -2013,7 +2016,33 @@ const Billing = memo((props) => { : null} -
+
+
+ {isCloud && + selectedOrganization.subscriptions !== undefined && + selectedOrganization.subscriptions !== null && + selectedOrganization.subscriptions.length > 0 && + !isChildOrg ? + selectedOrganization.subscriptions + .reverse() + .map((sub, index) => { + return ( + + ) + }) + : null} + +
{isCloud && billingInfo.subscription !== undefined && billingInfo.subscription !== null ? isChildOrg ? null : { isCloud={isCloud} userdata={userdata} stripeKey={stripeKey} + isScale={isScale} {...props} /> : !isCloud ? @@ -2061,6 +2091,7 @@ const Billing = memo((props) => { isCloud={isCloud} userdata={userdata} stripeKey={stripeKey} + isScale={isScale} /> {/* { /> */} : null} +
+
- {isCloud && - selectedOrganization.subscriptions !== undefined && - selectedOrganization.subscriptions !== null && - selectedOrganization.subscriptions.length > 0 && - !isChildOrg ? - selectedOrganization.subscriptions - .reverse() - .map((sub, index) => { - return ( - - ) - }) - : null} {/* { */}
- {/*isCloud && selectedOrganization.partner_info !== undefined && selectedOrganization.partner_info.reseller === true ? ( diff --git a/frontend/src/components/EditWorkflow.jsx b/frontend/src/components/EditWorkflow.jsx index b67bde93..48574126 100644 --- a/frontend/src/components/EditWorkflow.jsx +++ b/frontend/src/components/EditWorkflow.jsx @@ -577,12 +577,20 @@ const EditWorkflow = (props) => { Multi-Tenancy, Backups & Security - - Multi-Tenant Workflows. Make one workflow, and keep a separate, synced copy in all your tenants. Control distributed auth, runtime locations, files, datastore keys etc. (contact support@shuffler.io if you want a demo. Please try it!) + Control mechanisms for multi-tenancy, backups, and security. - {userdata !== undefined && userdata !== null && userdata.orgs !== undefined && userdata.orgs !== null && userdata.orgs.length > 0 ? + + Multi-Tenant Workflows + + + + Make one workflow, and keep a separate, synced copy in your other tenants. Control distributed auth, runtime locations, files, datastore keys etc. Can only distribute from parent org to child org. Need help trying it? Contact us for a demo + + + {userdata !== undefined && userdata !== null && userdata.orgs !== undefined && userdata.orgs !== null && userdata.orgs.length > 0 ? + userdata.orgs.filter(org => org.creator_org === userdata.active_org.id).length === 0 ? userdata.active_org.creator_org === undefined || userdata.active_org.creator_org === null || userdata.active_org.creator_org === "" ? @@ -600,6 +608,7 @@ const EditWorkflow = (props) => { multiple style={{ marginTop: 10, }} value={innerWorkflow.suborg_distribution === undefined || innerWorkflow.suborg_distribution === null ? ["none"] : innerWorkflow.suborg_distribution} + disabled={workflow?.parentorg_workflow !== undefined && workflow?.parentorg_workflow !== null && workflow?.parentorg_workflow.length > 0} onChange={(e) => { var newvalue = e.target.value if (newvalue.length > 1 && newvalue[0] === "none") { diff --git a/frontend/src/components/LicencePopup.jsx b/frontend/src/components/LicencePopup.jsx index f52ffa45..9492ae99 100644 --- a/frontend/src/components/LicencePopup.jsx +++ b/frontend/src/components/LicencePopup.jsx @@ -46,7 +46,7 @@ import { handlePayasyougo } from "../views/HandlePaymentNew.jsx" import Billing from "./Billing.jsx"; const LicencePopup = (props) => { - const { globalUrl, userdata, serverside, billingInfo, stripeKey, setModalOpen, isLoggedIn, isMobile, selectedOrganization, isCloud } = props; + const { globalUrl, userdata, serverside, billingInfo, stripeKey, setModalOpen, isScale, isLoggedIn, isMobile, selectedOrganization, isCloud } = props; //const alert = useAlert(); let navigate = useNavigate(); const [selectedDealModalOpen, setSelectedDealModalOpen] = React.useState(false); @@ -165,11 +165,25 @@ const LicencePopup = (props) => { ] } + if (userdata?.app_execution_limit >= 300000) { + subscription.name = "Enterprise" + subscription.currency_text = "$" + subscription.price = typecost_single + subscription.limit = userdata?.app_execution_limit + subscription.interval = "app run / month" + subscription.features = [ + "Includes " + (userdata?.app_execution_limit/1000) + "K app runs/month. ", + "Multi-Tenancy and Region-Selection", + "And all other features from /pricing", + ] + } + + var newPaperstyle = JSON.parse(JSON.stringify(paperStyle)) if (subscription.name === "Enterprise" && subscription.active === true) { - top_text = "Current Plan" + top_text = "Enterprise Plan" - newPaperstyle.border = "1px solid #f85a3e" + // newPaperstyle.border = "1px solid #f85a3e" } var showSupport = false @@ -255,7 +269,7 @@ const LicencePopup = (props) => { style={{ borderRadius: theme.palette?.borderRadius, }} placement="bottom" > -
+
setHovered(true)} @@ -343,9 +357,9 @@ const LicencePopup = (props) => {
- + {subscription.active === true && !isScale && }
- {top_text === "Base Cloud Access" && userdata.has_card_available === true ? + {top_text === "Base Cloud Access" && userdata.has_card_available === true && !isScale ? { "While you have a card attached to your account, Shuffle will no longer prevent workflows from running. Billing will occur at the start of each month." : isCloud ? - `You are not subscribed to any plan and are using the free plan with max 10,000 app runs per month. Upgrade to deactivate this limit.` + userdata?.app_execution_limit && userdata?.app_execution_limit >= 300000 ? + "You have subscribed to the Enterprise plan, which includes " + (userdata?.app_execution_limit/1000) + "K app runs/month. You can increase the limit by upgrading current plan. Contact support@shuffler.io for more information." : + `You are not subscribed to any plan and are using the free plan with max 10,000 app runs per month. Upgrade to deactivate this limit.` + : `You are not subscribed to any plan and are using the free, open source plan. This plan has no enforced limits, but scale issues may occur due to CPU congestion.` } @@ -699,8 +716,33 @@ const LicencePopup = (props) => { setCalculatedCost("$960") setSelectedValue(8) } else { - setCalculatedCost("$960") - setSelectedValue(300) + if (userdata && userdata?.app_execution_limit) { + if (userdata.app_execution_limit >= 300000 && userdata.app_execution_limit < 400000) { + setSelectedValue(400) + setCalculatedCost("$1280") + }else if (userdata?.app_execution_limit >= 400000 && userdata?.app_execution_limit < 500000) { + setSelectedValue(500) + setCalculatedCost("$1600") + } else if (userdata?.app_execution_limit >= 500000 && userdata?.app_execution_limit < 600000) { + setSelectedValue(600) + setCalculatedCost("$1920") + } else if (userdata?.app_execution_limit >= 600000 && userdata?.app_execution_limit < 700000) { + setSelectedValue(700) + setCalculatedCost("$2240") + } else if (userdata?.app_execution_limit >= 700000 && userdata?.app_execution_limit < 800000) { + setSelectedValue(800) + setCalculatedCost("$2560") + } else if (userdata?.app_execution_limit >= 800000 && userdata?.app_execution_limit < 900000) { + setSelectedValue(900) + setCalculatedCost("$2880") + }else { + setCalculatedCost("$960") + setSelectedValue(300) + } + }else { + setCalculatedCost("$960") + setSelectedValue(300) + } } }, [shuffleVariant]) @@ -977,7 +1019,7 @@ const LicencePopup = (props) => { : null} - {isCloud && + {/* {isCloud && selectedOrganization.subscriptions !== undefined && selectedOrganization.subscriptions !== null && selectedOrganization.subscriptions.length > 0 ? @@ -998,7 +1040,7 @@ const LicencePopup = (props) => { /> ) }) - : null} + : null} */} @@ -1006,12 +1048,12 @@ const LicencePopup = (props) => {
- + color="primary">Recommended }
{shuffleVariant === 1 ? "Scale" : "Enterprise"} diff --git a/frontend/src/components/Navbar.jsx b/frontend/src/components/Navbar.jsx index ebee1db5..c3cca3f8 100644 --- a/frontend/src/components/Navbar.jsx +++ b/frontend/src/components/Navbar.jsx @@ -47,7 +47,7 @@ const menuData = { title: "Shuffle", description: "The most versatile automation engine with focus on security.", - icon: "images/logos/orange_logo.svg", + icon: "images/icons/shuffleLogo.svg", path: "/docs/about", gaData: { category: "navbar", diff --git a/frontend/src/views/AngularWorkflow.jsx b/frontend/src/views/AngularWorkflow.jsx index 79a9d98a..374ce85b 100755 --- a/frontend/src/views/AngularWorkflow.jsx +++ b/frontend/src/views/AngularWorkflow.jsx @@ -575,11 +575,17 @@ const AngularWorkflow = (defaultprops) => { }, [editWorkflowModalOpen]) useEffect(() => { - if(selectedTrigger?.trigger_type === "SUBFLOW"){ + // Check if selectedTriggerIndex is a number >= 0 + if (isNaN(selectedTriggerIndex) || selectedTriggerIndex < 0) { + return + } + + if (selectedTrigger?.trigger_type === "SUBFLOW" && selectedTriggerIndex !== undefined && selectedTriggerIndex !== null && workflow?.triggers[selectedTriggerIndex].parameters.length > 1) { setSelectedTriggerValue(workflow.triggers[selectedTriggerIndex].parameters[1].value || "") - } else if(selectedTrigger?.trigger_type === "USERINPUT"){ + } else if (selectedTrigger?.trigger_type === "USERINPUT" && selectedTriggerIndex !== undefined && selectedTriggerIndex !== null && workflow?.triggers[selectedTriggerIndex].parameters.length > 0) { setSelectedTriggerValue(workflow.triggers[selectedTriggerIndex].parameters[0].value || "Do you want to continue the workflow? Start parameters: $exec") } + }, [selectedTriggerIndex, selectedTrigger, workflow]) useEffect(() => { @@ -948,11 +954,15 @@ const AngularWorkflow = (defaultprops) => { } useEffect(() => { - if (workflow.actions?.length == 1) { - if (workflow.actions[0].app_id == "3e320a20966d33c9b7e6790b2705f0bf") { + if (workflow?.actions?.length == 1) { + if (workflow?.actions[0].app_id == "3e320a20966d33c9b7e6790b2705f0bf") { setWorkflowAsCode(true); } } + + if (workflow?.suborg_distribution !== undefined && workflow?.suborg_distribution !== null && workflow?.suborg_distribution.length > 0) { + getChildWorkflows(workflow.id) + } }, [workflow]); // Event for making sure app is correct @@ -1495,6 +1505,8 @@ const AngularWorkflow = (defaultprops) => { if (workflow.org_id !== undefined && workflow.org_id !== null && workflow.org_id.length > 0) { headers["Org-Id"] = workflow.org_id } + + setWorkflows([]) fetch(globalUrl + "/api/v1/workflows?subflow=true", { method: "GET", @@ -1512,7 +1524,7 @@ const AngularWorkflow = (defaultprops) => { if (responseJson !== undefined) { // Sets up subflow trigger with the right info - if (trigger_index > -1) { + if (isNaN(trigger_index) === false && trigger_index > -1) { var baseSubflow = {} const trigger = workflow.triggers[trigger_index]; @@ -2494,15 +2506,19 @@ const AngularWorkflow = (defaultprops) => { if (cy !== undefined && cy !== null) { // scale: 0.3, // bg: "#27292d", - if (cy.png !== undefined && cy.png !== null) { - const cyImageData = cy.png({ - output: "base64uri", - maxWidth: 480, - maxHeight: 270, - }) + if (cy?.png !== undefined && cy?.png !== null) { + try { + const cyImageData = cy.png({ + output: "base64uri", + maxWidth: 480, + maxHeight: 270, + }) - if (cyImageData !== undefined && cyImageData !== null && cyImageData.length > 0) { - useworkflow.image = cyImageData + if (cyImageData !== undefined && cyImageData !== null && cyImageData.length > 0) { + useworkflow.image = cyImageData + } + } catch (e) { + console.log("Failed to get image data: ", e) } } } @@ -3914,6 +3930,10 @@ const AngularWorkflow = (defaultprops) => { } const findWorkflowDiff = (parentWorkflow, childWorkflow) => { + // Ensures new memory is used + parentWorkflow = JSON.parse(JSON.stringify(parentWorkflow)) + childWorkflow = JSON.parse(JSON.stringify(childWorkflow)) + var diff = { "different": false, "environment": false, @@ -3931,9 +3951,13 @@ const AngularWorkflow = (defaultprops) => { return diff } - var parentEnvironment = "" var childEnvironment = "" + + if (parentWorkflow.triggers !== undefined && parentWorkflow.triggers !== null && parentWorkflow.triggers.length > 0) { + parentWorkflow.actions = parentWorkflow.actions.concat(parentWorkflow.triggers) + } + for (var parentKey in parentWorkflow.actions) { const parentAction = parentWorkflow.actions[parentKey] if (parentAction.environment !== undefined && parentAction.environment !== null && parentAction.environment !== "") { @@ -3945,6 +3969,19 @@ const AngularWorkflow = (defaultprops) => { } var found = false + if (childWorkflow.triggers !== undefined && childWorkflow.triggers !== null && childWorkflow.triggers.length > 0) { + for (var triggerKey in childWorkflow.triggers) { + if (childWorkflow.triggers[triggerKey].replacement_for_trigger !== parentAction.id) { + continue + } + + // Rewrapping the ID just in case + childWorkflow.triggers[triggerKey].id = childWorkflow.triggers[triggerKey].replacement_for_trigger + childWorkflow.actions.push(childWorkflow.triggers[triggerKey]) + break + } + } + for (var childKey in childWorkflow.actions) { const childAction = childWorkflow.actions[childKey] if (childAction.environment !== undefined && childAction.environment !== null && childAction.environment !== "") { @@ -3991,6 +4028,10 @@ const AngularWorkflow = (defaultprops) => { continue } + var parentworkflow = "" + var parentstartnode = "" + var childworkflow = "" + var childstartnode = "" for (var parentParamIndex in parentAction.parameters) { const parentParam = parentAction.parameters[parentParamIndex] for (var childParamIndex in childAction.parameters) { @@ -3999,20 +4040,38 @@ const AngularWorkflow = (defaultprops) => { continue } + if (childParam.name === "workflow" || childParam.name === "subflow") { + parentworkflow = parentParam.value + childworkflow = childParam.value + continue + } + + if (childParam.name == "startnode") { + parentstartnode = parentParam.value + childstartnode = childParam.value + continue + } + if (childParam.value !== parentParam.value) { actionDiff.parameters.push(childParam.name) } } } + + if (parentworkflow !== childworkflow && parentstartnode !== childstartnode) { + actionDiff.parameters.push("subflow") + } } if (actionDiff.parameters.length > 0) { actionDiff.params = true } + /* if (!found) { actionDiff.new = true } + */ if (actionDiff !== undefined && actionDiff !== null && Object.keys(actionDiff).length > 1) { actionDiff.label = parentAction.label.replaceAll("_", " ") @@ -6305,11 +6364,15 @@ const AngularWorkflow = (defaultprops) => { if (workflow.org_id !== undefined && workflow.org_id !== null && workflow.org_id.length > 0 && originalWorkflow.org_id !== undefined && originalWorkflow.org_id !== null && originalWorkflow.org_id.length > 0 && workflow.org_id === originalWorkflow.org_id) { // Allows a parent workflow to control the schedule } else if (data.replacement_for_trigger !== undefined && data.replacement_for_trigger !== null && data.replacement_for_trigger.length > 0) { - toast.warning("This schedule is controlled by the parent workflow. If you want additional schedule control, please add a custom schedule to this workflow.", { - autoClose: 30000, - }) - event.target.unselect() - return + + // No custom control on cloud + if (isCloud) { + toast.warning("This schedule is controlled by the parent workflow. If you want additional schedule control, please add a custom schedule to this workflow.", { + autoClose: 30000, + }) + event.target.unselect() + return + } } } else if (data.app_name === "Webhook" && trigger_index >= 0) { @@ -6885,7 +6948,7 @@ const AngularWorkflow = (defaultprops) => { ) if (targetnode !== -1) { if (workflow.triggers[targetnode].app_name === "User Input" || workflow.triggers[targetnode].app_name === "Shuffle Workflow" || workflow.triggers[targetnode].app_name === "Shuffle Subflow") { - console.log("User Input or Shuffle Workflow") + //console.log("User Input or Shuffle Workflow") } else { toast("Can't have triggers as target of branch") event.target.remove() @@ -9217,11 +9280,15 @@ const AngularWorkflow = (defaultprops) => { } insertedNodes = insertedNodes.concat(newedges); - setWorkflow(inputworkflow); + setWorkflow(inputworkflow) // Reset view for cytoscape if (cy !== undefined && cy !== null) { - cy.add(insertedNodes) + try { + cy.add(insertedNodes) + } catch (error) { + console.log("Error adding nodes to cytoscape (6): ", error) + } try { cy.fit(null, 250) @@ -9300,12 +9367,6 @@ const AngularWorkflow = (defaultprops) => { } } - //if (selectedNode.data("id") === selectedAction.id) { - // setSelectedApp({}); - // setSelectedAction({}); - //setSelectedTrigger({}); - //setSelectedTriggerIndex({}); - //} const parsedSelection = cy.$(":selected"); if (selectedNode.data().decorator === true && selectedNode.data("type") !== "COMMENT") { toast("This node can't be deleted."); @@ -9505,7 +9566,6 @@ const AngularWorkflow = (defaultprops) => { if (firstrequest) { setFirstrequest(false) getWorkflow(props.match.params.key, {}) - getChildWorkflows(props.match.params.key) getRevisionHistory(props.match.params.key) loadTriggers() getApps() @@ -14891,12 +14951,15 @@ const AngularWorkflow = (defaultprops) => { }} >
- Select a workflow to execute + Select a workflow to run
+ {workflow.triggers[selectedTriggerIndex].parameters[0].value .length === 0 ? null : workflow.triggers[selectedTriggerIndex] - .parameters[0].value === props.match.params.key ? null : ( + .parameters[0].value === props.match.params.key ? + null + : (
{ {workflows === undefined || workflows === null || - workflows.length === 0 ? null : ( - + workflows.length === 0 ? + + : ( { value={workflow.org_id} disabled={savingState !== 0 || suborgWorkflows?.length === 0 || allTriggers === undefined} onChange={(e) => { + if (cy !== undefined && cy !== null) { + cy.nodes().unselect() + } + if (lastSaved === false && originalWorkflow.id === workflow.id) { setSuborgWorkflows([]) diff --git a/frontend/src/views/LoginPage.jsx b/frontend/src/views/LoginPage.jsx index 385a2d8e..97c8b5cf 100755 --- a/frontend/src/views/LoginPage.jsx +++ b/frontend/src/views/LoginPage.jsx @@ -427,14 +427,14 @@ const LoginPage = props => { window.location.pathname = "/workflows" } - const checkAdmin = () => { - const url = globalUrl + "/api/v1/checkusers"; - fetch(url, { - method: "GET", - headers: { - "Content-Type": "application/json", - }, - }) + const checkAdmin = () => { + const url = globalUrl + "/api/v1/checkusers"; + fetch(url, { + method: "GET", + headers: { + "Content-Type": "application/json", + }, + }) .then((response) => response.json().then((responseJson) => { if (responseJson["success"] === false) { @@ -459,7 +459,7 @@ const LoginPage = props => { navigate("/adminsetup") }, 2500) }) - }; + } const onSubmit = (e) => { @@ -952,7 +952,7 @@ const LoginPage = props => {
return ( -
+
{loadedCheck}
) diff --git a/frontend/src/views/Workflows.jsx b/frontend/src/views/Workflows.jsx index 1eee8317..100eed13 100755 --- a/frontend/src/views/Workflows.jsx +++ b/frontend/src/views/Workflows.jsx @@ -1946,7 +1946,10 @@ const Workflows = (props) => { .then((response) => { if (response.status !== 200) { console.log("Status not 200 for setting workflows :O!"); - toast("Failed deleting workflow. Do you have access?"); + + if (bulk !== true) { + toast(`Failed deleting workflow ${id}. Do you have access?`); + } } else { if (bulk !== true) { toast(`Deleted workflow ${id}. Child Workflows in Suborgs were also removed.`) diff --git a/frontend/src/views/Workflows2.jsx b/frontend/src/views/Workflows2.jsx index 0b079407..f1e18120 100644 --- a/frontend/src/views/Workflows2.jsx +++ b/frontend/src/views/Workflows2.jsx @@ -2043,7 +2043,10 @@ const Workflows2 = (props) => { .then((response) => { if (response.status !== 200) { console.log("Status not 200 for setting workflows :O!"); - toast("Failed deleting workflow. Do you have access?"); + + if (bulk !== true) { + toast("Failed deleting workflow. Do you have access?"); + } } else { if (bulk !== true) { toast(`Deleted workflow ${id}. Child Workflows in Suborgs were also removed.`) @@ -2821,7 +2824,7 @@ const Workflows2 = (props) => { {(data.sharing !== undefined && data.sharing !== null && data.sharing === "form") || (data?.form_control?.input_markdown !== undefined && data?.form_control?.input_markdown !== null && data?.form_control?.input_markdown !== "") && type !== "public" ? -
+
{ : null} {(data?.validation?.validation_ran === true && data?.validation?.valid === false && data?.validation?.errors?.length > 0 ) ? - -
+ +
{
- : null} +
@@ -3235,6 +3238,7 @@ const Workflows2 = (props) => {
} +