Tabs are working except myApps and categories and labels are displayed
This commit is contained in:
+7
-24
@@ -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"
|
||||
"github.com/go-git/go-git/v5/storage/memory"
|
||||
gitProxy "github.com/go-git/go-git/v5/plumbing/transport"
|
||||
"github.com/go-git/go-git/v5/storage/memory"
|
||||
|
||||
// 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",
|
||||
@@ -414,13 +413,13 @@ func isGitNoProxy(rawURL string) bool {
|
||||
}
|
||||
host := parsedURL.Hostname()
|
||||
|
||||
for _,value := range noProxyList {
|
||||
for _, value := range noProxyList {
|
||||
value = strings.TrimSpace(value)
|
||||
|
||||
if host == value {
|
||||
return true
|
||||
}
|
||||
if strings.HasPrefix(value, "*.") && strings.HasSuffix(host, value[2:]){
|
||||
if strings.HasPrefix(value, "*.") && strings.HasSuffix(host, value[2:]) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -428,7 +427,7 @@ func isGitNoProxy(rawURL string) bool {
|
||||
}
|
||||
|
||||
func checkGitProxy(cloneOptions *git.CloneOptions) *git.CloneOptions {
|
||||
if os.Getenv("HTTP_PROXY") != "" && !isGitNoProxy(cloneOptions.URL){
|
||||
if os.Getenv("HTTP_PROXY") != "" && !isGitNoProxy(cloneOptions.URL) {
|
||||
cloneOptions.ProxyOptions = gitProxy.ProxyOptions{
|
||||
URL: os.Getenv("HTTP_PROXY"),
|
||||
}
|
||||
@@ -559,7 +558,6 @@ func createNewUser(username, password, role, apikey string, org shuffle.OrgMini)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -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 == "" {
|
||||
@@ -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,7 +3720,6 @@ 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)
|
||||
@@ -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(
|
||||
@@ -4340,7 +4331,6 @@ 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
|
||||
log.Printf("[INFO] Starting healthcheck job every %d minute. Stats available on /api/v1/health/stats. Disable with SHUFFLE_HEALTHCHECK_DISABLED=true", healthcheckInterval)
|
||||
@@ -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")
|
||||
@@ -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")
|
||||
|
||||
@@ -417,6 +417,7 @@ const App = (message, props) => {
|
||||
<Apps2
|
||||
serverside={false}
|
||||
isLoaded={isLoaded}
|
||||
isLoggedIn={isLoggedIn}
|
||||
userdata={userdata}
|
||||
globalUrl={globalUrl}
|
||||
surfaceColor={theme.palette.surfaceColor}
|
||||
|
||||
+614
-203
@@ -1,4 +1,4 @@
|
||||
import React, { useState, useEffect, useContext } from "react";
|
||||
import React, { useState, useEffect, useContext, useCallback, memo, useMemo } from "react";
|
||||
import theme from "../theme.jsx";
|
||||
import { isMobile } from "react-device-detect";
|
||||
import AppGrid from "../components/AppGrid.jsx";
|
||||
@@ -9,6 +9,8 @@ import {
|
||||
Paper,
|
||||
ButtonBase,
|
||||
Tooltip,
|
||||
Box,
|
||||
CircularProgress,
|
||||
} from "@mui/material";
|
||||
import { Context } from "../context/ContextApi.jsx";
|
||||
import Add from '@mui/icons-material/Add';
|
||||
@@ -16,12 +18,19 @@ 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, RefinementList } from "react-instantsearch-dom";
|
||||
import { ClearRefinements, connectHits, connectSearchBox, connectStateResults, InstantSearch, RefinementList } from "react-instantsearch-dom";
|
||||
import { removeQuery } from "../components/ScrollToTop.jsx";
|
||||
import { toast } from "react-toastify";
|
||||
import algoliasearch from "algoliasearch/lite";
|
||||
|
||||
const AppCard = ({ data, index, mouseHoverIndex, setMouseHoverIndex, globalUrl }) => {
|
||||
const isCloud = window.location.host === "localhost:3002" || window.location.host === "shuffler.io";
|
||||
|
||||
const searchClient = algoliasearch(
|
||||
"JNSS5CFDZZ",
|
||||
"db08e40265e2941b9a7d8f644b6e5240"
|
||||
);
|
||||
|
||||
const AppCard = ({ data, index, mouseHoverIndex, setMouseHoverIndex, globalUrl, deactivatedIndexes }) => {
|
||||
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 = {
|
||||
@@ -33,23 +42,75 @@ const AppCard = ({ data, index, mouseHoverIndex, setMouseHoverIndex, globalUrl }
|
||||
height: 96,
|
||||
borderRadius: 8,
|
||||
boxShadow: "0px 0px 10px 0px rgba(0, 0, 0, 0.1)",
|
||||
marginBottom: 20,
|
||||
// marginBottom: 20,
|
||||
};
|
||||
|
||||
return (
|
||||
<Grid item xs={12} key={index}>
|
||||
<a href={appUrl} rel="noopener noreferrer" style={{ textDecoration: "none", color: "#f85a3e" }}>
|
||||
<Paper elevation={0} style={paperStyle} onMouseOver={() => setMouseHoverIndex(index)} onMouseOut={() => setMouseHoverIndex(-1)}>
|
||||
<ButtonBase style={{ borderRadius: 6, fontSize: 16, overflow: "hidden", display: "flex", alignItems: "flex-start", width: '100%', backgroundColor: mouseHoverIndex === index ? "#2F2F2F" : "#212121" }}>
|
||||
<img alt={data.name} src={data.small_image || data.large_image ? (data.small_image || data.large_image) : "/images/no_image.png"} style={{ width: 100, height: 100, borderRadius: 6, margin: 10, border: "1px solid #212122", boxShadow: "0px 0px 10px 0px rgba(0, 0, 0, 0.1)" }} />
|
||||
<div style={{ display: "flex", flexDirection: "column", alignItems: "flex-start", width: 339, gap: 8, fontWeight: '400', overflow: "hidden", margin: "12px 0 12px 0", fontFamily: "var(--zds-typography-base,Inter,Helvetica,arial,sans-serif)" }}>
|
||||
<div style={{ display: 'flex', flexDirection: 'row', overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap", marginLeft: 8, gap: 8 }}>
|
||||
<ButtonBase style={{
|
||||
borderRadius: 6,
|
||||
fontSize: 16,
|
||||
overflow: "hidden",
|
||||
display: "flex",
|
||||
alignItems: "flex-start",
|
||||
width: '100%',
|
||||
backgroundColor: mouseHoverIndex === index ? "#2F2F2F" : "#212121"
|
||||
}}>
|
||||
<img
|
||||
alt={data.name}
|
||||
src={data.small_image || data.large_image ? (data.small_image || data.large_image) : "/images/no_image.png"}
|
||||
style={{
|
||||
width: 100,
|
||||
height: 100,
|
||||
borderRadius: 6,
|
||||
margin: 10,
|
||||
border: "1px solid #212122",
|
||||
boxShadow: "0px 0px 10px 0px rgba(0, 0, 0, 0.1)"
|
||||
}}
|
||||
/>
|
||||
<div style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "flex-start",
|
||||
width: 339,
|
||||
gap: 8,
|
||||
fontWeight: '400',
|
||||
overflow: "hidden",
|
||||
margin: "12px 0",
|
||||
fontFamily: "var(--zds-typography-base,Inter,Helvetica,arial,sans-serif)"
|
||||
}}>
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
whiteSpace: "nowrap",
|
||||
marginLeft: 8,
|
||||
gap: 8
|
||||
}}>
|
||||
{data.name.replace(/_/g, ' ').replace(/\b\w/g, char => char.toUpperCase())}
|
||||
</div>
|
||||
<div style={{ overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap", marginLeft: 8, color: "rgba(158, 158, 158, 1)" }}>
|
||||
<div style={{
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
whiteSpace: "nowrap",
|
||||
marginLeft: 8,
|
||||
color: "rgba(158, 158, 158, 1)"
|
||||
}}>
|
||||
{data.categories ? data.categories.join(", ") : "NA"}
|
||||
</div>
|
||||
<div style={{ overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap", width: 230, textAlign: 'start', marginLeft: 8, color: "rgba(158, 158, 158, 1)", display: "flex" }}>
|
||||
<div style={{
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
whiteSpace: "nowrap",
|
||||
width: 230,
|
||||
textAlign: 'start',
|
||||
marginLeft: 8,
|
||||
color: "rgba(158, 158, 158, 1)",
|
||||
display: "flex"
|
||||
}}>
|
||||
<div style={{ minWidth: 120, overflow: "hidden" }}>
|
||||
{data.generated !== true && data.tags && data.tags.slice(0, 2).map((tag, tagIndex) => (
|
||||
<span key={tagIndex}>
|
||||
@@ -59,11 +120,44 @@ const AppCard = ({ data, index, mouseHoverIndex, setMouseHoverIndex, globalUrl }
|
||||
))}
|
||||
</div>
|
||||
{/* Deactivate button logic */}
|
||||
{data.generated === true ? (
|
||||
<Button style={{ marginLeft: 15, width: 102, height: 35, borderRadius: 200, backgroundColor: "rgba(73, 73, 73, 1)", color: "rgba(241, 241, 241, 1)", textTransform: "none" }}>
|
||||
{mouseHoverIndex === index ?
|
||||
<Button style={{
|
||||
marginLeft: 15,
|
||||
width: 102,
|
||||
height: 35,
|
||||
borderRadius: 200,
|
||||
backgroundColor: "rgba(73, 73, 73, 1)",
|
||||
color: "rgba(241, 241, 241, 1)",
|
||||
textTransform: "none",
|
||||
}}
|
||||
onClick={(event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
const url = `${globalUrl}/api/v1/apps/${data.id}/deactivate`;
|
||||
|
||||
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 Deactivated Successfully. Reload UI to see updated changes.");
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.log("app error: ", error.toString());
|
||||
});
|
||||
}}>
|
||||
Deactivate
|
||||
</Button>
|
||||
) : null}
|
||||
: null}
|
||||
</div>
|
||||
</div>
|
||||
</ButtonBase>
|
||||
@@ -73,8 +167,364 @@ const AppCard = ({ data, index, mouseHoverIndex, setMouseHoverIndex, globalUrl }
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
// Component to fetch all public app from the algolia.
|
||||
const Hits = memo(({
|
||||
userdata,
|
||||
hits,
|
||||
setIsAnyAppActivated,
|
||||
searchQuery,
|
||||
globalUrl,
|
||||
setIsLoggedIn,
|
||||
isLoading,
|
||||
isLoggedIn,
|
||||
}) => {
|
||||
var counted = 0;
|
||||
const [hoverEffect, setHoverEffect] = useState(-1);
|
||||
const [allActivatedAppIds, setAllActivatedAppIds] = useState(userdata?.active_apps);
|
||||
const isCloud = window.location.host === "localhost:3002" || window.location.host === "shuffler.io" || window.location.host === "localhost:3000";
|
||||
const [deactivatedIndexes, setDeactivatedIndexes] = React.useState([]);
|
||||
|
||||
const normalizedString = (name) => {
|
||||
if (typeof name === 'string') {
|
||||
return name.replace(/_/g, ' ');
|
||||
} else {
|
||||
return name;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// const fetchUserData = useCallback(async () => {
|
||||
// try {
|
||||
// const response = await fetch(`${globalUrl}/api/v1/me`, {
|
||||
// credentials: "include",
|
||||
// headers: {
|
||||
// 'Content-Type': 'application/json',
|
||||
// },
|
||||
// });
|
||||
// const responseJson = await response.json();
|
||||
// console.log("responseJson : user data", responseJson)
|
||||
// if (responseJson.success) {
|
||||
// setUserdata(responseJson);
|
||||
// setAllActivatedAppIds(responseJson.active_apps);
|
||||
// setIsLoggedIn(true);
|
||||
// } else {
|
||||
// setIsLoggedIn(false);
|
||||
// }
|
||||
// } catch (error) {
|
||||
// console.log("Failed login check: ", error);
|
||||
// }
|
||||
// }, [globalUrl]); // Added globalUrl as a dependency
|
||||
|
||||
// useEffect(() => {
|
||||
// fetchUserData();
|
||||
// }, [fetchUserData]); // Ensure fetchUserData is stable
|
||||
|
||||
//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);
|
||||
}, []);
|
||||
|
||||
const memoizedHits = useMemo(() => hits, [hits]);
|
||||
|
||||
|
||||
return (
|
||||
<div>
|
||||
{!isLoading ? (
|
||||
<div>
|
||||
{memoizedHits?.length === 0 && searchQuery.length >= 0 && showNoAppFound ? (
|
||||
<Typography variant="body1" style={{ marginTop: '30%' }}>No Apps Found</Typography>
|
||||
) : (
|
||||
<Grid item spacing={2} justifyContent="flex-start">
|
||||
<div
|
||||
style={{
|
||||
gap: 16,
|
||||
marginTop: 16,
|
||||
display: "flex",
|
||||
flexWrap: "wrap",
|
||||
justifyContent: "flex-start",
|
||||
marginLeft: 24,
|
||||
overflowY: "auto",
|
||||
overflowX: "hidden",
|
||||
maxHeight: 570,
|
||||
scrollbarWidth: "thin",
|
||||
scrollbarColor: "#494949 #2f2f2f",
|
||||
}}
|
||||
>
|
||||
{memoizedHits.map((data, index) => {
|
||||
const appUrl =
|
||||
isCloud
|
||||
? `/apps/${data.objectID}?queryID=${data.__queryID}`
|
||||
: `https://shuffler.io/apps/${data.objectID}?queryID=${data.__queryID}`;
|
||||
|
||||
return (
|
||||
<Zoom
|
||||
key={index}
|
||||
in={true}
|
||||
style={{
|
||||
transitionDelay: `${workflowDelay}ms`,
|
||||
}}
|
||||
>
|
||||
<Grid>
|
||||
<a
|
||||
href={appUrl}
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
style={{
|
||||
textDecoration: "none",
|
||||
color: "#f85a3e",
|
||||
}}
|
||||
>
|
||||
<Paper
|
||||
elevation={0}
|
||||
style={{
|
||||
backgroundColor: hoverEffect === 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,
|
||||
}}
|
||||
onMouseEnter={() => {
|
||||
setHoverEffect(index);
|
||||
}}
|
||||
onMouseLeave={() => {
|
||||
setHoverEffect(-1);
|
||||
}}
|
||||
>
|
||||
<ButtonBase style={{
|
||||
borderRadius: 6,
|
||||
fontSize: 16,
|
||||
overflow: "hidden",
|
||||
display: "flex",
|
||||
alignItems: "flex-start",
|
||||
width: '100%',
|
||||
backgroundColor: hoverEffect === index ? "#2F2F2F" : "#212121"
|
||||
}}>
|
||||
<img
|
||||
alt={data.name}
|
||||
src={data.image_url ? data.image_url : "/images/no_image.png"}
|
||||
style={{
|
||||
width: 100,
|
||||
height: 100,
|
||||
borderRadius: 6,
|
||||
margin: 10,
|
||||
border: "1px solid #212122",
|
||||
boxShadow: "0px 0px 10px 0px rgba(0, 0, 0, 0.1)"
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "flex-start",
|
||||
width: 339,
|
||||
gap: 8,
|
||||
fontWeight: '400',
|
||||
overflow: "hidden",
|
||||
margin: "12px 0",
|
||||
fontFamily: "var(--zds-typography-base,Inter,Helvetica,arial,sans-serif)"
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: "row",
|
||||
overflow: "hidden",
|
||||
gap: 8,
|
||||
textOverflow: "ellipsis",
|
||||
whiteSpace: "nowrap",
|
||||
color: '#F1F1F1'
|
||||
}}
|
||||
>
|
||||
{(allActivatedAppIds && allActivatedAppIds.includes(data.objectID)) && <Box sx={{ width: 8, height: 8, backgroundColor: "#02CB70", borderRadius: '50%' }} />}
|
||||
{normalizedString(data.name)}
|
||||
</div>
|
||||
|
||||
<div
|
||||
style={{
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
whiteSpace: "nowrap",
|
||||
color: "rgba(158, 158, 158, 1)",
|
||||
marginTop: 5
|
||||
}}
|
||||
>
|
||||
{data.categories !== null
|
||||
? normalizedString(data.categories).join(", ")
|
||||
: "NA"}
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: 'space-between',
|
||||
width: 230,
|
||||
textAlign: 'start',
|
||||
color: "rgba(158, 158, 158, 1)",
|
||||
}}
|
||||
>
|
||||
<div style={{ marginBottom: 15, }}>
|
||||
{hoverEffect === index && isCloud ? (
|
||||
<div>
|
||||
{data.tags && (
|
||||
<Tooltip
|
||||
title={data.tags.join(", ")}
|
||||
placement="bottom"
|
||||
componentsProps={{
|
||||
tooltip: {
|
||||
sx: {
|
||||
backgroundColor: "rgba(33, 33, 33, 1)",
|
||||
color: "rgba(241, 241, 241, 1)",
|
||||
width: "auto",
|
||||
height: "auto",
|
||||
fontSize: 16,
|
||||
border: "1px solid rgba(73, 73, 73, 1)",
|
||||
}
|
||||
}
|
||||
}}
|
||||
>
|
||||
<span>
|
||||
{data.tags.slice(0, 1).map((tag, tagIndex) => (
|
||||
<span key={tagIndex}>
|
||||
{normalizedString(tag)}
|
||||
{tagIndex < 1 ? ", " : ""}
|
||||
</span>
|
||||
))}
|
||||
</span>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div style={{ width: 230, textOverflow: "ellipsis", overflow: 'hidden', whiteSpace: 'nowrap', }}>
|
||||
{data.tags &&
|
||||
data.tags.map((tag, tagIndex) => (
|
||||
<span key={tagIndex}>
|
||||
{normalizedString(tag)}
|
||||
{tagIndex < data.tags.length - 1 ? ", " : ""}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div style={{ position: 'relative', bottom: 5 }}>
|
||||
{hoverEffect === index && isCloud && (
|
||||
<div>
|
||||
{allActivatedAppIds && allActivatedAppIds.includes(data.objectID) ? (
|
||||
<Button style={{
|
||||
width: 102,
|
||||
height: 35,
|
||||
borderRadius: 200,
|
||||
backgroundColor: "rgba(73, 73, 73, 1)",
|
||||
color: "rgba(241, 241, 241, 1)",
|
||||
textTransform: "none",
|
||||
}}
|
||||
onClick={(event) => {
|
||||
handleActivateButton(event, data, "deactivate");
|
||||
}}>
|
||||
Deactivate
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
style={{
|
||||
width: 102,
|
||||
height: 35,
|
||||
borderRadius: 200,
|
||||
backgroundColor: "rgba(242, 101, 59, 1)",
|
||||
color: "rgba(255, 255, 255, 1)",
|
||||
textTransform: "none",
|
||||
}}
|
||||
onClick={(event) => {
|
||||
handleActivateButton(event, data, "activate");
|
||||
}}
|
||||
>
|
||||
Activate
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ButtonBase>
|
||||
</Paper>
|
||||
</a>
|
||||
</Grid>
|
||||
</Zoom>
|
||||
);
|
||||
})
|
||||
}
|
||||
</div>
|
||||
</Grid >
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div><Box sx={{ position: 'absolute', top: '30%', left: '50%', }}> <CircularProgress /></Box></div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
const Apps2 = (props) => {
|
||||
const { globalUrl, isLoaded, serverside, userdata } = props;
|
||||
const { globalUrl, isLoaded, serverside, userdata, isLoggedIn } = props;
|
||||
let navigate = useNavigate();
|
||||
const { leftSideBarOpenByClick } = useContext(Context);
|
||||
const location = useLocation();
|
||||
@@ -91,22 +541,13 @@ const Apps2 = (props) => {
|
||||
const [selectedTagsForUserAndOrgApps, setSelectedTagsForUserAndOrgApps] = 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 isCloud =
|
||||
window.location.host === "localhost:3002" ||
|
||||
window.location.host === "shuffler.io";
|
||||
|
||||
useEffect(() => {
|
||||
const timer = setTimeout(() => {
|
||||
setShowNoAppFound(true);
|
||||
}, 1000);
|
||||
return () => clearTimeout(timer);
|
||||
}, []);
|
||||
const CustomHits = connectHits(Hits)
|
||||
|
||||
useEffect(() => {
|
||||
const queryParams = new URLSearchParams(location.search);
|
||||
@@ -124,45 +565,43 @@ const Apps2 = (props) => {
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
if (currTab === 1) {
|
||||
const fetchApps = async () => {
|
||||
const baseUrl = globalUrl;
|
||||
const userAppsUrl = `${baseUrl}/api/v1/users/me/apps`;
|
||||
fetch(userAppsUrl, {
|
||||
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`;
|
||||
} else {
|
||||
return; // No need to fetch if not in the relevant tabs
|
||||
}
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
method: "GET",
|
||||
credentials: "include",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
})
|
||||
.then((response) => response.json())
|
||||
.then((data) => {
|
||||
});
|
||||
const data = await response.json();
|
||||
if (currTab === 1) {
|
||||
console.log("data from userApps", data)
|
||||
setUserApps(data);
|
||||
setIsLoading(false);
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error("Error fetching user apps:", err);
|
||||
});
|
||||
} else if (currTab === 0) {
|
||||
const baseUrl = globalUrl;
|
||||
const appsUrl = `${baseUrl}/api/v1/apps`;
|
||||
fetch(appsUrl, {
|
||||
method: "GET",
|
||||
credentials: "include",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
})
|
||||
.then((response) => response.json())
|
||||
.then((data) => {
|
||||
console.log("data of orgApps", data)
|
||||
setOrgApps(data);
|
||||
setIsLoading(false)
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error("Error fetching apps:", err);
|
||||
});
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [currTab]);
|
||||
} catch (err) {
|
||||
console.error("Error fetching apps:", err);
|
||||
}
|
||||
};
|
||||
|
||||
fetchApps();
|
||||
}, [currTab, globalUrl]); // Added globalUrl to dependencies to avoid stale closures
|
||||
|
||||
|
||||
|
||||
@@ -174,8 +613,6 @@ const Apps2 = (props) => {
|
||||
}, [serverside]);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
const findTopCategories = () => {
|
||||
const categoryCountMap = {};
|
||||
|
||||
@@ -230,18 +667,6 @@ const Apps2 = (props) => {
|
||||
return topTags;
|
||||
};
|
||||
|
||||
const categories = findTopCategories();
|
||||
if (categories) {
|
||||
setCategories(categories)
|
||||
}
|
||||
console.log(categories)
|
||||
const tags = findTopTags();
|
||||
if (tags) {
|
||||
setLabels(tags);
|
||||
}
|
||||
console.log(tags)
|
||||
}, [currTab])
|
||||
|
||||
const handleCreateApp = () => {
|
||||
navigate('/create-app');
|
||||
};
|
||||
@@ -280,11 +705,16 @@ const Apps2 = (props) => {
|
||||
|
||||
|
||||
const handleTabChange = (newTab) => {
|
||||
console.log("Current Tab", newTab)
|
||||
setCurrTab(newTab);
|
||||
if (currTab === 0) {
|
||||
if (newTab === 0) {
|
||||
setAppsToShow(orgApps)
|
||||
const categories = findTopCategories();
|
||||
const labels = findTopTags();
|
||||
setCategories(categories);
|
||||
setLabels(labels);
|
||||
}
|
||||
if (currTab === 1) {
|
||||
if (newTab === 1) {
|
||||
setAppsToShow(userApps)
|
||||
}
|
||||
const newQueryParam = newTab === 0 ? 'org_apps' : newTab === 1 ? 'my_apps' : 'all_apps';
|
||||
@@ -292,7 +722,6 @@ const Apps2 = (props) => {
|
||||
queryParams.set('tab', newQueryParam);
|
||||
queryParams.delete('q');
|
||||
window.history.replaceState({}, '', `${location.pathname}?${queryParams.toString()}`);
|
||||
console.log("Current Tab", currTab)
|
||||
console.log("Apps to show", appsToShow)
|
||||
};
|
||||
|
||||
@@ -315,130 +744,10 @@ const Apps2 = (props) => {
|
||||
});
|
||||
|
||||
|
||||
const SearchBox = ({ refine, searchQuery, setSearchQuery }) => {
|
||||
var defaultSearch = "";
|
||||
|
||||
|
||||
//useEffect(() => {
|
||||
if (
|
||||
window !== undefined &&
|
||||
window.location !== undefined &&
|
||||
window.location.search !== undefined &&
|
||||
window.location.search !== null
|
||||
) {
|
||||
const urlSearchParams = new URLSearchParams(window.location.search);
|
||||
const params = Object.fromEntries(urlSearchParams.entries());
|
||||
const foundQuery = params["q"];
|
||||
if (foundQuery !== null && foundQuery !== undefined) {
|
||||
console.log("Got query: ", foundQuery);
|
||||
refine(foundQuery);
|
||||
defaultSearch = foundQuery;
|
||||
searchQuery = foundQuery
|
||||
}
|
||||
}
|
||||
//}, [])
|
||||
|
||||
|
||||
const handleSearch = () => {
|
||||
refine(searchQuery.trim());
|
||||
};
|
||||
|
||||
return (
|
||||
<form noValidate action="" role="search">
|
||||
<TextField
|
||||
value={searchQuery}
|
||||
fullWidth
|
||||
style={{
|
||||
backgroundColor: theme.palette.inputColor,
|
||||
marginTop: 20,
|
||||
marginLeft: 10,
|
||||
marginRight: 12,
|
||||
width: 693,
|
||||
borderRadius: 8,
|
||||
fontSize: 16,
|
||||
}}
|
||||
InputProps={{
|
||||
style: {
|
||||
color: "white",
|
||||
fontSize: "1em",
|
||||
height: 50,
|
||||
width: 693,
|
||||
borderRadius: 8,
|
||||
},
|
||||
startAdornment: (
|
||||
<InputAdornment position="start">
|
||||
<Search style={{ marginLeft: 5 }} />
|
||||
</InputAdornment>
|
||||
),
|
||||
endAdornment: (
|
||||
<InputAdornment position="end">
|
||||
{searchQuery.length > 0 && (
|
||||
<ClearIcon
|
||||
style={{
|
||||
color: "white",
|
||||
cursor: "pointer",
|
||||
marginRight: 10
|
||||
}}
|
||||
onClick={() => {
|
||||
setSearchQuery('')
|
||||
removeQuery("q");
|
||||
refine('')
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleSearch}
|
||||
style={{
|
||||
backgroundImage:
|
||||
"linear-gradient(to right, rgb(248, 106, 62), rgb(243, 64, 121))",
|
||||
color: "white",
|
||||
border: "none",
|
||||
padding: "10px 20px",
|
||||
width: 100,
|
||||
height: 35,
|
||||
borderRadius: 17.5,
|
||||
cursor: "pointer",
|
||||
}}
|
||||
>
|
||||
Search
|
||||
</button>
|
||||
</InputAdornment>
|
||||
),
|
||||
|
||||
}}
|
||||
autoComplete="off"
|
||||
color="primary"
|
||||
placeholder="Search more than 2500 Apps"
|
||||
id="shuffle_search_field"
|
||||
onChange={(event) => {
|
||||
setSearchQuery(event.currentTarget.value);
|
||||
removeQuery("q");
|
||||
refine(event.currentTarget.value);
|
||||
}}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === "Enter") {
|
||||
event.preventDefault();
|
||||
}
|
||||
}}
|
||||
limit={5}
|
||||
/>
|
||||
{/*isSearchStalled ? 'My search is stalled' : ''*/}
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
const CustomSearchBox = connectSearchBox(SearchBox);
|
||||
// const CustomHits = connectHits(Hits);
|
||||
|
||||
|
||||
|
||||
|
||||
return (
|
||||
<InstantSearch searchClient={searchClient} indexName="appsearch">
|
||||
<div style={boxStyle}>
|
||||
<Typography variant="h4" style={{ marginBottom: 20, paddingLeft: 15, textTransform: 'none' }}>
|
||||
Apps
|
||||
@@ -501,11 +810,24 @@ const Apps2 = (props) => {
|
||||
<MenuItem value="" >
|
||||
All Categories
|
||||
</MenuItem>
|
||||
{categories?.map((category) => (
|
||||
{
|
||||
currTab === 0 &&
|
||||
(
|
||||
categories?.map((category) => (
|
||||
<MenuItem value={category.category}>
|
||||
{category.category}
|
||||
</MenuItem>
|
||||
))}
|
||||
))
|
||||
)
|
||||
}
|
||||
{
|
||||
currTab === 2 &&
|
||||
(
|
||||
<div style={{ padding: "5px 10px" }}>
|
||||
<RefinementList attribute="categories" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
</Select>
|
||||
</div>
|
||||
@@ -521,12 +843,24 @@ const Apps2 = (props) => {
|
||||
<MenuItem value="">
|
||||
All Labels
|
||||
</MenuItem>
|
||||
{labels?.map((tag) => (
|
||||
{
|
||||
currTab === 0 &&
|
||||
(
|
||||
labels?.map((tag) => (
|
||||
<MenuItem value={tag.tag}>
|
||||
{tag.tag}
|
||||
</MenuItem>
|
||||
))}
|
||||
{/* Add label options here */}
|
||||
))
|
||||
)
|
||||
}
|
||||
{
|
||||
currTab === 2 &&
|
||||
(
|
||||
<div style={{ padding: "5px 10px" }}>
|
||||
<RefinementList attribute="action_labels" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</Select>
|
||||
</div>
|
||||
<div style={{ flex: 1, marginRight: 10 }}>
|
||||
@@ -542,20 +876,97 @@ const Apps2 = (props) => {
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
|
||||
{
|
||||
currTab === 0 &&
|
||||
(orgApps.length > 0 ? (
|
||||
<div style={{ rowGap: 16, columnGap: 16, marginTop: 16, display: "flex", flexWrap: "wrap", justifyContent: "flex-start", overflowY: "auto", overflowX: "hidden", maxHeight: 570, scrollbarWidth: "thin", scrollbarColor: "#494949 #2f2f2f", maxHeight: 570, minHeight: 570 }}>
|
||||
{
|
||||
isLoading ? <div
|
||||
style={{
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
}}
|
||||
> <CircularProgress /></div>
|
||||
:
|
||||
(
|
||||
filteredUserAppdata?.map((data, index) => (
|
||||
<AppCard key={index} data={data} index={index} mouseHoverIndex={mouseHoverIndex} setMouseHoverIndex={setMouseHoverIndex} globalUrl={globalUrl} deactivatedIndexes={deactivatedIndexes} />
|
||||
))
|
||||
)
|
||||
}
|
||||
</div>
|
||||
) : <div
|
||||
style={{
|
||||
marginTop: 100,
|
||||
fontSize: 20,
|
||||
fontWeight: 500,
|
||||
width: "100%",
|
||||
textAlign: "center",
|
||||
}}
|
||||
>No apps found</div>
|
||||
)
|
||||
}
|
||||
{
|
||||
currTab === 1 &&
|
||||
(userApps.length > 0 ? (
|
||||
<div style={{ rowGap: 16, columnGap: 16, marginTop: 16, display: "flex", flexWrap: "wrap", justifyContent: "flex-start", overflowY: "auto", overflowX: "hidden", maxHeight: 570, scrollbarWidth: "thin", scrollbarColor: "#494949 #2f2f2f", maxHeight: 570 }}>
|
||||
{orgApps.map((data, index) => (
|
||||
<AppCard key={index} data={data} index={index} mouseHoverIndex={mouseHoverIndex} setMouseHoverIndex={setMouseHoverIndex} globalUrl={globalUrl} />
|
||||
))}
|
||||
{
|
||||
isLoading ? <div
|
||||
style={{
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
}}
|
||||
> <CircularProgress /></div>
|
||||
:
|
||||
(
|
||||
userApps.map((data, index) => (
|
||||
<AppCard key={index} data={data} index={index} mouseHoverIndex={mouseHoverIndex} setMouseHoverIndex={setMouseHoverIndex} globalUrl={globalUrl} deactivatedIndexes={deactivatedIndexes} />
|
||||
))
|
||||
)
|
||||
}
|
||||
</div>
|
||||
) : <div
|
||||
style={{
|
||||
marginTop: 100,
|
||||
fontSize: 20,
|
||||
fontWeight: 500,
|
||||
width: "100%",
|
||||
textAlign: "center",
|
||||
}}
|
||||
>No apps found</div>
|
||||
)
|
||||
}
|
||||
|
||||
{
|
||||
currTab === 2 &&
|
||||
<CustomHits
|
||||
isLoggedIn={isLoggedIn}
|
||||
userdata={userdata}
|
||||
setIsAnyAppActivated={setIsAnyAppActivated}
|
||||
hitsPerPage={5}
|
||||
globalUrl={globalUrl}
|
||||
searchQuery={searchQuery}
|
||||
mouseHoverIndex={mouseHoverIndex}
|
||||
setMouseHoverIndex={setMouseHoverIndex}
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
<AppGrid
|
||||
{/* <AppGrid
|
||||
maxRows={4}
|
||||
showSuggestion={true}
|
||||
globalUrl={globalUrl}
|
||||
isMobile={isMobile}
|
||||
userdata={userdata}
|
||||
/>
|
||||
</div>
|
||||
/> */}
|
||||
</div >
|
||||
</InstantSearch>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user