import { Box, Tooltip, Typography, Switch, Button, Dialog, DialogTitle, DialogContent, DialogActions, TextField, Select, MenuItem, FormControl, InputLabel, IconButton, Menu, ListItemIcon, ListItemText, Checkbox, CircularProgress, Skeleton, } from "@mui/material"; 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"; import MoreVertIcon from "@mui/icons-material/MoreVert"; import EditIcon from "@mui/icons-material/Edit"; import DeleteIcon from "@mui/icons-material/Delete"; import AddCircleOutlineIcon from "@mui/icons-material/AddCircleOutline"; import OpenInNewIcon from '@mui/icons-material/OpenInNew'; import DeleteOutlineIcon from "@mui/icons-material/DeleteOutline"; 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) => { if (!category) return "/images/appCategories/other.svg"; const lowerCategory = category.toLowerCase(); if (lowerCategory.includes("communication")) { return "/images/appCategories/cases.svg"; } else if (lowerCategory.includes("cases")) { return "/images/appCategories/cases.svg"; } else if (lowerCategory.includes("network")) { return "/images/appCategories/network.svg"; } else if (lowerCategory.includes("siem")) { return "/images/appCategories/siem.svg"; } else if (lowerCategory.includes("edr") || lowerCategory.includes("eradication")) { return "/images/appCategories/edr.svg"; } else if (lowerCategory.includes("iam")) { return "/images/appCategories/iam.svg"; } else if (lowerCategory.includes("assets")) { return "/images/appCategories/assets.svg"; } else if (lowerCategory.includes("intel")) { 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 = ({ isArticlesTab }) => { const { themeMode } = useContext(Context); const theme = getTheme(themeMode); // Skeleton for Articles Tab if (isArticlesTab) { return ( {/* Header image skeleton */} {/* Body skeleton */} ); } // Skeleton for Usecases Tab return (
{/* Source App Icon Skeleton */} {/* Destination App Icon Skeleton */}
); }; // 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); const theme = getTheme(themeMode); const navigate = useNavigate(); const handleClick = (event) => { event.stopPropagation(); event.preventDefault(); setAnchorEl(event.currentTarget); }; const handleClose = () => { setAnchorEl(null); }; const handleEdit = () => { console.log("Edit usecase:", usecase.id); handleClose(); }; const handleStar = () => { console.log("Star usecase:", usecase.id); handleClose(); }; const handleCardClick = (e) => { // Navigate to usecase detail page navigate(`/usecases/${usecase.id}`); }; // Article Card Layout if (isArticlesTab) { return ( {usecase.name} {usecase.created && ( {new Date(usecase.created * 1000).toLocaleDateString("en-US", { year: "numeric", month: "short", day: "numeric", })} )} e.stopPropagation()} > { 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", }} /> { 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", }, }} /> { 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)", }, }} > 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" }} > { e.stopPropagation(); handleOpenDialog(usecase); setAnchorEl(null); }} > Edit Article { e.stopPropagation(); handleDeleteUsecase(usecase.id); }} > Delete Article ); } // Usecase Card Layout return (
{/* Source App Icon */} Source {/* Destination App Icon */} Destination
{ e.stopPropagation(); window.open(`${window.location.origin}/usecases/${usecase.name.toLowerCase().replaceAll(" ", "_")}`, "_blank"); }} sx={{ color: theme.palette.primary.main, cursor: "pointer", }} /> { 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", }, }} /> { e.stopPropagation(); handleClick(e); }} size="small" sx={{ color: theme.palette.text.primary, zIndex: 20, "&:hover": { backgroundColor: themeMode === "dark" ? "rgba(255, 255, 255, 0.1)" : "rgba(0, 0, 0, 0.05)", }, }} > 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" }} > { e.stopPropagation(); handleOpenDialog(usecase); setAnchorEl(null); }} > Edit Usecase { e.stopPropagation(); handleDeleteUsecase(usecase.id); }}> Delete Usecase
{usecase.name}
); }; 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); const [publicWorkflows, setPublicWorkflows] = useState([]); const [usecaseData, setUsecaseData] = useState([]); const [isSubmitting, setIsSubmitting] = useState(false); const [isLoading, setIsLoading] = useState(true); const [isWorkflowLoading, setIsWorkflowLoading] = useState(false); const [formData, setFormData] = useState({ id: "", mainContent: { title: "", description: "", categories: [], publicWorkflowId: "", sourceAppType: "", destinationAppType: "", }, navigation: { items: [ { name: `About ${entityLabel}`, content: [""], }, ], }, public: false, created: null, }); // App categories for dropdowns const appCategories = [ { value: "communication", label: "Communication" }, { value: "cases", label: "Cases" }, { value: "network", label: "Network" }, { value: "siem", label: "SIEM" }, { value: "edr", label: "EDR" }, { value: "eradication", label: "Eradication" }, { value: "iam", label: "IAM" }, { 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" } ]; // Sample workflow options const workflowOptions = [ "Email Analysis Workflow", "Incident Response Workflow", "Alert Triage Workflow", "Threat Hunting Workflow", "Vulnerability Management", ]; useEffect(() => { // Set loading state to true when fetching starts setIsLoading(true); if(!isCloud || !userdata?.active_org?.is_partner) { // If the user is not a partner or if it's not a cloud environment do not make api call :) return; } // Load usecase data from API fetch(`${globalUrl}/api/v1/partners/${partnerData?.id}/usecases`, { method: "GET", headers: { "Content-Type": "application/json", Accept: "application/json", }, credentials: "include", }) .then((response) => { if (response.status !== 200) { console.log("Status not 200 for PARTNER USECASES :O!"); } return response.json(); }) .then((responseJson) => { if (responseJson.success !== false) { const usecases = responseJson.usecases.map((usecase) => ({ id: usecase.id, sourceAppType: usecase.mainContent?.sourceAppType || "", destinationAppType: usecase.mainContent?.destinationAppType || "", srcImg: getCategoryImagePath(usecase.mainContent?.sourceAppType), dstImg: getCategoryImagePath(usecase.mainContent?.destinationAppType), publicWorkflowId: usecase.mainContent?.publicWorkflowId || "", name: usecase.mainContent?.title, description: usecase.mainContent?.description, navigation: usecase.navigation || { items: [] }, categories: usecase.mainContent?.categories || [], public: usecase?.public || false, created: usecase?.created || null, headerImage: getFirstImageFromNavigation(usecase.navigation), })); if (usecases.length > 0) { setUsecaseData(usecases); } else { setUsecaseData([]); } } }) .catch((error) => { console.log(error); }) .finally(() => { // Set loading state to false when fetching completes (success or error) setIsLoading(false); }); }, [partnerData?.id]); // 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 ? `![image](${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", headers: { "Content-Type": "application/json", Accept: "application/json", }, credentials: "include", }) .then((response) => { if (response.status !== 200) { console.log("Status not 200 for WORKFLOW EXECUTION :O!"); } return response.json(); }) .then((responseJson) => { if (responseJson.success !== false) { setPublicWorkflows(responseJson || []); setIsWorkflowLoading(false); }else { toast.info(responseJson.message || "No public workflows found for this organization."); setPublicWorkflows([]); } }) .catch((error) => { console.log(error); setIsWorkflowLoading(false); }); } useEffect(() => { const orgId = userdata?.active_org?.id; if(!isCloud || !userdata?.active_org?.is_partner) { // If the user is not a partner or if it's not a cloud environment do not make api call :) return; } getUserProfileWorkflows(orgId); }, []); const handleUpdateUsecase = async (usecaseId, updatedData) => { try { // Transform the frontend data structure to match the backend expected structure const backendUsecaseData = { id: updatedData.id, companyInfo: { id: partnerData?.id.trim(), name: partnerData?.name.trim(), }, mainContent: { title: updatedData.name.trim(), description: updatedData.description.trim(), categories: updatedData.categories, publicWorkflowId: updatedData.publicWorkflowId.trim(), sourceAppType: updatedData.sourceAppType.trim(), destinationAppType: updatedData.destinationAppType.trim(), }, navigation: updatedData.navigation, public: updatedData.public, edited: Date.now(), }; // return const response = await fetch(`${globalUrl}/api/v1/usecases/${usecaseId}`, { method: "POST", headers: { "Content-Type": "application/json", Accept: "application/json", }, credentials: "include", body: JSON.stringify(backendUsecaseData), }); if (response.status !== 200) { console.error("Failed to update usecase"); toast.error("Failed to update usecase"); return false; } const responseData = await response.json(); toast.success("Usecase published status updated successfully"); return true; } catch (error) { console.error("Error updating usecase:", error); toast.error("Error updating usecase"); return false; } }; const handleDeleteUsecase = async (usecaseId) => { try { const response = await fetch( `${globalUrl}/api/v1/usecases/${usecaseId}`, { method: "DELETE", headers: { "Content-Type": "application/json", Accept: "application/json", }, credentials: "include", } ); if (response.status !== 200) { console.error("Failed to delete usecase"); toast.error("Failed to delete usecase"); return false; } const responseData = await response.json(); toast.success("Usecase deleted successfully"); // Remove the deleted usecase from the state setUsecaseData((prevData) => prevData?.filter((usecase) => usecase.id !== usecaseId) ); // Remove the usecase ID from partnerData setPartnerData((prevData) => ({ ...prevData, usecases: prevData.usecases?.filter((id) => id !== usecaseId), })); return true; } catch (error) { console.error("Error deleting usecase:", error); toast.error("Error deleting usecase"); return false; } }; const handleToggle = async (currentId) => { // Find the current usecase const currentUsecase = usecaseData.find(usecase => usecase.id === currentId); if (!currentUsecase) return; // Optimistically update the UI setUsecaseData((prevData) => prevData.map((usecase) => usecase.id === currentId ? { ...usecase, public: !usecase.public } : usecase ) ); // Prepare the updated data const updatedPublishedStatus = !currentUsecase.public; // Create a copy of the usecase with updated published status const updatedUsecase = { ...currentUsecase, public: updatedPublishedStatus }; toast.info(`Updating usecase ${updatedUsecase.name}...`,{ autoClose: 1000, }) // Send the update to the server const success = await handleUpdateUsecase(currentId, updatedUsecase); // If the update failed, revert the UI change if (!success) { setUsecaseData((prevData) => prevData.map((usecase) => usecase.id === currentId ? { ...usecase, public: currentUsecase.public } : usecase ) ); } }; const handleOpenDialog = (usecase) => { setFormData({ id: usecase?.id || undefined, companyInfo: { id: usecase.companyInfo?.id || "", name: usecase.companyInfo?.name || "", }, mainContent: { title: usecase?.name, description: usecase?.description, categories: usecase?.categories || [], publicWorkflowId: usecase?.publicWorkflowId || "", sourceAppType: usecase?.sourceAppType || "", destinationAppType: usecase?.destinationAppType || "", }, navigation: usecase?.navigation || { items: [ { name: `About ${entityLabel}`, content: [""], }, ], }, public: usecase?.public || false, created: usecase?.created || Math.floor(Date.now() / 1000), }); setOpenDialog(true); }; const handleCloseDialog = () => { setOpenDialog(false); setIsSubmitting(false); // Reset loading state when closing dialog setFormData({ id: "", mainContent: { title: "", description: "", categories: [], publicWorkflowId: "", sourceAppType: "", destinationAppType: "", }, navigation: { items: [ { name: "About Usecase", content: [""], }, ], }, public: false, }); }; const handleSubmit = () => { // Validate required fields const validationErrors = []; // Check for public workflow selection if (!isArticlesTab && (!formData.mainContent.publicWorkflowId || formData.mainContent.publicWorkflowId.trim() === "")) { validationErrors.push("Please select a public workflow"); } if (!isArticlesTab && (!formData.mainContent.sourceAppType || formData.mainContent.sourceAppType.trim() === "")) { validationErrors.push("Source app type is required"); } if (!isArticlesTab && (!formData.mainContent.destinationAppType || formData.mainContent.destinationAppType.trim() === "")) { validationErrors.push("Destination app type is required"); } // Validate that source and destination are different if (formData.mainContent.sourceAppType && formData.mainContent.destinationAppType && formData.mainContent.sourceAppType.trim() === formData.mainContent.destinationAppType.trim()) { validationErrors.push("Source and destination app types must be different"); } // Check main content fields if (!formData.mainContent.title || formData.mainContent.title.trim() === "") { validationErrors.push("Title is required"); } else if (formData.mainContent.title.length < 5) { validationErrors.push("Title must be not be less than 5 characters"); } if (!formData.mainContent.description || formData.mainContent.description.trim() === "") { validationErrors.push("Description is required"); } // Check categories if (!isArticlesTab && (!formData.mainContent.categories || !Array.isArray(formData.mainContent.categories) || formData.mainContent.categories.length === 0)) { validationErrors.push("At least one category must be selected"); } // Check navigation items if (!formData.navigation.items || !Array.isArray(formData.navigation.items) || formData.navigation.items.length === 0) { validationErrors.push("Please add at least one section"); } else { // Validate each section for (let i = 0; i < formData.navigation.items.length; i++) { const item = formData.navigation.items[i]; if (!item.name || item.name.trim() === "") { validationErrors.push(`Section ${i+1} must have a name`); } if (!item.content || !Array.isArray(item.content) || item.content.length === 0 || !item.content.some(content => content && content.trim() !== "")) { validationErrors.push(`Section "${item.name || i+1}" must have content`); } } } // If there are validation errors, show the first one and return if (validationErrors.length > 0) { toast.error(validationErrors[0]); return; } // Set loading state to true setIsSubmitting(true); // Get the image paths for source and destination app types const srcImg = getCategoryImagePath(formData.mainContent.sourceAppType); const dstImg = getCategoryImagePath(formData.mainContent.destinationAppType); const response = { ...formData, companyInfo: { id: partnerData?.id, name: partnerData?.name, }, } fetch(`${globalUrl}/api/v1/usecases/${formData.id}`, { method: "POST", headers: { "Content-Type": "application/json", Accept: "application/json", }, credentials: "include", body: JSON.stringify(response), }) .then((res) => { if (res.status !== 200) { console.error("Failed to submit usecase"); toast.error("Failed to submit usecase"); setIsSubmitting(false); return null; } return res.json(); }) .then((responseData) => { // Reset loading state setIsSubmitting(false); if (responseData) { // Only close the dialog if the request was successful const usecaseData = { id: responseData.usecaseId.trim(), sourceAppType: formData.mainContent?.sourceAppType.trim() || "", destinationAppType: formData.mainContent?.destinationAppType.trim() || "", srcImg: getCategoryImagePath(formData.mainContent?.sourceAppType.trim()), dstImg: getCategoryImagePath(formData.mainContent?.destinationAppType.trim()), publicWorkflowId: formData.mainContent?.publicWorkflowId.trim() || "", name: formData.mainContent?.title.trim(), description: formData.mainContent?.description.trim(), categories: formData.mainContent?.categories || [], navigation: formData.navigation || {}, public: formData?.public || false, created: formData?.created || null, headerImage: getFirstImageFromNavigation(formData.navigation), } setUsecaseData((prevData) => { // Handle case when prevData is null or undefined if (!prevData) { return [usecaseData]; } // Check if the usecase already exists const existingIndex = prevData.findIndex( (usecase) => usecase.id === responseData.usecaseId ); if (existingIndex !== -1) { // Update existing usecase const updatedData = [...prevData]; updatedData[existingIndex] = usecaseData; return updatedData; } else { // Add new usecase return [...prevData, usecaseData]; } }); setPartnerData((prevData) => { // Handle case when prevData is null or undefined if (!prevData) { return { usecases: [responseData.usecaseId.trim()] }; } return { ...prevData, usecases: [ ...(prevData.usecases || []), responseData.usecaseId.trim(), ], }; }); // Show success message if(formData?.id){ toast.success("Usecase updated successfully"); }else{ toast.success("Usecase created successfully, publish it with toggle button"); } handleCloseDialog(); } }) .catch((error) => { console.error("Error submitting usecase:", error); toast.error("Error submitting usecase"); // Reset loading state on error setIsSubmitting(false); }); }; // 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 ( {/* Add Usecase / Article Button */} {selectedTab === "usecases" && "Usecases"} {selectedTab === "articles" && "Articles"} {/* Usecases / Articles Grid */} {isLoading ? ( // Skeleton loading state {/* Display 4 skeleton cards while loading */} {[...Array(4)].map((_, index) => ( ))} ) : filteredUsecases.length > 0 ? ( // Actual data display (filtered based on public workflow ID) {filteredUsecases.map((usecase) => ( ))} ) : ( // Empty state {isArticlesTab ? "No articles found" : "No usecases found"} )} {/* Add Usecase Dialog */} { if (reason !== "backdropClick") { handleCloseDialog(); } }} disableEscapeKeyDown maxWidth="md" fullWidth PaperProps={{ style: { background: theme.palette.backgroundColor, overflow: "hidden", margin: 0, borderRadius: 8, border: theme.palette.defaultBorder, }, }} > {formData?.id ? "Update" : "Add New"} {entityLabel} : {formData?.public ? "Published" : "Draft"} {!isArticlesTab && ( Public Workflow { // Open workflow in new tab - adjust URL as needed window.open(`${window.location.origin}/workflows/${formData.mainContent.publicWorkflowId}`, "_blank"); }} > )} {/* Other data selection (use all data types) */} {!isArticlesTab && ( Coming soon: Files (detection) Coming soon: Datastore category (threatlists) )} {/* App Type Selection */} {!isArticlesTab && ( Source App Type Destination App Type )} Main Content Title setFormData({ ...formData, mainContent: { ...formData.mainContent, title: e.target.value, }, }) } sx={{ "& .MuiOutlinedInput-root": { height: 40, backgroundColor: theme.palette.usecaseDialogFieldColor, color: theme.palette.text.primary, "& fieldset": { border: theme.palette.defaultBorder }, }, }} /> Description setFormData({ ...formData, mainContent: { ...formData.mainContent, description: e.target.value, }, }) } sx={{ "& .MuiOutlinedInput-root": { backgroundColor: theme.palette.usecaseDialogFieldColor, p: 1.5, color: theme.palette.text.primary, "& fieldset": { border: theme.palette.defaultBorder }, }, }} /> {!isArticlesTab && ( Categories )} {/* Navigation Items Section */} Navigation Items {formData.navigation.items.map((item, itemIndex) => ( { const newItems = [...formData.navigation.items]; newItems[itemIndex].name = e.target.value; setFormData({ ...formData, navigation: { items: newItems }, }); }} placeholder="Section Name" sx={{ flex: 1, "& .MuiOutlinedInput-root": { height: 40, backgroundColor: theme.palette.usecaseDialogFieldColor, color: theme.palette.text.primary, "& fieldset": { border: theme.palette.defaultBorder }, }, }} /> { if (itemIndex > 0) { const newItems = [...formData.navigation.items]; [newItems[itemIndex - 1], newItems[itemIndex]] = [ newItems[itemIndex], newItems[itemIndex - 1], ]; setFormData({ ...formData, navigation: { items: newItems }, }); } }} disabled={itemIndex === 0} sx={{ color: theme.palette.text.secondary, "&:not(:disabled):hover": { backgroundColor: themeMode === "dark" ? "rgba(255, 255, 255, 0.08)" : "rgba(0, 0, 0, 0.05)", }, }} > { if ( itemIndex < formData.navigation.items.length - 1 ) { const newItems = [...formData.navigation.items]; [newItems[itemIndex], newItems[itemIndex + 1]] = [ newItems[itemIndex + 1], newItems[itemIndex], ]; setFormData({ ...formData, navigation: { items: newItems }, }); } }} disabled={ itemIndex === formData.navigation.items.length - 1 } sx={{ color: theme.palette.text.secondary, "&:not(:disabled):hover": { backgroundColor: themeMode === "dark" ? "rgba(255, 255, 255, 0.08)" : "rgba(0, 0, 0, 0.05)", }, }} > { const newItems = formData.navigation.items?.filter( (_, i) => i !== itemIndex ); setFormData({ ...formData, navigation: { items: newItems }, }); }} sx={{ color: "#ff4444", "&:hover": { backgroundColor: themeMode === "dark" ? "rgba(255, 68, 68, 0.08)" : "rgba(255, 68, 68, 0.15)", }, }} > {item.content.map((content, contentIndex) => ( { const newItems = [...formData.navigation.items]; newItems[itemIndex].content[contentIndex] = e.target.value; setFormData({ ...formData, navigation: { items: newItems }, }); }} onPaste={(event) => handleContentPaste(event, itemIndex, contentIndex)} onDrop={(event) => handleContentDrop(event, itemIndex, contentIndex)} onDragOver={(event) => event.preventDefault()} placeholder="Content (Markdown supported)" helperText={ Markdown syntax is supported and Use ### for subItem for Table of Contents. Drag and drop or paste images to the content to upload them. } fullWidth sx={{ "& .MuiOutlinedInput-root": { backgroundColor: theme.palette.usecaseDialogFieldColor, color: theme.palette.text.primary, "& fieldset": { border: theme.palette.defaultBorder }, }, "& .MuiOutlinedInput-root textarea": { overflow: "auto", resize: "vertical", }, }} /> {item.content.length > 1 && ( )} ))} {/* */} ))} ); }; export default PartnersUsecasesTab;