diff --git a/backend/go-app/go.mod b/backend/go-app/go.mod index f9094264..cf2eb962 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.12 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..99179e7f 100644 --- a/backend/go-app/go.sum +++ b/backend/go-app/go.sum @@ -350,6 +350,14 @@ 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/shuffle/shuffle-shared v0.8.6 h1:chnGJRKsO1gR5Vt/8hp5SQg1yP8/DmiK8sRhWVaTgEg= +github.com/shuffle/shuffle-shared v0.8.6/go.mod h1:bBXhEsPKjxln0mFnSeri7gIJ3tL/636Sh5NyTyNrvIQ= +github.com/shuffle/shuffle-shared v0.8.12 h1:fg6jGAuevOGLgR7kpoZDJh//tzFjrEqj/bpxRFzxUTw= +github.com/shuffle/shuffle-shared v0.8.12/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/backend/go-app/walkoff.go b/backend/go-app/walkoff.go index df7addb8..dfdccefe 100755 --- a/backend/go-app/walkoff.go +++ b/backend/go-app/walkoff.go @@ -1032,10 +1032,14 @@ func deleteWorkflow(resp http.ResponseWriter, request *http.Request) { log.Printf("[INFO] Should have deleted workflow %s (%s)", workflow.Name, fileId) - cacheKey := fmt.Sprintf("%s_workflows", user.Id) - shuffle.DeleteCache(ctx, cacheKey) + shuffle.DeleteCache(ctx, fmt.Sprintf("%s_workflows", user.Id)) shuffle.DeleteCache(ctx, fmt.Sprintf("%s_workflows", user.ActiveOrg.Id)) + shuffle.DeleteCache(ctx, fmt.Sprintf("%s_%s", user.Username, fileId)) log.Printf("[DEBUG] Cleared workflow cache for %s (%s)", user.Username, user.Id) + shuffle.DeleteCache(ctx, fmt.Sprintf("workflow_%s_childworkflows", workflow.ID)) + if len(workflow.ParentWorkflowId) > 0 { + shuffle.DeleteCache(ctx, fmt.Sprintf("workflow_%s_childworkflows", workflow.ParentWorkflowId)) + } resp.WriteHeader(200) resp.Write([]byte(`{"success": true}`)) @@ -1094,10 +1098,12 @@ func handleExecution(id string, workflow shuffle.Workflow, request *http.Request workflow.Errors = []string{} } + /* if !workflow.IsValid { log.Printf("[ERROR] Stopped execution as workflow %s is not valid.", workflow.ID) return shuffle.WorkflowExecution{}, fmt.Sprintf(`workflow %s is invalid`, workflow.ID), errors.New("Failed getting workflow") } + */ maxExecutionDepth := 10 if os.Getenv("SHUFFLE_MAX_EXECUTION_DEPTH") != "" { 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: 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 b6f3e6b0..419df99d 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 LoginPageOld from "./views/LoginPageOld.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'; @@ -59,6 +60,7 @@ import 'react-toastify/dist/ReactToastify.css'; import Drift from "react-driftjs"; import { AppContext } from './context/ContextApi.jsx'; +import Navbar from "./components/Navbar.jsx"; import Workflows2 from "./views/Workflows2.jsx"; import AppExplorer from "./views/AppExplorer.jsx"; @@ -212,7 +214,20 @@ const App = (message, props) => { { window?.location?.pathname === "/" || window?.location?.pathname === "/training" || !(isLoggedIn && isLoaded) ? (
-
*/} + {
*/} + { { /> } /> + + + } + /> + + + } + /> + + + } + /> + { } /> ) : null} + { /> } /> + {
- + + {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..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) @@ -59,6 +58,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 ( @@ -256,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 = () => { @@ -416,7 +417,7 @@ const AppCreationModal = ({ open, onClose, theme, globalUrl, isCloud }) => { -
+
{ diff --git a/frontend/src/components/Billing.jsx b/frontend/src/components/Billing.jsx index 577d249c..39e15706 100644 --- a/frontend/src/components/Billing.jsx +++ b/frontend/src/components/Billing.jsx @@ -77,6 +77,8 @@ const Billing = memo((props) => { const [currentIndex, setCurrentIndex] = useState(0); const [deleteAlertIndex, setDeleteAlertIndex] = useState(-1); const [deleteAlertVerification, setDeleteAlertVerification] = useState(false); + const [isScale, setIsScale] = useState(false); + useEffect(() => { if (userdata.app_execution_limit !== undefined && userdata.app_execution_usage !== undefined) { const percentage = (userdata.app_execution_usage / userdata.app_execution_limit) * 100; @@ -176,11 +178,10 @@ const Billing = memo((props) => { padding: 20, // maxWidth: 400, width: 340, - height: 480, + height: 'auto', // width: "100%", - backgroundColor: theme.palette.backgroundColor, - borderRadius: theme.palette?.borderRadius * 2, - border: "1px solid rgba(255,255,255,0.3)", + backgroundColor: "#1e1e1e", + borderRadius: 10, marginRight: 10, marginTop: 15, } @@ -361,7 +362,7 @@ const Billing = memo((props) => { var showSupport = false if (subscription.name.includes("default")) { top_text = "Custom Contract" - newPaperstyle.border = "1px solid rgba(255,255,255,0.3)" + // newPaperstyle.border = "1px solid rgba(255,255,255,0.3)" showSupport = true } @@ -377,16 +378,17 @@ const Billing = memo((props) => { if (subscription.name.includes("Scale")) { top_text = "Scale access" + setIsScale(true) } if (highlight === true) { // Add an "Upgrade now" button - newPaperstyle.border = "1px solid rgba(255,255,255,0.3)" + // newPaperstyle.border = "1px solid rgba(255,255,255,0.3)" } - if (hovered) { - newPaperstyle.backgroundColor = "#2b2b2b" - } + // if (hovered) { + // newPaperstyle.backgroundColor = "#2b2b2b" + // } const handleClickOpen = () => { setOpenChangeEmailBox(true); @@ -556,6 +558,7 @@ const Billing = memo((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/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/LeftSideBar.jsx b/frontend/src/components/LeftSideBar.jsx index 432075ec..3c999dbe 100644 --- a/frontend/src/components/LeftSideBar.jsx +++ b/frontend/src/components/LeftSideBar.jsx @@ -87,21 +87,21 @@ const LeftSideBar = ({ userdata, serverside, globalUrl, notifications, }) => { 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/LicencePopup.jsx b/frontend/src/components/LicencePopup.jsx index c29095e4..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]) @@ -864,10 +906,14 @@ const LicencePopup = (props) => { return } - const priceItem = window.location.origin === "https://shuffler.io" ? - shuffleVariant === 0 ? "app_executions" : "cores" - : - shuffleVariant === 0 ? "price_1PZPSSEJjT17t98NLJoTMYja" : "price_1PZPQuEJjT17t98N3yORUtd9" + const priceItem = + window.location.origin === "https://shuffler.io/" || "https://sandbox.shuffler.io/" + ? shuffleVariant === 0 + ? "app_executions" + : "cores" + : shuffleVariant === 0 + ? "price_1PZPSSEJjT17t98NLJoTMYja" + : "price_1PZPQuEJjT17t98N3yORUtd9"; const successUrl = `${window.location.origin}/admin?admin_tab=billingstats&payment=success` const failUrl = `${window.location.origin}/admin?admin_tab=billingstats&payment=failure` @@ -973,7 +1019,7 @@ const LicencePopup = (props) => { : null} - {isCloud && + {/* {isCloud && selectedOrganization.subscriptions !== undefined && selectedOrganization.subscriptions !== null && selectedOrganization.subscriptions.length > 0 ? @@ -994,7 +1040,7 @@ const LicencePopup = (props) => { /> ) }) - : null} + : null} */} @@ -1002,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 new file mode 100644 index 00000000..c3cca3f8 --- /dev/null +++ b/frontend/src/components/Navbar.jsx @@ -0,0 +1,2587 @@ +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/icons/shuffleLogo.svg", + path: "/docs/about", + 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/about", + 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/2.0_release", + 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; diff --git a/frontend/src/components/RuntimeDebugger.jsx b/frontend/src/components/RuntimeDebugger.jsx index 2209ad4c..62f8de0e 100644 --- a/frontend/src/components/RuntimeDebugger.jsx +++ b/frontend/src/components/RuntimeDebugger.jsx @@ -359,7 +359,7 @@ const RuntimeDebugger = (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/components/TenantsTab.jsx b/frontend/src/components/TenantsTab.jsx index 0d8c3df2..907c982e 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,9 +783,9 @@ const TenantsTab = memo((props) => {
-

Organizations

+

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/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/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/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 16d5e9d0..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 @@ -1288,56 +1298,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 @@ -1545,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", @@ -1562,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]; @@ -1592,10 +1554,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) } } } @@ -2540,18 +2502,25 @@ 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) { + 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) + } + } } if (useworkflow.id === undefined || useworkflow.id === null || useworkflow.id.length === 0) { @@ -3948,7 +3917,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 @@ -3961,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, @@ -3978,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 !== "") { @@ -3992,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 !== "") { @@ -4038,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) { @@ -4046,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("_", " ") @@ -6352,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) { @@ -6932,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() @@ -9264,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) @@ -9347,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."); @@ -9552,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() @@ -9785,9 +9798,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) @@ -11446,7 +11464,7 @@ const AngularWorkflow = (defaultprops) => { var type = "app" const baseImage = return ( -
+
{hits.length === 0 ? @@ -11736,7 +11754,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") @@ -14637,20 +14655,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()) } } @@ -14934,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 + : (
+ { 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/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?
) : ( - {hoverEffect === index && isCloud ? ( + {hoverEffect === index ? (
{data.tags && ( { { }} /> { { } + 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", @@ -638,8 +694,7 @@ const Docs = (defaultprops) => {
- - {extraInfo} + {isArticlePage ? (userdata?.support ? extraInfo : "") : extraInfo} ) } @@ -647,21 +702,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 +760,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 +783,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 +798,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 +880,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 +922,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 +1069,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 +1118,62 @@ const Docs = (defaultprops) => { - {newname} - + style={{ + color: itemMatching ? "#f86a3e" : "inherit", + flex: 1, + }} + primary={ + + {newname} + + } + /> + ); })} + { + isArticlePage && ( + + {sidebarOpen ? : } + + ) + }
-
+
{props.match.params.key === undefined ? mainpageInfo : @@ -1003,12 +1195,26 @@ const Docs = (defaultprops) => { }
+ {userdata?.support && isArticlePage && ( + + )} {tocLines.length > 0 ? ( -

Table of Content

+

Table Of Content

) : null} - +
@@ -1083,7 +1296,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 +1321,8 @@ const Docs = (defaultprops) => { key={index} style={{ color: "white" }} onClick={() => { - window.location.pathname = path; + navigate(path) + handleClose() }} > {newname} @@ -1140,8 +1354,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 +1406,7 @@ const Docs = (defaultprops) => { color="primary" onClick={handleClick} > -
More docs
+
More {isArticlePage ? "articles" : "docs"}
); @@ -1198,7 +1426,7 @@ export default Docs; const DocsContent = memo(({postDataBrowser, postDataMobile}) => { return( -
+
{postDataBrowser} {postDataMobile}
@@ -1210,12 +1438,14 @@ const DocsWrapper = memo(({isLoggedIn, isLoaded, children })=>{ return (
{children} diff --git a/frontend/src/views/LoginPage.jsx b/frontend/src/views/LoginPage.jsx index aaf376cd..97c8b5cf 100755 --- a/frontend/src/views/LoginPage.jsx +++ b/frontend/src/views/LoginPage.jsx @@ -1,546 +1,961 @@ /* eslint-disable react/no-multi-comp */ -import React, { useState, useEffect } from "react"; -import { makeStyles } from "@mui/styles"; +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 theme from '../theme.jsx'; + import { - CircularProgress, - TextField, - Button, - Paper, - Typography, -} from "@mui/material"; + ConnectingAirportsOutlined, + ConstructionOutlined, + DoneRounded, + Done as DoneIcon +} from '@mui/icons-material'; -import { useNavigate } from "react-router-dom"; +import { + Checkbox, + CircularProgress, + TextField, + Button, + Paper, + Typography, + Tooltip, + TableHead, + TableRow, + TableContainer, + TableCell, + TableBody, + Table, +} from '@mui/material'; const hrefStyle = { - color: "white", - textDecoration: "none", -}; + color: "#FF8444", + fontSize: "14px", + textDecoration: "none", + display: "flex", +} -const bodyDivStyle = { - margin: "auto", - marginTop: 150, - width: "500px", -}; +const googleLoginIcon = { + boxSizing: "border-box", + display: "flex", + flexDirection: "row", + justifyContent: "center", + alignItems: "center", + padding: "16px", + gap: "8px", + // position: "sticky", + width: 171, + height: "51px", + left: "352px", + top: "725px", + background: "#1A1A1A", + border: "1px solid #494949", + borderRadius: "8px", + +} + +const githubLoginIcon = { + boxSizing: "border-box", + display: "flex", + flexDirection: "row", + justifyContent: "center", + alignItems: "center", + padding: "16px", + gap: "8px", + position: "sticky", + width: "173px", + height: "51px", + left: "539px", + top: "725px", + backgroundColor: "#1A1A1A", + border: "1px solid #494949", + borderRadius: "8px", +} + +const surfaceColor = "#27292D" +const inputColor = "#383B40" const useStyles = makeStyles({ - notchedOutline: { - borderColor: "#f85a3e !important", - }, + notchedOutline: { + borderColor: "#f85a3e !important" + }, + marketplaceButton: { + width: "100%", + justifyContent: "flex-start", + padding: "12px", + marginBottom: "12px", + backgroundColor: "#1A1A1A", + border: "1px solid #494949", + borderRadius: "8px", + color: "white", + opacity: 0.7, + '&:hover': { + opacity: 1, + cursor: "not-allowed", + }, + }, + marketplaceIcon: { + width: 28, + height: 28, + marginRight: 12 + }, + divider: { + display: "flex", + alignItems: "center", + margin: "0 20px", + '&::before, &::after': { + content: '""', + flex: 1, + borderBottom: "1px solid #494949" + }, + '& span': { + margin: "0 10px", + color: "#9E9E9E" + } + }, + freePlanCard: { + padding: "40px", + background: "#212121", + borderRadius: "12px", + width: "100%", + maxWidth: "500px" + }, + freePlanTitle: { + fontSize: "28px", + fontWeight: 600, + color: "white", + marginTop: 0, + marginBottom: "32px" + }, + featureItem: { + display: "flex", + alignItems: "center", + marginBottom: "20px", + color: "white", + fontSize: "16px", + fontWeight: 500 + }, + checkIcon: { + color: "#4CAF50", + marginRight: "16px", + width: "24px", + height: "24px" + } }); -const LoginDialog = (props) => { - const { - globalUrl, - isLoaded, - isLoggedIn, - setIsLoggedIn, - setCookie, - register, - checkLogin, - } = props; +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" + ]; + + return ( +
+

+ The free plan includes: +

+ + {features.map((feature, index) => ( +
+ + {feature} +
+ ))} +
+ ); +}; + +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, checkLogin, } = props; let navigate = useNavigate(); - const classes = useStyles(); + const [username, setUsername] = useState(""); + const [password, setPassword] = useState(""); + const [message, setMessage] = useState(""); + const [loginLoading, setLoginLoading] = useState(false); - 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(""); + const [register, setRegister] = useState(inregister); + const [checkboxClicked, setCheckboxClicked] = useState(false); + const [loginWithSSO, setLoginWithSSO] = 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 + 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(() => { - checkAdmin() - }, [loginViewLoading]) + if (!isCloud) { + checkAdmin() + } + }, []) - // 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", + 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) { + 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 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); - } - } - + // Stay = 0 users + // Redirect = >1 user if (responseJson.reason === "stay") { - navigate("/adminsetup") - } + setTimeout(() => { + navigate("/adminsetup") + }, 2500) + } } }) ) .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; + setTimeout(() => { + navigate("/adminsetup") + }, 2500) + }) } - 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: "/" } - ); - } + const onSubmit = (e) => { + //toast("Testing from login page") - 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 - } + setMessage("") + setLoginLoading(true) + e.preventDefault() - 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" - } + // Just use this one? + var data = { "username": username, "password": password } + if (MFAValue !== undefined && MFAValue !== null && MFAValue.length > 0) { + data["mfa_code"] = MFAValue + } - 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); - }); - } - }; + localStorage.setItem("globalUrl", "") - const onChangeUser = (e) => { - setUsername(e.target.value); - }; + var baseurl = globalUrl + if (register) { + var url = baseurl + '/api/v1/login'; - 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() - } - } - } + if (loginWithSSO === true) { + url = baseurl + '/api/v1/login/sso' + setLoginInfo("Logging in with SSO. Please wait while we find a relevant org...") } - }, 200); - }, [ssoUrl]) - return
{loadedCheck}
; -}; + 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!"); + } -export default LoginDialog; + 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} +
+ + {ssoUrl !== undefined && ssoUrl !== null && ssoUrl.length > 0 ? ( +
+ Or +
+ +
+
+ ) : null} + + + + {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..541cd903 --- /dev/null +++ b/frontend/src/views/LoginPageOld.jsx @@ -0,0 +1,548 @@ +/* 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); + } + + navigate("/login") + + 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 two minutes. + + + {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; 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 33f8618f..f1e18120 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; @@ -2035,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.`) @@ -2813,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} +
@@ -3227,6 +3238,7 @@ const Workflows2 = (props) => {
} +