Tons of minor fixes
This commit is contained in:
@@ -24,7 +24,7 @@ require (
|
|||||||
github.com/gorilla/mux v1.8.1
|
github.com/gorilla/mux v1.8.1
|
||||||
github.com/h2non/filetype v1.1.3
|
github.com/h2non/filetype v1.1.3
|
||||||
github.com/satori/go.uuid v1.2.0
|
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
|
github.com/shuffle/singul v0.0.17
|
||||||
golang.org/x/crypto v0.40.0
|
golang.org/x/crypto v0.40.0
|
||||||
google.golang.org/api v0.236.0
|
google.golang.org/api v0.236.0
|
||||||
|
|||||||
@@ -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/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 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8=
|
||||||
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
|
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.32 h1:hsF2YkKHgaNpqhh2oZs31BgPSjN+YjGNnd9WmD0qh3w=
|
||||||
github.com/shuffle/shuffle-shared v0.9.31/go.mod h1:vfI2QDGphZGrcwuUPQ1yI/Hgc8aseFro5+2k36irfkQ=
|
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 h1:mxaPtj6z85Nf6tl7L2gwDliTfEZtRQqApuu9iKcP75o=
|
||||||
github.com/shuffle/singul v0.0.17/go.mod h1:8c42n1NahhCIPxzLxwp9eYbWkvY4+ct0jfbhkRsRRsY=
|
github.com/shuffle/singul v0.0.17/go.mod h1:8c42n1NahhCIPxzLxwp9eYbWkvY4+ct0jfbhkRsRRsY=
|
||||||
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
|
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
|
||||||
|
|||||||
+48
-1
@@ -1282,7 +1282,6 @@ func checkAdminLogin(resp http.ResponseWriter, request *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
count := len(users)
|
count := len(users)
|
||||||
|
|
||||||
if count == 0 {
|
if count == 0 {
|
||||||
log.Printf("[WARNING] No users - redirecting for management user")
|
log.Printf("[WARNING] No users - redirecting for management user")
|
||||||
resp.WriteHeader(200)
|
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)")
|
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/{key}/executions", shuffle.GetWorkflowExecutionsV2).Methods("GET", "OPTIONS")
|
||||||
r.HandleFunc("/api/v2/workflows/generate/llm", shuffle.HandleWorkflowGenerationResponse).Methods("POST", "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/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
|
// New for recommendations in Shuffle
|
||||||
r.HandleFunc("/api/v1/recommendations/get_actions", shuffle.HandleActionRecommendation).Methods("POST", "OPTIONS")
|
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}/stats/{key}", shuffle.GetSpecificStats).Methods("GET", "OPTIONS")
|
||||||
r.HandleFunc("/api/v1/orgs/{orgId}/statistics", shuffle.HandleGetStatistics).Methods("GET", "OPTIONS")
|
r.HandleFunc("/api/v1/orgs/{orgId}/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.HandleListCacheKeys).Methods("GET", "OPTIONS")
|
||||||
r.HandleFunc("/api/v1/orgs/{orgId}/cache", shuffle.HandleSetCacheKey).Methods("POST", "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")
|
r.HandleFunc("/api/v1/orgs/{orgId}/cache/{cache_key}", shuffle.HandleDeleteCacheKey).Methods("DELETE", "OPTIONS")
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import React, { useState, useEffect, useContext, memo } from 'react';
|
import React, { useState, useEffect, useContext, memo } from 'react';
|
||||||
import { useNavigate, useLocation } from 'react-router-dom';
|
import { useNavigate, useLocation } from 'react-router-dom';
|
||||||
|
|
||||||
import OrganizationTab from '../components/OrganizationTab.jsx';
|
import OrganizationTab from '../components/OrganizationTab.jsx';
|
||||||
import PartnerTab from '../components/PartnerTab.jsx';
|
import PartnerTab from '../components/PartnerTab.jsx';
|
||||||
import UserManagmentTab from '../components/UserManagmentTab.jsx';
|
import UserManagmentTab from '../components/UserManagmentTab.jsx';
|
||||||
@@ -20,6 +21,7 @@ import {
|
|||||||
FmdGoodOutlined as FmdGoodOutlinedIcon,
|
FmdGoodOutlined as FmdGoodOutlinedIcon,
|
||||||
GroupOutlined as GroupOutlinedIcon
|
GroupOutlined as GroupOutlinedIcon
|
||||||
} from '@mui/icons-material';
|
} from '@mui/icons-material';
|
||||||
|
|
||||||
import theme, { getTheme } from '../theme.jsx';
|
import theme, { getTheme } from '../theme.jsx';
|
||||||
import { Button, Skeleton, Tooltip } from '@mui/material';
|
import { Button, Skeleton, Tooltip } from '@mui/material';
|
||||||
import { Index } from 'react-instantsearch-dom';
|
import { Index } from 'react-instantsearch-dom';
|
||||||
|
|||||||
@@ -47,7 +47,9 @@ import {
|
|||||||
CheckCircle,
|
CheckCircle,
|
||||||
Padding,
|
Padding,
|
||||||
Edit,
|
Edit,
|
||||||
Search as SearchIcon
|
Search as SearchIcon,
|
||||||
|
CheckCircle as CheckCircleIcon,
|
||||||
|
Cancel as CancelIcon,
|
||||||
} from "@mui/icons-material";
|
} from "@mui/icons-material";
|
||||||
|
|
||||||
//import { useAlert
|
//import { useAlert
|
||||||
@@ -61,6 +63,62 @@ import { Context } from "../context/ContextApi.jsx";
|
|||||||
import DeleteIcon from '@mui/icons-material/Delete';
|
import DeleteIcon from '@mui/icons-material/Delete';
|
||||||
import { DataGrid } from "@mui/x-data-grid";
|
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 Billing = memo((props) => {
|
||||||
const { globalUrl, userdata, serverside, billingInfo, stripeKey,isLoaded, selectedOrganization, handleGetOrg, clickedFromOrgTab, removeCookie} = props;
|
const { globalUrl, userdata, serverside, billingInfo, stripeKey,isLoaded, selectedOrganization, handleGetOrg, clickedFromOrgTab, removeCookie} = props;
|
||||||
//const alert = useAlert();
|
//const alert = useAlert();
|
||||||
@@ -2085,27 +2143,32 @@ const Billing = memo((props) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Wrapper clickedFromOrgTab={clickedFromOrgTab}>
|
<Wrapper clickedFromOrgTab={clickedFromOrgTab}>
|
||||||
<div style={{ height: "100%", width: "100%"}}>
|
<div style={{ width: "100%", padding: 24, }}>
|
||||||
<div style={{ width: "100%",}}>
|
<div style={{ width: "100%", maxWidth: 800, }}>
|
||||||
|
|
||||||
|
{isCloud ? null : <ProductionStatus selectedOrganization={selectedOrganization} userdata={userdata} isCloud={isCloud} theme={theme} />}
|
||||||
|
|
||||||
{addDealModal}
|
{addDealModal}
|
||||||
{clickedFromOrgTab ?
|
{clickedFromOrgTab ?
|
||||||
<Typography variant="h5" style={{fontSize: 24, fontWeight: 500, marginBottom: 8, marginTop: 0, }}>Billing & Licensing</Typography> :
|
<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 variant="h4" style={{ marginTop: 20, marginBottom: 10 }}>
|
||||||
</Typography>}
|
Billing & Licensing
|
||||||
|
</Typography>
|
||||||
|
}
|
||||||
{userdata?.org_status?.includes("integration_partner") && userdata?.org_status?.includes("sub_org") ? null :
|
{userdata?.org_status?.includes("integration_partner") && userdata?.org_status?.includes("sub_org") ? null :
|
||||||
<>
|
<>
|
||||||
{clickedFromOrgTab ?
|
{clickedFromOrgTab ?
|
||||||
<Typography variant="body2" color="textSecondary" style={{ fontSize: 16 }}>{isCloud ?
|
<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."
|
"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> :
|
||||||
<Typography variant="body1" color="textSecondary" style={{ marginTop: 0, marginBottom: 10, fontSize: 16 }}>
|
<Typography variant="body1" color="textSecondary" style={{ marginTop: 0, marginBottom: 10, fontSize: 16 }}>
|
||||||
{isCloud ?
|
{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."
|
"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>}
|
</Typography>}
|
||||||
</> }
|
</> }
|
||||||
@@ -3454,7 +3517,7 @@ const PaddingWrapper = memo(({ clickedFromOrgTab, children }) => {
|
|||||||
height: '100%',
|
height: '100%',
|
||||||
boxSizing: 'border-box',
|
boxSizing: 'border-box',
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
maxHeight: "1700px",
|
maxHeight: 3000,
|
||||||
overflowY: "auto",
|
overflowY: "auto",
|
||||||
scrollbarColor: theme.palette.scrollbarColorTransparent,
|
scrollbarColor: theme.palette.scrollbarColorTransparent,
|
||||||
scrollbarWidth: 'thin'
|
scrollbarWidth: 'thin'
|
||||||
|
|||||||
@@ -575,7 +575,10 @@ const CacheView = memo((props) => {
|
|||||||
.then((responseJson) => {
|
.then((responseJson) => {
|
||||||
setAddCache(responseJson);
|
setAddCache(responseJson);
|
||||||
toast.success("Edit saved");
|
toast.success("Edit saved");
|
||||||
listOrgCache(orgId, selectedCategory, 0, pageSize, page);
|
setTimeout(() => {
|
||||||
|
listOrgCache(orgId, selectedCategory, 0, pageSize, page);
|
||||||
|
}, 7500);
|
||||||
|
|
||||||
setModalOpen(false);
|
setModalOpen(false);
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
@@ -613,7 +616,10 @@ const CacheView = memo((props) => {
|
|||||||
.then((responseJson) => {
|
.then((responseJson) => {
|
||||||
setAddCache(responseJson);
|
setAddCache(responseJson);
|
||||||
toast.success("New key added!");
|
toast.success("New key added!");
|
||||||
listOrgCache(orgId, selectedCategory, 0, pageSize, page);
|
|
||||||
|
setTimeout(() => {
|
||||||
|
listOrgCache(orgId, selectedCategory, 0, pageSize, page);
|
||||||
|
}, 5000);
|
||||||
setModalOpen(false);
|
setModalOpen(false);
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
|
|||||||
@@ -6,7 +6,10 @@ import {
|
|||||||
Stack,
|
Stack,
|
||||||
styled,
|
styled,
|
||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
|
|
||||||
import theme from "../theme.jsx";
|
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
|
// Simple icon placeholders; replace with proper assets if desired
|
||||||
const StepIcon = styled("div")(({ completed }) => ({
|
const StepIcon = styled("div")(({ completed }) => ({
|
||||||
@@ -127,6 +130,9 @@ const DashboardOnboarding = ({
|
|||||||
footer,
|
footer,
|
||||||
globalUrl,
|
globalUrl,
|
||||||
onExplore,
|
onExplore,
|
||||||
|
setOnboardingOpen,
|
||||||
|
isProdStatusOn,
|
||||||
|
isCloud,
|
||||||
}) => {
|
}) => {
|
||||||
// Internal completion state only; handlers are defined separately
|
// Internal completion state only; handlers are defined separately
|
||||||
const [completed, setCompleted] = React.useState({
|
const [completed, setCompleted] = React.useState({
|
||||||
@@ -140,6 +146,7 @@ const DashboardOnboarding = ({
|
|||||||
const [checkingWait, setCheckingWait] = React.useState(false);
|
const [checkingWait, setCheckingWait] = React.useState(false);
|
||||||
const [flashKeys, setFlashKeys] = React.useState([]);
|
const [flashKeys, setFlashKeys] = React.useState([]);
|
||||||
const [waitProgress, setWaitProgress] = React.useState(0);
|
const [waitProgress, setWaitProgress] = React.useState(0);
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
// Load persisted completion state
|
// Load persisted completion state
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
@@ -294,7 +301,7 @@ const DashboardOnboarding = ({
|
|||||||
{
|
{
|
||||||
index: 5,
|
index: 5,
|
||||||
key: 'invite',
|
key: 'invite',
|
||||||
title: 'Invite more team members (optional)',
|
title: 'Invite your team members',
|
||||||
description: 'Add teammates to collaborate in your org.',
|
description: 'Add teammates to collaborate in your org.',
|
||||||
primaryCta: { label: 'Open users page', onClick: handleOpenUsers },
|
primaryCta: { label: 'Open users page', onClick: handleOpenUsers },
|
||||||
completed: completed.invite,
|
completed: completed.invite,
|
||||||
@@ -320,7 +327,7 @@ const DashboardOnboarding = ({
|
|||||||
if (!open) return null;
|
if (!open) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box sx={{ position: "fixed", inset: 0, zIndex: 2000 }}>
|
<Box sx={{ position: "fixed", inset: 0, zIndex: 2000, }}>
|
||||||
{/* Blur overlay with visible background */}
|
{/* Blur overlay with visible background */}
|
||||||
<Box
|
<Box
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
@@ -354,6 +361,8 @@ const DashboardOnboarding = ({
|
|||||||
border: "1px solid rgba(255,255,255,0.08)",
|
border: "1px solid rgba(255,255,255,0.08)",
|
||||||
p: 3,
|
p: 3,
|
||||||
boxShadow: "0 10px 40px rgba(0,0,0,0.5)",
|
boxShadow: "0 10px 40px rgba(0,0,0,0.5)",
|
||||||
|
|
||||||
|
position: "relative",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
@@ -382,8 +391,55 @@ const DashboardOnboarding = ({
|
|||||||
</Typography>
|
</Typography>
|
||||||
</Box>
|
</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>
|
</Box>
|
||||||
|
|
||||||
|
|
||||||
{/* Steps list with a single continuous rail */}
|
{/* Steps list with a single continuous rail */}
|
||||||
<Box sx={{ position: "relative", display: "flex", flexDirection: "column", gap: 3, marginLeft: -1.5, marginTop: 4 }}>
|
<Box sx={{ position: "relative", display: "flex", flexDirection: "column", gap: 3, marginLeft: -1.5, marginTop: 4 }}>
|
||||||
{/* Base grey rail */}
|
{/* Base grey rail */}
|
||||||
@@ -427,12 +483,25 @@ const DashboardOnboarding = ({
|
|||||||
<Box sx={{ mt: 4, display: 'flex', justifyContent: 'center', gap: 1.5 }}>
|
<Box sx={{ mt: 4, display: 'flex', justifyContent: 'center', gap: 1.5 }}>
|
||||||
{footer}
|
{footer}
|
||||||
<Button variant="contained" color="primary" onClick={handleFinalDone}
|
<Button variant="contained" color="primary" onClick={handleFinalDone}
|
||||||
sx={{
|
sx={{
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
padding: "8px 60px",
|
padding: "8px 60px",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Explore Now
|
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>
|
</Button>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -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", }}>
|
<Box sx={{ display: "flex", flexDirection: "row", marginTop: 2.5, width: expandLeftNav ? "100%" : 48, padding: "0px", }}>
|
||||||
<Button
|
<Button
|
||||||
component={Link}
|
component={Link}
|
||||||
to={userdata?.support ? "/new-dashboard" : "/usecases"}
|
to={"/new-dashboard"}
|
||||||
onClick={(event) => {
|
onClick={(event) => {
|
||||||
setOpenautomateTab(true);
|
setOpenautomateTab(true);
|
||||||
setOpenSecurityTab(false);
|
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 ? (
|
{!isCloud ? (
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
@@ -1942,7 +1961,7 @@ const LeftSideBar = ({ userdata, serverside, globalUrl, notifications, }) => {
|
|||||||
cursor: "pointer",
|
cursor: "pointer",
|
||||||
}}
|
}}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
navigate("/admin?admin_tab=prodstatus")
|
navigate("/admin?admin_tab=billingstats")
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
@@ -1962,24 +1981,11 @@ const LeftSideBar = ({ userdata, serverside, globalUrl, notifications, }) => {
|
|||||||
color: isProdStatusOn ? "#2BC07E" : "#FD4C62",
|
color: isProdStatusOn ? "#2BC07E" : "#FD4C62",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{expandLeftNav ? isProdStatusOn ? "Prod. Status ON" : "Prod. Status OFF" : isProdStatusOn ? "ON" : "OFF"}
|
{expandLeftNav ? isProdStatusOn ? "Production" : "NOT production" : isProdStatusOn ? "ON" : "OFF"}
|
||||||
</Typography>
|
</Typography>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : 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}>
|
<Box ref={autocompleteRef}>
|
||||||
<Autocomplete
|
<Autocomplete
|
||||||
disablePortal
|
disablePortal
|
||||||
|
|||||||
@@ -1507,14 +1507,14 @@ const LicencePopup = (props) => {
|
|||||||
{!isPaidPlan && (
|
{!isPaidPlan && (
|
||||||
<Button
|
<Button
|
||||||
fullWidth
|
fullWidth
|
||||||
variant="outlined"
|
variant="contained"
|
||||||
color="primary"
|
color="primary"
|
||||||
style={{ textTransform: "none" }}
|
style={{ textTransform: "none" }}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if(isCloud) {
|
if(isCloud) {
|
||||||
navigate("/pricing");
|
navigate("/pricing?ref=cloud_billing");
|
||||||
}else {
|
}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"
|
variant="outlined"
|
||||||
color="primary"
|
color="primary"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if(isCloud) {
|
if (isCloud) {
|
||||||
navigate("/contact?category=contact")
|
navigate("/contact?category=contact&ref=cloud_billing")
|
||||||
}else {
|
} else {
|
||||||
window.open("https://shuffler.io/contact?category=contact", "_blank")
|
window.open("https://shuffler.io/contact?category=contact&ref=onprem_billing", "_blank")
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
style={{ textTransform: "none" }}
|
style={{ textTransform: "none" }}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import React, { useEffect, useState, useCallback, useContext } from 'react';
|
import React, { useEffect, useState, useCallback, useContext } from 'react';
|
||||||
import { Link, useNavigate, useLocation } from "react-router-dom";
|
import { Link, useNavigate, useLocation } from "react-router-dom";
|
||||||
|
|
||||||
import Billing from "../components/Billing.jsx";
|
import Billing from "../components/Billing.jsx";
|
||||||
import Priorities from "../components/Priorities.jsx";
|
import Priorities from "../components/Priorities.jsx";
|
||||||
import Branding from "../components/Branding.jsx";
|
import Branding from "../components/Branding.jsx";
|
||||||
@@ -7,10 +8,20 @@ import EditOrgTab from '../components/EditOrgTab.jsx';
|
|||||||
import CloudSyncTab from '../components/CloudSyncTab.jsx';
|
import CloudSyncTab from '../components/CloudSyncTab.jsx';
|
||||||
import SSOTab from "../components/ssoTab.jsx"
|
import SSOTab from "../components/ssoTab.jsx"
|
||||||
import { ToastContainer, toast } from "react-toastify";
|
import { ToastContainer, toast } from "react-toastify";
|
||||||
import { Button, Tooltip, Typography } from '@mui/material';
|
import {
|
||||||
import { CheckCircle as CheckCircleIcon, Cancel as CancelIcon } from '@mui/icons-material';
|
Button,
|
||||||
|
Tooltip,
|
||||||
|
Typography,
|
||||||
|
Divider,
|
||||||
|
} from '@mui/material';
|
||||||
|
|
||||||
|
import {
|
||||||
|
CheckCircle as CheckCircleIcon,
|
||||||
|
Cancel as CancelIcon,
|
||||||
|
} from '@mui/icons-material';
|
||||||
import { getTheme } from '../theme.jsx';
|
import { getTheme } from '../theme.jsx';
|
||||||
import { Context } from '../context/ContextApi.jsx';
|
import { Context } from '../context/ContextApi.jsx';
|
||||||
|
|
||||||
const OrganizationTab = (props) => {
|
const OrganizationTab = (props) => {
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@@ -37,7 +48,7 @@ const OrganizationTab = (props) => {
|
|||||||
const [billingInfo, setBillingInfo] = useState({});
|
const [billingInfo, setBillingInfo] = useState({});
|
||||||
const [orgRequest, setOrgRequest] = React.useState(true);
|
const [orgRequest, setOrgRequest] = React.useState(true);
|
||||||
const [curIndex, setCurIndex] = React.useState(0);
|
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 [visibleTabs, setVisibleTabs] = useState(items);
|
||||||
const [unreadNotifications, setUnreadNotifications] = React.useState(
|
const [unreadNotifications, setUnreadNotifications] = React.useState(
|
||||||
notifications?.filter((notification) => notification.read === false)?.length
|
notifications?.filter((notification) => notification.read === false)?.length
|
||||||
@@ -127,12 +138,10 @@ const OrganizationTab = (props) => {
|
|||||||
switch (selectedTab) {
|
switch (selectedTab) {
|
||||||
case 'org_config':
|
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} />;
|
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':
|
case 'sso':
|
||||||
return <SSOTab isEditOrgTab={true} globalUrl={globalUrl} isCloud={isCloud} userdata={userdata} handleEditOrg={handleEditOrg} selectedOrganization={selectedOrganization}/>
|
return <SSOTab isEditOrgTab={true} globalUrl={globalUrl} isCloud={isCloud} userdata={userdata} handleEditOrg={handleEditOrg} selectedOrganization={selectedOrganization}/>
|
||||||
case `notifications`:
|
case `notifications`:
|
||||||
|
case `errors`:
|
||||||
case `priorities`:
|
case `priorities`:
|
||||||
return (
|
return (
|
||||||
<Priorities
|
<Priorities
|
||||||
@@ -267,46 +276,3 @@ const OrganizationTab = (props) => {
|
|||||||
|
|
||||||
export default OrganizationTab;
|
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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -1518,6 +1518,7 @@ print('"' + encoded + '"')
|
|||||||
/>
|
/>
|
||||||
:
|
:
|
||||||
<TextField
|
<TextField
|
||||||
|
disabled={workflows === undefined || workflows === null || workflows.length === 0}
|
||||||
required
|
required
|
||||||
InputProps={{
|
InputProps={{
|
||||||
style: {
|
style: {
|
||||||
|
|||||||
@@ -307,6 +307,7 @@ const SuccessFailedRunsWidget = (props) => {
|
|||||||
setSeriesFail(failSeries);
|
setSeriesFail(failSeries);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const successKey =
|
const successKey =
|
||||||
mode === "workflows"
|
mode === "workflows"
|
||||||
? "workflow_executions_finished"
|
? "workflow_executions_finished"
|
||||||
@@ -383,7 +384,7 @@ const SuccessFailedRunsWidget = (props) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
fetchSeries();
|
fetchSeries();
|
||||||
}, [days, globalUrl, mode]);
|
}, [dummyMode, days, globalUrl, mode]);
|
||||||
|
|
||||||
// Apply external days override (e.g. after onboarding completes)
|
// Apply external days override (e.g. after onboarding completes)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -885,7 +886,7 @@ const SuccessFailedRunsWidget = (props) => {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Typography sx={{ fontSize: 18, fontWeight: 500, fontFamily: theme.typography.fontFamily}}>
|
<Typography sx={{ fontSize: 18, fontWeight: 500, fontFamily: theme.typography.fontFamily}}>
|
||||||
{mode === "workflows" ? "Workflows" : "Apps"} Success Rates
|
{mode === "workflows" ? "Workflow" : "App"} Success Rates
|
||||||
</Typography>
|
</Typography>
|
||||||
<div style={{ display: "flex", flexDirection: "column", alignItems: "flex-start", justifyContent: "center", height: "100%", gap: 15, marginTop: -5, marginLeft: -2 }}>
|
<div style={{ display: "flex", flexDirection: "column", alignItems: "flex-start", justifyContent: "center", height: "100%", gap: 15, marginTop: -5, marginLeft: -2 }}>
|
||||||
{(() => {
|
{(() => {
|
||||||
|
|||||||
@@ -476,7 +476,7 @@ const LoginPage = props => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
window.location.pathname = "/workflows"
|
window.location.pathname = "/new-dashboard"
|
||||||
}, 2000);
|
}, 2000);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -658,7 +658,7 @@ const LoginPage = props => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
window.location.pathname = "/workflows"
|
window.location.pathname = "/new-dashboard"
|
||||||
}, 2000);
|
}, 2000);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -10,22 +10,31 @@ import {
|
|||||||
Divider,
|
Divider,
|
||||||
Select,
|
Select,
|
||||||
MenuItem,
|
MenuItem,
|
||||||
|
Tooltip,
|
||||||
|
CircularProgress,
|
||||||
} from '@mui/material';
|
} from '@mui/material';
|
||||||
import TrendingUpIcon from '@mui/icons-material/TrendingUp';
|
import { toast } from "react-toastify";
|
||||||
import TrendingDownIcon from '@mui/icons-material/TrendingDown';
|
|
||||||
import ErrorOutlineIcon from '@mui/icons-material/ErrorOutline';
|
|
||||||
import TaskAltIcon from '@mui/icons-material/TaskAlt';
|
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 SuccessFailedRunsWidget from '../components/SuccessFailedRunsWidget.jsx';
|
||||||
import RunsOverTimeWidget from '../components/RunsOverTimeWidget.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 DashboardOnboarding from '../components/DashboardOnboarding.jsx';
|
||||||
|
import { Context } from '../context/ContextApi.jsx';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
const NewDashboard = (props) => {
|
const NewDashboard = (props) => {
|
||||||
const { globalUrl, userdata } = props;
|
const { globalUrl, serverside, userdata } = props;
|
||||||
|
|
||||||
// const [workflows, setWorkflows] = useState([]);
|
|
||||||
const { leftSideBarOpenByClick } = useContext(Context);
|
const { leftSideBarOpenByClick } = useContext(Context);
|
||||||
const [sfwControls, setSfwControls] = useState(null);
|
const [sfwControls, setSfwControls] = useState(null);
|
||||||
const [loadingSfw, setLoadingSfw] = useState(true);
|
const [loadingSfw, setLoadingSfw] = useState(true);
|
||||||
@@ -40,9 +49,18 @@ const NewDashboard = (props) => {
|
|||||||
} catch {
|
} catch {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
})
|
||||||
const [overrideDays, setOverrideDays] = useState(undefined);
|
const [overrideDays, setOverrideDays] = useState(undefined);
|
||||||
const [rotMonthOverride, setRotMonthOverride] = 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 navigate = useNavigate();
|
||||||
const handleSfwControls = useCallback((node) => {
|
const handleSfwControls = useCallback((node) => {
|
||||||
@@ -71,8 +89,6 @@ const NewDashboard = (props) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const timeFmt = formatTimeDisplay(totals.timeSavedMinutes);
|
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 unreadCount = notifications.filter(n => n && n.read === false).length;
|
||||||
const readCount = notifications.filter(n => n && n.read === true).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 = 15 minutes
|
||||||
// 1 Workflow run = $25
|
// 1 Workflow run = $25
|
||||||
|
|
||||||
|
const STATIC_TIME_PERCENT = 'TBD'
|
||||||
|
const STATIC_MONEY_PERCENT = 'TBD'
|
||||||
const kpis = [
|
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: 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' },
|
{ 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(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' },
|
{ value: String(readCount), label: 'Errors resolved', icon: <TaskAltIcon sx={{ color: '#5cc879', fontSize: 34, opacity: 0.9 }} />, percentage: "", color: '#5cc879' },
|
||||||
];
|
];
|
||||||
@@ -178,12 +196,50 @@ const NewDashboard = (props) => {
|
|||||||
// loadWorkflows();
|
// loadWorkflows();
|
||||||
// }, [globalUrl]);
|
// }, [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 (
|
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' }}>
|
<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
|
<DashboardOnboarding
|
||||||
open={onboardingOpen}
|
open={onboardingOpen}
|
||||||
globalUrl={globalUrl}
|
globalUrl={globalUrl}
|
||||||
onClose={() => setOnboardingOpen(false)}
|
onClose={() => {
|
||||||
|
setOnboardingOpen(false)
|
||||||
|
}}
|
||||||
|
setOnboardingOpen={setOnboardingOpen}
|
||||||
|
isProdStatusOn={isProdStatusOn}
|
||||||
|
isCloud={isCloud}
|
||||||
onExplore={() => {
|
onExplore={() => {
|
||||||
// Ensure overrides are set before closing modal
|
// Ensure overrides are set before closing modal
|
||||||
setOverrideDays(5);
|
setOverrideDays(5);
|
||||||
@@ -215,28 +271,31 @@ const NewDashboard = (props) => {
|
|||||||
<Grid container spacing={2}>
|
<Grid container spacing={2}>
|
||||||
{kpis.map((kpi) => (
|
{kpis.map((kpi) => (
|
||||||
<Grid item xs={12} sm={6} md={3} key={kpi.label}>
|
<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
|
<Tooltip title={kpi.disabled ? "This metric is coming soon!" : ""} arrow>
|
||||||
,cursor: kpi.label.toLowerCase().includes('total errors') ? 'pointer' : 'default'
|
<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={() => {
|
transparency: kpi.disabled ? 0.5 : 1
|
||||||
if (kpi.label.toLowerCase().includes('total errors')) {
|
}}
|
||||||
// navigate to notifications page
|
onClick={() => {
|
||||||
navigate('/admin?admin_tab=notifications');
|
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 direction="row" alignItems="center" justifyContent="space-between">
|
||||||
<Stack sx={{py:2, paddingLeft: 1}}>
|
<Stack sx={{py:2, paddingLeft: 1}}>
|
||||||
<Typography variant="h4" title={kpi.title || ''}>{kpi.value}</Typography>
|
<Typography variant="h4" title={kpi.title || ''}>{kpi.value}</Typography>
|
||||||
<Typography sx={{fontSize: 13}} color="textSecondary">{kpi.label}</Typography>
|
<Typography sx={{fontSize: 13}} color="textSecondary">{kpi.label}</Typography>
|
||||||
</Stack>
|
</Stack>
|
||||||
<Stack sx={{py: 2, paddingRight: 1, marginTop: kpi.label.toLowerCase().includes('errors') ? -1 : 0}}>
|
<Stack sx={{py: 2, paddingRight: 1, marginTop: kpi.label.toLowerCase().includes('errors') ? -1 : 0}}>
|
||||||
{kpi.icon}
|
{kpi.icon}
|
||||||
<Typography variant="body2" color={kpi.color}>{kpi.percentage}</Typography>
|
<Typography variant="body2" color={kpi.color}>{kpi.percentage}</Typography>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Paper>
|
</Paper>
|
||||||
|
</Tooltip>
|
||||||
</Grid>
|
</Grid>
|
||||||
))}
|
))}
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|||||||
@@ -818,6 +818,7 @@ const Workflows2 = (props) => {
|
|||||||
const [highlightIds, setHighlightIds] = React.useState([])
|
const [highlightIds, setHighlightIds] = React.useState([])
|
||||||
|
|
||||||
const [apps, setApps] = React.useState([]);
|
const [apps, setApps] = React.useState([]);
|
||||||
|
const [isProdStatusOn, setIsProdStatusOn] = React.useState(false);
|
||||||
|
|
||||||
const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
|
const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
|
||||||
|
|
||||||
@@ -839,6 +840,39 @@ const Workflows2 = (props) => {
|
|||||||
}
|
}
|
||||||
}, [location.search]);
|
}, [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) => {
|
const handleTabChange = (event, newValue) => {
|
||||||
setCurrTab(newValue);
|
setCurrTab(newValue);
|
||||||
|
|
||||||
@@ -3043,11 +3077,33 @@ 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)
|
const foundTimeline = workflowTimelines.find((timeline) => timeline.id === data.id)
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
id={`workflowbox-${data.id}`}
|
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}>
|
<Paper square style={paperAppStyle}>
|
||||||
{selectedCategory !== "" ?
|
{selectedCategory !== "" ?
|
||||||
@@ -4934,7 +4990,7 @@ const Workflows2 = (props) => {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Tab
|
<Tab
|
||||||
label="Discover Workflows"
|
label="Community Workflows"
|
||||||
style={{
|
style={{
|
||||||
...tabStyle,
|
...tabStyle,
|
||||||
marginRight: 0,
|
marginRight: 0,
|
||||||
@@ -5313,12 +5369,58 @@ const Workflows2 = (props) => {
|
|||||||
</div>
|
</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={{
|
<div style={{
|
||||||
width: "100%",
|
width: "100%",
|
||||||
position: "relative",
|
position: "relative",
|
||||||
zIndex: 1
|
zIndex: 1
|
||||||
}}>
|
}}>
|
||||||
|
|
||||||
|
|
||||||
{
|
{
|
||||||
(isLoadingWorkflow && currTab !== 2) ? (
|
(isLoadingWorkflow && currTab !== 2) ? (
|
||||||
<LoadingWorkflowGrid />
|
<LoadingWorkflowGrid />
|
||||||
|
|||||||
Reference in New Issue
Block a user