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 (
+
+
+
+
+
+
+
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
+
+
+
+
+
+
+
+
+
+
+
+
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 (
+
+ )
+ }
+
+ 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")
+ }
+ }}>
+
+

+
+ {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 (
+
+ )
+ }
+
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 : (