diff --git a/backend/go-app/go.mod b/backend/go-app/go.mod index b563af28..6a7775c9 100644 --- a/backend/go-app/go.mod +++ b/backend/go-app/go.mod @@ -24,7 +24,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.9.31 + github.com/shuffle/shuffle-shared v0.9.32 github.com/shuffle/singul v0.0.17 golang.org/x/crypto v0.40.0 google.golang.org/api v0.236.0 diff --git a/backend/go-app/go.sum b/backend/go-app/go.sum index 2f614c0b..1f1f22c7 100644 --- a/backend/go-app/go.sum +++ b/backend/go-app/go.sum @@ -363,8 +363,8 @@ github.com/sendgrid/sendgrid-go v3.16.1+incompatible h1:zWhTmB0Y8XCDzeWIm2/BIt1G github.com/sendgrid/sendgrid-go v3.16.1+incompatible/go.mod h1:QRQt+LX/NmgVEvmdRw0VT/QgUn499+iza2FnDca9fg8= 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.9.31 h1:BMoDO4Sgz4+I12aHhc3cWHiCuAQ827XIxajPqCJ9cXA= -github.com/shuffle/shuffle-shared v0.9.31/go.mod h1:vfI2QDGphZGrcwuUPQ1yI/Hgc8aseFro5+2k36irfkQ= +github.com/shuffle/shuffle-shared v0.9.32 h1:hsF2YkKHgaNpqhh2oZs31BgPSjN+YjGNnd9WmD0qh3w= +github.com/shuffle/shuffle-shared v0.9.32/go.mod h1:vfI2QDGphZGrcwuUPQ1yI/Hgc8aseFro5+2k36irfkQ= github.com/shuffle/singul v0.0.17 h1:mxaPtj6z85Nf6tl7L2gwDliTfEZtRQqApuu9iKcP75o= github.com/shuffle/singul v0.0.17/go.mod h1:8c42n1NahhCIPxzLxwp9eYbWkvY4+ct0jfbhkRsRRsY= github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= diff --git a/backend/go-app/main.go b/backend/go-app/main.go index d24a7bbe..633cd6f4 100755 --- a/backend/go-app/main.go +++ b/backend/go-app/main.go @@ -1282,7 +1282,6 @@ func checkAdminLogin(resp http.ResponseWriter, request *http.Request) { } count := len(users) - if count == 0 { log.Printf("[WARNING] No users - redirecting for management user") resp.WriteHeader(200) @@ -4662,6 +4661,45 @@ func runInitEs(ctx context.Context) { } } + // Self-cleaning + go func() { + cursor := "" + cnt := 0 + newCtx := context.Background() + for _, org := range activeOrgs { + if len(org.Id) == 0 { + log.Printf("[DEBUG] No ID found for org with name '%s'. Why was it made?", org.Name) + continue + } + + log.Printf("[INFO] Starting self-cleanup of cache keys for org %s", org.Id) + + for { + keys, newCursor, err := shuffle.GetAllCacheKeys(newCtx, org.Id, "", 1000, cursor) + if err != nil { + //log.Printf("[ERROR] Failed getting all cache keys for cleanup: %s", err) + break + } + + if newCursor == cursor || len(newCursor) == 0 { + break + } + + if len(keys) == 0 { + break + } + + cursor = newCursor + cnt += 1 + if cnt > 10 { + break + } + } + + log.Printf("[INFO] Finished self-cleanup of cache keys for org %s", org.Id) + } + }() + log.Printf("[INFO] Finished INIT (ES)") } @@ -5481,6 +5519,11 @@ func initHandlers() { r.HandleFunc("/api/v2/workflows/{key}/executions", shuffle.GetWorkflowExecutionsV2).Methods("GET", "OPTIONS") r.HandleFunc("/api/v2/workflows/generate/llm", shuffle.HandleWorkflowGenerationResponse).Methods("POST", "OPTIONS") r.HandleFunc("/api/v2/workflows/edit/llm", shuffle.HandleEditWorkflowWithLLM).Methods("POST", "OPTIONS") + r.HandleFunc("/api/v2/workflows/generate", shuffle.GenerateSingulWorkflows).Methods("POST", "OPTIONS") + r.HandleFunc("/api/v2/datastore", shuffle.HandleListCacheKeys).Methods("GET", "OPTIONS") + r.HandleFunc("/api/v2/datastore", shuffle.HandleSetDatastoreKey).Methods("POST", "OPTIONS") + r.HandleFunc("/api/v2/datastore/category/{category_key}", shuffle.HandleListCacheKeys).Methods("GET", "OPTIONS") + r.HandleFunc("/api/v2/datastore/automate", shuffle.HandleDatastoreCategoryConfig).Methods("POST", "OPTIONS") // New for recommendations in Shuffle r.HandleFunc("/api/v1/recommendations/get_actions", shuffle.HandleActionRecommendation).Methods("POST", "OPTIONS") @@ -5575,6 +5618,10 @@ func initHandlers() { r.HandleFunc("/api/v1/orgs/{orgId}/stats/{key}", shuffle.GetSpecificStats).Methods("GET", "OPTIONS") r.HandleFunc("/api/v1/orgs/{orgId}/statistics", shuffle.HandleGetStatistics).Methods("GET", "OPTIONS") + r.HandleFunc("/api/v1/stats", shuffle.HandleGetStatistics).Methods("GET", "OPTIONS") + r.HandleFunc("/api/v1/stats", shuffle.HandleAppendStatistics).Methods("POST", "OPTIONS") + r.HandleFunc("/api/v1/stats/{key}", shuffle.GetSpecificStats).Methods("GET", "OPTIONS") + r.HandleFunc("/api/v1/orgs/{orgId}/cache", shuffle.HandleListCacheKeys).Methods("GET", "OPTIONS") r.HandleFunc("/api/v1/orgs/{orgId}/cache", shuffle.HandleSetCacheKey).Methods("POST", "OPTIONS") r.HandleFunc("/api/v1/orgs/{orgId}/cache/{cache_key}", shuffle.HandleDeleteCacheKey).Methods("DELETE", "OPTIONS") diff --git a/frontend/src/components/AdminNavBar.jsx b/frontend/src/components/AdminNavBar.jsx index fbbc391d..9ebea75b 100644 --- a/frontend/src/components/AdminNavBar.jsx +++ b/frontend/src/components/AdminNavBar.jsx @@ -1,5 +1,6 @@ import React, { useState, useEffect, useContext, memo } from 'react'; import { useNavigate, useLocation } from 'react-router-dom'; + import OrganizationTab from '../components/OrganizationTab.jsx'; import PartnerTab from '../components/PartnerTab.jsx'; import UserManagmentTab from '../components/UserManagmentTab.jsx'; @@ -20,6 +21,7 @@ import { FmdGoodOutlined as FmdGoodOutlinedIcon, GroupOutlined as GroupOutlinedIcon } from '@mui/icons-material'; + import theme, { getTheme } from '../theme.jsx'; import { Button, Skeleton, Tooltip } from '@mui/material'; import { Index } from 'react-instantsearch-dom'; diff --git a/frontend/src/components/Billing.jsx b/frontend/src/components/Billing.jsx index b93848b1..2bc8290b 100644 --- a/frontend/src/components/Billing.jsx +++ b/frontend/src/components/Billing.jsx @@ -47,7 +47,9 @@ import { CheckCircle, Padding, Edit, - Search as SearchIcon + Search as SearchIcon, + CheckCircle as CheckCircleIcon, + Cancel as CancelIcon, } from "@mui/icons-material"; //import { useAlert @@ -61,6 +63,62 @@ import { Context } from "../context/ContextApi.jsx"; import DeleteIcon from '@mui/icons-material/Delete'; import { DataGrid } from "@mui/x-data-grid"; +const ProductionStatus = ({ selectedOrganization, userdata, isCloud, theme }) => { + var isProdStatusOn; + if (selectedOrganization !== undefined && selectedOrganization?.subscriptions !== undefined && selectedOrganization?.subscriptions[0] !== undefined) { + isProdStatusOn = selectedOrganization?.subscriptions[0]?.name?.toLowerCase()?.includes("enterprise") && selectedOrganization?.subscriptions[0]?.active; + } else { + isProdStatusOn = false; + } + const rows = [ + { label: 'Licensed', ok: isProdStatusOn }, + { label: 'Multi-Tenant', ok: isProdStatusOn }, + { label: 'High Availability', ok: isProdStatusOn }, + { label: 'Robust Infrastructure', ok: isProdStatusOn }, + ]; + + return ( +
+
+ Production Status +
+ + {isProdStatusOn ? "ON" : "OFF"} +
+
+ + Monitor your production status to stay informed about available features. + +
+ {rows.map((row) => ( +
+ {row.ok ? ( + + ) : ( + + )} + {row.label} +
+ ))} +
+ + + + Shuffle Enterprise is designed for organizations that require scalability, high availability, dedicated support and more to run mission-critical workflows in production environments. + + + More about upgrading below. If you want to know more, please contact support@shuffler.io directly. + +
+ ); +}; + const Billing = memo((props) => { const { globalUrl, userdata, serverside, billingInfo, stripeKey,isLoaded, selectedOrganization, handleGetOrg, clickedFromOrgTab, removeCookie} = props; //const alert = useAlert(); @@ -2085,27 +2143,32 @@ const Billing = memo((props) => { return ( -
-
+
+
+ + {isCloud ? null : } + {addDealModal} - {clickedFromOrgTab ? - Billing & Licensing : - - Billing & Licensing - } + {clickedFromOrgTab ? + Billing & Licensing + : + + Billing & Licensing + + } {userdata?.org_status?.includes("integration_partner") && userdata?.org_status?.includes("sub_org") ? null : <> {clickedFromOrgTab ? {isCloud ? "Get more out of Shuffle by adding your credit card, such as no App Run limitations, and priority support from our team. We use Stripe to manage subscriptions and do not store any of your billing information. You can manage your subscription and billing information below." : - !(selectedOrganization?.subscriptions !== undefined && selectedOrganization?.subscriptions.length > 0 && selectedOrganization?.subscriptions[0]?.name?.toLowerCase().includes("enterprise") && selectedOrganization?.subscriptions[0]?.active) ? "Shuffle is an Enterprise automation platform, and a license is required. We do however offer a Scale license with HA guarantees, along with support hours. By buying a license on https://shuffler.io, you can get access to the license immediately, and if Cloud Syncronisation is enabled, the UI in your local instance will also update." : "Here you can check your license and billing information." + !(selectedOrganization?.subscriptions !== undefined && selectedOrganization?.subscriptions.length > 0 && selectedOrganization?.subscriptions[0]?.name?.toLowerCase().includes("enterprise") && selectedOrganization?.subscriptions[0]?.active) ? "Shuffle is an Enterprise automation platform, and a license is required at scale. We offer a license with HA guarantees, higher limits, along along with support hours. By buying a license on https://shuffler.io, you can get access to the license immediately, and if Cloud Syncronisation is enabled, the UI in your local instance will also update." : "Here you can check your license and billing information." } : {isCloud ? "Get more out of Shuffle by adding your credit card, such as no App Run limitations, and priority support from our team. We use Stripe to manage subscriptions and do not store any of your billing information. You can manage your subscription and billing information below." : - !(selectedOrganization?.subscriptions !== undefined && selectedOrganization?.subscriptions.length > 0 && selectedOrganization?.subscriptions[0]?.name?.toLowerCase().includes("enterprise") && selectedOrganization?.subscriptions[0]?.active) ? "Shuffle is an Enterprise automation platform, and a license is required. We do however offer a Scale license with HA guarantees, along with support hours. By buying a license on https://shuffler.io, you can get access to the license immediately, and if Cloud Syncronisation is enabled, the UI in your local instance will also update." : "Here you can check your license and billing information." + !(selectedOrganization?.subscriptions !== undefined && selectedOrganization?.subscriptions.length > 0 && selectedOrganization?.subscriptions[0]?.name?.toLowerCase().includes("enterprise") && selectedOrganization?.subscriptions[0]?.active) ? "Shuffle is an Enterprise automation platform, and a license is required at scale. We offer a Scale license with HA guarantees, along with support hours. By buying a license on https://shuffler.io, you can get access to the license immediately, and if Cloud Syncronisation is enabled, the UI in your local instance will also update." : "Here you can check your license and billing information." } } } @@ -3454,7 +3517,7 @@ const PaddingWrapper = memo(({ clickedFromOrgTab, children }) => { height: '100%', boxSizing: 'border-box', overflow: 'hidden', - maxHeight: "1700px", + maxHeight: 3000, overflowY: "auto", scrollbarColor: theme.palette.scrollbarColorTransparent, scrollbarWidth: 'thin' diff --git a/frontend/src/components/CacheView.jsx b/frontend/src/components/CacheView.jsx index bbc93d11..3c19da36 100644 --- a/frontend/src/components/CacheView.jsx +++ b/frontend/src/components/CacheView.jsx @@ -575,7 +575,10 @@ const CacheView = memo((props) => { .then((responseJson) => { setAddCache(responseJson); toast.success("Edit saved"); - listOrgCache(orgId, selectedCategory, 0, pageSize, page); + setTimeout(() => { + listOrgCache(orgId, selectedCategory, 0, pageSize, page); + }, 7500); + setModalOpen(false); }) .catch((error) => { @@ -613,7 +616,10 @@ const CacheView = memo((props) => { .then((responseJson) => { setAddCache(responseJson); toast.success("New key added!"); - listOrgCache(orgId, selectedCategory, 0, pageSize, page); + + setTimeout(() => { + listOrgCache(orgId, selectedCategory, 0, pageSize, page); + }, 5000); setModalOpen(false); }) .catch((error) => { diff --git a/frontend/src/components/DashboardOnboarding.jsx b/frontend/src/components/DashboardOnboarding.jsx index 3ad7a728..54cebcc2 100644 --- a/frontend/src/components/DashboardOnboarding.jsx +++ b/frontend/src/components/DashboardOnboarding.jsx @@ -6,7 +6,10 @@ import { Stack, styled, } from "@mui/material"; + import theme from "../theme.jsx"; +import { toast } from "react-toastify"; +import { useNavigate } from 'react-router-dom'; // Simple icon placeholders; replace with proper assets if desired const StepIcon = styled("div")(({ completed }) => ({ @@ -127,6 +130,9 @@ const DashboardOnboarding = ({ footer, globalUrl, onExplore, + setOnboardingOpen, + isProdStatusOn, + isCloud, }) => { // Internal completion state only; handlers are defined separately const [completed, setCompleted] = React.useState({ @@ -140,6 +146,7 @@ const DashboardOnboarding = ({ const [checkingWait, setCheckingWait] = React.useState(false); const [flashKeys, setFlashKeys] = React.useState([]); const [waitProgress, setWaitProgress] = React.useState(0); + const navigate = useNavigate(); // Load persisted completion state React.useEffect(() => { @@ -294,7 +301,7 @@ const DashboardOnboarding = ({ { index: 5, key: 'invite', - title: 'Invite more team members (optional)', + title: 'Invite your team members', description: 'Add teammates to collaborate in your org.', primaryCta: { label: 'Open users page', onClick: handleOpenUsers }, completed: completed.invite, @@ -320,7 +327,7 @@ const DashboardOnboarding = ({ if (!open) return null; return ( - + {/* Blur overlay with visible background */} {/* Header */} @@ -382,8 +391,55 @@ const DashboardOnboarding = ({ + {!isCloud ? ( +
{ + navigate("/admin?admin_tab=billingstats") + }} + > + + + {isProdStatusOn ? "Production" : "NOT Production"} + +
+ ) : null} +
+ {/* Steps list with a single continuous rail */} {/* Base grey rail */} @@ -427,12 +483,25 @@ const DashboardOnboarding = ({ {footer} + + diff --git a/frontend/src/components/LeftSideBar.jsx b/frontend/src/components/LeftSideBar.jsx index 0ca71daf..c25c6fc2 100644 --- a/frontend/src/components/LeftSideBar.jsx +++ b/frontend/src/components/LeftSideBar.jsx @@ -1226,7 +1226,7 @@ const LeftSideBar = ({ userdata, serverside, globalUrl, notifications, }) => { +
+ } + {!isCloud ? (
{ cursor: "pointer", }} onClick={() => { - navigate("/admin?admin_tab=prodstatus") + navigate("/admin?admin_tab=billingstats") }} > { color: isProdStatusOn ? "#2BC07E" : "#FD4C62", }} > - {expandLeftNav ? isProdStatusOn ? "Prod. Status ON" : "Prod. Status OFF" : isProdStatusOn ? "ON" : "OFF"} + {expandLeftNav ? isProdStatusOn ? "Production" : "NOT production" : isProdStatusOn ? "ON" : "OFF"}
) : null} - - {userdata?.licensed !== true && !userdata?.org_status?.includes("integration_partner") && expandLeftNav && !isProdStatusOn && -
- -
- } + { {!isPaidPlan && (