Tons of minor fixes

This commit is contained in:
Frikky
2025-10-21 00:34:57 +02:00
parent 23a6b4eff3
commit d26b7779e1
15 changed files with 463 additions and 141 deletions
+1 -1
View File
@@ -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
+2 -2
View File
@@ -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=
+48 -1
View File
@@ -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")
+2
View File
@@ -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';
+74 -11
View File
@@ -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 (
<div style={{ width: '100%', maxWidth: 800, padding: '0px 24px 24px 0px', height: 445 , display: 'flex', flexDirection: 'column', alignItems: 'flex-start'}}>
<div style={{ display: 'flex', alignItems: 'flex-start', gap: 12, marginBottom: 8 }}>
<Typography variant="h5" style={{ fontWeight: 600, fontFamily: theme.typography.fontFamily }}>Production Status</Typography>
<div style={{ display: 'flex', alignItems: 'center', gap: 8, padding: '5px 14px', borderRadius: 16, background: isProdStatusOn ? 'rgba(43,192,126,0.1)' : 'rgba(253,76,98,0.1)' }}>
<span style={{ width: 8, height: 8, borderRadius: 999, background: isProdStatusOn ? '#2BC07E' : '#FD4C62' }} />
<Typography variant="caption" style={{ color: isProdStatusOn ? '#2BC07E' : '#FD4C62', fontWeight: 400, fontFamily: theme.typography.fontFamily }}>{isProdStatusOn ? "ON" : "OFF"}</Typography>
</div>
</div>
<Typography variant="body2" color="textSecondary" style={{ marginBottom: 18, fontFamily: theme.typography.fontFamily }}>
Monitor your production status to stay informed about available features.
</Typography>
<div style={{ display: 'flex', flexDirection: 'column', gap: 14 }}>
{rows.map((row) => (
<div key={row.label} style={{ display: 'flex', alignItems: 'center', gap: 12, fontFamily: theme.typography.fontFamily }}>
{row.ok ? (
<CheckCircleIcon style={{ color: '#2BC07E' }} />
) : (
<CancelIcon style={{ color: '#FD4C62' }} />
)}
<Typography variant="body1" style={{ fontWeight: 400, fontFamily: theme.typography.fontFamily }}>{row.label}</Typography>
</div>
))}
</div>
<Divider style={{
width: '100%',
marginTop: 32,
marginBottom: 16,
borderColor: theme.palette.defaultBorder,
}} />
<Typography variant="body1" color="textPrimary" style={{ marginTop: 24, fontFamily: theme.typography.fontFamily }}>
Shuffle Enterprise is designed for organizations that require scalability, high availability, dedicated support and more to run mission-critical workflows in production environments.
</Typography>
<Typography variant="body1" color="textSecondary" style={{ marginTop: 24, fontFamily: theme.typography.fontFamily }}>
More about upgrading below. If you want to know more, please contact <a href="mailto:support@shuffler.io?subject=Tell%20me%20about%20Production%20readiness" style={{ color: '#f85a3e', textDecoration: 'none' }}>support@shuffler.io</a> directly.
</Typography>
</div>
);
};
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 (
<Wrapper clickedFromOrgTab={clickedFromOrgTab}>
<div style={{ height: "100%", width: "100%"}}>
<div style={{ width: "100%",}}>
<div style={{ width: "100%", padding: 24, }}>
<div style={{ width: "100%", maxWidth: 800, }}>
{isCloud ? null : <ProductionStatus selectedOrganization={selectedOrganization} userdata={userdata} isCloud={isCloud} theme={theme} />}
{addDealModal}
{clickedFromOrgTab ?
<Typography variant="h5" style={{fontSize: 24, fontWeight: 500, marginBottom: 8, marginTop: 0, }}>Billing & Licensing</Typography> :
<Typography variant="h4" style={{ marginTop: 20, marginBottom: 10 }}>
Billing & Licensing
</Typography>}
{clickedFromOrgTab ?
<Typography variant="h5" style={{fontSize: 24, fontWeight: 500, marginBottom: 8, marginTop: 24, }}>Billing & Licensing</Typography>
:
<Typography variant="h4" style={{ marginTop: 20, marginBottom: 10 }}>
Billing & Licensing
</Typography>
}
{userdata?.org_status?.includes("integration_partner") && userdata?.org_status?.includes("sub_org") ? null :
<>
{clickedFromOrgTab ?
<Typography variant="body2" color="textSecondary" style={{ fontSize: 16 }}>{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."
}</Typography> :
<Typography variant="body1" color="textSecondary" style={{ marginTop: 0, marginBottom: 10, fontSize: 16 }}>
{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."
}
</Typography>}
</> }
@@ -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'
+8 -2
View File
@@ -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) => {
@@ -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 (
<Box sx={{ position: "fixed", inset: 0, zIndex: 2000 }}>
<Box sx={{ position: "fixed", inset: 0, zIndex: 2000, }}>
{/* Blur overlay with visible background */}
<Box
onClick={onClose}
@@ -354,6 +361,8 @@ const DashboardOnboarding = ({
border: "1px solid rgba(255,255,255,0.08)",
p: 3,
boxShadow: "0 10px 40px rgba(0,0,0,0.5)",
position: "relative",
}}
>
{/* Header */}
@@ -382,8 +391,55 @@ const DashboardOnboarding = ({
</Typography>
</Box>
{!isCloud ? (
<div
style={{
display: "flex",
alignItems: "center",
gap: 20,
padding: "4px 10px",
marginLeft: "5px",
marginRight: "5px",
borderRadius: 20,
marginBottom: "14px",
background: isProdStatusOn
? "rgba(43, 192, 126, 0.1)"
: "rgba(255, 82, 82, 0.1)",
cursor: "pointer",
position: "absolute",
top: 20,
right: 20,
}}
onClick={() => {
navigate("/admin?admin_tab=billingstats")
}}
>
<span
style={{
width: 8,
height: 8,
marginLeft: 10,
background: isProdStatusOn ? "#2BC07E" : "#FD4C62",
borderRadius: 999,
display: "inline",
}}
/>
<Typography
style={{
fontFamily: "12px",
opacity: 0.9,
color: isProdStatusOn ? "#2BC07E" : "#FD4C62",
}}
>
{isProdStatusOn ? "Production" : "NOT Production"}
</Typography>
</div>
) : null}
</Box>
{/* Steps list with a single continuous rail */}
<Box sx={{ position: "relative", display: "flex", flexDirection: "column", gap: 3, marginLeft: -1.5, marginTop: 4 }}>
{/* Base grey rail */}
@@ -427,12 +483,25 @@ const DashboardOnboarding = ({
<Box sx={{ mt: 4, display: 'flex', justifyContent: 'center', gap: 1.5 }}>
{footer}
<Button variant="contained" color="primary" onClick={handleFinalDone}
sx={{
fontSize: 14,
padding: "8px 60px",
}}
>
Explore Now
sx={{
fontSize: 14,
padding: "8px 60px",
}}
>
Explore
</Button>
<Button variant="text" color="secondary" onClick={() => {
setOnboardingOpen(false)
toast.warn("The dashboard is not fully set up yet. You can complete the steps later from the onboarding section.", { timeout: 10000 })
}}
sx={{
fontSize: 14,
padding: "8px 60px",
}}
>
Skip for now
</Button>
</Box>
</Box>
+23 -17
View File
@@ -1226,7 +1226,7 @@ const LeftSideBar = ({ userdata, serverside, globalUrl, notifications, }) => {
<Box sx={{ display: "flex", flexDirection: "row", marginTop: 2.5, width: expandLeftNav ? "100%" : 48, padding: "0px", }}>
<Button
component={Link}
to={userdata?.support ? "/new-dashboard" : "/usecases"}
to={"/new-dashboard"}
onClick={(event) => {
setOpenautomateTab(true);
setOpenSecurityTab(false);
@@ -1925,6 +1925,25 @@ const LeftSideBar = ({ userdata, serverside, globalUrl, notifications, }) => {
}}
>
{userdata?.licensed !== true && !userdata?.org_status?.includes("integration_partner") && expandLeftNav && !isProdStatusOn &&
<div style={{display: "flex", flexDirection: "column", alignItems: "center", justifyContent: "center", }}>
<Button
variant="outlined"
style={{marginBottom: 15, borderWidth: 2, }}
onClick={() => {
if (isCloud) {
window.open("https://shuffler.io/contact?category=book_a_demo&ref=cloud", "_blank")
} else {
window.open("https://shuffler.io/contact?category=book_a_demo&ref=onprem", "_blank")
}
}}
>
Book a Demo
</Button>
</div>
}
{!isCloud ? (
<div
style={{
@@ -1942,7 +1961,7 @@ const LeftSideBar = ({ userdata, serverside, globalUrl, notifications, }) => {
cursor: "pointer",
}}
onClick={() => {
navigate("/admin?admin_tab=prodstatus")
navigate("/admin?admin_tab=billingstats")
}}
>
<span
@@ -1962,24 +1981,11 @@ const LeftSideBar = ({ userdata, serverside, globalUrl, notifications, }) => {
color: isProdStatusOn ? "#2BC07E" : "#FD4C62",
}}
>
{expandLeftNav ? isProdStatusOn ? "Prod. Status ON" : "Prod. Status OFF" : isProdStatusOn ? "ON" : "OFF"}
{expandLeftNav ? isProdStatusOn ? "Production" : "NOT production" : isProdStatusOn ? "ON" : "OFF"}
</Typography>
</div>
) : null}
{userdata?.licensed !== true && !userdata?.org_status?.includes("integration_partner") && expandLeftNav && !isProdStatusOn &&
<div style={{display: "flex", flexDirection: "column", alignItems: "center", justifyContent: "center", }}>
<Button
variant="outlined"
style={{marginBottom: 15, borderWidth: 2, }}
onClick={() => {
window.open("https://shuffler.io/contact?category=book_a_demo", "_blank")
}}
>
Book a Demo
</Button>
</div>
}
<Box ref={autocompleteRef}>
<Autocomplete
disablePortal
+7 -7
View File
@@ -1507,14 +1507,14 @@ const LicencePopup = (props) => {
{!isPaidPlan && (
<Button
fullWidth
variant="outlined"
variant="contained"
color="primary"
style={{ textTransform: "none" }}
onClick={() => {
if(isCloud) {
navigate("/pricing");
navigate("/pricing?ref=cloud_billing");
}else {
window.open("https://shuffler.io/pricing?env=Self-Hosted", "_blank")
window.open("https://shuffler.io/pricing?env=Self-Hosted&ref=onprem_billing", "_blank")
}
}}
>
@@ -1526,10 +1526,10 @@ const LicencePopup = (props) => {
variant="outlined"
color="primary"
onClick={() => {
if(isCloud) {
navigate("/contact?category=contact")
}else {
window.open("https://shuffler.io/contact?category=contact", "_blank")
if (isCloud) {
navigate("/contact?category=contact&ref=cloud_billing")
} else {
window.open("https://shuffler.io/contact?category=contact&ref=onprem_billing", "_blank")
}
}}
style={{ textTransform: "none" }}
+15 -49
View File
@@ -1,5 +1,6 @@
import React, { useEffect, useState, useCallback, useContext } from 'react';
import { Link, useNavigate, useLocation } from "react-router-dom";
import Billing from "../components/Billing.jsx";
import Priorities from "../components/Priorities.jsx";
import Branding from "../components/Branding.jsx";
@@ -7,10 +8,20 @@ import EditOrgTab from '../components/EditOrgTab.jsx';
import CloudSyncTab from '../components/CloudSyncTab.jsx';
import SSOTab from "../components/ssoTab.jsx"
import { ToastContainer, toast } from "react-toastify";
import { Button, Tooltip, Typography } from '@mui/material';
import { CheckCircle as CheckCircleIcon, Cancel as CancelIcon } from '@mui/icons-material';
import {
Button,
Tooltip,
Typography,
Divider,
} from '@mui/material';
import {
CheckCircle as CheckCircleIcon,
Cancel as CancelIcon,
} from '@mui/icons-material';
import { getTheme } from '../theme.jsx';
import { Context } from '../context/ContextApi.jsx';
const OrganizationTab = (props) => {
const location = useLocation();
const navigate = useNavigate();
@@ -37,7 +48,7 @@ const OrganizationTab = (props) => {
const [billingInfo, setBillingInfo] = useState({});
const [orgRequest, setOrgRequest] = React.useState(true);
const [curIndex, setCurIndex] = React.useState(0);
const items = ['Org Configuration', 'Production Status', "SSO", "Notifications", 'Billing & Stats'];
const items = ['Org Configuration', "SSO", "Notifications", 'Billing & Stats'];
const [visibleTabs, setVisibleTabs] = useState(items);
const [unreadNotifications, setUnreadNotifications] = React.useState(
notifications?.filter((notification) => notification.read === false)?.length
@@ -127,12 +138,10 @@ const OrganizationTab = (props) => {
switch (selectedTab) {
case 'org_config':
return <EditOrgTab isCloud={isCloud} selectedStatus={selectedStatus} setSelectedStatus={setSelectedStatus} handleStatusChange={handleStatusChange} handleEditOrg={handleEditOrg} userdata={userdata} globalUrl={globalUrl} serverside={serverside} handleGetOrg={handleGetOrg} setSelectedOrganization={setSelectedOrganization} selectedOrganization={selectedOrganization} />;
case 'prodstatus':
case 'productionstatus':
return isCloud ? null : <ProductionStatus selectedOrganization={selectedOrganization} userdata={userdata} isCloud={isCloud} theme={theme} />;
case 'sso':
return <SSOTab isEditOrgTab={true} globalUrl={globalUrl} isCloud={isCloud} userdata={userdata} handleEditOrg={handleEditOrg} selectedOrganization={selectedOrganization}/>
case `notifications`:
case `errors`:
case `priorities`:
return (
<Priorities
@@ -267,46 +276,3 @@ const OrganizationTab = (props) => {
export default OrganizationTab;
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: 'High Scale', ok: isProdStatusOn },
{ label: 'High Availability', ok: isProdStatusOn },
{ label: 'Stable Configuration', ok: isProdStatusOn },
{ label: 'Robust Infrastructure', ok: isProdStatusOn },
];
return (
<div style={{ width: '100%', maxWidth: 800, padding: '24px 24px 24px 34px', height: 445 , display: 'flex', flexDirection: 'column', alignItems: 'flex-start'}}>
<div style={{ display: 'flex', alignItems: 'flex-start', gap: 12, marginBottom: 8 }}>
<Typography variant="h5" style={{ fontWeight: 600, fontFamily: theme.typography.fontFamily }}>Production Status</Typography>
<div style={{ display: 'flex', alignItems: 'center', gap: 8, padding: '5px 14px', borderRadius: 16, background: isProdStatusOn ? 'rgba(43,192,126,0.1)' : 'rgba(253,76,98,0.1)' }}>
<span style={{ width: 8, height: 8, borderRadius: 999, background: isProdStatusOn ? '#2BC07E' : '#FD4C62' }} />
<Typography variant="caption" style={{ color: isProdStatusOn ? '#2BC07E' : '#FD4C62', fontWeight: 400, fontFamily: theme.typography.fontFamily }}>{isProdStatusOn ? "ON" : "OFF"}</Typography>
</div>
</div>
<Typography variant="body2" color="textSecondary" style={{ marginBottom: 18, fontFamily: theme.typography.fontFamily }}>
Monitor your production status to stay informed about available features.
</Typography>
<div style={{ display: 'flex', flexDirection: 'column', gap: 14 }}>
{rows.map((row) => (
<div key={row.label} style={{ display: 'flex', alignItems: 'center', gap: 12, fontFamily: theme.typography.fontFamily }}>
{row.ok ? (
<CheckCircleIcon style={{ color: '#2BC07E' }} />
) : (
<CancelIcon style={{ color: '#FD4C62' }} />
)}
<Typography variant="body1" style={{ fontWeight: 400, fontFamily: theme.typography.fontFamily }}>{row.label}</Typography>
</div>
))}
</div>
</div>
);
};
+1
View File
@@ -1518,6 +1518,7 @@ print('"' + encoded + '"')
/>
:
<TextField
disabled={workflows === undefined || workflows === null || workflows.length === 0}
required
InputProps={{
style: {
@@ -307,6 +307,7 @@ const SuccessFailedRunsWidget = (props) => {
setSeriesFail(failSeries);
return;
}
const successKey =
mode === "workflows"
? "workflow_executions_finished"
@@ -383,7 +384,7 @@ const SuccessFailedRunsWidget = (props) => {
return;
}
fetchSeries();
}, [days, globalUrl, mode]);
}, [dummyMode, days, globalUrl, mode]);
// Apply external days override (e.g. after onboarding completes)
useEffect(() => {
@@ -885,7 +886,7 @@ const SuccessFailedRunsWidget = (props) => {
}}
>
<Typography sx={{ fontSize: 18, fontWeight: 500, fontFamily: theme.typography.fontFamily}}>
{mode === "workflows" ? "Workflows" : "Apps"} Success Rates
{mode === "workflows" ? "Workflow" : "App"} Success Rates
</Typography>
<div style={{ display: "flex", flexDirection: "column", alignItems: "flex-start", justifyContent: "center", height: "100%", gap: 15, marginTop: -5, marginLeft: -2 }}>
{(() => {
+2 -2
View File
@@ -476,7 +476,7 @@ const LoginPage = props => {
return;
}
window.location.pathname = "/workflows"
window.location.pathname = "/new-dashboard"
}, 2000);
}
@@ -658,7 +658,7 @@ const LoginPage = props => {
}
}
window.location.pathname = "/workflows"
window.location.pathname = "/new-dashboard"
}, 2000);
}
})
+96 -37
View File
@@ -10,22 +10,31 @@ import {
Divider,
Select,
MenuItem,
Tooltip,
CircularProgress,
} from '@mui/material';
import TrendingUpIcon from '@mui/icons-material/TrendingUp';
import TrendingDownIcon from '@mui/icons-material/TrendingDown';
import ErrorOutlineIcon from '@mui/icons-material/ErrorOutline';
import TaskAltIcon from '@mui/icons-material/TaskAlt';
import { toast } from "react-toastify";
import {
TrendingFlat as TrendingFlatIcon,
TrendingUp as TrendingUpIcon,
TrendingDown as TrendingDownIcon,
TaskAlt as TaskAltIcon,
SuccessFailed as SuccessFailedIcon,
RunsOverTime as RunsOverTimeIcon,
ErrorOutline as ErrorOutlineIcon,
} from '@mui/icons-material';
import SuccessFailedRunsWidget from '../components/SuccessFailedRunsWidget.jsx';
import RunsOverTimeWidget from '../components/RunsOverTimeWidget.jsx';
import { Context } from '../context/ContextApi.jsx';
import CircularProgress from '@mui/material/CircularProgress';
import { useNavigate } from 'react-router-dom';
import DashboardOnboarding from '../components/DashboardOnboarding.jsx';
import { Context } from '../context/ContextApi.jsx';
import { useNavigate } from 'react-router-dom';
const NewDashboard = (props) => {
const { globalUrl, userdata } = props;
const { globalUrl, serverside, userdata } = props;
// const [workflows, setWorkflows] = useState([]);
const { leftSideBarOpenByClick } = useContext(Context);
const [sfwControls, setSfwControls] = useState(null);
const [loadingSfw, setLoadingSfw] = useState(true);
@@ -40,9 +49,18 @@ const NewDashboard = (props) => {
} catch {
return true;
}
});
})
const [overrideDays, setOverrideDays] = useState(undefined);
const [rotMonthOverride, setRotMonthOverride] = useState(undefined);
const [isProdStatusOn, setIsProdStatusOn] = useState(false)
const isCloud =
serverside === true || typeof window === "undefined"
? true
: window.location.host === "localhost:3002" ||
window.location.host === "shuffler.io" ||
window.location.host === "localhost:5002";
const navigate = useNavigate();
const handleSfwControls = useCallback((node) => {
@@ -71,8 +89,6 @@ const NewDashboard = (props) => {
};
const timeFmt = formatTimeDisplay(totals.timeSavedMinutes);
const STATIC_TIME_PERCENT = '62%';
const STATIC_MONEY_PERCENT = '46%';
const unreadCount = notifications.filter(n => n && n.read === false).length;
const readCount = notifications.filter(n => n && n.read === true).length;
@@ -81,9 +97,11 @@ const NewDashboard = (props) => {
// 1 Workflow run = 15 minutes
// 1 Workflow run = $25
const STATIC_TIME_PERCENT = 'TBD'
const STATIC_MONEY_PERCENT = 'TBD'
const kpis = [
{ value: timeFmt.display, title: timeFmt.title, label: 'Time saved', icon: <TrendingUpIcon sx={{ color: '#5cc879', fontSize: 34 }} />, percentage: STATIC_TIME_PERCENT, color: '#5cc879' },
{ value: formatCurrencyCompact(totals.moneySavedDollars), label: 'Money saved', icon: <TrendingUpIcon sx={{ color: '#5cc879', fontSize: 34 }} />, percentage: STATIC_MONEY_PERCENT, color: '#5cc879' },
{ value: timeFmt.display, title: timeFmt.title, label: 'Time saved', icon: <TrendingUpIcon sx={{ color: '#5cc879', fontSize: 34 }} />, percentage: STATIC_TIME_PERCENT, color: '#5cc879', disabled: true},
{ value: formatCurrencyCompact(totals.moneySavedDollars), label: 'Money saved', icon: <TrendingUpIcon sx={{ color: '#5cc879', fontSize: 34 }} />, percentage: STATIC_MONEY_PERCENT, color: '#5cc879', disabled: true, },
{ value: String(unreadCount), label: 'Total errors', icon: <ErrorOutlineIcon sx={{ color: '#f87171', fontSize: 34, opacity: 0.9 }} />, percentage: "", color: '#f87171' },
{ value: String(readCount), label: 'Errors resolved', icon: <TaskAltIcon sx={{ color: '#5cc879', fontSize: 34, opacity: 0.9 }} />, percentage: "", color: '#5cc879' },
];
@@ -177,13 +195,51 @@ const NewDashboard = (props) => {
// loadWorkflows();
// }, [globalUrl]);
useEffect(() => {
const orgId = userdata?.active_org?.id;
if (!orgId) {
return;
}
let fetched = false;
fetch(`${globalUrl}/api/v1/orgs/${orgId}`, {
method: "GET",
credentials: "include",
headers: { "Content-Type": "application/json" },
})
.then((response) => (response.ok ? response.json() : null))
.then((org) => {
if (!fetched && org) {
if (!isCloud) {
if (org?.cloud_sync && org?.subscriptions[0]?.name?.toLowerCase().includes("enterprise") && org?.subscriptions[0]?.active) {
setIsProdStatusOn(true);
} else if (org?.subscriptions[0]?.name?.toLowerCase().includes("enterprise") && org?.subscriptions[0]?.active) {
setIsProdStatusOn(true);
} else {
setIsProdStatusOn(false);
}
}
}
})
.catch(() => {});
return () => {
fetched = true;
};
}, [userdata?.active_org?.id, globalUrl]);
return (
<div style={{ maxWidth: 1366, margin: '0 auto', padding: 16, paddingTop: 50, paddingBottom: 30, paddingLeft: leftSideBarOpenByClick ? 270 : 80, transition: 'padding-left 0.3s ease', position: 'relative' }}>
<DashboardOnboarding
open={onboardingOpen}
globalUrl={globalUrl}
onClose={() => setOnboardingOpen(false)}
onClose={() => {
setOnboardingOpen(false)
}}
setOnboardingOpen={setOnboardingOpen}
isProdStatusOn={isProdStatusOn}
isCloud={isCloud}
onExplore={() => {
// Ensure overrides are set before closing modal
setOverrideDays(5);
@@ -215,28 +271,31 @@ const NewDashboard = (props) => {
<Grid container spacing={2}>
{kpis.map((kpi) => (
<Grid item xs={12} sm={6} md={3} key={kpi.label}>
<Paper style={{ padding: 16, background: 'rgba(255,255,255,0.03)', border: '1px solid rgba(255,255,255,0.08)', borderRadius: 12
,cursor: kpi.label.toLowerCase().includes('total errors') ? 'pointer' : 'default'
}}
onClick={() => {
if (kpi.label.toLowerCase().includes('total errors')) {
// navigate to notifications page
navigate('/admin?admin_tab=notifications');
}
}}
>
<Stack direction="row" alignItems="center" justifyContent="space-between">
<Stack sx={{py:2, paddingLeft: 1}}>
<Typography variant="h4" title={kpi.title || ''}>{kpi.value}</Typography>
<Typography sx={{fontSize: 13}} color="textSecondary">{kpi.label}</Typography>
</Stack>
<Stack sx={{py: 2, paddingRight: 1, marginTop: kpi.label.toLowerCase().includes('errors') ? -1 : 0}}>
{kpi.icon}
<Typography variant="body2" color={kpi.color}>{kpi.percentage}</Typography>
</Stack>
</Stack>
</Paper>
<Tooltip title={kpi.disabled ? "This metric is coming soon!" : ""} arrow>
<Paper style={{ padding: 16, background: 'rgba(255,255,255,0.03)', border: '1px solid rgba(255,255,255,0.08)', borderRadius: 12
, cursor: kpi.label.toLowerCase().includes('total errors') ? 'pointer' : 'default',
transparency: kpi.disabled ? 0.5 : 1
}}
onClick={() => {
if (kpi.label.toLowerCase().includes('total errors')) {
// navigate to notifications page
navigate('/admin?admin_tab=notifications');
}
}}
>
<Stack direction="row" alignItems="center" justifyContent="space-between">
<Stack sx={{py:2, paddingLeft: 1}}>
<Typography variant="h4" title={kpi.title || ''}>{kpi.value}</Typography>
<Typography sx={{fontSize: 13}} color="textSecondary">{kpi.label}</Typography>
</Stack>
<Stack sx={{py: 2, paddingRight: 1, marginTop: kpi.label.toLowerCase().includes('errors') ? -1 : 0}}>
{kpi.icon}
<Typography variant="body2" color={kpi.color}>{kpi.percentage}</Typography>
</Stack>
</Stack>
</Paper>
</Tooltip>
</Grid>
))}
</Grid>
+104 -2
View File
@@ -818,6 +818,7 @@ const Workflows2 = (props) => {
const [highlightIds, setHighlightIds] = React.useState([])
const [apps, setApps] = React.useState([]);
const [isProdStatusOn, setIsProdStatusOn] = React.useState(false);
const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
@@ -839,6 +840,39 @@ const Workflows2 = (props) => {
}
}, [location.search]);
useEffect(() => {
const orgId = userdata?.active_org?.id;
if (!orgId) {
return;
}
let fetched = false;
fetch(`${globalUrl}/api/v1/orgs/${orgId}`, {
method: "GET",
credentials: "include",
headers: { "Content-Type": "application/json" },
})
.then((response) => (response.ok ? response.json() : null))
.then((org) => {
if (!fetched && org) {
if (!isCloud) {
if (org?.cloud_sync && org?.subscriptions[0]?.name?.toLowerCase().includes("enterprise") && org?.subscriptions[0]?.active) {
setIsProdStatusOn(true);
} else if (org?.subscriptions[0]?.name?.toLowerCase().includes("enterprise") && org?.subscriptions[0]?.active) {
setIsProdStatusOn(true);
} else {
setIsProdStatusOn(false);
}
}
}
})
.catch(() => {});
return () => {
fetched = true;
};
}, [userdata?.active_org?.id, globalUrl]);
const handleTabChange = (event, newValue) => {
setCurrTab(newValue);
@@ -3043,12 +3077,34 @@ const Workflows2 = (props) => {
}
}
const isPublicWorkflow = data?.objectId === undefined || data?.objectId === null
const foundImage = !isPublicWorkflow ? "" : data?.image_url === undefined || data?.image_url === null || data?.image_url === "" ? data?.image : data?.image_url
const foundTimeline = workflowTimelines.find((timeline) => timeline.id === data.id)
return (
<div
id={`workflowbox-${data.id}`}
style={{ width: "100%", minWidth: 320, position: "relative", border: highlightIds.includes(data.id) ? "2px solid #f85a3e" : isDistributed || hasSuborgs ? `2px solid ${theme.palette.distributionColor}` : "inherit", borderRadius: theme.palette?.borderRadius, backgroundColor: "#212121", fontFamily: theme.typography?.fontFamily }}>
style={{ width: "100%", minWidth: 320, position: "relative", border: highlightIds.includes(data.id) ? "2px solid #f85a3e" : isDistributed || hasSuborgs ? `2px solid ${theme.palette.distributionColor}` : "inherit", borderRadius: theme.palette?.borderRadius, backgroundColor: "#212121", fontFamily: theme.typography?.fontFamily, overflow: "hidden", }}
>
{isPublicWorkflow && foundImage?.length > 0 ?
<img src={foundImage} alt="image" style={{
maxHeight: 250,
minHeight: 250,
minWidth: 100,
marginLeft: 12,
cursor: "pointer",
}}
onClick={() => {
if (isCloud) {
navigate(`/workflows/${data.objectID}`)
} else {
window.open(`https://shuffler.io/workflows/${data.objectID}`, "_blank")
}
}}
/>
: null}
<Paper square style={paperAppStyle}>
{selectedCategory !== "" ?
<Tooltip title={`Usecase Category: ${selectedCategory}`} placement="bottom">
@@ -4934,7 +4990,7 @@ const Workflows2 = (props) => {
}}
/>
<Tab
label="Discover Workflows"
label="Community Workflows"
style={{
...tabStyle,
marginRight: 0,
@@ -5313,12 +5369,58 @@ const Workflows2 = (props) => {
</div>
}
{!isCloud ? (
<div
style={{
display: "flex",
alignItems: "center",
gap: 20,
padding: "4px 10px",
marginLeft: "5px",
marginRight: "5px",
borderRadius: 20,
marginBottom: "14px",
background: isProdStatusOn
? "rgba(43, 192, 126, 0.1)"
: "rgba(255, 82, 82, 0.1)",
cursor: "pointer",
position: "absolute",
top: 20,
right: 20,
}}
onClick={() => {
navigate("/admin?admin_tab=billingstats")
}}
>
<span
style={{
width: 8,
height: 8,
marginLeft: 10,
background: isProdStatusOn ? "#2BC07E" : "#FD4C62",
borderRadius: 999,
display: "inline",
}}
/>
<Typography
style={{
fontFamily: "12px",
opacity: 0.9,
color: isProdStatusOn ? "#2BC07E" : "#FD4C62",
}}
>
{isProdStatusOn ? "Production" : "NOT Production"}
</Typography>
</div>
) : null}
<div style={{
width: "100%",
position: "relative",
zIndex: 1
}}>
{
(isLoadingWorkflow && currTab !== 2) ? (
<LoadingWorkflowGrid />