diff --git a/backend/go-app/main.go b/backend/go-app/main.go index 6b3ba779..1e8dfddf 100755 --- a/backend/go-app/main.go +++ b/backend/go-app/main.go @@ -35,8 +35,8 @@ import ( "github.com/go-git/go-billy/v5/memfs" "github.com/go-git/go-git/v5" "github.com/go-git/go-git/v5/plumbing" + gitProxy "github.com/go-git/go-git/v5/plumbing/transport" "github.com/go-git/go-git/v5/storage/memory" - gitProxy "github.com/go-git/go-git/v5/plumbing/transport" // Random xj "github.com/basgys/goxml2json" @@ -257,7 +257,6 @@ type Hook struct { Environment string `json:"environment" datastore:"environment"` } - func GetUsersHandler(w http.ResponseWriter, r *http.Request) { data := map[string]interface{}{ "id": "12345", @@ -398,49 +397,49 @@ func checkUsername(Username string) error { } func isGitNoProxy(rawURL string) bool { - noProxy := os.Getenv("NO_PROXY") - if noProxy == "" { - return false - } - - if noProxy == "*" { - return true - } + noProxy := os.Getenv("NO_PROXY") + if noProxy == "" { + return false + } - noProxyList := strings.Split(noProxy, ",") - parsedURL, err := url.Parse(rawURL) - if err != nil { - return false - } - host := parsedURL.Hostname() + if noProxy == "*" { + return true + } - for _,value := range noProxyList { - value = strings.TrimSpace(value) + noProxyList := strings.Split(noProxy, ",") + parsedURL, err := url.Parse(rawURL) + if err != nil { + return false + } + host := parsedURL.Hostname() - if host == value { - return true - } - if strings.HasPrefix(value, "*.") && strings.HasSuffix(host, value[2:]){ - return true - } - } - return false + for _, value := range noProxyList { + value = strings.TrimSpace(value) + + if host == value { + return true + } + if strings.HasPrefix(value, "*.") && strings.HasSuffix(host, value[2:]) { + return true + } + } + return false } func checkGitProxy(cloneOptions *git.CloneOptions) *git.CloneOptions { - if os.Getenv("HTTP_PROXY") != "" && !isGitNoProxy(cloneOptions.URL){ - cloneOptions.ProxyOptions = gitProxy.ProxyOptions{ - URL: os.Getenv("HTTP_PROXY"), - } - } + if os.Getenv("HTTP_PROXY") != "" && !isGitNoProxy(cloneOptions.URL) { + cloneOptions.ProxyOptions = gitProxy.ProxyOptions{ + URL: os.Getenv("HTTP_PROXY"), + } + } - if os.Getenv("HTTPS_PROXY") != "" && !isGitNoProxy(cloneOptions.URL) { - cloneOptions.ProxyOptions = gitProxy.ProxyOptions{ - URL: os.Getenv("HTTPS_PROXY"), - } - } + if os.Getenv("HTTPS_PROXY") != "" && !isGitNoProxy(cloneOptions.URL) { + cloneOptions.ProxyOptions = gitProxy.ProxyOptions{ + URL: os.Getenv("HTTPS_PROXY"), + } + } - return cloneOptions + return cloneOptions } func createNewUser(username, password, role, apikey string, org shuffle.OrgMini) error { @@ -559,7 +558,6 @@ func createNewUser(username, password, role, apikey string, org shuffle.OrgMini) } } - return nil } @@ -663,7 +661,7 @@ func handleRegister(resp http.ResponseWriter, request *http.Request) { Name: newOrg.Name, } - user.ActiveOrg = currentOrg + user.ActiveOrg = currentOrg } } } @@ -932,7 +930,6 @@ func handleInfo(resp http.ResponseWriter, request *http.Request) { log.Printf("[DEBUG] Failed to get org during getinfo: %s", err) } - //if err == nil { if len(org.Id) > 0 { if userInfo.Role == "" { @@ -1069,9 +1066,9 @@ func handleInfo(resp http.ResponseWriter, request *http.Request) { ChatDisabled: chatDisabled, Tutorials: tutorialsFinished, - Interests: orgInterests, - Priorities: orgPriorities, - Licensed: licensed, + Interests: orgInterests, + Priorities: orgPriorities, + Licensed: licensed, } returnData, err := json.Marshal(returnValue) @@ -1092,7 +1089,6 @@ type passwordReset struct { Reference string `json:"reference"` } - func checkAdminLogin(resp http.ResponseWriter, request *http.Request) { cors := shuffle.HandleCors(resp, request) if cors { @@ -3338,7 +3334,6 @@ func buildSwaggerApp(resp http.ResponseWriter, body []byte, user shuffle.User, s } } - log.Printf("[DEBUG] Successfully built app %s (%s)", api.Name, api.ID) if len(user.Id) > 0 { resp.WriteHeader(200) @@ -3379,8 +3374,6 @@ func verifySwagger(resp http.ResponseWriter, request *http.Request) { buildSwaggerApp(resp, body, user, false) } - - // Hotloads new apps from a folder func handleAppHotload(ctx context.Context, location string, forceUpdate bool) error { @@ -3727,11 +3720,10 @@ func remoteOrgJobController(org shuffle.Org, body []byte) error { return nil } - func remoteOrgJobHandler(org shuffle.Org, interval int) error { // Check if it's 1 in 10 (10% chance random) - backupJob := shuffle.BackupJob{} + backupJob := shuffle.BackupJob{} // Check if workflow backup is active // Check if app backup is active @@ -3777,7 +3769,6 @@ func remoteOrgJobHandler(org shuffle.Org, interval int) error { backupJobData = []byte{} } - syncUrl := fmt.Sprintf("%s/api/v1/cloud/sync", syncUrl) client := shuffle.GetExternalClient(syncUrl) req, err := http.NewRequest( @@ -3983,7 +3974,7 @@ func runInitEs(ctx context.Context) { } // FIXME: Add a randomized timer to avoid all schedules running at the same time - // Many are at 5 minutes / 1 hour. The point is to spread these out + // Many are at 5 minutes / 1 hour. The point is to spread these out // a bit instead of all of them starting at the exact same time //log.Printf("Schedule: %#v", schedule) @@ -4275,7 +4266,7 @@ func runInitEs(ctx context.Context) { } } - cloneOptions = checkGitProxy(cloneOptions) + cloneOptions = checkGitProxy(cloneOptions) branch := os.Getenv("SHUFFLE_DOWNLOAD_AUTH_BRANCH") if len(branch) > 0 && branch != "master" && branch != "main" { @@ -4322,7 +4313,7 @@ func runInitEs(ctx context.Context) { URL: apis, } - cloneOptions = checkGitProxy(cloneOptions) + cloneOptions = checkGitProxy(cloneOptions) _, err = git.Clone(storer, fs, cloneOptions) if err != nil { @@ -4340,17 +4331,16 @@ func runInitEs(ctx context.Context) { log.Printf("[INFO] Skipping download of extra API samples as %d were found", len(workflowapps)) } - if os.Getenv("SHUFFLE_HEALTHCHECK_DISABLED") != "true" { - healthcheckInterval := 30 + healthcheckInterval := 30 log.Printf("[INFO] Starting healthcheck job every %d minute. Stats available on /api/v1/health/stats. Disable with SHUFFLE_HEALTHCHECK_DISABLED=true", healthcheckInterval) job := func() { - // Prepare a fake http.responsewriter + // Prepare a fake http.responsewriter resp := httptest.NewRecorder() request := http.Request{} // Add the "force=true" query to the fake request - request.URL, err = url.Parse("/api/v1/health/stats?force=true") + request.URL, err = url.Parse("/api/v1/health/stats?force=true") if err != nil { log.Printf("[ERROR] Failed to parse test url for healthstats: %s", err) } @@ -4369,7 +4359,6 @@ func runInitEs(ctx context.Context) { log.Printf("[INFO] Finished INIT (ES)") } - func handleVerifyCloudsync(orgId string) (shuffle.SyncFeatures, error) { ctx := context.Background() org, err := shuffle.GetOrg(ctx, orgId) @@ -4948,8 +4937,6 @@ func makeWorkflowPublic(resp http.ResponseWriter, request *http.Request) { resp.Write([]byte(fmt.Sprintf(`{"success": true}`))) } - - func handleAppZipUpload(resp http.ResponseWriter, request *http.Request) { cors := shuffle.HandleCors(resp, request) if cors { @@ -5008,8 +4995,6 @@ func handleAppZipUpload(resp http.ResponseWriter, request *http.Request) { resp.Write([]byte("OK")) } - - func initHandlers() { var err error ctx := context.Background() @@ -5056,7 +5041,7 @@ func initHandlers() { r.HandleFunc("/api/v1/users/register", handleRegister).Methods("POST", "OPTIONS") r.HandleFunc("/api/v1/users/checkusers", checkAdminLogin).Methods("GET", "OPTIONS") r.HandleFunc("/api/v1/users/getinfo", handleInfo).Methods("GET", "OPTIONS") - + r.HandleFunc("/api/v1/users/{userId}/apps", shuffle.HandleGetUserApps).Methods("GET", "OPTIONS") r.HandleFunc("/api/v1/users/apps", shuffle.HandleGetUserApps).Methods("GET", "OPTIONS") r.HandleFunc("/api/v1/users/generateapikey", shuffle.HandleApiGeneration).Methods("GET", "POST", "OPTIONS") r.HandleFunc("/api/v1/users/logout", shuffle.HandleLogout).Methods("POST", "OPTIONS") @@ -5192,7 +5177,7 @@ func initHandlers() { r.HandleFunc("/api/v1/triggers/gmail/register", shuffle.HandleNewGmailRegister).Methods("GET", "OPTIONS") r.HandleFunc("/api/v1/triggers/gmail/getFolders", shuffle.HandleGetGmailFolders).Methods("GET", "OPTIONS") r.HandleFunc("/api/v1/triggers/pipeline", shuffle.HandleNewPipelineRegister).Methods("POST", "OPTIONS") - //r.HandleFunc("/api/v1/triggers/pipeline/save", shuffle.HandleSavePipelineInfo).Methods("PUT", "OPTIONS") + //r.HandleFunc("/api/v1/triggers/pipeline/save", shuffle.HandleSavePipelineInfo).Methods("PUT", "OPTIONS") r.HandleFunc("/api/v1/pipelines/{key}", handlePipelineCallback).Methods("POST", "GET", "PATCH", "PUT", "DELETE", "OPTIONS") r.HandleFunc("/api/v1/triggers", shuffle.HandleGetTriggers).Methods("GET", "OPTIONS") //r.HandleFunc("/api/v1/triggers/gmail/routing", handleGmailRouting).Methods("POST", "OPTIONS") @@ -5220,7 +5205,7 @@ func initHandlers() { r.HandleFunc("/api/v1/orgs/{orgId}", shuffle.HandleDeleteOrg).Methods("DELETE", "OPTIONS") r.HandleFunc("/api/v1/orgs/{orgId}/suborgs", shuffle.HandleGetSubOrgs).Methods("GET", "OPTIONS") - + // This is a new API that validates if a key has been seen before. // Not sure what the best course of action is for it. r.HandleFunc("/api/v1/environments/{key}/stop", shuffle.HandleStopExecutions).Methods("GET", "POST", "OPTIONS") @@ -5244,7 +5229,6 @@ func initHandlers() { r.HandleFunc("/api/v1/orgs/{orgId}/datastore", shuffle.HandleSetCacheKey).Methods("POST", "OPTIONS") 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/login_sso", shuffle.HandleSSO).Methods("GET", "POST", "OPTIONS") @@ -5266,15 +5250,14 @@ func initHandlers() { // This structure is horrendous. Needs fixing after we got the prototype up r.HandleFunc("/api/v1/detections/{detectionType}/connect", shuffle.HandleDetectionAutoConnect).Methods("GET", "OPTIONS") r.HandleFunc("/api/v1/detections/{detection_type}", shuffle.HandleGetDetectionRules).Methods("GET", "OPTIONS") - r.HandleFunc("/api/v1/detections/{triggerId}/selected_rules", shuffle.HandleGetSelectedRules).Methods("GET","OPTIONS") - r.HandleFunc("/api/v1/detections/{triggerId}/selected_rules/save", shuffle.HandleSaveSelectedRules).Methods("POST","OPTIONS") + r.HandleFunc("/api/v1/detections/{triggerId}/selected_rules", shuffle.HandleGetSelectedRules).Methods("GET", "OPTIONS") + r.HandleFunc("/api/v1/detections/{triggerId}/selected_rules/save", shuffle.HandleSaveSelectedRules).Methods("POST", "OPTIONS") r.HandleFunc("/api/v1/detections/{action}", shuffle.HandleFolderToggle).Methods("PUT", "OPTIONS") // This is weird. r.HandleFunc("/api/v1/detections/{fileId}/{action}", shuffle.HandleToggleRule).Methods("PUT", "OPTIONS") //r.HandleFunc("/api/v1/detections/siem/node_health", shuffle.HandleTenzirHealthUpdate).Methods("POST","OPTIONS") - // Introduced in 0.9.21 to handle notifications for e.g. failed Workflow r.HandleFunc("/api/v1/notifications", shuffle.HandleCreateNotification).Methods("POST", "OPTIONS") r.HandleFunc("/api/v1/notifications", shuffle.HandleGetNotifications).Methods("GET", "OPTIONS") diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 7349952b..04cdd17c 100755 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -14,6 +14,7 @@ import HealthPage from "./components/HealthPage.jsx"; //import Header from "./components/Header.jsx"; import theme from "./theme"; import Apps from "./views/Apps"; +import Apps2 from "./views/Apps2.jsx"; import AppCreator from "./views/AppCreator"; import DetectionDashBoard from "./views/DetectionDashboard.jsx"; @@ -408,6 +409,23 @@ const App = (message, props) => { {...props} /> } + /> + + } /> { defaultSearch, setDefaultSearch, checkLogin, + isAppPage=false } = props; const [discoveryData, setDiscoveryData] = React.useState({}) @@ -507,26 +508,31 @@ const AppSelection = props => { })} - {!moreButton ? ( -
- { + { + !isAppPage && ( + <> + {!moreButton ? ( +
+ { setMoreButton(true) - setTimeout(() => { navigate("/welcome?tab=2") }, 250) }} >See More Apps -
) : ""} +
) : ""} -
- -
+
+ +
+ + ) + } ) diff --git a/frontend/src/views/Apps2.jsx b/frontend/src/views/Apps2.jsx new file mode 100644 index 00000000..079d4b2e --- /dev/null +++ b/frontend/src/views/Apps2.jsx @@ -0,0 +1,1174 @@ +import React, { useState, useEffect, useContext, useCallback, memo, useMemo, useRef } from "react"; +import theme from "../theme.jsx"; +import { isMobile } from "react-device-detect"; +import AppGrid from "../components/AppGrid.jsx"; +import { useLocation, useNavigate } from "react-router-dom"; +import { + TextField, Button, Typography, MenuItem, Select, Tabs, Tab, Zoom, + Grid, + Paper, + ButtonBase, + Tooltip, + Box, + CircularProgress, + Checkbox, +} from "@mui/material"; +import { Context } from "../context/ContextApi.jsx"; +import Add from '@mui/icons-material/Add'; +import InputAdornment from '@mui/material/InputAdornment'; +import Search from '@mui/icons-material/Search'; +import ClearIcon from '@mui/icons-material/Clear'; + +import { ClearRefinements, connectHits, connectSearchBox, connectStateResults, InstantSearch, RefinementList, connectRefinementList, Configure } from "react-instantsearch-dom"; +import { removeQuery } from "../components/ScrollToTop.jsx"; +import { toast } from "react-toastify"; +import algoliasearch from "algoliasearch/lite"; +import { debounce } from "lodash"; +import AppSelection from "../components/AppSelection.jsx"; + + +const searchClient = algoliasearch( + "JNSS5CFDZZ", + "db08e40265e2941b9a7d8f644b6e5240" +); + +// AppCard Component +const AppCard = ({ data, index, mouseHoverIndex, setMouseHoverIndex, globalUrl, deactivatedIndexes, currTab }) => { + const isCloud = window.location.host === "localhost:3002" || window.location.host === "shuffler.io" || window.location.host === "localhost:3000"; + const appUrl = isCloud ? `/apps/${data.id}` : `https://shuffler.io/apps/${data.id}`; + + const paperStyle = { + backgroundColor: mouseHoverIndex === index ? "rgba(26, 26, 26, 1)" : "#1A1A1A", + color: "rgba(241, 241, 241, 1)", + cursor: "pointer", + position: "relative", + width: 365, + height: 96, + borderRadius: 8, + boxShadow: "0px 0px 10px 0px rgba(0, 0, 0, 0.1)", + marginBottom: 20, + }; + + return ( + + setMouseHoverIndex(index)} + onMouseOut={() => setMouseHoverIndex(-1)} + > + { + // if (!event.target.closest('.deactivate-button')) { + // window.location.href = appUrl; + // } + console.log("clicked"); + }} + > + {data.name} +
+
+ {data.name.replace(/_/g, ' ').replace(/\b\w/g, char => char.toUpperCase())} +
+
+ {data.categories ? data.categories.join(", ") : "NA"} +
+
+
+ {data.generated !== true && data.tags && data.tags.slice(0, 2).map((tag, tagIndex) => ( + + {tag} + {tagIndex < data.tags.length - 1 ? ", " : ""} + + ))} +
+ {/* Deactivate button */} + {currTab === 0 && !deactivatedIndexes.includes(index) && mouseHoverIndex === index && data.generated === true && ( + + )} +
+
+
+
+
+ ); +}; + + +// Component to fetch all public app from the algolia. +const Hits = ({ + userdata, + hits, + setIsAnyAppActivated, + searchQuery, + globalUrl, + isLoading, + isLoggedIn, +}) => { + const [hoverEffect, setHoverEffect] = useState(-1); + const [allActivatedAppIds, setAllActivatedAppIds] = useState(userdata?.active_apps); + const isCloud = window.location.host === "localhost:3002" || window.location.host === "shuffler.io"; + const [deactivatedIndexes, setDeactivatedIndexes] = React.useState([]); + + const normalizedString = (name) => { + if (typeof name === 'string') { + return name.replace(/_/g, ' '); + } else { + return name; + } + }; + + + + //Function for activation and deactivation of app + const handleActivateButton = (event, data, type) => { + + //use prevent default so it will stop redirection to the app page + event.preventDefault(); + if (!isLoggedIn) { + toast.error("Please log in to your account to activate the app.") + return; + } + if (type === "activate") { + toast.success(`The ${normalizedString(data.name)} app is activating. Please wait...`); + } + if (type === "deactivate") { + toast.success(`The ${normalizedString(data.name)} app is deactivating. Please wait...`); + } + + const baseURL = globalUrl; + const url = `${baseURL}/api/v1/apps/${data.objectID}/${type}`; + + fetch(url, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + }, + credentials: "include", + }) + .then((response) => response.json()) + .then((responseJson) => { + if (responseJson.success === false) { + toast.error(responseJson.reason); + } else { + //toast.success(`App ${type}d Successfully!`); + if (type === 'activate') { + setAllActivatedAppIds(prev => [...prev, data.objectID]); + setIsAnyAppActivated(true); + } + if (type === 'deactivate') { + const updatedIds = allActivatedAppIds.filter(id => id !== data.objectID); + setAllActivatedAppIds(updatedIds); + } + } + }) + .catch(error => { + console.log("app error: ", error.toString()); + }); + } + + let workflowDelay = 0; + const isHeader = true; + + const [showNoAppFound, setShowNoAppFound] = useState(false); + + //show some delay to show the "App Not Found." so it doesn't not show while changing tab. + useEffect(() => { + const timer = setTimeout(() => { + setShowNoAppFound(true); + }, 1000); + return () => clearTimeout(timer); + }, []); + + + + return ( +
+ {!isLoading ? ( +
+ {hits?.length === 0 && searchQuery.length >= 0 && showNoAppFound ? ( +
+ No Apps Found +
+ ) : ( + +
+
+ {hits?.map((data, index) => { + const appUrl = + isCloud + ? `/apps/${data.objectID}?queryID=${data.__queryID}` + : `https://shuffler.io/apps/${data.objectID}?queryID=${data.__queryID}`; + + return ( + + + + { + setHoverEffect(index); + }} + onMouseLeave={() => { + setHoverEffect(-1); + }} + > + + {data.name} +
+
+ {(allActivatedAppIds && allActivatedAppIds.includes(data.objectID)) && } + {normalizedString(data.name)} +
+ +
+ {data.categories !== null + ? normalizedString(data.categories).join(", ") + : "NA"} +
+
+
+ {hoverEffect === index && isCloud ? ( +
+ {data.tags && ( + + + {data.tags.slice(0, 1).map((tag, tagIndex) => ( + + {normalizedString(tag)} + {tagIndex < 1 ? ", " : ""} + + ))} + + + )} +
+ ) : ( +
+ {data.tags && + data.tags.map((tag, tagIndex) => ( + + {normalizedString(tag)} + {tagIndex < data.tags.length - 1 ? ", " : ""} + + ))} +
+ )} +
+
+ {hoverEffect === index && isCloud && ( +
+ {allActivatedAppIds && allActivatedAppIds.includes(data.objectID) ? ( + + ) : ( + + )} +
+ )} +
+
+
+
+
+
+
+
+ ); + })} +
+
+
+ )} +
+ ) : ( +
+ )} +
+ ); +} + +// Custom Category Dropdown Component +const CategoryDropdown = ({ items, currentRefinement, refine }) => { + const handleChange = (event) => { + const value = event.target.value; + refine(value); + }; + + return ( + + ); +}; + +const CustomCategoryDropdown = connectRefinementList(CategoryDropdown); + +// Custom SearchBox Component +const SearchBox = ({ refine, searchQuery, setSearchQuery }) => { + const inputRef = useRef(null); + const [localQuery, setLocalQuery] = useState(searchQuery); + + // Debounced function to refine search + const debouncedRefine = useRef( + debounce((value) => { + setSearchQuery(value); + removeQuery("q"); + refine(value); + }, 300) // Adjust the delay as needed + ).current; + + useEffect(() => { + const urlSearchParams = new URLSearchParams(window.location.search); + const params = Object.fromEntries(urlSearchParams.entries()); + const foundQuery = params["q"]; + if (foundQuery) { + setLocalQuery(foundQuery); + debouncedRefine(foundQuery); + } + }, [debouncedRefine]); + + useEffect(() => { + if (searchQuery === "") { + return; + } + if (inputRef.current) { + inputRef.current.focus(); + } + }, [searchQuery]); + + const handleChange = (event) => { + const value = event.target.value; + setLocalQuery(value); + debouncedRefine(value); + }; + + return ( + { + if (event.key === "Enter") { + event.preventDefault(); + } + }} + style={{ width: '100%', borderRadius: '7px', fontFamily: "var(--zds-typography-base,Inter,Helvetica,arial,sans-serif)" }} + InputProps={{ + style: { + borderRadius: 8, + }, + endAdornment: ( + + {localQuery?.length === 0 ? : ( + { + setLocalQuery(''); + debouncedRefine(''); + }} + /> + )} + + ), + }} + /> + ); +}; + +const CustomSearchBox = connectSearchBox(SearchBox); +const CustomHits = connectHits(Hits); + +// Custom Label Dropdown Component +const LabelDropdown = ({ items, currentRefinement, refine }) => { + const handleChange = (event) => { + const value = event.target.value; + refine(value); + }; + + return ( + + ); +}; + +const CustomLabelDropdown = connectRefinementList(LabelDropdown); + +// Main Apps Component +const Apps2 = (props) => { + const { globalUrl, isLoaded, serverside, userdata, isLoggedIn, checkLogin } = props; + let navigate = useNavigate(); + const { leftSideBarOpenByClick } = useContext(Context); + const location = useLocation(); + const [searchQuery, setSearchQuery] = useState(""); + const [selectedCategory, setSelectedCategory] = useState([]); + const [selectedLabel, setSelectedLabel] = useState([]); + const [currTab, setCurrTab] = useState(0); + const [categories, setCategories] = useState([]); + const [labels, setLabels] = useState([]); + const [isLoading, setIsLoading] = useState(false); + const [userApps, setUserApps] = useState([]); + const [orgApps, setOrgApps] = useState([]); + const [appsToShow, setAppsToShow] = useState([]); + const [deactivatedIndexes, setDeactivatedIndexes] = React.useState([]); + const [IsAnyAppActivated, setIsAnyAppActivated] = useState(false); + const [mouseHoverIndex, setMouseHoverIndex] = useState(-1); + var counted = 0; + const [showNoAppFound, setShowNoAppFound] = useState(false); + + + const [appFramework, setAppFramework] = useState(undefined); + const [defaultSearch, setDefaultSearch] = useState(""); + // Set the current tab based on the query parameter + useEffect(() => { + const queryParams = new URLSearchParams(location.search); + const tabParam = queryParams.get('tab'); + if (tabParam !== null && tabParam !== undefined) { + if (tabParam === 'org_apps') { + setCurrTab(0); + } else if (tabParam === 'my_apps') { + setCurrTab(1); + } else { + setCurrTab(2); + } + } + }, [location.search]); + + useEffect(() => { + + const getFramework = () => { + fetch(globalUrl + "/api/v1/apps/frameworkConfiguration", { + method: "GET", + headers: { + "Content-Type": "application/json", + Accept: "application/json", + }, + credentials: "include", + }) + .then((response) => { + if (response.status !== 200) { + console.log("Status not 200 for framework!"); + } + + return response.json(); + }) + .then((responseJson) => { + if (responseJson.success === false) { + setAppFramework({}) + + if (responseJson.reason !== undefined) { + //toast("Failed loading: " + responseJson.reason) + } else { + //toast("Failed to load framework for your org.") + } + } else { + setAppFramework(responseJson) + } + }) + .catch((error) => { + console.log("err in framework: ", error.toString()); + }) + } + + getFramework(); + }, []); + + // Fetch apps based on the current tab : 0 -> org_apps, 1 -> my_apps, 2 -> all_apps + useEffect(() => { + const fetchApps = async () => { + const baseUrl = globalUrl; + let url; + + const userId = userdata?.id; + if (currTab === 1 && userId) { + url = `${baseUrl}/api/v1/users/${userId}/apps`; + } else if (currTab === 0) { + url = `${baseUrl}/api/v1/apps`; + } + setIsLoading(true); + try { + const response = await fetch(url, { + method: "GET", + credentials: "include", + headers: { + "Content-Type": "application/json", + }, + }); + const data = await response.json(); + if (currTab === 1) { + setAppsToShow(data); + setUserApps(data); + } else if (currTab === 0) { + setAppsToShow(data); + setOrgApps(data); + // For testing the empty state + // setAppsToShow([]); + // setOrgApps([]); + } + setIsLoading(false); + } catch (err) { + console.error("Error fetching apps:", err); + setIsLoading(false); + } + }; + + // Only fetch if we have required data + if (globalUrl && (currTab === 0 || (currTab === 1 && userdata?.id))) { + fetchApps(); + } + }, [currTab, globalUrl, userdata?.id]); // Remove location.search dependency + + useEffect(() => { + setSearchQuery(""); + }, [currTab]) + + + // Find top categories and tags based on the current tab + useEffect(() => { + if (currTab === 0) { + setCategories(findTopCategories()); + setLabels(findTopTags()); + } + }, [currTab, appsToShow]) + + + useEffect(() => { + if (serverside) { + return null; + } + }, [serverside]); + + + const findTopCategories = () => { + const categoryCountMap = {}; + const apps = currTab === 1 ? userApps : orgApps; + // Check if userAndOrgsApp is an array before iterating over it and Find top 10 Category from the apps + if (Array.isArray(apps)) { + apps.forEach((app) => { + const categories = app.categories; + + if (categories && categories.length > 0) { + categories.forEach((category) => { + categoryCountMap[category] = (categoryCountMap[category] || 0) + 1; + }); + } + }); + + const categoryArray = Object.keys(categoryCountMap).map((category) => ({ + category, + count: categoryCountMap[category], + })); + + categoryArray.sort((a, b) => b.count - a.count); + + const topCategories = categoryArray.slice(0, 7); + + return topCategories; + } + }; + + const findTopTags = () => { + const tagCountMap = {}; + const apps = currTab === 1 ? userApps : orgApps; + + if (Array.isArray(apps)) { + apps.forEach((app) => { + const tags = app.tags; + if (tags && tags.length > 0) { + tags.forEach((tag) => { + tagCountMap[tag] = (tagCountMap[tag] || 0) + 1; + }); + } + }); + } + + const tagArray = Object.keys(tagCountMap).map((tag) => ({ + tag, + count: tagCountMap[tag], + })); + + tagArray.sort((a, b) => b.count - a.count); + + const topTags = tagArray.slice(0, 8); + + return topTags; + }; + + const handleCreateApp = (e) => { + e.preventDefault(); + console.log("Create app clicked") + }; + + + useEffect(() => { + const apps = currTab === 1 ? userApps : orgApps; + // Search app based on app name, category, and tag + const filteredUserAppdata = Array.isArray(apps) ? apps.filter((app) => { + + const matchesSearchQuery = ( + searchQuery === "" || // If searchQuery is empty, match all apps + app.name.toLowerCase().includes(searchQuery.toLowerCase()) || + (app.tags && app.tags.some(tag => + tag.toLowerCase().includes(searchQuery.toLowerCase()) + )) || + (app.categories && app.categories.some((category) => + category.toLowerCase().includes(searchQuery.toLowerCase()) + )) + ); + + const matchesSelectedCategories = ( + selectedCategory.length === 0 || // If no category is selected, match all apps + (app.categories && app.categories.some(category => + selectedCategory.includes(category) + )) + ); + + const matchesSelectedTags = ( + selectedLabel.length === 0 || // If no label is selected, match all apps + (app.tags && app.tags.some(tag => + selectedLabel.includes(tag) + )) + ); + + return matchesSearchQuery && matchesSelectedCategories && matchesSelectedTags; + }) : []; + + setAppsToShow(filteredUserAppdata); + }, [searchQuery, selectedCategory, selectedLabel]); + + + const handleTabChange = (newTab) => { + setCurrTab(newTab); + if (newTab === 0) { + setAppsToShow(orgApps) + const categories = findTopCategories(); + const labels = findTopTags(); + setCategories(categories); + setLabels(labels); + } + if (newTab === 1) { + setAppsToShow(userApps) + } + const newQueryParam = newTab === 0 ? 'org_apps' : newTab === 1 ? 'my_apps' : 'all_apps'; + const queryParams = new URLSearchParams(location.search); + queryParams.set('tab', newQueryParam); + queryParams.delete('q'); + window.history.replaceState({}, '', `${location.pathname}?${queryParams.toString()}`); + console.log("Apps to show", appsToShow) + }; + + + + + const boxStyle = { + color: "white", + display: "flex", + flexDirection: "column", + width: "100%", + margin: "auto", + maxWidth: "60%", + // padding: '20px 380px', + }; + + + const handleCategoryChange = (event) => { + const value = event.target.value; + setSelectedCategory(value); + }; + + const handleLabelChange = (event) => { + const value = event.target.value; + setSelectedLabel(value); + }; + + + return ( + +
+ + Apps + +
+ handleTabChange(newTab)} + TabIndicatorProps={{ style: { height: '3px', borderRadius: 10 } }} + > + + + + +
+
+
+ { + (currTab === 0 || currTab === 1) && + { + setSearchQuery(event.target.value); + }} + onKeyDown={(event) => { + if (event.key === "Enter") { + event.preventDefault(); + } + }} + limit={5} + style={{ width: '100%', borderRadius: '7px', fontFamily: "var(--zds-typography-base,Inter,Helvetica,arial,sans-serif)" }} + InputProps={{ + style: { + borderRadius: 8, + }, + endAdornment: ( + + { + searchQuery.length === 0 ? : + } + + ), + }} + /> + } + { + currTab === 2 && + + } +
+
+ {currTab === 2 ? ( + + ) : ( + + )} +
+
+ {currTab === 2 ? ( + + ) : ( + + )} +
+
+ +
+
+
+ + { + currTab === 0 && ( +
+ {isLoading ? ( +
+ +
+ ) : ( + <> + {appsToShow?.length > 0 && appsToShow !== undefined && !isLoading ? ( +
+ {appsToShow.map((data, index) => ( + + ))} +
+ ) : ( +
+ +
+ )} + + )} +
+ ) + } + { + currTab === 1 && ( +
+ {isLoading ? ( +
+ +
+ ) : ( + <> + {appsToShow?.length > 0 && appsToShow !== undefined ? ( +
+ {appsToShow.map((data, index) => ( + + ))} +
+ ) : ( +
+ No Apps Found +
+ )} + + )} +
+ ) + } + + { + currTab === 2 && + + } +
+
+ +
+ ); +}; + +export default Apps2;