diff --git a/backend/go-app/go.mod b/backend/go-app/go.mod index 8be40701..ec7db684 100644 --- a/backend/go-app/go.mod +++ b/backend/go-app/go.mod @@ -2,7 +2,7 @@ module shuffle go 1.22.2 -replace github.com/shuffle/shuffle-shared => ../../../shuffle-shared +//replace github.com/shuffle/shuffle-shared => ../../../shuffle-shared require ( cloud.google.com/go/datastore v1.15.0 @@ -18,7 +18,7 @@ require ( github.com/gorilla/mux v1.8.1 github.com/h2non/filetype v1.1.3 github.com/satori/go.uuid v1.2.0 - github.com/shuffle/shuffle-shared v0.7.0 + github.com/shuffle/shuffle-shared v0.7.92 golang.org/x/crypto v0.32.0 google.golang.org/api v0.176.1 google.golang.org/grpc v1.68.1 diff --git a/backend/go-app/main.go b/backend/go-app/main.go index a4ac387f..e1f0defa 100755 --- a/backend/go-app/main.go +++ b/backend/go-app/main.go @@ -5107,6 +5107,8 @@ func initHandlers() { r.HandleFunc("/api/v1/apps/categories/run", shuffle.RunCategoryAction).Methods("POST", "OPTIONS") r.HandleFunc("/api/v1/apps/upload", handleAppZipUpload).Methods("POST", "OPTIONS") r.HandleFunc("/api/v1/apps/{appId}/activate", activateWorkflowAppDocker).Methods("GET", "OPTIONS") + r.HandleFunc("/api/v1/apps/{appId}/deactivate", activateWorkflowAppDocker).Methods("GET", "OPTIONS") + r.HandleFunc("/api/v1/apps/{appId}/distribute", activateWorkflowAppDocker).Methods("GET", "OPTIONS") r.HandleFunc("/api/v1/apps/frameworkConfiguration", shuffle.GetFrameworkConfiguration).Methods("GET", "OPTIONS") r.HandleFunc("/api/v1/apps/frameworkConfiguration", shuffle.SetFrameworkConfiguration).Methods("POST", "OPTIONS") r.HandleFunc("/api/v1/apps/{appId}", shuffle.UpdateWorkflowAppConfig).Methods("PATCH", "OPTIONS") @@ -5261,7 +5263,7 @@ func initHandlers() { r.HandleFunc("/api/v1/orgs/{orgId}/datastore/{cache_key}", shuffle.HandleDeleteCacheKey).Methods("DELETE", "OPTIONS") // Docker orborus specific - downloads an image - r.HandleFunc("/api/v1/get_docker_image", getDockerImage).Methods("POST", "OPTIONS") + r.HandleFunc("/api/v1/get_docker_image", getDockerImage).Methods("POST", "GET", "OPTIONS") r.HandleFunc("/api/v1/login_sso", shuffle.HandleSSO).Methods("GET", "POST", "OPTIONS") r.HandleFunc("/api/v1/login_openid", shuffle.HandleOpenId).Methods("GET", "POST", "OPTIONS") diff --git a/frontend/src/components/AnalyticsTab.jsx b/frontend/src/components/AnalyticsTab.jsx new file mode 100644 index 00000000..8938262e --- /dev/null +++ b/frontend/src/components/AnalyticsTab.jsx @@ -0,0 +1,275 @@ +import React, { useEffect, useState } from 'react'; +import Switch from '@mui/material/Switch'; +import { Typography, Button } from '@mui/material'; +import { useNavigate, Link, useParams } from "react-router-dom"; +import { Bar } from 'react-chartjs-2'; +import Grid from '@mui/material/Grid'; +import SearchIcon from '@mui/icons-material/Search'; +import NewReleasesIcon from '@mui/icons-material/NewReleases'; +import MailOutlineIcon from '@mui/icons-material/MailOutline'; + +const AnalyticsTab = (props) => { + const { userdata, globalUrl, serverside } = props; + const [checked, setChecked] = useState(false); + const [selectedOption, setSelectedOption] = useState('all'); + const [expand, setExpand] = useState(false) + const isCloud = (window.location.host === "localhost:3002" || window.location.host === "shuffler.io") ? true : (process.env.IS_SSR === "true"); + + const handleOptionChange = (option) => { + setSelectedOption(option); + // You can perform actions based on the selected option, such as filtering data + }; + + const handleChange = (event) => { + setChecked(event.target.checked); + }; + + const allData = { + labels: ['Email analysis', 'Email Management', 'EDR to Ticket', 'Ticket Analysis'], + datasets: [ + { + label: 'Revisions', + data: [12, 19, 3, 5], // Data for revisions + backgroundColor: '#FF8444', + borderWidth: 1, + // borderRadius: 60, + barPercentage: 0.7, + categoryPercentage: 0.5, + }, + { + label: 'Runs', + data: [8, 15, 5, 8], // Data for runs + backgroundColor: '#9747FF', + borderWidth: 1, + // borderRadius: 60, + barPercentage: 0.7, + categoryPercentage: 0.5 + } + ] + }; + + let data; + if (selectedOption === 'all') { + data = allData; + } else if (selectedOption === 'revisions') { + data = { + labels: allData.labels, + datasets: [allData.datasets[0]] // Show only revisions data + }; + } else if (selectedOption === 'run') { + data = { + labels: allData.labels, + datasets: [allData.datasets[1]] // Show only runs data + }; + } + + // Options for the chart + const options = { + scales: { + yAxes: [ + { + ticks: { + beginAtZero: true + } + } + ] + }, + }; + return ( +
+
+
Timeline
+
+
+
+
+
+
Apps
+ Category +
+ { setExpand(prevExpand => !prevExpand); }} style={{ color: "#FF8444" }}>Expand +
+ {expand ? null : +
+
+ Onboarding + + +
+ + {checked ? +
: + null + } +
+
+ +
+ + {checked ? +
: + null + } +
+
+ +
+ + {checked ? +
: + null + } +
+
+
+
+
+
+ Other + + +
+ + {checked ? +
: + null + } +
+
+ +
+ + {checked ? +
: + null + } +
+
+ +
+ + {checked ? +
: + null + } +
+
+
+
+
} +
+
+
Workflows
+
+ + + +
+ +
+
+
+
Insights
+
+
+
+
Sessions Overview
+
+
+
+ 2.8 Hours +
+
+ Avg. Activity per session +
+
+
+
+ /usercases/edr to ticket +
+
+ Last visited page +
+
+
+
+ /workflow/email management +
+
+ Most visited page +
+
+
+
+
+ ); +}; + +export default AnalyticsTab; diff --git a/frontend/src/components/AppAuthTab.jsx b/frontend/src/components/AppAuthTab.jsx index dbd29e3d..fb2af4ff 100644 --- a/frontend/src/components/AppAuthTab.jsx +++ b/frontend/src/components/AppAuthTab.jsx @@ -67,7 +67,7 @@ import { Context } from '../context/ContextApi.jsx'; const searchClient = algoliasearch( "JNSS5CFDZZ", "db08e40265e2941b9a7d8f644b6e5240" -); +) const AppAuthTab = memo((props) => { const { globalUrl, userdata, isCloud, selectedOrganization } = props; diff --git a/frontend/src/components/AppSearch1.jsx b/frontend/src/components/AppSearch1.jsx new file mode 100644 index 00000000..3947f640 --- /dev/null +++ b/frontend/src/components/AppSearch1.jsx @@ -0,0 +1,214 @@ +import React, { useState, useEffect, useRef } from 'react'; +import theme from '../theme.jsx'; +import { Link, useNavigate } from 'react-router-dom'; +import { Search as SearchIcon, CloudQueue as CloudQueueIcon, Code as CodeIcon } from '@mui/icons-material'; + +//import algoliasearch from 'algoliasearch/lite'; +import algoliasearch from 'algoliasearch'; +import { InstantSearch, connectSearchBox, connectHits } from 'react-instantsearch-dom'; +import { + Grid, + Paper, + TextField, + InputAdornment, + Typography, +} from '@mui/material'; +const searchClient = algoliasearch("JNSS5CFDZZ", "db08e40265e2941b9a7d8f644b6e5240") +const Appsearch = props => { + const { maxRows, showName, showSuggestion, isMobile, globalUrl, parsedXs, newSelectedApp, setNewSelectedApp, defaultSearch, showSearch, ConfiguredHits, userdata, cy, isCreatorPage, actionImageList, setActionImageList, setUserSpecialzedApp, placeholder, + + } = props + + let navigate = useNavigate(); + const rowHandler = maxRows === undefined || maxRows === null ? 50 : maxRows + const xs = parsedXs === undefined || parsedXs === null ? 12 : parsedXs + const [open, setOpen] = React.useState(false); + const [value, setValue] = useState(""); + window.title = "Shuffle | Apps | Find and integration any app" + + const useCloseOnBlur = (setOpen) => { + useEffect(() => { + // Add event listener to detect clicks outside of the search box + const handleClickOutside = (event) => { + // Check if the click is outside the search box + if (!event.target.closest('.search-box')) { + setOpen(false); + } + }; + + document.addEventListener('mousedown', handleClickOutside); + + // Clean up event listener + return () => { + document.removeEventListener('mousedown', handleClickOutside); + }; + }, [setOpen]); // Ensure that this effect runs whenever setOpen changes + }; + + useCloseOnBlur(setOpen); + + const SearchBox = ({ currentRefinement, refine, isSearchStalled }) => { + useEffect(() => { + //console.log("FIRST LOAD ONLY? RUN REFINEMENT: !", currentRefinement) + if (defaultSearch !== undefined && defaultSearch !== null) { + refine(defaultSearch) + } + }, []) + + return ( +
+
+ +
+ { + navigate("/search?q=" + currentRefinement, { state: value, replace: true }) + //navigate("/search?q="+currentRefinement, { state: value, replace: true }) + //window.open("/apps"+currentRefinement, "_blank") + }} + style={{ cursor: 'pointer', width: isMobile ? 20 : "", marginright: 5, marginTop: isMobile ? 6 : 7 }} + /> +
+ + ), + }} + autoComplete="off" + type="search" + color="primary" + placeholder={placeholder !== undefined ? placeholder : "Search more than 2500 Apps"} + id="shuffle_search_field" + onChange={(event) => { + // Remove "q" from URL + // removeQuery("q") + refine(event.currentTarget.value) + }} + onKeyDown={(event) => { + if (event.keyCode === 13) { + navigate("/search?q=" + currentRefinement, { state: value, replace: true }); + } + }} + onClick={(event) => { + setOpen(true); + }} + /> + {/*isSearchStalled ? 'My search is stalled' : ''*/} + +
+ ) + } + + const Hits = ({ hits, currentRefinement }) => { + const [mouseHoverIndex, setMouseHoverIndex] = useState(-1) + var counted = 0 + + return ( + + {hits.map((data, index) => { + const paperStyle = { + backgroundColor: index === mouseHoverIndex ? "rgba(255,255,255,1)" : "#38383A", + color: index === mouseHoverIndex ? theme.palette.inputColor : "rgba(255,255,255,0.8)", + // border: newSelectedApp.objectID !== data.objectID ? `1px solid rgba(255,255,255,0.2)` : "2px solid #f86a3e", + textAlign: "left", + padding: 10, + cursor: "pointer", + position: "relative", + overflow: "hidden", + width: 402, + minHeight: 37, + maxHeight: 52, + } + + if (counted === 12 / xs * rowHandler) { + return null + } + + counted += 1 + var parsedname = data.name.valueOf() + parsedname = (parsedname.charAt(0).toUpperCase() + parsedname.substring(1)).replaceAll("_", " ") + return ( + { + setMouseHoverIndex(index) + }} onMouseOut={() => { + setMouseHoverIndex(-1) + }} onClick={() => { + if (setNewSelectedApp !== undefined) { + setNewSelectedApp(data.name) + } else { + //need to add perfect url which redirect direct to app page + const newname = data.name.toLowerCase().replaceAll(" ", "_") + window.open("/apps/" + newname, "_blank") + } + }}> +
+ {data.name} + + {parsedname} + +
+
+ ) + })} +
+ ) + } + + const InputHits = ConfiguredHits === undefined ? Hits : ConfiguredHits + const CustomSearchBox = connectSearchBox(SearchBox) + const CustomHits = connectHits(InputHits) + + return ( +
+ +
+ +
+
+ {open ? : null} +
+
+
+ ) +} + +export default Appsearch; diff --git a/frontend/src/components/AppStats.jsx b/frontend/src/components/AppStats.jsx index a290ba92..d23d0833 100644 --- a/frontend/src/components/AppStats.jsx +++ b/frontend/src/components/AppStats.jsx @@ -343,14 +343,14 @@ const AppStats = (defaultprops) => { */}
- {clickData === undefined ? + {clickData === undefined || clickData === null || clickData?.length === 0 ? null : }
- {conversionData === undefined ? + {conversionData === undefined || conversionData === null || conversionData?.length === 0 ? null : @@ -365,4 +365,4 @@ const AppStats = (defaultprops) => { return dataWrapper; } -export default AppStats; \ No newline at end of file +export default AppStats; diff --git a/frontend/src/components/EnvironmentTab.jsx b/frontend/src/components/EnvironmentTab.jsx index f7b29d5b..be7c2c0d 100644 --- a/frontend/src/components/EnvironmentTab.jsx +++ b/frontend/src/components/EnvironmentTab.jsx @@ -39,6 +39,7 @@ import { import { toast } from 'react-toastify'; import { Context } from '../context/ContextApi.jsx'; import { green, red } from '../views/AngularWorkflow.jsx' +import AppSearch from "../components/AppSearch1.jsx"; const EnvironmentTab = memo((props) => { const { globalUrl, isCloud, userdata, selectedOrganization } = props; @@ -59,6 +60,9 @@ const EnvironmentTab = memo((props) => { const [showDistributionPopup, setShowDistributionPopup] = React.useState(false); const [selectedEnvironment, setSelectedEnvironment] = React.useState(null); const [selectedSubOrg, setSelectedSubOrg] = React.useState([]); + const [showLocationActionModal, setShowLocationActionModal] = React.useState(undefined) + + useEffect(() => { getEnvironments(); setModalUser({}); @@ -420,7 +424,9 @@ const EnvironmentTab = memo((props) => { -e SHUFFLE_WORKER_IMAGE="ghcr.io/shuffle/shuffle-worker:nightly" \\ -e SHUFFLE_SWARM_CONFIG=run \\ -e SHUFFLE_LOGS_DISABLED=true \\ - -e BASE_URL="${newUrl}" \\${addProxy ? "\n -e HTTPS_PROXY=IP:PORT \\" : ""}${skipPipeline ? "\n -e SHUFFLE_SKIP_PIPELINES=true \\" : ""} + -e BASE_URL="${newUrl}" \\${addProxy ? ` + -e HTTPS_PROXY=IP:PORT \\` : ""}${skipPipeline ? ` + -e SHUFFLE_SKIP_PIPELINES=true \\` : ""} ghcr.io/shuffle/shuffle-orborus:latest `) } else if (installationTab === 2) { @@ -435,7 +441,9 @@ const EnvironmentTab = memo((props) => { -e AUTH="${auth}" \\ -e ENVIRONMENT_NAME="${environment.Name}" \\ -e ORG="${props.userdata.active_org.id}" \\ - -e BASE_URL="${newUrl}" \\${addProxy ? "\n -e HTTPS_PROXY=IP:PORT \\" : ""}${skipPipeline ? "\n -e SHUFFLE_SKIP_PIPELINES=true \\" : ""} + -e BASE_URL="${newUrl}" \\${addProxy ? ` + -e HTTPS_PROXY=IP:PORT \\` : ""}${skipPipeline ? ` + -e SHUFFLE_SKIP_PIPELINES=true \\` : ""} ghcr.io/shuffle/shuffle-orborus:latest` return commandData @@ -592,6 +600,70 @@ const EnvironmentTab = memo((props) => { return queue; }; + const LocationActionModal = (props) => { + const { showLocationActionModal } = props + + const [searchQuery, setSearchQuery] = React.useState(""); + + if (showLocationActionModal === undefined || showLocationActionModal === null) { + return null + } + + if (showLocationActionModal?.open !== true) { + return null + } + + + + return ( + { + setShowLocationActionModal(undefined) + }} + PaperProps={{ + sx: { + borderRadius: theme?.palette?.DialogStyle?.borderRadius, + border: theme?.palette?.DialogStyle?.border, + fontFamily: theme?.typography?.fontFamily, + backgroundColor: theme?.palette?.DialogStyle?.backgroundColor, + zIndex: 1000, + minWidth: 600, + minHeight: 500, + overflow: "auto", + '& .MuiDialogContent-root': { + backgroundColor: theme?.palette?.DialogStyle?.backgroundColor, + }, + '& .MuiDialogTitle-root': { + backgroundColor: theme?.palette?.DialogStyle?.backgroundColor, + }, + '& .MuiDialogActions-root': { + backgroundColor: theme?.palette?.DialogStyle?.backgroundColor, + }, + }, + }} + > + +
+ Select a job to send +
+
+ + + + Find app to re-download + + + + +
+ ) + } + const editEnvironmentConfig = (id, selectedSubOrg, cacheKey) => { const data = { action: "suborg_distribute", @@ -804,20 +876,23 @@ const EnvironmentTab = memo((props) => { borderBottom: "1px solid #494949", }} > - {["Type", "Status", "Scale", "Pipeline", "Name", "Type", "Queue", "Actions", "Distribution"].map((header, index) => ( - - ))} + {["Type", "Status", "Scale", "Pipeline", "Name", "Type", "Queue", "Actions", "Distribution"].map((header, index) => { + + return ( + + ) + })} {showLoader ? [...Array(6)].map((_, rowIndex) => ( @@ -919,9 +994,10 @@ const EnvironmentTab = memo((props) => { return ( <> + { if (environment.Type === "cloud") { toast("Cloud environments are not configurable. To see what is possible, create a new environment.") @@ -1156,7 +1232,7 @@ const EnvironmentTab = memo((props) => { primary={environment.Type} primaryTypographyProps={{ style:{ - minWidth: 100, + minWidth: 70, overflow: "hidden", whiteSpace: 'nowrap', textOverflow: 'ellipsis', @@ -1228,6 +1304,7 @@ const EnvironmentTab = memo((props) => { > {environment.archived ? "Activate" : "Disable"} + + + {environment.Type === "cloud" ? null : + + } + + {setIsExpanded(prev => !prev)}}> {listItemExpanded === index ? : } @@ -1429,7 +1533,7 @@ const EnvironmentTab = memo((props) => { } setCommandController(commandController) - setUpdate(Math.random()) + setUpdate(Math.random()) }} />
@@ -1544,11 +1648,20 @@ const EnvironmentTab = memo((props) => { ) }
- {/* */} -
- + + {showLocationActionModal !== undefined && showLocationActionModal !== null && showLocationActionModal?.open === true ? +
+ +
+ + : null } + + + ) }); diff --git a/frontend/src/views/ApiExplorerWrapper.jsx b/frontend/src/views/ApiExplorerWrapper.jsx index dec13a5b..72dc93ca 100644 --- a/frontend/src/views/ApiExplorerWrapper.jsx +++ b/frontend/src/views/ApiExplorerWrapper.jsx @@ -243,7 +243,7 @@ const ApiExplorerWrapper = (props) => { .then((responseJson) => { if (responseJson.success === true) { if (responseJson.openapi === undefined || responseJson.openapi === null) { - toast.warning("Loaded App, but no API found. Redirecting to app search..") + toast.warning("Loaded App, but no API found. Redirecting back to app..") navigate(`/apps/${appid}`) } else { handleDecodeOfOpenApiData(responseJson); @@ -378,7 +378,7 @@ const ApiExplorerWrapper = (props) => { const parsedHeaders = {}; if (typeof headers === 'string' && headers) { - const splitHeaders = headers.split("\n"); + const splitHeaders = headers.split(`\n`) splitHeaders.forEach(header => { let splitItem; @@ -529,7 +529,7 @@ const ApiExplorerWrapper = (props) => { return array .map(item => (item.key.trim().length > 0 && item.value.trim().length > 0 ? `${item.key}=${item.value}` : "")) .filter(str => str.length > 0) - .join("\n"); + .join(``); }; var appid = ""; diff --git a/frontend/src/views/AppExplorer.jsx b/frontend/src/views/AppExplorer.jsx index bcdc77b5..93bc9f89 100644 --- a/frontend/src/views/AppExplorer.jsx +++ b/frontend/src/views/AppExplorer.jsx @@ -226,7 +226,7 @@ const AppExplorer = (props) => { const [anchorEl, setAnchorEl] = React.useState(null); const [creatorProfile, setCreatorProfile] = React.useState({}); const [selectedTab, setSelectedTab] = React.useState(0); - const defaultDocs = "\n\n## No Shuffle-specific app documentation is available yet.\n\n## Need more information about the app? [Contact us](/contact) and [Join the Community](https://discord.gg/B2CBzUm) and find others using this app." + const defaultDocs = `\n\n## No Shuffle-specific app documentation is available yet.\n\n## Need more information about the app? [Contact us](/contact) and [Join the Community](https://discord.gg/B2CBzUm) and find others using this app.` const [sharingConfiguration, setSharingConfiguration] = React.useState("you"); const [appdata, setAppData] = React.useState({}); const [appDocumentation, setAppDocumentation] = useState(defaultDocs) @@ -739,13 +739,16 @@ const AppExplorer = (props) => { } }; - const activateApp = () => { + const activateApp = (action) => { if (serverside === true) { - return; + return } const appExists = userdata.active_apps !== undefined && userdata.active_apps !== null && userdata.active_apps.includes(appId) - const url = appExists ? `${globalUrl}/api/v1/apps/${appId}/deactivate` : `${globalUrl}/api/v1/apps/${appId}/activate` + var url = appExists ? `${globalUrl}/api/v1/apps/${appId}/deactivate` : `${globalUrl}/api/v1/apps/${appId}/activate` + if (action !== undefined && action !== null) { + url = `${globalUrl}/api/v1/apps/${appId}/${action}` + } fetch(url, { method: "GET", headers: { @@ -763,21 +766,32 @@ const AppExplorer = (props) => { }) .then((responseJson) => { if (responseJson.success === false) { - if (responseJson.reason !== undefined) { - toast("Failed to activate the app: "+responseJson.reason); - } else { - toast("Failed to activate the app"); - } + if (action === undefined || action === null) { + if (responseJson.reason !== undefined) { + toast("Failed to activate the app: "+responseJson.reason); + } else { + toast("Failed to activate the app"); + } + } else { + if (responseJson.reason !== undefined) { + toast("Failed to perform action: "+responseJson.reason); + } else { + toast("Failed to perform action. Please try again or contact support@shuffler.io"); + } + } } else { if (checkLogin !== undefined && checkLogin !== null) { checkLogin() } - if (appExists) { - toast("App deactivated for your organization! Existing workflows with the app will continue to work.") - } else { - toast("App activated for your organization!") - } + if (action === undefined || action === null) { + if (appExists) { + toast("App deactivated for your organization! Existing workflows with the app will continue to work.") + } else { + toast("App activated for your organization!") + } + } else { + } } }) .catch((error) => { @@ -4028,6 +4042,29 @@ const AppExplorer = (props) => { : null} + {isMobile || userdata?.active_apps === undefined || userdata?.active_apps === null || !userdata?.active_apps?.includes(appId) ? null : + + } + {isMobile ? null : (