Merge for 2.2.0 release
This commit is contained in:
@@ -211,15 +211,7 @@ const AdminNavBar = (props) => {
|
||||
} else if (userdata && isOrgLoaded && isUserDataLoaded && userdata?.active_org?.role !== "admin" && !userdata?.support) {
|
||||
const queryParams = new URLSearchParams(location.search);
|
||||
const tabName = queryParams?.get('admin_tab')?.toLowerCase();
|
||||
if (tabName === "sso") {
|
||||
toast.info("You are not allowed to access this tab. Please contact your admin for more information. Redirecting to Organization Configuration tab.");
|
||||
setTimeout(() => {
|
||||
setSelectedItem("Organization");
|
||||
navigate(`?admin_tab=org_config`, { replace: true });
|
||||
window.location.reload();
|
||||
}
|
||||
, 3000);
|
||||
}
|
||||
// SSO tab is now accessible to non-admins (removed restriction)
|
||||
|
||||
const params = new URLSearchParams(location.search);
|
||||
const tab = params?.get('tab')?.toLowerCase();
|
||||
|
||||
@@ -1050,7 +1050,7 @@ const AppAuthTab = memo((props) => {
|
||||
|
||||
<ListItem style={{ width: "100%", paddingTop: 10, paddingBottom: 10, paddingRight: 10, borderBottom: theme.palette.defaultBorder, display: 'table-row'}}>
|
||||
|
||||
{["Valid", "Label", "App Name", "Workflows", "Fields", "Edited", "Actions", "Distribution"].map((header, index) => (
|
||||
{["Logo", "Valid", "Label", "App Name", "Fields", "Edited", "Actions", "Distribute"].map((header, index) => (
|
||||
<ListItemText
|
||||
key={index}
|
||||
primary={header}
|
||||
@@ -1145,9 +1145,33 @@ const AppAuthTab = memo((props) => {
|
||||
}
|
||||
return (
|
||||
<ListItem key={index} style={{ backgroundColor: bgColor, borderBottomLeftRadius: authentication?.length - 1 === index ? 8 : 0, borderBottomRightRadius: authentication?.length - 1 === index ? 8 : 0, display: 'table-row' }}>
|
||||
|
||||
<ListItemText
|
||||
primary= {(
|
||||
<Tooltip title={data.validation !== null && data.validation !== undefined && data.validation.valid === true ? "Valid. Click to explore." : "Configuration failed. Click to learn why"} placement="top">
|
||||
<Tooltip title={`Auth for ${data.app.name}. Click to explore.`} placement="top">
|
||||
<IconButton>
|
||||
<img
|
||||
style={{height: 30, width: 30, borderRadius: theme.palette?.borderRadius }}
|
||||
src={data.app.large_image ? data.app.large_image : '/images/no_image.png'}
|
||||
/>
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
style={{ display: "table-cell", verticalAlign: 'middle', minWidth: 60 }}
|
||||
primaryTypographyProps={{
|
||||
style: {
|
||||
padding: "8px 8px 8px 15px",
|
||||
}
|
||||
}}
|
||||
onClick={() => {
|
||||
const url = `/apps/${data.app.id}`
|
||||
window.open(url, "_blank")
|
||||
}}
|
||||
/>
|
||||
|
||||
<ListItemText
|
||||
primary= {(
|
||||
<Tooltip title={data.validation !== null && data.validation !== undefined && data.validation.valid === true ? `Last Valid ${new Date(data?.validation?.last_valid).toISOString()}. Click to explore.` : "Configuration failed. Click to learn why"} placement="top">
|
||||
<IconButton>
|
||||
{validIcon}
|
||||
</IconButton>
|
||||
@@ -1202,22 +1226,7 @@ const AppAuthTab = memo((props) => {
|
||||
}}
|
||||
style={{ marginLeft: 10, display: "table-cell", textAlign: 'center', verticalAlign: 'middle', padding: 8 }}
|
||||
/>
|
||||
<ListItemText
|
||||
primary={
|
||||
data.workflow_count === null ? 0 : data.workflow_count
|
||||
}
|
||||
primaryTypographyProps={{
|
||||
style: {
|
||||
padding: 8
|
||||
}
|
||||
}}
|
||||
style={{
|
||||
display: "table-cell",
|
||||
textAlign: "center",
|
||||
overflow: "hidden",
|
||||
verticalAlign: 'middle',
|
||||
}}
|
||||
/>
|
||||
|
||||
<ListItemText
|
||||
primary={
|
||||
data.fields === null || data.fields === undefined
|
||||
@@ -1239,6 +1248,7 @@ const AppAuthTab = memo((props) => {
|
||||
verticalAlign: 'middle'
|
||||
}}
|
||||
/>
|
||||
|
||||
<ListItemText
|
||||
style={{
|
||||
overflow: "hidden",
|
||||
|
||||
@@ -22,10 +22,10 @@ import {
|
||||
import aa from 'search-insights'
|
||||
const searchClient = algoliasearch("JNSS5CFDZZ", "c8f882473ff42d41158430be09ec2b4e")
|
||||
const Appsearch = props => {
|
||||
const { maxRows, showName, showSuggestion, isMobile, globalUrl, parsedXs, newSelectedApp, setNewSelectedApp, defaultSearch, showSearch, ConfiguredHits, userdata, cy, isCreatorPage, actionImageList, setActionImageList, setUserSpecialzedApp, inputHeight, } = props
|
||||
const { maxRows, showName, showSuggestion, isMobile, globalUrl, parsedXs, newSelectedApp, setNewSelectedApp, defaultSearch, showSearch, ConfiguredHits, userdata, cy, isCreatorPage, actionImageList, setActionImageList, setUserSpecialzedApp, inputHeight, apps, } = props
|
||||
const { themeMode } = useContext(Context)
|
||||
const theme = getTheme(themeMode)
|
||||
const isCloud = (window.location.host === "localhost:3002" || window.location.host === "shuffler.io") ? true : (import.meta.env.VITE_IS_SSR === "true");
|
||||
const isCloud = window.location.host === "localhost:3002" || window.location.host === "shuffler.io";
|
||||
const rowHandler = maxRows === undefined || maxRows === null ? 50 : maxRows
|
||||
const xs = parsedXs === undefined || parsedXs === null ? 12 : parsedXs
|
||||
const [formMail, setFormMail] = React.useState("");
|
||||
@@ -75,7 +75,7 @@ const Appsearch = props => {
|
||||
color="primary"
|
||||
defaultValue={defaultSearch}
|
||||
// placeholder={`Find ${defaultSearch} Apps...`}
|
||||
placeholder= {defaultSearch ? `${defaultSearch}` : "Search Cases "}
|
||||
placeholder= {defaultSearch ? `${defaultSearch}` : "Search Apps "}
|
||||
id="shuffle_workflow_search_field"
|
||||
onChange={(event) => {
|
||||
refine(event.currentTarget.value)
|
||||
@@ -92,6 +92,30 @@ const Appsearch = props => {
|
||||
const [mouseHoverIndex, setMouseHoverIndex] = useState(-1)
|
||||
var counted = 0
|
||||
|
||||
if (hits?.length < 20 && apps !== undefined && apps !== null && apps?.length > 0) {
|
||||
// Find the textbox html
|
||||
const findId = "shuffle_workflow_search_field"
|
||||
const searchField = document.getElementById(findId)
|
||||
const searchValue = searchField === null || searchField === undefined ? "" : searchField?.value
|
||||
if (searchValue?.length > 0) {
|
||||
const parsedsearch = searchValue.toLowerCase().replaceAll(" ", "")
|
||||
for (var appkey in apps) {
|
||||
const app = apps[appkey]
|
||||
const parsedAppname = app?.name?.toLowerCase().replaceAll(" ", "")
|
||||
if (parsedAppname.includes(parsedsearch)) {
|
||||
hits.push({
|
||||
objectID: app.id,
|
||||
id: app.id,
|
||||
name: app.name,
|
||||
image_url: app.large_image,
|
||||
})
|
||||
}
|
||||
}
|
||||
} else {
|
||||
console.log("No value in textfield: ", searchValue)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Grid container spacing={0} style={{border: "1px solid rgba(255,255,255,0.2)", maxHeight: parsedInputHeight, minHeight: parsedInputHeight, overflowY: "auto", overflowX: "hidden", }}>
|
||||
{hits.map((data, index) => {
|
||||
|
||||
@@ -97,6 +97,12 @@ const AuthenticationModal = (props) => {
|
||||
}
|
||||
|
||||
if (authenticationModalOpen === false) {
|
||||
// Add 5px for every character over 12
|
||||
var addednumber = selectedAppData?.name?.length > 12 ? (selectedAppData?.name?.length - 12) * 5 : 0
|
||||
if (addednumber > 50) {
|
||||
addednumber = 50
|
||||
}
|
||||
|
||||
return (
|
||||
<Button
|
||||
fullWidth
|
||||
@@ -111,8 +117,8 @@ const AuthenticationModal = (props) => {
|
||||
backgroundColor: "#ffffff",
|
||||
color: "#2f2f2f",
|
||||
borderRadius: theme.palette?.borderRadius,
|
||||
minWidth: 275,
|
||||
maxWidth: 275,
|
||||
minWidth: 275+addednumber,
|
||||
maxWidth: 275+addednumber,
|
||||
maxHeight: 50,
|
||||
overflow: "hidden",
|
||||
border: `1px solid ${theme.palette.inputColor}`,
|
||||
@@ -128,10 +134,10 @@ const AuthenticationModal = (props) => {
|
||||
<span style={{display: "flex"}}>
|
||||
<img
|
||||
alt={selectedAppData?.name}
|
||||
style={{ margin: 4, minHeight: 30, maxHeight: 30, borderRadius: theme.palette?.borderRadius, }}
|
||||
style={{ margin: "4px 4px 4px 0px", minHeight: 30, maxHeight: 30, borderRadius: theme.palette?.borderRadius, }}
|
||||
src={selectedAppData?.large_image}
|
||||
/>
|
||||
<Typography style={{ margin: 0, marginLeft: 10, marginTop: 8, color: "#2f2f2f",}} variant="body1">
|
||||
<Typography style={{ margin: 0, marginLeft: 5, marginTop: 8, color: "#2f2f2f",}} variant="body1">
|
||||
Authenticate {selectedAppData?.name?.replaceAll("_", " ", -1)}
|
||||
</Typography>
|
||||
</span>
|
||||
|
||||
@@ -50,6 +50,15 @@ import {
|
||||
Search as SearchIcon,
|
||||
CheckCircle as CheckCircleIcon,
|
||||
Cancel as CancelIcon,
|
||||
Shield as ShieldIcon,
|
||||
Cancel as XCircleIcon,
|
||||
FlashOn as ZapIcon,
|
||||
People as UsersIcon,
|
||||
FmdGoodOutlined as FmdGoodOutlinedIcon,
|
||||
Palette as PaletteIcon,
|
||||
Info as InfoIcon,
|
||||
Email as MailIcon,
|
||||
ArrowForward as ArrowRightIcon,
|
||||
} from "@mui/icons-material";
|
||||
|
||||
//import { useAlert
|
||||
@@ -70,51 +79,301 @@ const ProductionStatus = ({ selectedOrganization, userdata, isCloud, theme }) =>
|
||||
} else {
|
||||
isProdStatusOn = false;
|
||||
}
|
||||
const rows = [
|
||||
{ label: 'Licensed', ok: isProdStatusOn },
|
||||
{ label: 'Multi-Tenant', ok: isProdStatusOn },
|
||||
{ label: 'High Availability', ok: isProdStatusOn },
|
||||
{ label: 'Robust Infrastructure', ok: isProdStatusOn },
|
||||
|
||||
const themeMode = theme.palette.mode;
|
||||
const workflowActive = selectedOrganization?.sync_features?.workflow_executions?.active;
|
||||
const multiTenantActive = selectedOrganization?.sync_features?.multi_tenant?.active;
|
||||
const multiEnvActive = selectedOrganization?.sync_features?.multi_env?.active;
|
||||
const brandingActive = selectedOrganization?.sync_features?.branding?.active;
|
||||
const colors = {
|
||||
textPrimary: theme.palette.text.primary,
|
||||
textSecondary: theme.palette.text.secondary,
|
||||
textMuted: themeMode === "dark" ? '#6e7681' : '#9ca3af',
|
||||
border: themeMode === "dark" ? '#30363d' : '#e1e4e8',
|
||||
divider: themeMode === "dark" ? '#21262d' : '#e5e7eb',
|
||||
success: themeMode === "dark" ? '#10b981' : '#059669',
|
||||
successBg: themeMode === "dark" ? 'rgba(16, 185, 129, 0.12)' : 'rgba(5, 150, 105, 0.08)',
|
||||
warning: '#f59e0b',
|
||||
warningBg: themeMode === "dark" ? 'rgba(245, 158, 11, 0.12)' : 'rgba(245, 158, 11, 0.08)',
|
||||
disabled: themeMode === "dark" ? '#6e7681' : '#d1d5db',
|
||||
disabledBg: themeMode === "dark" ? 'rgba(110, 118, 129, 0.1)' : 'rgba(156, 163, 175, 0.08)',
|
||||
accent: '#f85a3e',
|
||||
cardBg: theme.palette.surfaceColor,
|
||||
};
|
||||
|
||||
const features = [
|
||||
{
|
||||
icon: ZapIcon,
|
||||
label: 'Workflow Executions',
|
||||
licensed: `${selectedOrganization?.sync_features?.workflow_executions?.limit}/month limit`,
|
||||
unlicensed: '10,000/month limit',
|
||||
isActive: workflowActive,
|
||||
},
|
||||
{
|
||||
icon: UsersIcon,
|
||||
label: 'Multi-Tenant',
|
||||
licensed: `${selectedOrganization?.sync_features?.multi_tenant?.limit} tenants`,
|
||||
unlicensed: '3 tenants maximum',
|
||||
isActive: multiTenantActive,
|
||||
},
|
||||
{
|
||||
icon: FmdGoodOutlinedIcon,
|
||||
label: 'Runtime Locations',
|
||||
licensed: `${selectedOrganization?.sync_features?.multi_env?.limit} Runtime Locations`,
|
||||
unlicensed: '1 Runtime Location only',
|
||||
isActive: multiEnvActive,
|
||||
},
|
||||
{
|
||||
icon: PaletteIcon,
|
||||
label: 'Custom Branding',
|
||||
licensed: `${brandingActive ? "Full branding control" : "Branding not available"}`,
|
||||
unlicensed: 'Branding not available',
|
||||
isActive: brandingActive,
|
||||
},
|
||||
{
|
||||
icon: ShieldIcon,
|
||||
label: 'High Availability',
|
||||
licensed: 'Enterprise SLA guarantee',
|
||||
unlicensed: 'Standard availability',
|
||||
isActive: isProdStatusOn, // High availability is tied to license status
|
||||
},
|
||||
];
|
||||
|
||||
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
|
||||
style={{
|
||||
width: '100%',
|
||||
maxWidth: 800,
|
||||
padding: "0px 24px 24px 0px",
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
fontFamily: theme.typography.fontFamily,
|
||||
}}
|
||||
>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 12, marginBottom: 8 }}>
|
||||
<Typography variant="h5" style={{ fontWeight: 600, margin: 0 }}>
|
||||
License Status
|
||||
</Typography>
|
||||
|
||||
<div
|
||||
style={{
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
gap: 8,
|
||||
padding: '8px 16px',
|
||||
borderRadius: 20,
|
||||
background: isProdStatusOn ? colors.successBg : colors.warningBg,
|
||||
border: `2px solid ${isProdStatusOn ? colors.success : colors.warning}`,
|
||||
boxShadow: isProdStatusOn
|
||||
? `0 2px 8px ${colors.success}30`
|
||||
: `0 2px 8px ${colors.warning}30`,
|
||||
height: 12,
|
||||
}}
|
||||
>
|
||||
<span
|
||||
style={{
|
||||
width: 8,
|
||||
height: 8,
|
||||
borderRadius: '50%',
|
||||
background: isProdStatusOn ? colors.success : colors.warning,
|
||||
boxShadow: `0 0 10px ${isProdStatusOn ? colors.success : colors.warning}`,
|
||||
}}
|
||||
/>
|
||||
<span
|
||||
style={{
|
||||
fontSize: 14,
|
||||
fontWeight: 700,
|
||||
color: isProdStatusOn ? colors.success : colors.warning,
|
||||
letterSpacing: '0.5px',
|
||||
}}
|
||||
>
|
||||
{isProdStatusOn ? 'Licensed' : 'Unlicensed'}
|
||||
</span>
|
||||
</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>
|
||||
{/* Subtitle */}
|
||||
<Typography
|
||||
variant="body2"
|
||||
color="textSecondary"
|
||||
style={{
|
||||
margin: '0 0 24px 0',
|
||||
lineHeight: 1.5,
|
||||
}}
|
||||
>
|
||||
{isProdStatusOn
|
||||
? 'Your organization has full access to all enterprise features and capabilities.'
|
||||
: 'View your current limits and available features. Upgrade to unlock enterprise capabilities.'}
|
||||
</Typography>
|
||||
|
||||
{/* Features Grid */}
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: 12,
|
||||
marginBottom: 32,
|
||||
}}
|
||||
>
|
||||
{features
|
||||
.sort((a, b) => {
|
||||
const aActive = isProdStatusOn && a.isActive;
|
||||
const bActive = isProdStatusOn && b.isActive;
|
||||
return bActive - aActive;
|
||||
})
|
||||
.map((feature, index) => {
|
||||
const Icon = feature.icon;
|
||||
const isAvailable = isProdStatusOn && feature.isActive;
|
||||
const statusColor = isAvailable ? colors.success : colors.disabled;
|
||||
const bgColor = isAvailable ? themeMode === "dark" ? "#212121" : "#ffffff" : colors.disabledBg;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={index}
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 14,
|
||||
padding: '14px 16px',
|
||||
borderRadius: 10,
|
||||
background: bgColor,
|
||||
border: `1px solid ${isAvailable ? colors.success + '40' : colors.border}`,
|
||||
transition: 'all 0.2s',
|
||||
}}
|
||||
>
|
||||
{/* Icon */}
|
||||
<div
|
||||
style={{
|
||||
width: 40,
|
||||
height: 40,
|
||||
borderRadius: 8,
|
||||
background: isAvailable ? colors.success + '20' : colors.disabledBg,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
flexShrink: 0,
|
||||
}}
|
||||
>
|
||||
<Icon style={{ color: statusColor, fontSize: 20 }} />
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div style={{ flex: 1, minWidth: 0 }}>
|
||||
<div
|
||||
style={{
|
||||
fontSize: 15,
|
||||
fontWeight: 600,
|
||||
color: colors.textPrimary,
|
||||
marginBottom: 4,
|
||||
}}
|
||||
>
|
||||
{feature.label}
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
fontSize: 13,
|
||||
color: colors.textSecondary,
|
||||
lineHeight: 1.4,
|
||||
}}
|
||||
>
|
||||
{isProdStatusOn ? feature.licensed : feature.unlicensed}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isAvailable ? (
|
||||
<CheckCircleIcon
|
||||
style={{ color: colors.success, fontSize: 20, flexShrink: 0 }}
|
||||
/>
|
||||
) : (
|
||||
<XCircleIcon
|
||||
style={{ color: colors.disabled, fontSize: 20, flexShrink: 0 }}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
<div
|
||||
style={{
|
||||
width: '100%',
|
||||
height: 1,
|
||||
background: colors.divider,
|
||||
margin: '8px 0 24px 0',
|
||||
}}
|
||||
/>
|
||||
|
||||
{!isProdStatusOn && (
|
||||
<div
|
||||
style={{
|
||||
padding: 20,
|
||||
borderRadius: 12,
|
||||
background: themeMode == "dark" ? "#212121" : "#ffffff",
|
||||
border: `1px solid ${colors.border}`,
|
||||
marginBottom: 20,
|
||||
}}
|
||||
>
|
||||
<div style={{ display: 'flex', gap: 12, marginBottom: 12 }}>
|
||||
<InfoIcon style={{ color: colors.accent, flexShrink: 0, marginTop: 2, fontSize: 20 }} />
|
||||
<div>
|
||||
<Typography variant="h6" style={{ fontWeight: 600, margin: '0 0 8px 0' }}>
|
||||
About Shuffle Enterprise
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="body2"
|
||||
color="textSecondary"
|
||||
style={{
|
||||
margin: 0,
|
||||
lineHeight: 1.6,
|
||||
}}
|
||||
>
|
||||
Shuffle Enterprise is designed for organizations that require scalability, high
|
||||
availability, dedicated support, and robust infrastructure to run mission-critical
|
||||
workflows in production environments. <a href="https://shuffler.io/articles/Shuffle_Open_Source" target="_blank" rel="noreferrer" style={{color: "#FF8544" }}>learn more</a>
|
||||
</Typography>
|
||||
</div>
|
||||
</div>
|
||||
</div>)}
|
||||
|
||||
{/* CTA Section */}
|
||||
{!isProdStatusOn && (
|
||||
<div
|
||||
style={{
|
||||
padding: 20,
|
||||
borderRadius: 12,
|
||||
background: `linear-gradient(135deg, #FF854415 0%, #FF854408 100%)`,
|
||||
border: `1px solid #FF854440`,
|
||||
marginBottom: 24,
|
||||
}}
|
||||
>
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<Typography variant="h6" style={{ fontWeight: 600, margin: '0 0 8px 0' }}>
|
||||
Ready to upgrade?
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="body2"
|
||||
color="textSecondary"
|
||||
style={{
|
||||
margin: 0,
|
||||
lineHeight: 1.6,
|
||||
}}
|
||||
>
|
||||
Contact our team to learn more about enterprise features and pricing.
|
||||
</Typography>
|
||||
</div>
|
||||
<Button
|
||||
href="https://shuffler.io/contact?category=talk_to_sales"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
variant="contained"
|
||||
color="primary"
|
||||
startIcon={<MailIcon sx={{ fontSize: 16}} />}
|
||||
endIcon={<ArrowRightIcon sx={{ fontSize: 16 }} />}
|
||||
>
|
||||
Contact Sales
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -2143,14 +2402,14 @@ const Billing = memo((props) => {
|
||||
|
||||
return (
|
||||
<Wrapper clickedFromOrgTab={clickedFromOrgTab}>
|
||||
<div style={{ width: "100%", padding: 24, }}>
|
||||
<div style={{ width: "100%", padding: 24, paddingTop: 0}}>
|
||||
<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: 24, }}>Billing & Licensing</Typography>
|
||||
<Typography variant="h5" style={{fontSize: 24, fontWeight: 500, marginBottom: 8, }}>Billing & Licensing</Typography>
|
||||
:
|
||||
<Typography variant="h4" style={{ marginTop: 20, marginBottom: 10 }}>
|
||||
Billing & Licensing
|
||||
|
||||
@@ -5,7 +5,7 @@ import { toast } from 'react-toastify';
|
||||
|
||||
import ReactJson from "react-json-view-ssr";
|
||||
import { validateJson, handleReactJsonClipboard, GetIconInfo, } from "../views/Workflows2.jsx";
|
||||
import { red } from "../views/AngularWorkflow.jsx";
|
||||
import { green, red } from "../views/AngularWorkflow.jsx";
|
||||
import CollectIngestModal from "../components/CollectIngestModal.jsx";
|
||||
import CorrelationGraph from "../components/CorrelationGraph.jsx";
|
||||
import { useNavigate, Link, useParams } from "react-router-dom";
|
||||
@@ -37,9 +37,12 @@ import {
|
||||
Pagination,
|
||||
PaginationItem,
|
||||
Avatar,
|
||||
ListSubheader,
|
||||
} from "@mui/material";
|
||||
|
||||
import { DataGrid } from '@mui/x-data-grid';
|
||||
import {
|
||||
DataGrid,
|
||||
} from '@mui/x-data-grid';
|
||||
|
||||
import {
|
||||
Link as LinkIcon,
|
||||
@@ -52,6 +55,7 @@ import {
|
||||
OpenInNew as OpenInNewIcon,
|
||||
CloudDownload as CloudDownloadIcon,
|
||||
Description as DescriptionIcon,
|
||||
Polymer as PolymerIcon,
|
||||
CheckCircle as CheckCircleIcon,
|
||||
Close as CloseIcon,
|
||||
Apps as AppsIcon,
|
||||
@@ -59,6 +63,7 @@ import {
|
||||
Cached as CachedIcon,
|
||||
AccessibilityNew as AccessibilityNewIcon,
|
||||
Lock as LockIcon,
|
||||
Eco as EcoIcon,
|
||||
Schedule as ScheduleIcon,
|
||||
Cloud as CloudIcon,
|
||||
Business as BusinessIcon,
|
||||
@@ -66,6 +71,7 @@ import {
|
||||
VisibilityOff as VisibilityOffIcon,
|
||||
Clear as ClearIcon,
|
||||
Add as AddIcon,
|
||||
Remove as RemoveIcon,
|
||||
Rocket as RocketIcon,
|
||||
Webhook as WebhookIcon,
|
||||
Air as AirIcon,
|
||||
@@ -76,6 +82,8 @@ import {
|
||||
FilterAlt as FilterAltIcon,
|
||||
CompareArrows as CompareArrowsIcon,
|
||||
Hub as HubIcon,
|
||||
Key as KeyIcon,
|
||||
FlashOn as FlashOnIcon,
|
||||
} from "@mui/icons-material";
|
||||
import { Context } from "../context/ContextApi.jsx";
|
||||
|
||||
@@ -133,6 +141,7 @@ const CacheView = memo((props) => {
|
||||
const [selectAllChecked, setSelectAllChecked] = React.useState(false)
|
||||
const [renderTextBox, setRenderTextBox] = React.useState(false);
|
||||
const [datastoreCategories, setDatastoreCategories] = React.useState(["default", "protected"]);
|
||||
const [datastoreCategoryGroups, setDatastoreCategoryGroups] = React.useState([]);
|
||||
const [selectedCategory, setSelectedCategory] = React.useState("default");
|
||||
const [selectedFileId, setSelectedFileId] = React.useState("");
|
||||
const [updateToThisCategory, setUpdateToThisCategory] = useState("")
|
||||
@@ -163,6 +172,18 @@ const CacheView = memo((props) => {
|
||||
|
||||
var to_be_copied = "";
|
||||
const defaultAutomation = [
|
||||
{
|
||||
"name": "Security Rules",
|
||||
"description": "Describes security rules that are validated BEFORE an update occurs. This is in order for bad writes to be avoided. Control: allow, deny, merge, overwrite. Logic: if, or, and. Functions: same_shape, is_superset, has_deleted_field",
|
||||
"options": [{
|
||||
"key": "rule",
|
||||
"value": "",
|
||||
}],
|
||||
"icon": <KeyIcon />,
|
||||
"beta": true,
|
||||
"disabled": false,
|
||||
"enabled": false,
|
||||
},
|
||||
{
|
||||
"name": "Run workflow",
|
||||
"description": "Runs one or more workflows with the updated value as runtime argument",
|
||||
@@ -200,14 +221,16 @@ const CacheView = memo((props) => {
|
||||
|
||||
{
|
||||
"name": "Run AI Agent",
|
||||
"description": "Runs an AI Agent to process the updated value. Uses built-in ShuffleAI configs. Learn more: https://shuffler.io/docs/AI",
|
||||
"description": "Runs an AI Agent to process the updated value. Controllable based on your 'action' input, where the full updated value is added on the end. All actions run in order, meaning it needs to finish the previous one before the next is ran. Uses built-in ShuffleAI configs. Learn more: https://shuffler.io/docs/AI",
|
||||
"type": "singul",
|
||||
"options": [{
|
||||
"key": "",
|
||||
"key": "action-1",
|
||||
"description": "Describe the action to perform on this item",
|
||||
"value": "",
|
||||
}],
|
||||
"beta": true,
|
||||
"icon": <SmartToyIcon />,
|
||||
"enabled": false,
|
||||
"disabled": true,
|
||||
"disabled": false,
|
||||
},
|
||||
|
||||
{
|
||||
@@ -218,22 +241,10 @@ const CacheView = memo((props) => {
|
||||
"key": "",
|
||||
"value": "",
|
||||
}],
|
||||
"beta": true,
|
||||
"icon": "/images/logos/singul.svg",
|
||||
"disabled": false,
|
||||
},
|
||||
|
||||
{
|
||||
"name": "Send message",
|
||||
"description": "",
|
||||
"type": "singul",
|
||||
"options": [{
|
||||
"key": "app",
|
||||
"value": "",
|
||||
}],
|
||||
"icon": <SendIcon />,
|
||||
"disabled": true,
|
||||
"enabled": false,
|
||||
},
|
||||
]
|
||||
|
||||
const [categoryAutomations, setCategoryAutomations] = useState(defaultAutomation)
|
||||
@@ -317,8 +328,21 @@ const CacheView = memo((props) => {
|
||||
|
||||
var chosenCategory = selectedCategory
|
||||
const urlParams = new URLSearchParams(window.location.search)
|
||||
const categoryParam = urlParams.get("category")
|
||||
var categoryParam = urlParams.get("category")
|
||||
if (categoryParam && categoryParam !== undefined && categoryParam !== "default" && categoryParam !== "") {
|
||||
|
||||
// In order to make linking weird urls from workflow page work.
|
||||
if (urlParams.get("src") == "workflow") {
|
||||
if (categoryParam?.toLowerCase().startsWith("list")) {
|
||||
const newParam = categoryParam.substring(5).replaceAll("%20", "_")
|
||||
|
||||
// Replace the url param. There may be more params.
|
||||
urlParams.set("category", newParam)
|
||||
window.history.replaceState({}, '', `${window.location.pathname}?${urlParams.toString()}`)
|
||||
categoryParam = newParam
|
||||
}
|
||||
}
|
||||
|
||||
chosenCategory = categoryParam
|
||||
setSelectedCategory(categoryParam)
|
||||
}
|
||||
@@ -447,7 +471,31 @@ const CacheView = memo((props) => {
|
||||
}
|
||||
}
|
||||
|
||||
var foundstartwords = {}
|
||||
var categorygroups = []
|
||||
for (var key in newcategories) {
|
||||
const category = newcategories[key]
|
||||
if (!category.includes("_")) {
|
||||
continue
|
||||
}
|
||||
|
||||
const startword = category.split("_")[0]
|
||||
if (startword.length <= 2) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (!foundstartwords.hasOwnProperty(startword)) {
|
||||
foundstartwords[startword] = 1
|
||||
} else {
|
||||
foundstartwords[startword] += 1
|
||||
if (foundstartwords[startword] === 2) {
|
||||
categorygroups.push(startword)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setDatastoreCategories(newcategories)
|
||||
setDatastoreCategoryGroups(categorygroups)
|
||||
}
|
||||
|
||||
if (responseJson?.category_config !== undefined && responseJson?.category_config !== null) {
|
||||
@@ -463,6 +511,8 @@ const CacheView = memo((props) => {
|
||||
//if (responseJson.category_config.automations[key].icon === undefined || responseJson.category_config.automations[key].icon === null || responseJson.category_config.automations[key].icon === "") {
|
||||
const foundItem = defaultAutomation.find((automation) => automation.name === responseJson.category_config.automations[key].name)
|
||||
if (foundItem) {
|
||||
responseJson.category_config.automations[key].description = foundItem.description
|
||||
responseJson.category_config.automations[key].beta = foundItem.beta
|
||||
responseJson.category_config.automations[key].disabled = foundItem.disabled
|
||||
responseJson.category_config.automations[key].icon = foundItem.icon
|
||||
responseJson.category_config.automations[key].type = foundItem?.type
|
||||
@@ -542,6 +592,7 @@ const CacheView = memo((props) => {
|
||||
key: dataValue.key,
|
||||
value: value,
|
||||
category: selectedCategory,
|
||||
tags: dataValue?.tags || [],
|
||||
}
|
||||
|
||||
if (dataValue?.category !== undefined && dataValue?.category !== "" && dataValue?.category !== "default") {
|
||||
@@ -558,7 +609,9 @@ const CacheView = memo((props) => {
|
||||
|
||||
setCacheInput([entry]);
|
||||
|
||||
fetch(globalUrl + `/api/v1/orgs/${orgId}/set_cache`, {
|
||||
//entry = [entry]
|
||||
const parsedUrl = `${globalUrl}/api/v2/datastore`
|
||||
fetch(parsedUrl, {
|
||||
|
||||
method: "POST",
|
||||
headers: {
|
||||
@@ -780,6 +833,37 @@ const CacheView = memo((props) => {
|
||||
Category: {dataValue.category}
|
||||
</Typography>
|
||||
: null}
|
||||
{dataValue?.tags !== undefined && dataValue?.tags !== null && dataValue?.tags?.length > 0 ?
|
||||
<div style={{display: "flex", marginTop: 12, }}>
|
||||
<Typography variant="body2" color="textSecondary" style={{ marginRight: 10, marginTop: 4, }}>
|
||||
Tags
|
||||
</Typography>
|
||||
|
||||
{dataValue.tags?.map((tag, index) => {
|
||||
return (
|
||||
<Chip
|
||||
key={index}
|
||||
label={tag}
|
||||
style={{ marginRight: 5, }}
|
||||
onDelete={() => {
|
||||
var newTags = dataValue.tags.filter((t) => t !== tag)
|
||||
if (newTags.length === 0) {
|
||||
newTags = ["none"]
|
||||
} else if (newTags.length > 1) {
|
||||
newTags = newTags.filter((t) => t !== "none")
|
||||
}
|
||||
|
||||
setDataValue({
|
||||
...dataValue,
|
||||
tags: newTags,
|
||||
})
|
||||
|
||||
}}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
: null}
|
||||
|
||||
</div>
|
||||
: null}
|
||||
@@ -1065,7 +1149,6 @@ const CacheView = memo((props) => {
|
||||
saveAutomation(newAutomations)
|
||||
}
|
||||
|
||||
console.log("AUTO: ", automation)
|
||||
return (
|
||||
<Tooltip title={
|
||||
<Typography style={{margin: 10, }}>
|
||||
@@ -1089,7 +1172,7 @@ const CacheView = memo((props) => {
|
||||
style={{ marginRight: 10, marginTop: -10, }}
|
||||
checked={automation.enabled}
|
||||
// Check if automation options have a value
|
||||
disabled={automation.name !== "Enrich" && (automation?.disabled === true || automation.options.length === 0 || automation.options.some((option) => option.value === ""))}
|
||||
disabled={automation.name !== "Enrich" && automation.name != "Run AI Agent" && (automation?.disabled === true || automation.options.length === 0 || automation.options.some((option) => option.value === ""))}
|
||||
onChange={(e) => {
|
||||
e.stopPropagation()
|
||||
e.preventDefault()
|
||||
@@ -1140,6 +1223,19 @@ const CacheView = memo((props) => {
|
||||
}}>
|
||||
{automation.name}
|
||||
</Typography>
|
||||
{automation?.beta !== true ? null :
|
||||
<Chip
|
||||
label="Beta"
|
||||
size="small"
|
||||
style={{
|
||||
marginLeft: 10,
|
||||
height: 20,
|
||||
fontSize: 12,
|
||||
border: `1px solid ${green}`,
|
||||
color: "rgba(255,255,255,0.7)",
|
||||
}}
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
|
||||
{automation?.disabled !== true ?
|
||||
@@ -1166,8 +1262,9 @@ const CacheView = memo((props) => {
|
||||
: null}
|
||||
</div>
|
||||
|
||||
{showOptions && (
|
||||
updatedAutomation.options.map((option, optionIndex) => {
|
||||
{showOptions &&
|
||||
<div style={{ marginTop: 20, marginBottom: 20, }}>
|
||||
{updatedAutomation.options.map((option, optionIndex) => {
|
||||
if (option?.key === "datastore_categories") {
|
||||
if (datastoreCategories === undefined || datastoreCategories === null || datastoreCategories.length <= 1) {
|
||||
return (
|
||||
@@ -1446,6 +1543,87 @@ const CacheView = memo((props) => {
|
||||
}}
|
||||
/>
|
||||
)
|
||||
} else if (updatedAutomation?.name === "Run AI Agent") {
|
||||
return (
|
||||
<ButtonGroup style={{display: "flex", }}>
|
||||
<TextField
|
||||
key={optionIndex}
|
||||
fullWidth
|
||||
disabled={option?.disabled === true}
|
||||
style={theme.palette.textFieldStyle}
|
||||
InputProps={{
|
||||
disableUnderline: true,
|
||||
style: {
|
||||
backgroundColor: theme.palette.textFieldStyle.backgroundColor,
|
||||
color: theme.palette.textFieldStyle.color,
|
||||
height: 40,
|
||||
},
|
||||
}}
|
||||
label={option.key}
|
||||
defaultValue={option.value}
|
||||
onBlur={(e) => {
|
||||
if (e.target.value === "") {
|
||||
updatedAutomation.enabled = false
|
||||
} else {
|
||||
updatedAutomation.enabled = true
|
||||
}
|
||||
|
||||
updatedAutomation.options[optionIndex].value = e.target.value;
|
||||
setUpdatedAutomation(updatedAutomation)
|
||||
setUpdated(true)
|
||||
}}
|
||||
/>
|
||||
|
||||
{optionIndex === updatedAutomation.options.length - 1 ?
|
||||
<Button color="secondary" variant="outlined" style={{
|
||||
height: 40,
|
||||
maxWidth: 40,
|
||||
}}
|
||||
disabled={option?.value?.length < 20}
|
||||
onClick={() => {
|
||||
|
||||
var newindex = optionIndex + 2
|
||||
if (updatedAutomation.options.length > 0) {
|
||||
const lastOption = updatedAutomation.options[updatedAutomation.options.length - 1]
|
||||
const lastIndex = parseInt(lastOption.key.split("-")[1])
|
||||
if (!isNaN(lastIndex)) {
|
||||
newindex = lastIndex + 1
|
||||
}
|
||||
}
|
||||
|
||||
updatedAutomation.options.push({
|
||||
"key": `action-${newindex}`,
|
||||
"description": "Describe the action to perform on this item",
|
||||
"value": "",
|
||||
})
|
||||
|
||||
setUpdatedAutomation(updatedAutomation)
|
||||
setUpdate(Math.random())
|
||||
}}>
|
||||
<AddIcon/>
|
||||
</Button>
|
||||
:
|
||||
<Tooltip title={option?.disabled !== true ? "Disable action (won't run until re-enabled)" : "Re-Enable action (runs on all new edits)"} placement="top">
|
||||
<Button color="secondary" variant="outlined" style={{
|
||||
height: 40,
|
||||
maxWidth: 40,
|
||||
}} onClick={() => {
|
||||
if (option?.disabled === true) {
|
||||
updatedAutomation.options[optionIndex].disabled = false
|
||||
} else {
|
||||
updatedAutomation.options[optionIndex].disabled = true
|
||||
}
|
||||
|
||||
setUpdatedAutomation(updatedAutomation)
|
||||
setUpdate(Math.random())
|
||||
}}>
|
||||
{option?.disabled !== true ? <RemoveIcon /> : <FlashOnIcon />}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
}
|
||||
|
||||
</ButtonGroup>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -1478,8 +1656,9 @@ const CacheView = memo((props) => {
|
||||
}}
|
||||
/>
|
||||
)
|
||||
})
|
||||
)}
|
||||
})}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</Tooltip>
|
||||
)
|
||||
@@ -1506,6 +1685,8 @@ const CacheView = memo((props) => {
|
||||
)
|
||||
}
|
||||
|
||||
const urlParams = new URLSearchParams(window?.location?.search)
|
||||
const highlightedKey = urlParams.get("key") || ""
|
||||
const columns = [
|
||||
{
|
||||
field: 'key',
|
||||
@@ -1513,6 +1694,24 @@ const CacheView = memo((props) => {
|
||||
width: 200,
|
||||
filterable: true,
|
||||
sortable: true,
|
||||
renderCell: (props) => {
|
||||
return (
|
||||
<Typography
|
||||
variant="body2"
|
||||
style={{
|
||||
maxWidth: 180,
|
||||
whiteSpace: "nowrap",
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
cursor: "default",
|
||||
|
||||
border: props.row.key === highlightedKey ? `2px solid ${theme.palette.primary.main}` : "none",
|
||||
}}
|
||||
>
|
||||
{props.row.key}
|
||||
</Typography>
|
||||
)
|
||||
}
|
||||
},
|
||||
{
|
||||
width: 540,
|
||||
@@ -1552,8 +1751,10 @@ const CacheView = memo((props) => {
|
||||
backgroundColor: theme.palette.platformColor,
|
||||
border: theme.palette.defaultBorder,
|
||||
padding: 5,
|
||||
maxWidth: 500,
|
||||
minWidth: 500,
|
||||
maxHeight: 500,
|
||||
overflowX: "auto",
|
||||
overflowY: "auto",
|
||||
}}
|
||||
collapsed={true}
|
||||
@@ -1735,6 +1936,7 @@ const CacheView = memo((props) => {
|
||||
"created": data.created,
|
||||
"workflow_id": data.workflow_id,
|
||||
"category": data.category,
|
||||
"tags": data.tags,
|
||||
})
|
||||
setValue(newvalue)
|
||||
setModalOpen(true)
|
||||
@@ -1914,7 +2116,7 @@ const CacheView = memo((props) => {
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
var previousgroup = ""
|
||||
const isAutomating = categoryAutomations?.find((automation) => automation.enabled) !== undefined
|
||||
return (
|
||||
<div style={{
|
||||
@@ -2067,56 +2269,53 @@ const CacheView = memo((props) => {
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
|
||||
<ButtonGroup style={{marginTop: 0, }}>
|
||||
{datastoreCategories !== undefined &&
|
||||
<ButtonGroup style={{ position: "absolute", top: -6, }}>
|
||||
{datastoreCategories !== undefined &&
|
||||
datastoreCategories !== null &&
|
||||
datastoreCategories.length > 1 ? (
|
||||
|
||||
<FormControl style={{ minWidth: 150, maxWidth: 150, marginTop: 8, }}>
|
||||
<InputLabel id="category-choice" style={{
|
||||
color: "rgba(255, 255, 255, 0.65)",
|
||||
}}>
|
||||
Category
|
||||
</InputLabel>
|
||||
<Select
|
||||
<FormControl style={{ minWidth: 250, maxWidth: 250, marginTop: 8, }}>
|
||||
<Autocomplete
|
||||
labelId="category-choice"
|
||||
style={{
|
||||
minWidth: 150,
|
||||
maxWidth: 150,
|
||||
height: 35,
|
||||
borderRadius: "5px 0px 0px 5px",
|
||||
minWidth: 250,
|
||||
maxWidth: 250,
|
||||
}}
|
||||
ListboxProps={{
|
||||
style: {
|
||||
maxHeight: "70vh",
|
||||
border: "1px solid rgba(255,255,255,0.3)",
|
||||
}
|
||||
}}
|
||||
value={selectedCategory}
|
||||
onChange={(event) => {
|
||||
setCategoryConfig(undefined)
|
||||
setCategoryAutomations(defaultAutomation)
|
||||
|
||||
//if (selectAllChecked || listCache.length > 0) {
|
||||
if (selectAllChecked || selectedFiles.length > 0) {
|
||||
setUpdateToThisCategory(event.target.value)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
setSelectedCategory(event.target.value)
|
||||
if (event.target.value === "all" || event.target.value === "default") {
|
||||
listOrgCache(orgId, "", 0, pageSize, page)
|
||||
} else {
|
||||
listOrgCache(orgId, event.target.value, 0, pageSize, page)
|
||||
}
|
||||
|
||||
// Add it to the url as a query
|
||||
if (window.location.search.includes("category=")) {
|
||||
const newurl = window.location.href.replace(/category=[^&]+/, `category=${event.target.value}`)
|
||||
window.history.pushState({ path: newurl }, "", newurl)
|
||||
} else {
|
||||
window.history.pushState({ path: window.location.href }, "", `${window.location.href}&category=${event.target.value}`)
|
||||
}
|
||||
options={datastoreCategories}
|
||||
getOptionLabel={(data) => {
|
||||
const fixedname = data?.charAt(0)?.toUpperCase() + data?.slice(1)?.replaceAll("_", " ")
|
||||
return fixedname
|
||||
}}
|
||||
>
|
||||
{datastoreCategories.map((data, index) => {
|
||||
// Should find the icon for things
|
||||
// Fix uppercase at start of words
|
||||
groupBy={(data) => {
|
||||
if (!data.includes("_")) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
const firstword = data.split("_")[0]
|
||||
if (datastoreCategoryGroups.includes(firstword)) {
|
||||
return firstword.charAt(0).toUpperCase() + firstword.slice(1)
|
||||
}
|
||||
|
||||
return undefined
|
||||
}}
|
||||
renderInput={(params) => {
|
||||
return (
|
||||
<TextField
|
||||
{...params}
|
||||
label="Select Category"
|
||||
variant="outlined"
|
||||
size="small"
|
||||
/>
|
||||
)
|
||||
}}
|
||||
renderOption={(props, data, state) => {
|
||||
const fixedname = data?.charAt(0)?.toUpperCase() + data?.slice(1)?.replaceAll("_", " ")
|
||||
const iconDetails = GetIconInfo({
|
||||
"app_name": fixedname,
|
||||
@@ -2125,13 +2324,40 @@ const CacheView = memo((props) => {
|
||||
|
||||
return (
|
||||
<MenuItem
|
||||
key={index}
|
||||
key={data}
|
||||
value={data}
|
||||
style={{
|
||||
color: theme.palette.textFieldStyle.color,
|
||||
display: "flex",
|
||||
borderBottom: theme.palette.defaultBorder,
|
||||
}}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
e.preventDefault()
|
||||
|
||||
setCategoryConfig(undefined)
|
||||
setCategoryAutomations(defaultAutomation)
|
||||
|
||||
if (selectAllChecked || selectedFiles.length > 0) {
|
||||
setUpdateToThisCategory(data)
|
||||
return
|
||||
}
|
||||
|
||||
setSelectedCategory(data)
|
||||
if (data === "all" || data === "default") {
|
||||
listOrgCache(orgId, "", 0, pageSize, page)
|
||||
} else {
|
||||
listOrgCache(orgId, data, 0, pageSize, page)
|
||||
}
|
||||
|
||||
// Add it to the url as a query
|
||||
if (window.location.search.includes("category=")) {
|
||||
const newurl = window.location.href.replace(/category=[^&]+/, `category=${data}`)
|
||||
window.history.pushState({ path: newurl }, "", newurl)
|
||||
} else {
|
||||
window.history.pushState({ path: window.location.href }, "", `${window.location.href}&category=${data}`)
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Typography style={{display: "flex", marginTop: 5, }}>
|
||||
<div style={{marginRight: 10, }}>
|
||||
@@ -2143,9 +2369,69 @@ const CacheView = memo((props) => {
|
||||
{fixedname}
|
||||
</Typography>
|
||||
</MenuItem>
|
||||
)
|
||||
}}
|
||||
|
||||
/>
|
||||
{/*
|
||||
{datastoreCategories.map((data, index) => {
|
||||
var addition = ""
|
||||
if (data.includes("_")) {
|
||||
const firstword = data.split("_")[0]
|
||||
if (firstword !== previousgroup) {
|
||||
|
||||
if (datastoreCategoryGroups.includes(firstword)) {
|
||||
previousgroup = firstword
|
||||
addition =
|
||||
<ListSubheader>
|
||||
{firstword.charAt(0).toUpperCase() + firstword.slice(1)}
|
||||
</ListSubheader>
|
||||
} else {
|
||||
if (previousgroup !== "") {
|
||||
addition = <div style={{height: 8, backgroundColor: "rgba(0,0,0,0.8)",}} />
|
||||
previousgroup = ""
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (previousgroup !== "") {
|
||||
addition = <div style={{height: 8, backgroundColor: "rgba(0,0,0,0.8)",}} />
|
||||
previousgroup = ""
|
||||
}
|
||||
}
|
||||
|
||||
// Should find the icon for things
|
||||
// Fix uppercase at start of words
|
||||
const fixedname = data?.charAt(0)?.toUpperCase() + data?.slice(1)?.replaceAll("_", " ")
|
||||
const iconDetails = GetIconInfo({
|
||||
"app_name": fixedname,
|
||||
"name": fixedname,
|
||||
})
|
||||
|
||||
return (
|
||||
<MenuItem
|
||||
key={data}
|
||||
value={data}
|
||||
style={{
|
||||
color: theme.palette.textFieldStyle.color,
|
||||
display: "flex",
|
||||
borderBottom: theme.palette.defaultBorder,
|
||||
}}
|
||||
>
|
||||
{addition}
|
||||
<Typography style={{display: "flex", marginTop: 5, }}>
|
||||
<div style={{marginRight: 10, }}>
|
||||
{iconDetails?.originalIcon && (
|
||||
iconDetails?.originalIcon
|
||||
)}
|
||||
</div>
|
||||
|
||||
{fixedname}
|
||||
</Typography>
|
||||
</MenuItem>
|
||||
);
|
||||
})}
|
||||
</Select>
|
||||
*/}
|
||||
</FormControl>
|
||||
) : null}
|
||||
|
||||
@@ -2154,7 +2440,7 @@ const CacheView = memo((props) => {
|
||||
<Tooltip title={"Close"} style={{}} aria-label={""}>
|
||||
<Button
|
||||
style={{
|
||||
height: 35,
|
||||
height: 40,
|
||||
borderRadius: 4,
|
||||
textTransform: 'none',
|
||||
fontSize: 16,
|
||||
@@ -2173,14 +2459,13 @@ const CacheView = memo((props) => {
|
||||
</Button>
|
||||
</Tooltip>
|
||||
:
|
||||
<Tooltip title={"Add or find category"} style={{}} aria-label={""}>
|
||||
<Tooltip title={"Add new category object"} style={{}} aria-label={""}>
|
||||
<Button
|
||||
style={{
|
||||
whiteSpace: "nowrap",
|
||||
width: datastoreCategories !== undefined && datastoreCategories !== null && datastoreCategories.length > 1 ? 50 : 169,
|
||||
height: 35,
|
||||
height: 40,
|
||||
textTransform: 'none',
|
||||
fontSize: 16,
|
||||
|
||||
borderRadius: "0px 5px 5px 0px",
|
||||
}}
|
||||
@@ -2211,14 +2496,14 @@ const CacheView = memo((props) => {
|
||||
|
||||
}}
|
||||
style={{
|
||||
height: 35,
|
||||
height: 40,
|
||||
width: 200,
|
||||
marginTop: 0,
|
||||
}}
|
||||
InputProps={{
|
||||
style: {
|
||||
color: theme.palette.textFieldStyle.color,
|
||||
height: 35,
|
||||
height: 40,
|
||||
fontSize: 16,
|
||||
borderRadius: 4,
|
||||
paddingTop: 0,
|
||||
@@ -2245,7 +2530,7 @@ const CacheView = memo((props) => {
|
||||
height: 35,
|
||||
textTransform: 'none',
|
||||
|
||||
border: isAutomating ? `1px solid ${theme.palette.primary.main}` : null,
|
||||
border: isAutomating ? `1px solid ${green}` : null,
|
||||
}}
|
||||
variant="contained"
|
||||
color="secondary"
|
||||
@@ -2259,8 +2544,8 @@ const CacheView = memo((props) => {
|
||||
setShowAutomationMenu(true)
|
||||
}}
|
||||
>
|
||||
{isAutomating ? <RocketLaunchIcon style={{color: theme.palette.primary.main, marginRight: 10 }}/> : <RocketIcon style={{marginRight: 10, color: theme.palette.secondary.main, }} />}
|
||||
Automate (beta)
|
||||
{isAutomating ? <RocketLaunchIcon style={{color: green, marginRight: 10 }}/> : <RocketIcon style={{marginRight: 10, color: theme.palette.secondary.main, }} />}
|
||||
Automate
|
||||
</Button>
|
||||
</span>
|
||||
</Tooltip>
|
||||
@@ -2403,7 +2688,7 @@ const CacheView = memo((props) => {
|
||||
Subscribing
|
||||
</Typography>
|
||||
<Typography variant="body2" style={{ color: theme.palette.text.secondary, marginBottom: 20 }}>
|
||||
Enabling this feature will allow other organizations to subscribe to this category. This is NOT fully available yet.
|
||||
Enabling subscriptions allows other organizations to subscribe to this category and receive updates when keys are added or modified. Makes the category searchable.
|
||||
</Typography>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -223,14 +223,14 @@ const DiscordChat = props => {
|
||||
</Button>
|
||||
<Typography style={{color: "white"}} variant="body2">{formMessage}</Typography>
|
||||
</div>
|
||||
<span style={{position: "absolute", display: "flex", textAlign: "right", float: "right", right: 0, bottom: 120, }}>
|
||||
{/* <span style={{position: "absolute", display: "flex", textAlign: "right", float: "right", right: 0, bottom: 120, }}>
|
||||
<Typography variant="body2" color="textSecondary" style={{}}>
|
||||
Search by
|
||||
</Typography>
|
||||
<a rel="noopener noreferrer" href="https://www.algolia.com/" target="_blank" style={{textDecoration: "none", color: "white"}}>
|
||||
<img src={"/images/logo-algolia-nebula-blue-full.svg"} alt="Algolia logo" style={{height: 17, marginLeft: 5, marginTop: 3,}} />
|
||||
</a>
|
||||
</span>
|
||||
</span> */}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -349,14 +349,14 @@ const DocsGrid = props => {
|
||||
: null
|
||||
}
|
||||
|
||||
<span style={{position: "absolute", display: "flex", textAlign: "right", float: "right", right: 0, bottom: 120, }}>
|
||||
{/* <span style={{position: "absolute", display: "flex", textAlign: "right", float: "right", right: 0, bottom: 120, }}>
|
||||
<Typography variant="body2" color="textSecondary" style={{}}>
|
||||
Search by
|
||||
</Typography>
|
||||
<a rel="noopener noreferrer" href="https://www.algolia.com/" target="_blank" style={{textDecoration: "none", color: "white"}}>
|
||||
<img src={"/images/logo-algolia-nebula-blue-full.svg"} alt="Algolia logo" style={{height: 17, marginLeft: 5, marginTop: 3,}} />
|
||||
</a>
|
||||
</span>
|
||||
</span> */}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -432,7 +432,7 @@ const EnvironmentTab = memo((props) => {
|
||||
-e SHUFFLE_SWARM_CONFIG=run \\
|
||||
-e BASE_URL="${newUrl}" \\${addProxy ? `
|
||||
-e HTTPS_PROXY=IP:PORT \\` : ""}${skipPipeline ? `
|
||||
-e SHUFFLE_PIPELINE_URL=http://tenzir-node:5160 \ \n -e SHUFFLE_PIPELINE_STANDALONE=true \\` : ""}${!showDetection ? `
|
||||
-e SHUFFLE_PIPELINE_URL=http://tenzir-node:5160 \\\n -e SHUFFLE_PIPELINE_STANDALONE=true \\` : ""}${!showDetection ? `
|
||||
-v /tmp:/tmp \\` : ""}
|
||||
ghcr.io/shuffle/shuffle-orborus:latest
|
||||
`)
|
||||
@@ -1225,7 +1225,9 @@ const EnvironmentTab = memo((props) => {
|
||||
:
|
||||
(environment.running_ip === undefined ||
|
||||
environment.running_ip === null ||
|
||||
environment.running_ip.length === 0)
|
||||
environment.running_ip.length === 0) ||
|
||||
environment?.checkin === 0 ||
|
||||
Date.now() / 1000 - environment?.checkin > 180
|
||||
?
|
||||
<Chip
|
||||
key={index}
|
||||
|
||||
@@ -110,7 +110,11 @@ const [filesLoaded, setFilesLoaded] = useState(false);
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
title={params.row.filename}
|
||||
title={<Typography variant="b1">
|
||||
{params?.row?.filename}<br/>
|
||||
{params?.row?.tags}
|
||||
</Typography>
|
||||
}
|
||||
placement="left"
|
||||
arrow
|
||||
>
|
||||
@@ -1580,8 +1584,8 @@ const [filesLoaded, setFilesLoaded] = useState(false);
|
||||
columns={columns}
|
||||
checkboxSelection
|
||||
disableRowSelectionOnClick
|
||||
selectionModel={selectedRows}
|
||||
onSelectionModelChange={(newSelection) => {
|
||||
rowSelectionModel={selectedRows}
|
||||
onRowSelectionModelChange={(newSelection) => {
|
||||
setSelectedRows(newSelection);
|
||||
}}
|
||||
sx={{
|
||||
|
||||
@@ -2,14 +2,13 @@ import React, { useEffect, useRef, useState, useContext, useCallback, useMemo, m
|
||||
import {
|
||||
ExpandLess as ExpandLessIcon,
|
||||
ExpandMore as ExpandMoreIcon,
|
||||
GridView as GridViewIcon,
|
||||
ShieldOutlined as ShieldOutlinedIcon,
|
||||
Add as AddIcon,
|
||||
BorderColor,
|
||||
Close as CloseIcon,
|
||||
ConstructionOutlined as ConstructionOutlinedIcon,
|
||||
Toc as TocIcon,
|
||||
Settings as SettingsIcon
|
||||
Settings as SettingsIcon,
|
||||
Business as BusinessIcon,
|
||||
HelpOutline as HelpOutlineIcon,
|
||||
MeetingRoom as MeetingRoomIcon,
|
||||
Search as SearchIcon,
|
||||
Add as AddIcon
|
||||
} from "@mui/icons-material";
|
||||
import LightModeIcon from '@mui/icons-material/LightMode';
|
||||
import DarkModeIcon from '@mui/icons-material/DarkMode';
|
||||
@@ -44,23 +43,17 @@ import { useNavigate } from "react-router";
|
||||
import { getTheme } from "../theme.jsx";
|
||||
|
||||
import { Link } from "react-router-dom";
|
||||
import {
|
||||
Business as BusinessIcon,
|
||||
Notifications as NotificationsIcon,
|
||||
HelpOutline as HelpOutlineIcon,
|
||||
MeetingRoom as MeetingRoomIcon,
|
||||
Lightbulb as LightbulbIcon,
|
||||
Search as SearchIcon
|
||||
} from "@mui/icons-material";
|
||||
|
||||
import { toast } from "react-toastify";
|
||||
import { Context } from "../context/ContextApi.jsx";
|
||||
import Licensed from "./Licensed.jsx";
|
||||
|
||||
const ShuffleLogo = "/images/Shuffle_logo.png";
|
||||
const detectionIcon = "/icons/detection.svg";
|
||||
const documentationIcon = "/icons/documentation.svg";
|
||||
const ExpandMoreAndLessIcon = "/icons/expandMoreIcon.svg";
|
||||
|
||||
|
||||
const LeftSideBar = ({ userdata, serverside, globalUrl, notifications, }) => {
|
||||
|
||||
const navigate = useNavigate();
|
||||
@@ -206,7 +199,11 @@ const LeftSideBar = ({ userdata, serverside, globalUrl, notifications, }) => {
|
||||
setOpenAutocomplete(false)
|
||||
}}
|
||||
>
|
||||
Add suborg
|
||||
<Button variant="outlined" startIcon={<AddIcon />} sx={{
|
||||
textTransform: 'none',
|
||||
}}>
|
||||
Add suborg
|
||||
</Button>
|
||||
</Box>
|
||||
</Link>
|
||||
</Popper>
|
||||
@@ -1926,72 +1923,48 @@ const LeftSideBar = ({ userdata, serverside, globalUrl, notifications, }) => {
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
padding: 10,
|
||||
marginLeft: 5,
|
||||
padding: "0 8px",
|
||||
marginBottom: 8,
|
||||
}}
|
||||
>
|
||||
|
||||
|
||||
{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={() => {
|
||||
navigate("/admin?admin_tab=billingstats&ref=left_sidebar_upgrade")
|
||||
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 && activeOrgData?.old_org? (
|
||||
<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",
|
||||
}}
|
||||
onClick={() => {
|
||||
navigate("/admin?admin_tab=billingstats")
|
||||
}}
|
||||
>
|
||||
<span
|
||||
<>
|
||||
{userdata?.licensed !== true && !userdata?.org_status?.includes("integration_partner") && !isProdStatusOn && (
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="primary"
|
||||
fullWidth
|
||||
style={{
|
||||
width: 8,
|
||||
height: 8,
|
||||
marginLeft: 10,
|
||||
background: isProdStatusOn ? "#2BC07E" : "#FD4C62",
|
||||
borderRadius: 999,
|
||||
display: expandLeftNav ? "inline" : "none",
|
||||
marginBottom: 30,
|
||||
marginRight: 8,
|
||||
height: 40,
|
||||
borderWidth: 1.5,
|
||||
borderRadius: 10,
|
||||
fontSize: 13,
|
||||
fontWeight: 600,
|
||||
textTransform: 'none',
|
||||
width: "95%" ,
|
||||
display: expandLeftNav ? 'flex' : 'none',
|
||||
}}
|
||||
/>
|
||||
<Typography
|
||||
style={{
|
||||
fontFamily: "12px",
|
||||
opacity: 0.9,
|
||||
color: isProdStatusOn ? "#2BC07E" : "#FD4C62",
|
||||
}}
|
||||
>
|
||||
{expandLeftNav ? isProdStatusOn ? "Production" : "NOT production" : isProdStatusOn ? "ON" : "OFF"}
|
||||
</Typography>
|
||||
</div>
|
||||
) : null}
|
||||
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>
|
||||
)}
|
||||
{!isCloud && (activeOrgData?.old_org || isProdStatusOn) && (
|
||||
|
||||
<Licensed
|
||||
expanded={expandLeftNav}
|
||||
licensed={isProdStatusOn}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
|
||||
<Box ref={autocompleteRef}>
|
||||
<Autocomplete
|
||||
@@ -2317,4 +2290,4 @@ const ModalView = memo(({searchBarModalOpen, setSearchBarModalOpen, globalUrl, s
|
||||
</Dialog>
|
||||
)
|
||||
)
|
||||
});
|
||||
});
|
||||
@@ -1319,8 +1319,14 @@ const LicencePopup = (props) => {
|
||||
>
|
||||
{`${
|
||||
isPaidPlan ? "Next billing: " : "App runs resets on "
|
||||
}${new Date(
|
||||
(localSub.enddate || localSub.Enddate) * 1000
|
||||
}${(
|
||||
isPaidPlan
|
||||
? new Date((localSub.enddate || localSub.Enddate) * 1000)
|
||||
: new Date(
|
||||
new Date().getFullYear(),
|
||||
new Date().getMonth() + 1,
|
||||
1
|
||||
)
|
||||
).toLocaleDateString(undefined, {
|
||||
day: "2-digit",
|
||||
month: "short",
|
||||
|
||||
@@ -183,7 +183,7 @@ const menuData = {
|
||||
title: "Articles",
|
||||
icon: "/images/icons/articles.svg",
|
||||
hoverIcon: "/images/icons/articles_hover.svg",
|
||||
link: "/articles/2.0_release",
|
||||
link: "/articles",
|
||||
gaData: {
|
||||
category: "navbar",
|
||||
action: "resources_click",
|
||||
|
||||
@@ -535,7 +535,7 @@ const AuthenticationOauth2 = (props) => {
|
||||
}
|
||||
}
|
||||
|
||||
toast("Authentication successful!")
|
||||
toast.info("Authentication window closed")
|
||||
|
||||
// This is more a guess than anything
|
||||
// Should be handled in getAppAuthentication()
|
||||
|
||||
@@ -343,7 +343,7 @@ console.log("defatult in handleEditOrg", defaults)
|
||||
const url = `${globalUrl}/api/v1/orgs/${selectedOrganization?.id}/change`;
|
||||
const data = {
|
||||
org_id: selectedOrganization?.id,
|
||||
sso_test: true,
|
||||
sso: true,
|
||||
};
|
||||
|
||||
fetch(url, {
|
||||
|
||||
@@ -62,11 +62,8 @@ const OrganizationTab = (props) => {
|
||||
if (isIntegrationPartner && isChildOrg && !isGlobalUser) {
|
||||
setVisibleTabs(items.filter((item) => item !== 'Branding' && item !== 'SSO'));
|
||||
} else {
|
||||
if (userdata && userdata.active_org && userdata.active_org.role === 'admin') {
|
||||
setVisibleTabs(items);
|
||||
} else {
|
||||
setVisibleTabs(items.filter((item) => item !== 'SSO'));
|
||||
}
|
||||
// Show SSO tab for all users now
|
||||
setVisibleTabs(items);
|
||||
}
|
||||
|
||||
//if (isCloud) {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -13,9 +13,9 @@ const PartnerApps = () => {
|
||||
minHeight: "500px",
|
||||
}}
|
||||
>
|
||||
<Typography>Partner Apps</Typography>
|
||||
<Typography>Partner's Apps</Typography>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
export default PartnerApps
|
||||
export default PartnerApps
|
||||
File diff suppressed because one or more lines are too long
@@ -126,9 +126,9 @@ const PartnerTab = (props) => {
|
||||
case `apps`:
|
||||
return <PartnersApps isCloud={isCloud} partnerData={partnerData} setPartnerData={setPartnerData} handleStatusChange={handleStatusChange} userdata={userdata} globalUrl={globalUrl} serverside={serverside} handleGetOrg={handleGetOrg} setSelectedOrganization={setSelectedOrganization} selectedOrganization={selectedOrganization} selectedTab={selectedTab} />;
|
||||
case 'articles' :
|
||||
return <PartnerArticles isCloud={isCloud} partnerData={partnerData} setPartnerData={setPartnerData} handleStatusChange={handleStatusChange} userdata={userdata} globalUrl={globalUrl} serverside={serverside} handleGetOrg={handleGetOrg} setSelectedOrganization={setSelectedOrganization} selectedOrganization={selectedOrganization} selectedTab={selectedTab} />;
|
||||
case `ai_agents`:
|
||||
return <PartnerArticles isCloud={isCloud} partnerData={partnerData} setPartnerData={setPartnerData} handleStatusChange={handleStatusChange} userdata={userdata} globalUrl={globalUrl} serverside={serverside} handleGetOrg={handleGetOrg} setSelectedOrganization={setSelectedOrganization} selectedOrganization={selectedOrganization} selectedTab={selectedTab} />;
|
||||
return <PartnersUsecasesTab isCloud={isCloud} partnerData={partnerData} setPartnerData={setPartnerData} handleStatusChange={handleStatusChange} userdata={userdata} globalUrl={globalUrl} serverside={serverside} handleGetOrg={handleGetOrg} setSelectedOrganization={setSelectedOrganization} selectedOrganization={selectedOrganization} selectedTab={selectedTab} />;
|
||||
case `aiagents`:
|
||||
return <PartnerArticles tabName="AI Agents, Coming Soon.." isCloud={isCloud} partnerData={partnerData} setPartnerData={setPartnerData} handleStatusChange={handleStatusChange} userdata={userdata} globalUrl={globalUrl} serverside={serverside} handleGetOrg={handleGetOrg} setSelectedOrganization={setSelectedOrganization} selectedOrganization={selectedOrganization} selectedTab={selectedTab} />;
|
||||
case 'branding':
|
||||
return <Branding
|
||||
isCloud={isCloud}
|
||||
@@ -157,8 +157,7 @@ const PartnerTab = (props) => {
|
||||
!isCloud &&
|
||||
(tabName === "Usecases" ||
|
||||
tabName === "Apps" ||
|
||||
tabName === "AI Agents" ||
|
||||
tabName === "Articles")
|
||||
tabName === "AI Agents")
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
@@ -166,7 +165,6 @@ const PartnerTab = (props) => {
|
||||
// Disable Apps, Articles, and AI Agents for all non-support users
|
||||
if (
|
||||
tabName === "Apps" ||
|
||||
tabName === "Articles" ||
|
||||
tabName === "AI Agents"
|
||||
) {
|
||||
return true;
|
||||
@@ -196,7 +194,7 @@ const PartnerTab = (props) => {
|
||||
}
|
||||
|
||||
const isSupportOnlyTab = (tabName) => {
|
||||
return tabName === "Apps" || tabName === "Articles" || tabName === "AI Agents";
|
||||
return tabName === "Apps" || tabName === "AI Agents";
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { Box, Typography } from '@mui/material'
|
||||
import React from 'react'
|
||||
|
||||
const PartnersArticles = () => {
|
||||
const PartnersArticles = (props) => {
|
||||
const {tabName = "Partner's Articles"} = props;
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
@@ -13,9 +14,9 @@ const PartnersArticles = () => {
|
||||
minHeight: "500px",
|
||||
}}
|
||||
>
|
||||
<Typography>Partner Articles</Typography>
|
||||
<Typography>{tabName}</Typography>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
export default PartnersArticles
|
||||
export default PartnersArticles
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
import React, { useContext, useEffect, useState } from "react";
|
||||
import { getTheme } from "../theme.jsx";
|
||||
import { Context } from "../context/ContextApi.jsx";
|
||||
import { triggers } from "../views/AngularWorkflow.jsx";
|
||||
import AddIcon from "@mui/icons-material/Add";
|
||||
import CloseIcon from "@mui/icons-material/Close";
|
||||
import { toast } from "react-toastify";
|
||||
@@ -37,6 +38,35 @@ import KeyboardArrowUpIcon from "@mui/icons-material/KeyboardArrowUp";
|
||||
import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown";
|
||||
import InfoIcon from "@mui/icons-material/Info";
|
||||
import { Link, useNavigate } from "react-router-dom";
|
||||
import { grey } from "../views/AngularWorkflow.jsx"
|
||||
|
||||
const IMAGE_UPLOAD_PLACEHOLDER = "Uploading image...";
|
||||
|
||||
// Helper to extract the first markdown image URL from navigation content
|
||||
export const getFirstImageFromNavigation = (navigation) => {
|
||||
if (!navigation || !Array.isArray(navigation.items)) {
|
||||
return "";
|
||||
}
|
||||
|
||||
for (const item of navigation.items) {
|
||||
if (!item || !Array.isArray(item.content)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const paragraph of item.content) {
|
||||
if (typeof paragraph !== "string") {
|
||||
continue;
|
||||
}
|
||||
|
||||
const match = paragraph.match(/!\[[^\]]*]\(([^)]+)\)/);
|
||||
if (match && match[1]) {
|
||||
return match[1];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return "";
|
||||
};
|
||||
|
||||
// Helper function to get the correct image path based on app category
|
||||
export const getCategoryImagePath = (category) => {
|
||||
@@ -62,16 +92,132 @@ export const getCategoryImagePath = (category) => {
|
||||
return "/images/appCategories/intel.svg";
|
||||
} else if (lowerCategory.includes("email")) {
|
||||
return "/images/appCategories/email.svg";
|
||||
} else if (lowerCategory.includes("webhook")) {
|
||||
return triggers.find((trigger) => trigger?.name.toLowerCase() === "webhook")?.large_image;
|
||||
} else if (lowerCategory.includes("schedule")) {
|
||||
return triggers.find((trigger) => trigger?.name.toLowerCase() === "schedule")?.large_image;
|
||||
} else if (lowerCategory.includes("pipelines")) {
|
||||
return triggers.find((trigger) => trigger?.name.toLowerCase() === "pipelines")?.large_image;
|
||||
} else if (lowerCategory.includes("shuffle workflow")) {
|
||||
return triggers.find((trigger) => trigger?.name.toLowerCase() === "shuffle workflow")?.large_image;
|
||||
} else if (lowerCategory.includes("user input")) {
|
||||
return triggers.find((trigger) => trigger?.name.toLowerCase() === "user input")?.large_image;
|
||||
} else {
|
||||
return "/images/appCategories/other.svg";
|
||||
}
|
||||
};
|
||||
|
||||
// Skeleton component for loading state
|
||||
const UsecaseCardSkeleton = () => {
|
||||
const UsecaseCardSkeleton = ({ isArticlesTab }) => {
|
||||
const { themeMode } = useContext(Context);
|
||||
const theme = getTheme(themeMode);
|
||||
|
||||
|
||||
// Skeleton for Articles Tab
|
||||
if (isArticlesTab) {
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
textDecoration: "none",
|
||||
backgroundColor: theme.palette.usecaseCardColor,
|
||||
borderRadius: "16px",
|
||||
width: {
|
||||
xs: "230px",
|
||||
sm: "320px",
|
||||
},
|
||||
minWidth: {
|
||||
xs: "230px",
|
||||
sm: "320px",
|
||||
},
|
||||
overflow: "hidden",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
}}
|
||||
>
|
||||
{/* Header image skeleton */}
|
||||
<Skeleton
|
||||
variant="rectangular"
|
||||
height={160}
|
||||
sx={{
|
||||
width: "100%",
|
||||
bgcolor:
|
||||
themeMode === "dark"
|
||||
? "rgba(255, 255, 255, 0.08)"
|
||||
: "rgba(0, 0, 0, 0.06)",
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Body skeleton */}
|
||||
<Box
|
||||
sx={{
|
||||
p: 2,
|
||||
pt: 2.5,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: 1.5,
|
||||
}}
|
||||
>
|
||||
<Skeleton
|
||||
variant="text"
|
||||
width="80%"
|
||||
height={26}
|
||||
sx={{
|
||||
bgcolor:
|
||||
themeMode === "dark"
|
||||
? "rgba(255, 255, 255, 0.1)"
|
||||
: "rgba(0, 0, 0, 0.08)",
|
||||
}}
|
||||
/>
|
||||
<Skeleton
|
||||
variant="text"
|
||||
width="40%"
|
||||
height={20}
|
||||
sx={{
|
||||
bgcolor:
|
||||
themeMode === "dark"
|
||||
? "rgba(255, 255, 255, 0.08)"
|
||||
: "rgba(0, 0, 0, 0.06)",
|
||||
}}
|
||||
/>
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
mt: "auto",
|
||||
display: "flex",
|
||||
justifyContent: "flex-end",
|
||||
alignItems: "center",
|
||||
gap: 1,
|
||||
}}
|
||||
>
|
||||
<Skeleton
|
||||
variant="rectangular"
|
||||
width={38}
|
||||
height={22}
|
||||
sx={{
|
||||
borderRadius: "12px",
|
||||
bgcolor:
|
||||
themeMode === "dark"
|
||||
? "rgba(255, 255, 255, 0.1)"
|
||||
: "rgba(0, 0, 0, 0.06)",
|
||||
}}
|
||||
/>
|
||||
<Skeleton
|
||||
variant="circular"
|
||||
width={24}
|
||||
height={24}
|
||||
sx={{
|
||||
bgcolor:
|
||||
themeMode === "dark"
|
||||
? "rgba(255, 255, 255, 0.1)"
|
||||
: "rgba(0, 0, 0, 0.06)",
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
// Skeleton for Usecases Tab
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
@@ -177,7 +323,8 @@ const UsecaseCardSkeleton = () => {
|
||||
);
|
||||
};
|
||||
|
||||
const UsecaseCard = ({ usecase, handleToggle, handleOpenDialog, handleDeleteUsecase }) => {
|
||||
// Card renderer for both usecases and articles
|
||||
const UsecaseCard = ({ usecase, handleToggle, handleOpenDialog, handleDeleteUsecase, isArticlesTab, partnerData }) => {
|
||||
const [anchorEl, setAnchorEl] = useState(null);
|
||||
const open = Boolean(anchorEl);
|
||||
const { themeMode } = useContext(Context);
|
||||
@@ -209,9 +356,215 @@ const UsecaseCard = ({ usecase, handleToggle, handleOpenDialog, handleDeleteUsec
|
||||
navigate(`/usecases/${usecase.id}`);
|
||||
};
|
||||
|
||||
// Article Card Layout
|
||||
if (isArticlesTab) {
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
textDecoration: "none",
|
||||
backgroundColor: theme.palette.usecaseCardColor,
|
||||
borderRadius: "16px",
|
||||
width: {
|
||||
xs: "230px",
|
||||
sm: "320px",
|
||||
},
|
||||
minWidth: {
|
||||
xs: "230px",
|
||||
sm: "320px",
|
||||
},
|
||||
overflow: "hidden",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
"&:hover": {
|
||||
backgroundColor: theme.palette.usecaseCardHoverColor,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
component="img"
|
||||
src={usecase.headerImage || "/images/no_image.png"}
|
||||
alt={usecase.name}
|
||||
sx={{
|
||||
width: "100%",
|
||||
height: 160,
|
||||
objectFit: "cover",
|
||||
}}
|
||||
/>
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
p: 2,
|
||||
pt: 2.5,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: 1.5,
|
||||
height: "100%",
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
sx={{
|
||||
color: theme.palette.text.primary,
|
||||
fontSize: {
|
||||
xs: "15px",
|
||||
lg: "16px",
|
||||
},
|
||||
fontWeight: 600,
|
||||
fontFamily: theme.typography.fontFamily,
|
||||
display: "-webkit-box",
|
||||
WebkitLineClamp: 2,
|
||||
WebkitBoxOrient: "vertical",
|
||||
overflow: "hidden",
|
||||
}}
|
||||
>
|
||||
{usecase.name}
|
||||
</Typography>
|
||||
|
||||
{usecase.created && (
|
||||
<Typography
|
||||
sx={{
|
||||
color: theme.palette.text.secondary,
|
||||
fontSize: "13px",
|
||||
}}
|
||||
>
|
||||
{new Date(usecase.created * 1000).toLocaleDateString("en-US", {
|
||||
year: "numeric",
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
})}
|
||||
</Typography>
|
||||
)}
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
mt: -1,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "flex-end",
|
||||
gap: 0.5,
|
||||
}}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<OpenInNewIcon
|
||||
fontSize="small"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
window.open(`${window.location.origin}/partners/${partnerData?.name.toLowerCase().replaceAll(" ", "_")}/articles/${usecase.name.toLowerCase().replaceAll(" ", "_")}`, "_blank");
|
||||
}}
|
||||
sx={{
|
||||
color: theme.palette.primary.main,
|
||||
cursor: "pointer",
|
||||
}}
|
||||
/>
|
||||
<Switch
|
||||
checked={usecase.public}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
onChange={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
handleToggle(usecase.id);
|
||||
}}
|
||||
size="medium"
|
||||
sx={{
|
||||
"& .MuiSwitch-switchBase.Mui-checked": {
|
||||
color: "#4CAF50",
|
||||
},
|
||||
"& .MuiSwitch-switchBase.Mui-checked + .MuiSwitch-track": {
|
||||
backgroundColor: "#4CAF50",
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<IconButton
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleClick(e);
|
||||
}}
|
||||
size="small"
|
||||
sx={{
|
||||
color: theme.palette.text.primary,
|
||||
"&:hover": {
|
||||
backgroundColor:
|
||||
themeMode === "dark"
|
||||
? "rgba(255, 255, 255, 0.1)"
|
||||
: "rgba(0, 0, 0, 0.05)",
|
||||
},
|
||||
}}
|
||||
>
|
||||
<MoreVertIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</Box>
|
||||
|
||||
<Menu
|
||||
anchorEl={anchorEl}
|
||||
open={open}
|
||||
onClose={handleClose}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
PaperProps={{
|
||||
sx: {
|
||||
backgroundColor: theme.palette.DialogStyle.backgroundColor,
|
||||
border: theme.palette.defaultBorder,
|
||||
borderRadius: "8px",
|
||||
boxShadow: theme.palette.DialogStyle.boxShadow,
|
||||
"& .MuiMenuItem-root": {
|
||||
fontSize: "14px",
|
||||
color: theme.palette.text.primary,
|
||||
"&:hover": {
|
||||
backgroundColor:
|
||||
themeMode === "dark"
|
||||
? "rgba(255, 255, 255, 0.1)"
|
||||
: "rgba(0, 0, 0, 0.05)",
|
||||
},
|
||||
},
|
||||
},
|
||||
}}
|
||||
transformOrigin={{ horizontal: "right", vertical: "top" }}
|
||||
anchorOrigin={{ horizontal: "right", vertical: "bottom" }}
|
||||
>
|
||||
<MenuItem
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleOpenDialog(usecase);
|
||||
setAnchorEl(null);
|
||||
}}
|
||||
>
|
||||
<ListItemIcon>
|
||||
<EditIcon
|
||||
fontSize="small"
|
||||
sx={{
|
||||
color: theme.palette.text.primary,
|
||||
fontFamily: theme.typography.fontFamily,
|
||||
}}
|
||||
/>
|
||||
</ListItemIcon>
|
||||
<ListItemText>Edit Article</ListItemText>
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleDeleteUsecase(usecase.id);
|
||||
}}
|
||||
>
|
||||
<ListItemIcon>
|
||||
<DeleteIcon
|
||||
fontSize="small"
|
||||
sx={{
|
||||
color: theme.palette.text.primary,
|
||||
fontFamily: theme.typography.fontFamily,
|
||||
}}
|
||||
/>
|
||||
</ListItemIcon>
|
||||
<ListItemText>Delete Article</ListItemText>
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
// Usecase Card Layout
|
||||
return (
|
||||
<Box
|
||||
onClick={handleCardClick}
|
||||
sx={{
|
||||
textDecoration: "none",
|
||||
backgroundColor: theme.palette.usecaseCardColor,
|
||||
@@ -241,7 +594,6 @@ const UsecaseCard = ({ usecase, handleToggle, handleOpenDialog, handleDeleteUsec
|
||||
sm: "300px",
|
||||
},
|
||||
gap: "20px",
|
||||
cursor: "pointer",
|
||||
"&:hover": {
|
||||
backgroundColor: theme.palette.usecaseCardHoverColor,
|
||||
},
|
||||
@@ -302,6 +654,17 @@ const UsecaseCard = ({ usecase, handleToggle, handleOpenDialog, handleDeleteUsec
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div style={{ display: "flex", alignItems: "center", gap: "5px" }}>
|
||||
<OpenInNewIcon
|
||||
fontSize="small"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
window.open(`${window.location.origin}/usecases/${usecase.name.toLowerCase().replaceAll(" ", "_")}`, "_blank");
|
||||
}}
|
||||
sx={{
|
||||
color: theme.palette.primary.main,
|
||||
cursor: "pointer",
|
||||
}}
|
||||
/>
|
||||
<Switch
|
||||
checked={usecase.public}
|
||||
onClick={(e) => {
|
||||
@@ -414,9 +777,11 @@ const UsecaseCard = ({ usecase, handleToggle, handleOpenDialog, handleDeleteUsec
|
||||
);
|
||||
};
|
||||
|
||||
const PartnersUsecasesTab = ({ isCloud, globalUrl, userdata, partnerData, setPartnerData }) => {
|
||||
const PartnersUsecasesTab = ({ isCloud, globalUrl, userdata, partnerData, setPartnerData, selectedTab }) => {
|
||||
const { themeMode } = useContext(Context);
|
||||
const theme = getTheme(themeMode);
|
||||
const isArticlesTab = selectedTab?.toLowerCase() === "articles";
|
||||
const entityLabel = isArticlesTab ? "Article" : "Usecase";
|
||||
|
||||
// Dialog state
|
||||
const [openDialog, setOpenDialog] = useState(false);
|
||||
@@ -438,12 +803,13 @@ const PartnersUsecasesTab = ({ isCloud, globalUrl, userdata, partnerData, setPar
|
||||
navigation: {
|
||||
items: [
|
||||
{
|
||||
name: "About Usecase",
|
||||
name: `About ${entityLabel}`,
|
||||
content: [""],
|
||||
},
|
||||
],
|
||||
},
|
||||
public: false,
|
||||
created: null,
|
||||
});
|
||||
|
||||
// App categories for dropdowns
|
||||
@@ -458,6 +824,11 @@ const PartnersUsecasesTab = ({ isCloud, globalUrl, userdata, partnerData, setPar
|
||||
{ value: "assets", label: "Assets" },
|
||||
{ value: "intel", label: "Intel" },
|
||||
{ value: "email", label: "Email" },
|
||||
{value: "webhook", label: "Webhook"},
|
||||
{value: "schedule", label: "Schedule"},
|
||||
{value: "pipelines", label: "Pipelines"},
|
||||
{value: "shuffle workflow", label: "Shuffle Workflow"},
|
||||
{value: "user input", label: "User Input"},
|
||||
{ value: "other", label: "Other" }
|
||||
];
|
||||
|
||||
@@ -505,7 +876,9 @@ const PartnersUsecasesTab = ({ isCloud, globalUrl, userdata, partnerData, setPar
|
||||
description: usecase.mainContent?.description,
|
||||
navigation: usecase.navigation || { items: [] },
|
||||
categories: usecase.mainContent?.categories || [],
|
||||
public: usecase?.public || false,
|
||||
public: usecase?.public || false,
|
||||
created: usecase?.created || null,
|
||||
headerImage: getFirstImageFromNavigation(usecase.navigation),
|
||||
}));
|
||||
if (usecases.length > 0) {
|
||||
setUsecaseData(usecases);
|
||||
@@ -526,7 +899,195 @@ const PartnersUsecasesTab = ({ isCloud, globalUrl, userdata, partnerData, setPar
|
||||
// Sample categories
|
||||
const categoryOptions = ["Collect","Enrich", "Detect", "Respond", "Verify"];
|
||||
|
||||
// Uploading image to the public bucket
|
||||
const handleImageUpload = async (imageData) => {
|
||||
const folderName = "usecase_images";
|
||||
const usecaseId = formData?.id || partnerData?.id || "usecase";
|
||||
|
||||
try {
|
||||
const response = await fetch(`${globalUrl}/api/v1/image_upload`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Accept: "application/json",
|
||||
},
|
||||
credentials: "include",
|
||||
body: JSON.stringify({
|
||||
imageData,
|
||||
folder: folderName,
|
||||
id: usecaseId,
|
||||
}),
|
||||
});
|
||||
|
||||
// if (response.status !== 200) {
|
||||
// toast.error("Failed to upload image" + response?.reason);
|
||||
// return null;
|
||||
// }
|
||||
|
||||
const responseJson = await response.json();
|
||||
if (!responseJson?.success || !responseJson?.url) {
|
||||
toast.error("Failed to upload image: " + responseJson?.reason);
|
||||
return null;
|
||||
}
|
||||
|
||||
return responseJson.url;
|
||||
} catch (error) {
|
||||
toast.error("Failed to upload image" + error?.message);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
// This function is used to update the content with the public image url
|
||||
const updateContentWithImageUrl = (itemIndex, contentIndex, imageUrl) => {
|
||||
setFormData((prevData) => {
|
||||
const newItems = [...(prevData.navigation.items || [])];
|
||||
const item = newItems[itemIndex];
|
||||
if (!item) {
|
||||
return prevData;
|
||||
}
|
||||
|
||||
const contents = [...(item.content || [])];
|
||||
const existingContent = contents[contentIndex] || "";
|
||||
const markdownImage = imageUrl ? `` : "";
|
||||
|
||||
const placeholderIndex = existingContent.indexOf(IMAGE_UPLOAD_PLACEHOLDER);
|
||||
if (placeholderIndex !== -1) {
|
||||
const before = existingContent.slice(0, placeholderIndex);
|
||||
const after = existingContent.slice(
|
||||
placeholderIndex + IMAGE_UPLOAD_PLACEHOLDER.length,
|
||||
);
|
||||
|
||||
const trimmedBefore = before.replace(/\s*$/, "");
|
||||
const needsNewline =
|
||||
trimmedBefore.length > 0 && !trimmedBefore.endsWith("\n");
|
||||
const separator = needsNewline ? "\n" : "";
|
||||
|
||||
contents[contentIndex] = `${trimmedBefore}${separator}${markdownImage}${after}`;
|
||||
} else {
|
||||
const needsNewline =
|
||||
existingContent.length > 0 && !existingContent.endsWith("\n");
|
||||
const separator = needsNewline ? "\n" : "";
|
||||
|
||||
contents[contentIndex] = `${existingContent}${separator}${markdownImage}`;
|
||||
}
|
||||
|
||||
newItems[itemIndex] = {
|
||||
...item,
|
||||
content: contents,
|
||||
};
|
||||
|
||||
return {
|
||||
...prevData,
|
||||
navigation: {
|
||||
items: newItems,
|
||||
},
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
// This function is used to insert the image placeholder (Uploading image...) in the content to show that the image is being uploaded
|
||||
const insertImagePlaceholder = (itemIndex, contentIndex) => {
|
||||
setFormData((prevData) => {
|
||||
const newItems = [...(prevData.navigation.items || [])];
|
||||
const item = newItems[itemIndex];
|
||||
if (!item) {
|
||||
return prevData;
|
||||
}
|
||||
|
||||
const contents = [...(item.content || [])];
|
||||
const existingContent = contents[contentIndex] || "";
|
||||
const needsNewline =
|
||||
existingContent.length > 0 && !existingContent.endsWith("\n");
|
||||
const separator = needsNewline ? "\n" : "";
|
||||
|
||||
contents[contentIndex] = `${existingContent}${separator}${IMAGE_UPLOAD_PLACEHOLDER}`;
|
||||
|
||||
newItems[itemIndex] = {
|
||||
...item,
|
||||
content: contents,
|
||||
};
|
||||
|
||||
return {
|
||||
...prevData,
|
||||
navigation: {
|
||||
items: newItems,
|
||||
},
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
// This function is used to process the image file and upload it to the public bucket
|
||||
const processImageFile = (imageFile, itemIndex, contentIndex) => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = async (loadEvent) => {
|
||||
const imageData = loadEvent.target?.result;
|
||||
if (!imageData) {
|
||||
toast.error("Failed to read image");
|
||||
updateContentWithImageUrl(itemIndex, contentIndex, "");
|
||||
return;
|
||||
}
|
||||
|
||||
const imageUrl = await handleImageUpload(imageData);
|
||||
if (!imageUrl) {
|
||||
// Clean up the placeholder if upload failed
|
||||
updateContentWithImageUrl(itemIndex, contentIndex, "");
|
||||
return;
|
||||
}
|
||||
|
||||
updateContentWithImageUrl(itemIndex, contentIndex, imageUrl);
|
||||
};
|
||||
reader.readAsDataURL(imageFile);
|
||||
};
|
||||
|
||||
// This function is used to handle the image paste event
|
||||
const handleContentPaste = async (event, itemIndex, contentIndex) => {
|
||||
const items = event.clipboardData?.items;
|
||||
if (!items || items.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
let imageFile = null;
|
||||
for (let i = 0; i < items.length; i += 1) {
|
||||
const item = items[i];
|
||||
if (item.kind === "file" && item.type.startsWith("image/")) {
|
||||
imageFile = item.getAsFile();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!imageFile) {
|
||||
return;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
|
||||
insertImagePlaceholder(itemIndex, contentIndex);
|
||||
processImageFile(imageFile, itemIndex, contentIndex);
|
||||
};
|
||||
|
||||
// This function is used to handle the image drop event
|
||||
const handleContentDrop = async (event, itemIndex, contentIndex) => {
|
||||
event.preventDefault();
|
||||
|
||||
const files = event.dataTransfer?.files;
|
||||
if (!files || files.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const imageFile = Array.from(files).find((file) => file.type.startsWith("image/"));
|
||||
if (!imageFile) {
|
||||
toast.error("Failed to upload image, only images are supported");
|
||||
return;
|
||||
}
|
||||
|
||||
insertImagePlaceholder(itemIndex, contentIndex);
|
||||
processImageFile(imageFile, itemIndex, contentIndex);
|
||||
};
|
||||
|
||||
const getUserProfileWorkflows = (orgId) => {
|
||||
if (selectedTab === "articles") {
|
||||
return;
|
||||
}
|
||||
setIsWorkflowLoading(true);
|
||||
fetch(`${globalUrl}/api/v1/partners/${orgId}/workflows`, {
|
||||
method: "GET",
|
||||
@@ -713,12 +1274,13 @@ const PartnersUsecasesTab = ({ isCloud, globalUrl, userdata, partnerData, setPar
|
||||
navigation: usecase?.navigation || {
|
||||
items: [
|
||||
{
|
||||
name: "About Usecase",
|
||||
name: `About ${entityLabel}`,
|
||||
content: [""],
|
||||
},
|
||||
],
|
||||
},
|
||||
public: usecase?.public || false,
|
||||
created: usecase?.created || Math.floor(Date.now() / 1000),
|
||||
});
|
||||
setOpenDialog(true);
|
||||
};
|
||||
@@ -753,15 +1315,15 @@ const PartnersUsecasesTab = ({ isCloud, globalUrl, userdata, partnerData, setPar
|
||||
const validationErrors = [];
|
||||
|
||||
// Check for public workflow selection
|
||||
if (!formData.mainContent.publicWorkflowId || formData.mainContent.publicWorkflowId.trim() === "") {
|
||||
if (!isArticlesTab && (!formData.mainContent.publicWorkflowId || formData.mainContent.publicWorkflowId.trim() === "")) {
|
||||
validationErrors.push("Please select a public workflow");
|
||||
}
|
||||
|
||||
if (!formData.mainContent.sourceAppType || formData.mainContent.sourceAppType.trim() === "") {
|
||||
if (!isArticlesTab && (!formData.mainContent.sourceAppType || formData.mainContent.sourceAppType.trim() === "")) {
|
||||
validationErrors.push("Source app type is required");
|
||||
}
|
||||
|
||||
if (!formData.mainContent.destinationAppType || formData.mainContent.destinationAppType.trim() === "") {
|
||||
if (!isArticlesTab && (!formData.mainContent.destinationAppType || formData.mainContent.destinationAppType.trim() === "")) {
|
||||
validationErrors.push("Destination app type is required");
|
||||
}
|
||||
|
||||
@@ -783,7 +1345,7 @@ const PartnersUsecasesTab = ({ isCloud, globalUrl, userdata, partnerData, setPar
|
||||
}
|
||||
|
||||
// Check categories
|
||||
if (!formData.mainContent.categories || !Array.isArray(formData.mainContent.categories) || formData.mainContent.categories.length === 0) {
|
||||
if (!isArticlesTab && (!formData.mainContent.categories || !Array.isArray(formData.mainContent.categories) || formData.mainContent.categories.length === 0)) {
|
||||
validationErrors.push("At least one category must be selected");
|
||||
}
|
||||
|
||||
@@ -846,7 +1408,6 @@ const PartnersUsecasesTab = ({ isCloud, globalUrl, userdata, partnerData, setPar
|
||||
return res.json();
|
||||
})
|
||||
.then((responseData) => {
|
||||
console.log("Response data:", responseData);
|
||||
// Reset loading state
|
||||
setIsSubmitting(false);
|
||||
|
||||
@@ -863,7 +1424,9 @@ const PartnersUsecasesTab = ({ isCloud, globalUrl, userdata, partnerData, setPar
|
||||
description: formData.mainContent?.description.trim(),
|
||||
categories: formData.mainContent?.categories || [],
|
||||
navigation: formData.navigation || {},
|
||||
public: formData?.public || false,
|
||||
public: formData?.public || false,
|
||||
created: formData?.created || null,
|
||||
headerImage: getFirstImageFromNavigation(formData.navigation),
|
||||
}
|
||||
|
||||
setUsecaseData((prevData) => {
|
||||
@@ -921,6 +1484,28 @@ const PartnersUsecasesTab = ({ isCloud, globalUrl, userdata, partnerData, setPar
|
||||
});
|
||||
};
|
||||
|
||||
// This is used to filter the usecases based on the selected tab
|
||||
const filteredUsecases = Array.isArray(usecaseData)
|
||||
? usecaseData.filter((usecase) => {
|
||||
const hasWorkflowId =
|
||||
!!usecase.publicWorkflowId &&
|
||||
usecase.publicWorkflowId.toString().trim() !== "";
|
||||
|
||||
if (selectedTab === "articles") {
|
||||
// Articles: only items without a public workflow ID
|
||||
return !hasWorkflowId;
|
||||
}
|
||||
|
||||
if (selectedTab === "usecases") {
|
||||
// Usecases: only items with a public workflow ID
|
||||
return hasWorkflowId;
|
||||
}
|
||||
|
||||
// Fallback: show all
|
||||
return true;
|
||||
})
|
||||
: [];
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
@@ -934,7 +1519,7 @@ const PartnersUsecasesTab = ({ isCloud, globalUrl, userdata, partnerData, setPar
|
||||
justifyContent: "flex-start",
|
||||
}}
|
||||
>
|
||||
{/* Add Usecase Button */}
|
||||
{/* Add Usecase / Article Button */}
|
||||
<Box
|
||||
sx={{
|
||||
width: "100%",
|
||||
@@ -946,13 +1531,13 @@ const PartnersUsecasesTab = ({ isCloud, globalUrl, userdata, partnerData, setPar
|
||||
}}
|
||||
>
|
||||
<Typography sx={{ fontSize: 20, fontWeight: 500, color: "#FFFFFF" }}>
|
||||
Usecases
|
||||
{selectedTab === "usecases" && "Usecases"}
|
||||
{selectedTab === "articles" && "Articles"}
|
||||
</Typography>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
startIcon={<AddIcon />}
|
||||
disabled={isWorkflowLoading}
|
||||
onClick={handleOpenDialog}
|
||||
sx={{
|
||||
px: 3,
|
||||
@@ -961,11 +1546,11 @@ const PartnersUsecasesTab = ({ isCloud, globalUrl, userdata, partnerData, setPar
|
||||
fontWeight: 600,
|
||||
}}
|
||||
>
|
||||
Add Usecase
|
||||
{isArticlesTab ? "Add Article" : "Add Usecase"}
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
{/* Usecases Grid */}
|
||||
{/* Usecases / Articles Grid */}
|
||||
{isLoading ? (
|
||||
// Skeleton loading state
|
||||
<Box
|
||||
@@ -981,11 +1566,11 @@ const PartnersUsecasesTab = ({ isCloud, globalUrl, userdata, partnerData, setPar
|
||||
>
|
||||
{/* Display 4 skeleton cards while loading */}
|
||||
{[...Array(4)].map((_, index) => (
|
||||
<UsecaseCardSkeleton key={index} />
|
||||
<UsecaseCardSkeleton key={index} isArticlesTab={isArticlesTab} partnerData={partnerData} />
|
||||
))}
|
||||
</Box>
|
||||
) : usecaseData.length > 0 ? (
|
||||
// Actual data display
|
||||
) : filteredUsecases.length > 0 ? (
|
||||
// Actual data display (filtered based on public workflow ID)
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
@@ -997,13 +1582,15 @@ const PartnersUsecasesTab = ({ isCloud, globalUrl, userdata, partnerData, setPar
|
||||
pt: 4,
|
||||
}}
|
||||
>
|
||||
{usecaseData.map((usecase) => (
|
||||
{filteredUsecases.map((usecase) => (
|
||||
<UsecaseCard
|
||||
handleOpenDialog={handleOpenDialog}
|
||||
key={usecase.id}
|
||||
usecase={usecase}
|
||||
handleToggle={handleToggle}
|
||||
handleDeleteUsecase={handleDeleteUsecase}
|
||||
isArticlesTab={isArticlesTab}
|
||||
partnerData={partnerData}
|
||||
/>
|
||||
))}
|
||||
</Box>
|
||||
@@ -1027,7 +1614,7 @@ const PartnersUsecasesTab = ({ isCloud, globalUrl, userdata, partnerData, setPar
|
||||
color: "#FFFFFF",
|
||||
}}
|
||||
>
|
||||
No usecases found
|
||||
{isArticlesTab ? "No articles found" : "No usecases found"}
|
||||
</Typography>
|
||||
</Box>
|
||||
)}
|
||||
@@ -1064,7 +1651,7 @@ const PartnersUsecasesTab = ({ isCloud, globalUrl, userdata, partnerData, setPar
|
||||
}}
|
||||
>
|
||||
<Typography sx={{ fontSize: 20, fontWeight: 500, color: theme.palette.text.primary }}>
|
||||
{formData?.id ? "Update" : "Add New"} Usecase : {formData?.public ? "Published" : "Draft"}
|
||||
{formData?.id ? "Update" : "Add New"} {entityLabel} : {formData?.public ? "Published" : "Draft"}
|
||||
</Typography>
|
||||
<IconButton
|
||||
onClick={handleCloseDialog}
|
||||
@@ -1088,6 +1675,7 @@ const PartnersUsecasesTab = ({ isCloud, globalUrl, userdata, partnerData, setPar
|
||||
paddingTop: 3,
|
||||
}}
|
||||
>
|
||||
{!isArticlesTab && (
|
||||
<FormControl>
|
||||
<Box
|
||||
sx={{
|
||||
@@ -1207,9 +1795,40 @@ const PartnersUsecasesTab = ({ isCloud, globalUrl, userdata, partnerData, setPar
|
||||
)}
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
|
||||
{/* Other data selection (use all data types) */}
|
||||
{!isArticlesTab && (
|
||||
<Box sx={{ display: "flex", flexDirection: "row", gap: 2, mt: 2 }}>
|
||||
<FormControl fullWidth>
|
||||
<Typography
|
||||
sx={{
|
||||
color: grey,
|
||||
mb: 1,
|
||||
fontSize: "16px",
|
||||
}}
|
||||
>
|
||||
<b>Coming soon:</b> Files (detection)
|
||||
</Typography>
|
||||
</FormControl>
|
||||
|
||||
<FormControl fullWidth>
|
||||
<Typography
|
||||
sx={{
|
||||
color: grey,
|
||||
mb: 1,
|
||||
fontSize: "16px",
|
||||
}}
|
||||
>
|
||||
<b>Coming soon:</b> Datastore category (threatlists)
|
||||
</Typography>
|
||||
</FormControl>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* App Type Selection */}
|
||||
<Box sx={{ display: "flex", flexDirection: "row", gap: 2, mt: 2 }}>
|
||||
{!isArticlesTab && (
|
||||
<Box sx={{ display: "flex", flexDirection: "row", gap: 2, mt: isArticlesTab ? 0 : 2 }}>
|
||||
<FormControl fullWidth>
|
||||
<Typography
|
||||
sx={{
|
||||
@@ -1286,6 +1905,7 @@ const PartnersUsecasesTab = ({ isCloud, globalUrl, userdata, partnerData, setPar
|
||||
</Select>
|
||||
</FormControl>
|
||||
|
||||
|
||||
<FormControl fullWidth>
|
||||
<Typography
|
||||
sx={{
|
||||
@@ -1362,6 +1982,7 @@ const PartnersUsecasesTab = ({ isCloud, globalUrl, userdata, partnerData, setPar
|
||||
</Select>
|
||||
</FormControl>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<Box sx={{ mb: 2, mt: 2 }}>
|
||||
<Typography
|
||||
@@ -1433,6 +2054,7 @@ const PartnersUsecasesTab = ({ isCloud, globalUrl, userdata, partnerData, setPar
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
{!isArticlesTab && (
|
||||
<FormControl>
|
||||
<Typography
|
||||
sx={{ color: theme.palette.text.primary, mb: 1, fontSize: "14px" }}
|
||||
@@ -1513,6 +2135,7 @@ const PartnersUsecasesTab = ({ isCloud, globalUrl, userdata, partnerData, setPar
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
@@ -1648,7 +2271,7 @@ const PartnersUsecasesTab = ({ isCloud, globalUrl, userdata, partnerData, setPar
|
||||
<Box key={contentIndex} sx={{ mb: 1 }}>
|
||||
<TextField
|
||||
multiline
|
||||
rows={6}
|
||||
rows={14}
|
||||
value={content}
|
||||
onChange={(e) => {
|
||||
const newItems = [...formData.navigation.items];
|
||||
@@ -1659,12 +2282,15 @@ const PartnersUsecasesTab = ({ isCloud, globalUrl, userdata, partnerData, setPar
|
||||
navigation: { items: newItems },
|
||||
});
|
||||
}}
|
||||
onPaste={(event) => handleContentPaste(event, itemIndex, contentIndex)}
|
||||
onDrop={(event) => handleContentDrop(event, itemIndex, contentIndex)}
|
||||
onDragOver={(event) => event.preventDefault()}
|
||||
placeholder="Content (Markdown supported)"
|
||||
helperText={
|
||||
<Box sx={{ display: "flex", alignItems: "center", gap: 0.5, mt: 0.5 }}>
|
||||
<InfoIcon sx={{ fontSize: "14px", opacity: 0.7 }} />
|
||||
<Typography variant="caption" sx={{ opacity: 0.7 }}>
|
||||
Markdown syntax is supported
|
||||
Markdown syntax is supported and Use ### for subItem for Table of Contents. Drag and drop or paste images to the content to upload them.
|
||||
</Typography>
|
||||
</Box>
|
||||
}
|
||||
@@ -1708,7 +2334,7 @@ const PartnersUsecasesTab = ({ isCloud, globalUrl, userdata, partnerData, setPar
|
||||
)}
|
||||
</Box>
|
||||
))}
|
||||
|
||||
{/*
|
||||
<Button
|
||||
startIcon={<AddCircleOutlineIcon />}
|
||||
onClick={() => {
|
||||
@@ -1725,7 +2351,7 @@ const PartnersUsecasesTab = ({ isCloud, globalUrl, userdata, partnerData, setPar
|
||||
}}
|
||||
>
|
||||
Add Paragraph
|
||||
</Button>
|
||||
</Button> */}
|
||||
</Box>
|
||||
))}
|
||||
|
||||
@@ -1791,7 +2417,7 @@ const PartnersUsecasesTab = ({ isCloud, globalUrl, userdata, partnerData, setPar
|
||||
>
|
||||
{isSubmitting
|
||||
? (formData?.id ? "Updating..." : "Adding...")
|
||||
: (formData?.id ? "Update Usecase" : "Add Usecase")
|
||||
: (formData?.id ? `Update ${entityLabel}` : `Add ${entityLabel}`)
|
||||
}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
|
||||
@@ -168,11 +168,13 @@ const RunDetectionTest = (props) => {
|
||||
return foundCorrect
|
||||
}
|
||||
|
||||
console.log("DETECTION CHECK: ", haveDetectionPipelines().length, ticketWebhook, detectionWorkflowId, detectionTestRunning)
|
||||
|
||||
return (
|
||||
<div style={{display: "flex", }}>
|
||||
<ButtonGroup style={{minWidth: 150, maxWidth: 225,}}>
|
||||
<Tooltip title={
|
||||
`Run a detection test with Sigma rules on your Tenzir Orborus instance. Requires: TCPC Syslog- & Sigma pipeline\n\nEnvironments: ${haveDetectionPipelines().join(", ")}`
|
||||
`Run a detection test with Sigma rules on your Tenzir Orborus instance. Requires: TCP Syslog- & Sigma pipeline\n\nEnvironments: ${haveDetectionPipelines().join(", ")}`
|
||||
} style={{}} aria-label={"Run detection test"}>
|
||||
<div>
|
||||
<Button
|
||||
|
||||
@@ -1,7 +1,25 @@
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import { Typography, ToggleButton, ToggleButtonGroup, Select, MenuItem, FormControl, InputLabel, Box } from '@mui/material';
|
||||
import { makeStyles, } from "@mui/styles";
|
||||
import {
|
||||
Typography,
|
||||
ToggleButton,
|
||||
ToggleButtonGroup,
|
||||
Select,
|
||||
MenuItem,
|
||||
FormControl,
|
||||
InputLabel,
|
||||
Box,
|
||||
Autocomplete,
|
||||
TextField,
|
||||
Tooltip,
|
||||
IconButton,
|
||||
} from '@mui/material';
|
||||
import { BarChart, BarSeries, Bar, GridlineSeries, Gridline, TooltipArea, ChartTooltip, LinearYAxis, LinearYAxisTickSeries, LinearYAxisTickLabel } from 'reaviz';
|
||||
import theme from '../theme.jsx';
|
||||
import {green, yellow, red} from "../views/AngularWorkflow.jsx"
|
||||
import {
|
||||
OpenInNew as OpenInNewIcon,
|
||||
} from '@mui/icons-material';
|
||||
|
||||
// Compact number formatter for axis ticks (e.g. 12,000 -> 12k, 12,000,000 -> 12M)
|
||||
function formatCompactNumber(value) {
|
||||
@@ -59,34 +77,128 @@ function computeTodayValueForOrg(key, orgStats) {
|
||||
const RunsOverTimeWidget = (props) => {
|
||||
const { globalUrl, onLoadingChange, monthOverride, dummyMode, selectedOrganization, selectedOrgForStats, orgStats, orgForLimit, loadingSelectedOrgStats } = props;
|
||||
const [mode, setMode] = useState('workflows'); // 'apps' | 'workflows'
|
||||
const [selectedStatType, setSelectedStatType] = useState({
|
||||
"value": 'total_workflow_executions',
|
||||
"label": 'Workflows',
|
||||
"amount": 0,
|
||||
})
|
||||
const [series, setSeries] = useState([]);
|
||||
const [days, setDays] = useState(365); // aggregate to last 12 months by default
|
||||
const [selectedMonth, setSelectedMonth] = useState(null); // Date representing first day of target month, or null for yearly view
|
||||
const [statTypeOptions, setStatTypeOptions] = useState([])
|
||||
const [loading, setLoading] = useState(false);
|
||||
const useStyles = makeStyles({
|
||||
notchedOutline: {
|
||||
borderColor: "#FF8544 !important",
|
||||
},
|
||||
root: {
|
||||
"& .MuiAutocomplete-listbox": {
|
||||
border: "2px solid #FF8544",
|
||||
color: theme.palette.text.primary,
|
||||
fontSize: 18,
|
||||
"& li:nth-child(even)": {
|
||||
backgroundColor: "#CCC",
|
||||
},
|
||||
"& li:nth-child(odd)": {
|
||||
backgroundColor: "#FFF",
|
||||
},
|
||||
},
|
||||
},
|
||||
inputRoot: {
|
||||
color: theme.palette.text.primary,
|
||||
"&:hover .MuiOutlinedInput-notchedOutline": {
|
||||
borderColor: "#f86a3e",
|
||||
},
|
||||
},
|
||||
});
|
||||
const classes = useStyles();
|
||||
|
||||
// Helper: fetch time series for a specific statistics key
|
||||
const fetchSeriesForKey = async (key) => {
|
||||
|
||||
if (statTypeOptions?.length < 3) {
|
||||
var foundkeys = {}
|
||||
for (var key in orgStats) {
|
||||
if (!key.startsWith("total_")) {
|
||||
continue
|
||||
}
|
||||
|
||||
// Remove total
|
||||
foundkeys[key] = orgStats[key]
|
||||
}
|
||||
|
||||
for (var foundKey in orgStats?.daily_statistics) {
|
||||
const dailyStats = orgStats?.daily_statistics[foundKey]
|
||||
|
||||
for (var additionKey in dailyStats?.additions) {
|
||||
const addition = dailyStats?.additions[additionKey];
|
||||
if (foundkeys[addition?.key] === undefined) {
|
||||
foundkeys[addition?.key] = addition?.value
|
||||
} else {
|
||||
foundkeys[addition?.key] += addition?.value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var newarray = []
|
||||
for (var key in foundkeys) {
|
||||
const foundkey = key
|
||||
|
||||
var parsedname = key
|
||||
if (parsedname?.startsWith("total_")) {
|
||||
parsedname = parsedname.replace("total_", "")
|
||||
}
|
||||
|
||||
if (parsedname?.startsWith("categorylabel")) {
|
||||
parsedname = parsedname.replace("categorylabel", "")
|
||||
}
|
||||
|
||||
parsedname = (parsedname.charAt(0).toUpperCase() + parsedname.substring(1)).replaceAll("_", " ")
|
||||
|
||||
newarray.push({
|
||||
"value": key,
|
||||
"label": parsedname,
|
||||
"amount": foundkeys[key],
|
||||
})
|
||||
}
|
||||
|
||||
if (newarray.length > 0) {
|
||||
setStatTypeOptions(newarray)
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
// If specific org is selected and pre-fetched stats are available, use them directly
|
||||
if (selectedOrgForStats && selectedOrgForStats !== 'ALL' && orgStats && Array.isArray(orgStats?.daily_statistics)) {
|
||||
|
||||
const dailyStats = orgStats.daily_statistics;
|
||||
const processedEntries = [];
|
||||
const cutoff = new Date();
|
||||
cutoff.setDate(cutoff.getDate() - days);
|
||||
|
||||
if (key.startsWith("total_")) {
|
||||
key = key.replace("total_", "")
|
||||
}
|
||||
|
||||
for (const day of dailyStats) {
|
||||
if (!day?.date) continue;
|
||||
const dayDate = new Date(day.date);
|
||||
if (dayDate < cutoff) continue;
|
||||
|
||||
let value = 0;
|
||||
if (key === 'workflow_executions') {
|
||||
const finished = Number(day?.workflow_executions_finished || 0);
|
||||
const failed = Number(day?.workflow_executions_failed || 0);
|
||||
value = finished + failed;
|
||||
} else {
|
||||
value = Number(day?.[key] || 0);
|
||||
}
|
||||
|
||||
if (day[key] === undefined) {
|
||||
for (var additionKey in day?.additions) {
|
||||
const addition = day?.additions[additionKey];
|
||||
if (addition?.key === key) {
|
||||
value += Number(addition?.value || 0);
|
||||
break
|
||||
}
|
||||
}
|
||||
} else {
|
||||
value = Number(day[key] || 0);
|
||||
}
|
||||
|
||||
processedEntries.push({ date: day.date, value });
|
||||
}
|
||||
|
||||
@@ -185,7 +297,7 @@ function computeTodayValueForOrg(key, orgStats) {
|
||||
};
|
||||
|
||||
// Load and transform into monthly aggregation for last 12 months
|
||||
const load = async (curMode) => {
|
||||
const loadStats = async (curMode, inputType) => {
|
||||
setLoading(true);
|
||||
try {
|
||||
// Clear current series immediately to avoid any visual overlap while switching views
|
||||
@@ -203,8 +315,10 @@ function computeTodayValueForOrg(key, orgStats) {
|
||||
setSeries(dummy);
|
||||
return;
|
||||
}
|
||||
const key = curMode === 'apps' ? 'app_executions' : 'workflow_executions';
|
||||
|
||||
const key = inputType !== undefined ? inputType : selectedStatType?.value?.length > 0 ? selectedStatType?.value : curMode === 'apps' ? 'app_executions' : 'workflow_executions';
|
||||
const entries = await fetchSeriesForKey(key);
|
||||
|
||||
// Normalize variants: {Date, Value} or {date, value}
|
||||
const normalized = (entries || []).map((d) => ({
|
||||
date: d?.Date ? new Date(d.Date) : (d?.date ? new Date(d.date) : new Date()),
|
||||
@@ -272,6 +386,23 @@ function computeTodayValueForOrg(key, orgStats) {
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
setTimeout(() => {
|
||||
const starterWidgetStatType = localStorage.getItem("runsOverTimeWidgetStatType")
|
||||
if (starterWidgetStatType) {
|
||||
setSelectedStatType({
|
||||
"value": starterWidgetStatType,
|
||||
"label": (starterWidgetStatType.charAt(0).toUpperCase() + starterWidgetStatType.substring(1)).replaceAll("_", " "),
|
||||
"amount": 0,
|
||||
})
|
||||
|
||||
loadStats(mode, starterWidgetStatType)
|
||||
}
|
||||
|
||||
}, 1500)
|
||||
}, [])
|
||||
|
||||
// Apply month override (e.g. onboarding Explore Now) - consolidated with main load effect
|
||||
useEffect(() => {
|
||||
if (monthOverride instanceof Date) {
|
||||
@@ -287,8 +418,9 @@ function computeTodayValueForOrg(key, orgStats) {
|
||||
setSeries([]);
|
||||
return;
|
||||
}
|
||||
load(mode);
|
||||
}, [mode, globalUrl, days, selectedMonth, dummyMode, selectedOrgForStats, loadingSelectedOrgStats]);
|
||||
|
||||
loadStats(mode);
|
||||
}, [selectedStatType, mode, globalUrl, days, selectedMonth, dummyMode, selectedOrgForStats, loadingSelectedOrgStats]);
|
||||
|
||||
// Notify parent on loading changes
|
||||
useEffect(() => {
|
||||
@@ -341,55 +473,125 @@ function computeTodayValueForOrg(key, orgStats) {
|
||||
}
|
||||
/>;
|
||||
|
||||
|
||||
const barColorscheme = [
|
||||
"#f85a3e", // anchor orange
|
||||
"#ff7a57", // brighter, more playful
|
||||
"#e14b2e", // slightly darker + redder
|
||||
"#ff9b6b", // soft peachy highlight
|
||||
"#c83f24", // deep burnt orange
|
||||
]
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<Typography sx={{ fontSize: 18, fontWeight: 500, fontFamily: theme.typography.fontFamily, paddingLeft: 1 }}>Runs over time ({mode === "workflows" ? "Workflows" : "Apps"})</Typography>
|
||||
<Typography sx={{ fontSize: 18, fontWeight: 500, fontFamily: theme.typography.fontFamily, paddingLeft: 1 }}>{selectedStatType?.label} ({selectedStatType?.amount})</Typography>
|
||||
<Box sx={{ display: 'flex', gap: 1.5, alignItems: 'center', justifyContent: 'center' }}>
|
||||
<ToggleButtonGroup
|
||||
exclusive
|
||||
size="large"
|
||||
value={mode}
|
||||
onChange={(e, v) => v && setMode(v)}
|
||||
sx={{
|
||||
height: 37,
|
||||
backgroundColor: 'rgba(255,255,255,0.06)',
|
||||
border: '1px solid rgba(255,255,255,0.22)',
|
||||
borderRadius: '30px',
|
||||
padding: '2px',
|
||||
"& .MuiToggleButton-root": {
|
||||
border: "none",
|
||||
borderRadius: "30px",
|
||||
color: "#fff",
|
||||
padding: "6px 16px",
|
||||
textTransform: "none",
|
||||
fontSize: "14px",
|
||||
"&.Mui-selected": {
|
||||
backgroundColor: "#fff",
|
||||
color: "#222",
|
||||
fontWeight: "600",
|
||||
"&:hover": {
|
||||
backgroundColor: "#fff",
|
||||
},
|
||||
},
|
||||
"&:hover": {
|
||||
backgroundColor: "rgba(255, 255, 255, 0.2)",
|
||||
},
|
||||
|
||||
<Tooltip title="Learn about custom stats" arrow>
|
||||
<a href="/docs/API#count-stats-for-custom-key" target="_blank" rel="noopener noreferrer" style={{ textDecoration: 'none' }}>
|
||||
<IconButton>
|
||||
<OpenInNewIcon style={{ color: theme.palette.text.secondary, }} />
|
||||
</IconButton>
|
||||
</a>
|
||||
</Tooltip>
|
||||
|
||||
<FormControl size="small" variant="outlined" style={{ minWidth: 350, }} sx={{
|
||||
'& .MuiInputBase-root': {
|
||||
height: 40,
|
||||
backgroundColor: 'rgba(255,255,255,0.06)',
|
||||
borderRadius: '20px',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<ToggleButton value="workflows">
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
Workflows
|
||||
</Box>
|
||||
</ToggleButton>
|
||||
<ToggleButton value="apps">
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
Apps
|
||||
</Box>
|
||||
</ToggleButton>
|
||||
</ToggleButtonGroup>
|
||||
'& .MuiOutlinedInput-notchedOutline': { borderColor: 'rgba(255,255,255,0.22)' },
|
||||
}}>
|
||||
<Autocomplete
|
||||
labelId="data-type-choice"
|
||||
label="Select Datatype"
|
||||
autoHighlight
|
||||
value={selectedStatType}
|
||||
classes={{ inputRoot: classes.inputRoot }}
|
||||
ListboxProps={{
|
||||
style: {
|
||||
backgroundColor: theme.palette.inputColor,
|
||||
color: theme.palette.text.primary,
|
||||
},
|
||||
}}
|
||||
sx={{
|
||||
'& .MuiOutlinedInput-root': {
|
||||
height: 40, // Adjust the input height
|
||||
},
|
||||
'& .MuiAutocomplete-input': {
|
||||
padding: '8px', // Adjust the text padding
|
||||
},
|
||||
}}
|
||||
getOptionSelected={(option, value) => option.id === value.id}
|
||||
getOptionLabel={(option) => {
|
||||
if (
|
||||
option === undefined ||
|
||||
option === null ||
|
||||
option.label === undefined ||
|
||||
option.label === null
|
||||
) {
|
||||
if (option.value !== undefined && option.value !== null) {
|
||||
return option.value
|
||||
} else {
|
||||
return option
|
||||
}
|
||||
}
|
||||
|
||||
const newname = (
|
||||
option.label.charAt(0).toUpperCase() + option.label.substring(1)
|
||||
).replaceAll("_", " ");
|
||||
return newname;
|
||||
}}
|
||||
options={statTypeOptions}
|
||||
fullWidth
|
||||
style={{
|
||||
backgroundColor: theme.palette?.inputColor,
|
||||
borderRadius: theme.palette?.borderRadius,
|
||||
}}
|
||||
onChange={(event, newValue) => {
|
||||
console.log("CHANGE: ", newValue)
|
||||
}}
|
||||
renderOption={(props, data, state) => {
|
||||
// Format to thousand or million
|
||||
const formattedamount = formatCompactNumber(data?.amount)
|
||||
const numbercolor = data?.amount >= 1000000 ? red : (data?.amount >= 100000 ? yellow : green)
|
||||
|
||||
return (
|
||||
<MenuItem
|
||||
style={{
|
||||
backgroundColor: theme.palette.inputColor,
|
||||
}}
|
||||
value={data}
|
||||
onClick={() => {
|
||||
setSelectedStatType(data)
|
||||
|
||||
// Set local storage for the key
|
||||
if (data?.value) {
|
||||
localStorage.setItem("runsOverTimeWidgetStatType", data?.value)
|
||||
}
|
||||
}}
|
||||
>
|
||||
<span style={{minWidth: 60, maxWidth: 60, color: numbercolor, }}>{formattedamount}</span> {data?.label}
|
||||
</MenuItem>
|
||||
)
|
||||
}}
|
||||
renderInput={(params) => {
|
||||
return (
|
||||
<div style={{ display: "flex", }}>
|
||||
<TextField
|
||||
style={theme.palette.textFieldStyle}
|
||||
{...params}
|
||||
label="Find your Stat"
|
||||
variant="outlined"
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<FormControl size="small" variant="outlined" style={{ minWidth: 170 }} sx={{
|
||||
'& .MuiInputBase-root': {
|
||||
height: 40,
|
||||
@@ -438,7 +640,7 @@ function computeTodayValueForOrg(key, orgStats) {
|
||||
key={`${mode}-${selectedMonth ? `${selectedMonth.getFullYear()}-${selectedMonth.getMonth()}` : 'yearly'}`}
|
||||
height={300}
|
||||
data={barData}
|
||||
series={<BarSeries tooltip={tooltip} bar={<Bar rounded={true} />} />}
|
||||
series={<BarSeries colorScheme={barColorscheme} tooltip={tooltip} bar={<Bar rounded={true} />} />}
|
||||
gridlines={<GridlineSeries line={<Gridline direction="y" />} />}
|
||||
yAxis={
|
||||
<LinearYAxis
|
||||
|
||||
@@ -44,7 +44,7 @@ import {
|
||||
Send as SendIcon,
|
||||
} from '@mui/icons-material';
|
||||
|
||||
import { DataGrid } from '@mui/x-data-grid'
|
||||
import { DataGrid, GridValueGetterParams } from '@mui/x-data-grid'
|
||||
import {
|
||||
Search as SearchIcon,
|
||||
} from "@mui/icons-material";
|
||||
@@ -78,7 +78,7 @@ const RuntimeDebugger = (props) => {
|
||||
const [ignoreOrg, setIgnoreOrg] = useState(false)
|
||||
const [searchLoading, setSearchLoading] = useState(false)
|
||||
const [rowCursor, setCursor] = useState("")
|
||||
const [rowsPerPage, setRowsPerPage] = useState(10)
|
||||
const [rowsPerPage, setRowsPerPage] = useState(20)
|
||||
const [maxExecutionCount, setMaxExecutionCount] = useState(50)
|
||||
const [resultRows, setResultRows] = useState([])
|
||||
const [selectedWorkflowExecutions, setSelectedWorkflowExecutions] = useState([])
|
||||
@@ -219,6 +219,10 @@ const RuntimeDebugger = (props) => {
|
||||
if (data.runs[key].completed_at === 0 || data.runs[key].completed_at === null) {
|
||||
data.runs[key].endTimestamp = ""
|
||||
}
|
||||
|
||||
if (data.runs[key]?.workflow_id === data.runs[key]?.execution_id && data.runs[key].type === "AGENT") {
|
||||
data.runs[key].workflow.name = "Agent Execution"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -359,6 +363,8 @@ const RuntimeDebugger = (props) => {
|
||||
} else if (source === "single_action" || source == "single_api" || source === "direct_api") {
|
||||
foundSource = <SendIcon color="secondary" style={{height: imageSize-5, }} />
|
||||
source = "Single API call"
|
||||
} else if (params?.row?.type === "AGENT") {
|
||||
foundSource = <img src={theme.palette.singulBlackWhite} alt="agent" style={{borderRadius: theme.palette?.borderRadius, height: imageSize, width: imageSize, }} />
|
||||
} else {
|
||||
source = "manual"
|
||||
}
|
||||
@@ -626,19 +632,28 @@ const RuntimeDebugger = (props) => {
|
||||
return (
|
||||
<div style={{display: "flex", }}>
|
||||
<Tooltip arrow placement="left" title={
|
||||
<Typography variant="body2" style={{whiteSpace: "pre-line", padding: 10, }}>
|
||||
Workflow result: {errorReason}<br/><br/>
|
||||
{params.row.result !== null && params.row.result !== undefined && params.row.result !== "" ?
|
||||
params.row.result
|
||||
:
|
||||
null
|
||||
}
|
||||
</Typography>
|
||||
params.row.type === "AGENT" ?
|
||||
"See agent result"
|
||||
:
|
||||
<Typography variant="body2" style={{whiteSpace: "pre-line", padding: 10, }}>
|
||||
Workflow result: {errorReason}<br/><br/>
|
||||
{params.row.result !== null && params.row.result !== undefined && params.row.result !== "" ?
|
||||
params.row.result
|
||||
:
|
||||
null
|
||||
}
|
||||
</Typography>
|
||||
}>
|
||||
<span style={{backgroundColor: !hasError ? "inherit" : "rgba(244,0,0,0.45)", display: "flex", }}>
|
||||
<Link href={`/workflows/${params.row.workflow.id}?execution_id=${params.row.id}`} target="_blank" rel="noopener noreferrer">
|
||||
<OpenInNewIcon fontSize="small" style={{marginTop: 7, }} />
|
||||
</Link>
|
||||
{params.row.type === "AGENT" ?
|
||||
<Link href={`/agents?execution_id=${params.row.execution_id}&authorization=${params.row.authorization}`} target="_blank" rel="noopener noreferrer">
|
||||
<OpenInNewIcon fontSize="small" style={{marginTop: 7, }} />
|
||||
</Link>
|
||||
:
|
||||
<Link href={`/workflows/${params.row.workflow.id}?execution_id=${params.row.id}`} target="_blank" rel="noopener noreferrer">
|
||||
<OpenInNewIcon fontSize="small" style={{marginTop: 7, }} />
|
||||
</Link>
|
||||
}
|
||||
</span>
|
||||
</Tooltip>
|
||||
<Tooltip arrow title={`Force continue workflow. Only workflows for workflows in EXECUTING state. This is NOT a rerun, but way for Shuffle to figure out the next steps automatically. If the execution doesn't finish even after trying this, please contact ${supportEmail}`}>
|
||||
@@ -1069,7 +1084,10 @@ const RuntimeDebugger = (props) => {
|
||||
).replaceAll("_", " ");
|
||||
return newname;
|
||||
}}
|
||||
options={workflows}
|
||||
options={[{
|
||||
"name": "Agent Runs",
|
||||
"id": "AGENT",
|
||||
}].concat(workflows)}
|
||||
fullWidth
|
||||
style={{
|
||||
backgroundColor: theme.palette.backgroundColor,
|
||||
|
||||
@@ -73,15 +73,18 @@ const SchedulesTab = memo((props) => {
|
||||
setWorkflows(responseJson || []);
|
||||
|
||||
for (var i = 0; i < responseJson?.length; i++) {
|
||||
if (responseJson[i].background_processing === true && responseJson[i].name.toLowerCase().includes("ingest tickets") && responseJson[i].triggers !== undefined) {
|
||||
if (responseJson[i].background_processing === true && (responseJson[i].name.toLowerCase().includes("ticket") || responseJson[i].name.toLowerCase().includes("ingest")) && responseJson[i].triggers !== undefined) {
|
||||
|
||||
for (var triggerkey in responseJson[i].triggers) {
|
||||
if (responseJson[i].triggers[triggerkey].trigger_type === "WEBHOOK") {
|
||||
setDetectionWorkflowId(responseJson[i].id)
|
||||
setTicketWebhook(`${globalUrl}/api/v1/hooks/webhook_${responseJson[i].triggers[triggerkey].id}`)
|
||||
setNewPipelineValue(`export live=true | sigma /tmp/sigma_rules | to ${globalUrl}/api/v1/hooks/webhook_${responseJson[i].triggers[triggerkey].id}`)
|
||||
break;
|
||||
//console.log("TRIGGER: ", responseJson[i].triggers[triggerkey].trigger_type)
|
||||
if (responseJson[i].triggers[triggerkey].trigger_type !== "WEBHOOK") {
|
||||
continue
|
||||
}
|
||||
|
||||
setDetectionWorkflowId(responseJson[i].id)
|
||||
setTicketWebhook(`${globalUrl}/api/v1/hooks/webhook_${responseJson[i].triggers[triggerkey].id}`)
|
||||
setNewPipelineValue(`export live=true | sigma /tmp/sigma_rules | to ${globalUrl}/api/v1/hooks/webhook_${responseJson[i].triggers[triggerkey].id}`)
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -25,6 +25,7 @@ import {
|
||||
Skeleton,
|
||||
Switch,
|
||||
Box,
|
||||
Chip,
|
||||
} from "@mui/material";
|
||||
|
||||
import {
|
||||
@@ -158,7 +159,7 @@ const UserManagmentTab = memo((props) => {
|
||||
toast("Failed to update user");
|
||||
} else {
|
||||
//toast("Set the user field " + field + " to " + value);
|
||||
toast("Successfully updated user field " + field);
|
||||
toast.success(`Successfully updated user field '${field}'`)
|
||||
|
||||
if (field !== "suborgs") {
|
||||
setSelectedUserModalOpen(false);
|
||||
@@ -203,7 +204,7 @@ const UserManagmentTab = memo((props) => {
|
||||
getUsers();
|
||||
}, 1000);
|
||||
|
||||
toast("Invite sent! They will show up in the list when they have accepted the invite.")
|
||||
toast.success("The user will show up in the list when finished.")
|
||||
}
|
||||
})
|
||||
)
|
||||
@@ -260,7 +261,6 @@ const UserManagmentTab = memo((props) => {
|
||||
event.target.value = []
|
||||
}
|
||||
|
||||
console.log("event: ", event.target.value);
|
||||
setMatchingOrganizations(event.target.value);
|
||||
// Workaround for empty orgs
|
||||
if (event.target.value.length === 0) {
|
||||
@@ -336,6 +336,17 @@ const UserManagmentTab = memo((props) => {
|
||||
})
|
||||
.then((responseJson) => {
|
||||
setUsers(responseJson);
|
||||
|
||||
if (responseJson.success !== false) {
|
||||
for (var i = 0; i < responseJson.length; i++) {
|
||||
const data = responseJson[i];
|
||||
if (data?.login_type === "DELETED") {
|
||||
//toast.info("Found lost/half-deleted users you can recover. Please contact support@shuffler.io to learn more.")
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setShowLoader(false)
|
||||
})
|
||||
.catch((error) => {
|
||||
@@ -364,13 +375,13 @@ const UserManagmentTab = memo((props) => {
|
||||
})
|
||||
.then((responseJson) => {
|
||||
if (!responseJson.success && responseJson.reason !== undefined) {
|
||||
toast("Failed to deactivate user: " + responseJson.reason);
|
||||
toast.error("Failed to deactivate user: " + responseJson.reason);
|
||||
} else if (responseJson.success === false) {
|
||||
toast(
|
||||
toast.error(
|
||||
`Failed to deactivate user. Please contact ${supportEmail} if this persists.`,
|
||||
);
|
||||
} else {
|
||||
toast("Changed activation for user " + data.id);
|
||||
toast.success("Changed activation for user " + data.id);
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1529,6 +1540,7 @@ const UserManagmentTab = memo((props) => {
|
||||
<ListItemText
|
||||
primary={
|
||||
<Select
|
||||
disabled={data?.login_type === "DELETED"}
|
||||
SelectDisplayProps={{
|
||||
style: {
|
||||
// marginLeft: 10,
|
||||
@@ -1611,7 +1623,30 @@ const UserManagmentTab = memo((props) => {
|
||||
data.login_type === undefined ||
|
||||
data?.login_type === null ||
|
||||
data?.login_type?.length === 0
|
||||
? "Normal"
|
||||
?
|
||||
"Normal"
|
||||
:
|
||||
data?.login_type === "DELETED" ?
|
||||
<Chip
|
||||
style={{
|
||||
marginLeft: 0,
|
||||
padding: 0,
|
||||
marginRight: 0,
|
||||
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
label={"Recover"}
|
||||
variant="outlined"
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
|
||||
toast.warn("Re-adding the account to the org with 'user' role.")
|
||||
inviteUser({
|
||||
"Username": data.username,
|
||||
})
|
||||
}}
|
||||
/>
|
||||
: data.login_type
|
||||
}
|
||||
style={{ display:'table-cell',verticalAlign: 'middle', padding: "8px", }}
|
||||
@@ -1647,6 +1682,7 @@ const UserManagmentTab = memo((props) => {
|
||||
) : null}
|
||||
<ListItemText style={{ display:'table-cell', textAlign: "left", verticalAlign: 'middle', padding: "8px", }}>
|
||||
<IconButton
|
||||
disabled={data?.login_type === "DELETED"}
|
||||
onClick={() => {
|
||||
setSelectedUserModalOpen(true);
|
||||
setSelectedUser(data);
|
||||
|
||||
@@ -15,6 +15,7 @@ import theme from "../theme.jsx";
|
||||
import { toast } from "react-toastify";
|
||||
import { Context } from "../context/ContextApi.jsx";
|
||||
import { getTheme } from "../theme.jsx";
|
||||
import { red } from "../views/AngularWorkflow.jsx"
|
||||
|
||||
const useStyles = makeStyles({
|
||||
notchedOutline: {
|
||||
@@ -25,6 +26,14 @@ const useStyles = makeStyles({
|
||||
|
||||
const SSOTab = ({selectedOrganization, userdata, isEditOrgTab, globalUrl, handleEditOrg})=>{
|
||||
|
||||
// Check if user is admin
|
||||
const isAdmin = userdata?.active_org?.role === "admin" || userdata?.support === true;
|
||||
|
||||
// State for tracking user SSO connection status
|
||||
const [users, setUsers] = React.useState([]);
|
||||
const [userSSOConnected, setUserSSOConnected] = React.useState(false);
|
||||
const [checkingSSOStatus, setCheckingSSOStatus] = React.useState(true);
|
||||
|
||||
const classes = useStyles();
|
||||
const [show2faSetup, setShow2faSetup] = React.useState(false);
|
||||
const [autoPrivision, setAutoProvision] = React.useState(selectedOrganization?.sso_config?.auto_provision)
|
||||
@@ -97,6 +106,53 @@ const SSOTab = ({selectedOrganization, userdata, isEditOrgTab, globalUrl, handle
|
||||
const { themeMode, supportEmail, brandColor } = useContext(Context);
|
||||
const theme = getTheme(themeMode, brandColor);
|
||||
|
||||
// Function to fetch users and check current user's SSO status
|
||||
const checkUserSSOStatus = () => {
|
||||
setCheckingSSOStatus(true);
|
||||
fetch(globalUrl + "/api/v1/getusers", {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Accept: "application/json",
|
||||
},
|
||||
credentials: "include",
|
||||
})
|
||||
.then((response) => {
|
||||
if (response.status !== 200) {
|
||||
return;
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then((responseJson) => {
|
||||
if (responseJson && Array.isArray(responseJson)) {
|
||||
setUsers(responseJson);
|
||||
|
||||
// Find current user and check if they have SSO info for this org
|
||||
const currentUser = responseJson.find(user => user.id === userdata?.id);
|
||||
if (currentUser && currentUser.sso_infos && Array.isArray(currentUser.sso_infos)) {
|
||||
const hasSSOForThisOrg = currentUser.sso_infos.some(ssoInfo =>
|
||||
ssoInfo.org_id === selectedOrganization?.id && ssoInfo.sub
|
||||
);
|
||||
setUserSSOConnected(hasSSOForThisOrg);
|
||||
} else {
|
||||
setUserSSOConnected(false);
|
||||
}
|
||||
}
|
||||
setCheckingSSOStatus(false);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log("Error fetching users:", error);
|
||||
setCheckingSSOStatus(false);
|
||||
});
|
||||
};
|
||||
|
||||
// Check SSO status on component mount and when organization changes
|
||||
useEffect(() => {
|
||||
if (userdata?.id && selectedOrganization?.id) {
|
||||
checkUserSSOStatus();
|
||||
}
|
||||
}, [userdata?.id, selectedOrganization?.id]);
|
||||
|
||||
useEffect(()=>{
|
||||
|
||||
if (openidClientSecret !== selectedOrganization?.sso_config?.client_secret) {
|
||||
@@ -149,7 +205,7 @@ const SSOTab = ({selectedOrganization, userdata, isEditOrgTab, globalUrl, handle
|
||||
disabled={
|
||||
userdata === undefined ||
|
||||
userdata === null ||
|
||||
userdata.admin !== "true"
|
||||
!isAdmin
|
||||
}
|
||||
onClick={() =>
|
||||
handleEditOrg(
|
||||
@@ -256,7 +312,7 @@ const SSOTab = ({selectedOrganization, userdata, isEditOrgTab, globalUrl, handle
|
||||
const url = `${globalUrl}/api/v1/orgs/${selectedOrganization?.id}/change`;
|
||||
const data = {
|
||||
org_id: selectedOrganization?.id,
|
||||
sso_test: true,
|
||||
sso: true,
|
||||
};
|
||||
|
||||
fetch(url, {
|
||||
@@ -309,6 +365,59 @@ const SSOTab = ({selectedOrganization, userdata, isEditOrgTab, globalUrl, handle
|
||||
});
|
||||
};
|
||||
|
||||
const HandleDisconnectSSO = () => {
|
||||
const url = `${globalUrl}/api/v1/disconnect_sso`;
|
||||
const data = {
|
||||
org_id: selectedOrganization?.id,
|
||||
};
|
||||
|
||||
fetch(url, {
|
||||
mode: "cors",
|
||||
credentials: "include",
|
||||
crossDomain: true,
|
||||
method: "POST",
|
||||
body: JSON.stringify(data),
|
||||
withCredentials: true,
|
||||
headers: {
|
||||
"Content-Type": "application/json; charset=utf-8",
|
||||
},
|
||||
})
|
||||
.then((response) => {
|
||||
if (response.status !== 200) {
|
||||
toast.error(
|
||||
`Failed to disconnect SSO. Please try again later or contact ${supportEmail} if issue persists.`,
|
||||
{ duration: 3000 }
|
||||
);
|
||||
return null;
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then((responseJson) => {
|
||||
if (!responseJson) return;
|
||||
|
||||
if (responseJson.success === true) {
|
||||
toast.success(
|
||||
"Successfully disconnected from SSO.",
|
||||
{ duration: 3000 }
|
||||
);
|
||||
// Refresh the SSO status after disconnecting
|
||||
checkUserSSOStatus();
|
||||
} else {
|
||||
toast.error(
|
||||
responseJson.reason || "Failed to disconnect SSO.",
|
||||
{ duration: 3000 }
|
||||
);
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("Error disconnecting SSO:", error);
|
||||
toast.error(
|
||||
"An error occurred while disconnecting SSO. Please try again.",
|
||||
{ duration: 3000 }
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{ width: "100%", height: "100%",boxSizing: 'border-box', padding: "27px 10px 19px 27px", backgroundColor: theme.palette.platformColor , borderRadius: '16px', }}>
|
||||
<div style={{ height: "100%", width: "100%", overflowX: 'hidden', scrollbarColor: theme.palette.scrollbarColorTransparent, scrollbarWidth: 'thin'}} >
|
||||
@@ -316,6 +425,81 @@ const SSOTab = ({selectedOrganization, userdata, isEditOrgTab, globalUrl, handle
|
||||
<Typography variant="h5" style={{ width: "100%", fontWeight: 500, fontSize: 24}}>
|
||||
SSO Configuration
|
||||
</Typography>
|
||||
|
||||
{/* SSO Connection section - moved to top */}
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
marginTop: 20,
|
||||
width: "100%",
|
||||
paddingBottom: 10,
|
||||
}}
|
||||
>
|
||||
<Typography variant="body2" color="textSecondary" style={{ margin: "5px 0px 5px 0px", fontSize: 16, fontFamily: "var(--zds-typography-base,Inter,Helvetica,arial,sans-serif)" }}>
|
||||
{checkingSSOStatus
|
||||
? "Checking your SSO connection status..."
|
||||
: userSSOConnected
|
||||
? "Your account is connected with this org's SSO!"
|
||||
: "Connect your account with this org's SSO!"
|
||||
}
|
||||
</Typography>
|
||||
<Tooltip
|
||||
title={
|
||||
checkingSSOStatus
|
||||
? "Checking SSO connection status..."
|
||||
: userSSOConnected
|
||||
? "Your account is already connected to SSO"
|
||||
: !(
|
||||
ssoEntrypoint?.length > 0 ||
|
||||
ssoCertificate?.length > 0 ||
|
||||
openidAuthorization?.length > 0 ||
|
||||
openidClientId?.length > 0
|
||||
)
|
||||
? "SSO must be configured for this organization before you can connect."
|
||||
: ""
|
||||
}
|
||||
>
|
||||
<div style={{ display: 'flex', gap: '10px', alignItems: 'center' }}>
|
||||
<span style={{ width: 150 }}>
|
||||
<Button
|
||||
variant={userSSOConnected ? "contained" : "outlined"}
|
||||
color={userSSOConnected ? "success" : "primary"}
|
||||
style={{ width: 150, textTransform: "none", margin: "10px 10px 10px 0px", whiteSpace: "nowrap" }}
|
||||
disabled={
|
||||
checkingSSOStatus ||
|
||||
userSSOConnected ||
|
||||
!(
|
||||
ssoEntrypoint?.length > 0 ||
|
||||
ssoCertificate?.length > 0 ||
|
||||
openidAuthorization?.length > 0 ||
|
||||
openidClientId?.length > 0
|
||||
)
|
||||
}
|
||||
onClick={HandleTestSSO}
|
||||
>
|
||||
{checkingSSOStatus ? "Checking..." : userSSOConnected ? "Connected" : "Connect with SSO"}
|
||||
</Button>
|
||||
</span>
|
||||
|
||||
{userSSOConnected && (
|
||||
<Tooltip title="Disconnect your account from SSO">
|
||||
<span style={{ width: 120 }}>
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="error"
|
||||
style={{ width: 120, textTransform: "none", margin: "10px 0px 10px 0px", whiteSpace: "nowrap" }}
|
||||
onClick={HandleDisconnectSSO}
|
||||
>
|
||||
Disconnect
|
||||
</Button>
|
||||
</span>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
@@ -339,6 +523,7 @@ const SSOTab = ({selectedOrganization, userdata, isEditOrgTab, globalUrl, handle
|
||||
sx={{marginBottom: 0.6, marginTop: 0.6}}
|
||||
name="onOffSwitch"
|
||||
color="primary"
|
||||
disabled={!isAdmin}
|
||||
title="Make SAML SSO or OpenID Authentication Required or Optional for Your Organization"
|
||||
/>
|
||||
{SSORequired ? "Required" : "Optional"}
|
||||
@@ -369,7 +554,6 @@ const SSOTab = ({selectedOrganization, userdata, isEditOrgTab, globalUrl, handle
|
||||
sx={{marginBottom: 0.6, marginTop: 0.6}}
|
||||
name="onOffSwitch"
|
||||
color="primary"
|
||||
title="Disable auto-provisioning of users in SSO"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -399,6 +583,7 @@ const SSOTab = ({selectedOrganization, userdata, isEditOrgTab, globalUrl, handle
|
||||
sx={{marginBottom: 0.6, marginTop: 0.6}}
|
||||
name="onOffSwitch"
|
||||
color="primary"
|
||||
disabled={!isAdmin}
|
||||
title="Disable auto-provisioning of users in SSO"
|
||||
/>
|
||||
</div>
|
||||
@@ -428,58 +613,12 @@ const SSOTab = ({selectedOrganization, userdata, isEditOrgTab, globalUrl, handle
|
||||
sx={{marginBottom: 0.6, marginTop: 0.6}}
|
||||
name="onOffSwitch"
|
||||
color="primary"
|
||||
disabled={!isAdmin}
|
||||
title="Disable auto-provisioning of users in SSO"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
marginTop: 30,
|
||||
width: "100%",
|
||||
paddingBottom: 10,
|
||||
}}
|
||||
>
|
||||
<Typography variant="body2" color="textSecondary" style={{ margin: "5px 0px 5px 0px", fontSize: 16, fontFamily: "var(--zds-typography-base,Inter,Helvetica,arial,sans-serif)" }}>
|
||||
You can test your SSO configuration by clicking the button below.
|
||||
Before testing, ensure you have set Open ID Connect or SAML SSO
|
||||
credentials.
|
||||
</Typography>
|
||||
<Tooltip
|
||||
title={
|
||||
!(
|
||||
ssoEntrypoint?.length > 0 ||
|
||||
ssoCertificate?.length > 0 ||
|
||||
openidAuthorization?.length > 0 ||
|
||||
openidClientId?.length > 0
|
||||
)
|
||||
? "Please ensure all SSO credentials are set before testing."
|
||||
: ""
|
||||
}
|
||||
>
|
||||
<span style={{ width: 100 }}>
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="primary"
|
||||
style={{ width: 100, textTransform: "none", margin: "10px 10px 10px 0px", whiteSpace: "nowrap" }}
|
||||
disabled={
|
||||
!(
|
||||
ssoEntrypoint?.length > 0 ||
|
||||
ssoCertificate?.length > 0 ||
|
||||
openidAuthorization?.length > 0 ||
|
||||
openidClientId?.length > 0
|
||||
)
|
||||
}
|
||||
onClick={HandleTestSSO}
|
||||
>
|
||||
Test SSO
|
||||
</Button>
|
||||
</span>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<Grid item xs={12} sx={{marginTop: 2}}>
|
||||
<span style={{ display: "flex", flexDirection: "column" }}>
|
||||
<Typography variant="h5" color="textPrimary" style={{ textAlign: "left", fontFamily: "var(--zds-typography-base,Inter,Helvetica,arial,sans-serif)", fontSize: 24, fontWeight: 500, }}>OpenID connect</Typography>
|
||||
@@ -514,6 +653,7 @@ const SSOTab = ({selectedOrganization, userdata, isEditOrgTab, globalUrl, handle
|
||||
onChange={(e) => setShowOpenIdCred(e.target.checked)}
|
||||
name="showOpenIdCred"
|
||||
color="primary"
|
||||
disabled={!isAdmin}
|
||||
/>
|
||||
</div>
|
||||
<Grid container style={{ marginTop: 8, }} spacing={2}>
|
||||
@@ -536,6 +676,7 @@ const SSOTab = ({selectedOrganization, userdata, isEditOrgTab, globalUrl, handle
|
||||
variant="outlined"
|
||||
placeholder="The OpenID client ID from the identity provider"
|
||||
value={showOpenIdCred ? openidClientId : openidClientId?.length > 0 ? "•".repeat(50) : ""}
|
||||
disabled={!isAdmin}
|
||||
onChange={(e) => setOpenidClientId(e.target.value)}
|
||||
onFocus={(e) => setShowOpenIdCred(true)}
|
||||
onBlur={(e) => setShowOpenIdCred(false)}
|
||||
@@ -574,6 +715,7 @@ const SSOTab = ({selectedOrganization, userdata, isEditOrgTab, globalUrl, handle
|
||||
variant="outlined"
|
||||
placeholder="The OpenID client secret - DONT use this if dealing with implicit auth / PKCE"
|
||||
value={showOpenIdCred ? openidClientSecret : openidClientSecret?.length > 0 ? "•".repeat(50) : ""}
|
||||
disabled={!isAdmin}
|
||||
onChange={(e) => {
|
||||
setOpenidClientSecret(e.target.value);
|
||||
}}
|
||||
@@ -616,6 +758,7 @@ const SSOTab = ({selectedOrganization, userdata, isEditOrgTab, globalUrl, handle
|
||||
rows={2}
|
||||
placeholder="The OpenID authorization URL (usually ends with /authorize)"
|
||||
value={showOpenIdCred ? openidAuthorization : openidAuthorization?.length > 0 ? "•".repeat(50) : ""}
|
||||
disabled={!isAdmin}
|
||||
onChange={(e) => {
|
||||
setOpenidAuthorization(e.target.value)
|
||||
}}
|
||||
@@ -656,6 +799,7 @@ const SSOTab = ({selectedOrganization, userdata, isEditOrgTab, globalUrl, handle
|
||||
rows={2}
|
||||
placeholder="The OpenID token URL (usually ends with /token)"
|
||||
value={showOpenIdCred ? openidToken : openidToken?.length > 0 ? "•".repeat(50) : ""}
|
||||
disabled={!isAdmin}
|
||||
onChange={(e) => {
|
||||
setOpenidToken(e.target.value)
|
||||
}}
|
||||
@@ -682,8 +826,8 @@ const SSOTab = ({selectedOrganization, userdata, isEditOrgTab, globalUrl, handle
|
||||
{/*isCloud ? null : */}
|
||||
<Grid item xs={12} sx={{ marginTop: 3.5 }} >
|
||||
<Typography variant="h5" color="textPrimary" style={{ textAlign: "left", fontFamily: "var(--zds-typography-base,Inter,Helvetica,arial,sans-serif)", fontSize: 24, fontWeight: 500, }}>SAML SSO (v1.1)</Typography>
|
||||
<Typography variant="body2" color="textSecondary" style={{ textAlign: "left", marginTop: 8, fontFamily: "var(--zds-typography-base,Inter,Helvetica,arial,sans-serif)", fontSize: 16, fontWeight: 400, }}>
|
||||
IdP URL for Shuffle SAML/SSO: <Link to={`${globalUrl}/api/v1/login_sso`} target="_blank" style={{ color: theme.palette.text.secondary, textDecoration: "none" }}>{`${globalUrl}/api/v1/login_sso`}</Link>
|
||||
<Typography variant="body2" color="textSecondary" style={{ textAlign: "left", marginTop: 4, fontFamily: "var(--zds-typography-base,Inter,Helvetica,arial,sans-serif)", fontSize: 16, fontWeight: 400, fontStyle: "italic", color: red }}>
|
||||
Note: Support for SAML SSO was deprecated due to potential security issues. Please consider migrating to OpenID Connect for better compatibility and features.
|
||||
</Typography>
|
||||
<div style={{ display: 'flex', marginTop: 10, }}>
|
||||
<Typography
|
||||
@@ -702,6 +846,7 @@ const SSOTab = ({selectedOrganization, userdata, isEditOrgTab, globalUrl, handle
|
||||
onChange={(e) => setShowSamlCred(e.target.checked)}
|
||||
name="showSamlCred"
|
||||
color="primary"
|
||||
disabled={!isAdmin}
|
||||
/>
|
||||
</div>
|
||||
<Grid container style={{ marginTop: 10, }} spacing={2}>
|
||||
@@ -725,6 +870,7 @@ const SSOTab = ({selectedOrganization, userdata, isEditOrgTab, globalUrl, handle
|
||||
variant="outlined"
|
||||
placeholder="The entrypoint URL from your provider"
|
||||
value={showSamlCred ? ssoEntrypoint : ssoEntrypoint?.length > 0 ? "•".repeat(50) : ""}
|
||||
disabled={!isAdmin}
|
||||
onChange={(e) => {
|
||||
setSsoEntrypoint(e.target.value);
|
||||
}}
|
||||
@@ -765,6 +911,7 @@ const SSOTab = ({selectedOrganization, userdata, isEditOrgTab, globalUrl, handle
|
||||
rows={2}
|
||||
placeholder="The X509 certificate to use"
|
||||
value={showSamlCred ? ssoCertificate : ssoCertificate?.length > 0 ? "•".repeat(50) : ""}
|
||||
disabled={!isAdmin}
|
||||
onFocus={(e) => setShowSamlCred(true)}
|
||||
onBlur={(e) => setShowSamlCred(false)}
|
||||
onChange={(e) => {
|
||||
|
||||
Reference in New Issue
Block a user