{avatarMenu}
{notificationMenu}
diff --git a/frontend/src/components/Searchfield.js b/frontend/src/components/Searchfield.js
new file mode 100644
index 00000000..502b16ec
--- /dev/null
+++ b/frontend/src/components/Searchfield.js
@@ -0,0 +1,586 @@
+import React, {useState, useEffect, useRef} from 'react';
+
+import { useNavigate, Link, useParams } from "react-router-dom";
+import { useTheme } from '@material-ui/core/styles';
+import SearchIcon from '@material-ui/icons/Search';
+
+import {
+ Chip,
+ IconButton,
+ TextField,
+ InputAdornment,
+ List,
+ Card,
+ ListItem,
+ ListItemAvatar,
+ ListItemText,
+ Avatar,
+ Typography,
+ Tooltip,
+} from '@material-ui/core';
+
+import {
+ AvatarGroup,
+} from "@mui/material"
+
+import {Close as CloseIcon, Folder as FolderIcon, Polymer as PolymerIcon, LibraryBooks as LibraryBooksIcon} from '@material-ui/icons'
+
+import algoliasearch from 'algoliasearch/lite';
+import { InstantSearch, Configure, connectSearchBox, connectHits, Index } from 'react-instantsearch-dom';
+//import { InstantSearch, SearchBox, Hits, connectSearchBox, connectHits, Index } from 'react-instantsearch-dom';
+
+// https://www.algolia.com/doc/api-reference/widgets/search-box/react/
+//const searchClient = algoliasearch("WIC466KTV8", "fff61b6ae3802ec2b47fed9ab50b532e")
+const chipStyle = {
+ backgroundColor: "#3d3f43", height: 30, marginRight: 5, paddingLeft: 5, paddingRight: 5, height: 28, cursor: "pointer", borderColor: "#3d3f43", color: "white",
+}
+
+const searchClient = algoliasearch("JNSS5CFDZZ", "db08e40265e2941b9a7d8f644b6e5240")
+//const searchClient = algoliasearch("L55H18ZINA", "a19be455e7e75ee8f20a93d26b9fc6d6")
+const SearchField = props => {
+ const { serverside, } = props
+ const theme = useTheme();
+ let navigate = useNavigate();
+ const borderRadius = 3
+ const node = useRef()
+ const [searchOpen, setSearchOpen] = useState(false)
+ const [oldPath, setOldPath] = useState("")
+
+ if (serverside === true) {
+ return null
+ }
+
+ if (window !== undefined && window.location !== undefined && window.location.pathname === "/search") {
+ return null
+ }
+
+ const isCloud =
+ window.location.host === "localhost:3002" ||
+ window.location.host === "shuffler.io";
+
+ if (window.location.pathname !== oldPath) {
+ setSearchOpen(false)
+ setOldPath(window.location.pathname)
+ }
+
+ //useEffect(() => {
+ // if (searchOpen) {
+ // var tarfield = document.getElementById("shuffle_search_field")
+ // tarfield.focus()
+ // }
+ //}, searchOpen)
+
+ const SearchBox = ({currentRefinement, refine, isSearchStalled, } ) => {
+
+ /*
+ endAdornment: (
+
{
+ event.preventDefault()
+ }}>
+ {
+ setSearchOpen(false)
+ }} />
+
+ ),
+ */
+
+ return (
+
+ )
+ }
+
+ const WorkflowHits = ({ hits }) => {
+ const [mouseHoverIndex, setMouseHoverIndex] = useState(0)
+
+ var tmp = searchOpen
+ if (!searchOpen) {
+ return null
+ }
+
+
+ const positionInfo = document.activeElement.getBoundingClientRect()
+ const outerlistitemStyle = {
+ width: "100%",
+ overflowX: "hidden",
+ overflowY: "hidden",
+ borderBottom: "1px solid rgba(255,255,255,0.4)",
+ }
+
+ if (hits.length > 4) {
+ hits = hits.slice(0, 4)
+ }
+
+ var type = "workflows"
+ const baseImage =
+
+ return (
+
+
+ Workflows
+
+
+
+ {hits.length === 0 ?
+
+ console.log(hits)}>
+
+
+
+
+
+
+ :
+ hits.map((hit, index) => {
+ const innerlistitemStyle = {
+ width: positionInfo.width+35,
+ overflowX: "hidden",
+ overflowY: "hidden",
+ borderBottom: "1px solid rgba(255,255,255,0.4)",
+ backgroundColor: mouseHoverIndex === index ? "#1f2023" : "inherit",
+ cursor: "pointer",
+ marginLeft: 5,
+ marginRight: 5,
+ maxHeight: 75,
+ minHeight: 75,
+ maxWidth: 420,
+ minWidth: "100%",
+ }
+
+ const name = hit.name === undefined ?
+ hit.filename.charAt(0).toUpperCase() + hit.filename.slice(1).replaceAll("_", " ") + " - " + hit.title :
+ (hit.name.charAt(0).toUpperCase()+hit.name.slice(1)).replaceAll("_", " ")
+ const secondaryText = hit.description !== undefined && hit.description !== null && hit.description.length > 3 ? hit.description.slice(0, 40)+"..." : ""
+ const appGroup = hit.action_references === undefined || hit.action_references === null ? [] : hit.action_references
+ const avatar = baseImage
+
+ var parsedUrl = isCloud ? `/workflows/${hit.objectID}` : `https://shuffler.io/workflows/${hit.objectID}`
+ parsedUrl += `?queryID=${hit.__queryID}`
+
+ //
+ return (
+ {
+ //console.log("CLICK")
+ setSearchOpen(true)
+
+ if (!isCloud) {
+ event.preventDefault()
+ window.open(parsedUrl, '_blank');
+ }
+ }}>
+ {
+ setMouseHoverIndex(index)
+ }}>
+
+ {avatar}
+
+
+
+
+ {appGroup.map((app, index) => {
+ // Putting all this in secondary of ListItemText looked weird.
+ return (
+ {
+ navigate("/apps/"+app.id)
+ }}
+ >
+
+
+
+
+ )
+ })}
+
+
+ {/*
+
+
+
+
+
+ */}
+
+
+ )})
+ }
+
+ {/*
+
+
+
+ See all workflows
+
+
+
+ */}
+
+ )
+ }
+
+ const AppHits = ({ hits }) => {
+ const [mouseHoverIndex, setMouseHoverIndex] = useState(0)
+
+ var tmp = searchOpen
+ if (!searchOpen) {
+ return null
+ }
+
+ const positionInfo = document.activeElement.getBoundingClientRect()
+ const outerlistitemStyle = {
+ width: "100%",
+ overflowX: "hidden",
+ overflowY: "hidden",
+ borderBottom: "1px solid rgba(255,255,255,0.4)",
+ }
+
+ if (hits.length > 4) {
+ hits = hits.slice(0, 4)
+ }
+
+ var type = "app"
+ const baseImage =
+
+ return (
+
+ {
+ setSearchOpen(false)
+ }}>
+
+
+
+ Apps
+
+
+
+ {hits.length === 0 ?
+
+ console.log(hits)}>
+
+
+
+
+
+
+ :
+ hits.map((hit, index) => {
+ const innerlistitemStyle = {
+ width: positionInfo.width+35,
+ overflowX: "hidden",
+ overflowY: "hidden",
+ borderBottom: "1px solid rgba(255,255,255,0.4)",
+ backgroundColor: mouseHoverIndex === index ? "#1f2023" : "inherit",
+ cursor: "pointer",
+ marginLeft: 5,
+ marginRight: 5,
+ maxHeight: 75,
+ minHeight: 75,
+ maxWidth: 420,
+ minWidth: "100%",
+ }
+
+ const name = hit.name === undefined ?
+ hit.filename.charAt(0).toUpperCase() + hit.filename.slice(1).replaceAll("_", " ") + " - " + hit.title :
+ (hit.name.charAt(0).toUpperCase()+hit.name.slice(1)).replaceAll("_", " ")
+
+ var secondaryText = hit.data !== undefined ? hit.data.slice(0, 40)+"..." : ""
+ const avatar = hit.image_url === undefined ?
+ baseImage
+ :
+
+
+ //console.log(hit)
+ if (hit.categories !== undefined && hit.categories !== null && hit.categories.length > 0) {
+ secondaryText = hit.categories.slice(0,3).map((data, index) => {
+ if (index === 0) {
+ return data
+ }
+
+ return ", "+data
+
+ /*
+ {
+ //handleChipClick
+ }}
+ variant="outlined"
+ color="primary"
+ />
+ */
+ })
+ }
+
+ var parsedUrl = isCloud ? `/apps/${hit.objectID}` : `https://shuffler.io/apps/${hit.objectID}`
+ parsedUrl += `?queryID=${hit.__queryID}`
+
+ return (
+ {
+ console.log("CLICK")
+ setSearchOpen(true)
+
+ if (!isCloud) {
+ event.preventDefault()
+ window.open(parsedUrl, '_blank');
+ }
+ }}>
+ {
+ setMouseHoverIndex(index)
+ }}>
+
+ {avatar}
+
+
+ {/*
+
+
+
+
+
+ */}
+
+
+ )})
+ }
+
+
+
+
+ See more
+
+
+
+
+ )
+ }
+
+ const DocHits = ({ hits }) => {
+ const [mouseHoverIndex, setMouseHoverIndex] = useState(0)
+
+ var tmp = searchOpen
+ if (!searchOpen) {
+ return null
+ }
+
+
+ const positionInfo = document.activeElement.getBoundingClientRect()
+ const outerlistitemStyle = {
+ width: "100%",
+ overflowX: "hidden",
+ overflowY: "hidden",
+ borderBottom: "1px solid rgba(255,255,255,0.4)",
+ }
+
+ if (hits.length > 4) {
+ hits = hits.slice(0, 4)
+ }
+
+ const type = "documentation"
+ const baseImage =
+
+ //console.log(type, hits.length, hits)
+
+ return (
+
+ {
+ setSearchOpen(false)
+ }}>
+
+
+
+ Documentation
+
+ {/*
+ {
+ setSearchOpen(false)
+ }}>
+
+
+ */}
+
+ {hits.length === 0 ?
+
+ console.log(hits)}>
+
+
+
+
+
+
+ :
+ hits.map((hit, index) => {
+ const innerlistitemStyle = {
+ width: positionInfo.width+35,
+ overflowX: "hidden",
+ overflowY: "hidden",
+ borderBottom: "1px solid rgba(255,255,255,0.4)",
+ backgroundColor: mouseHoverIndex === index ? "#1f2023" : "inherit",
+ cursor: "pointer",
+ marginLeft: 5,
+ marginRight: 5,
+ maxHeight: 75,
+ minHeight: 75,
+ maxWidth: 420,
+ minWidth: "100%",
+ }
+
+ var name = hit.name === undefined ?
+ hit.filename.charAt(0).toUpperCase() + hit.filename.slice(1).replaceAll("_", " ") + " - " + hit.title :
+ (hit.name.charAt(0).toUpperCase()+hit.name.slice(1)).replaceAll("_", " ")
+
+ if (name.length > 30) {
+ name = name.slice(0, 30)+"..."
+ }
+ const secondaryText = hit.data !== undefined ? hit.data.slice(0, 40)+"..." : ""
+ const avatar = hit.image_url === undefined ?
+ baseImage
+ :
+
+
+ var parsedUrl = hit.urlpath !== undefined ? hit.urlpath : ""
+ parsedUrl += `?queryID=${hit.__queryID}`
+
+ return (
+ {
+ console.log("CLICK")
+ setSearchOpen(true)
+ }}>
+ {
+ setMouseHoverIndex(index)
+ }}>
+
+ {avatar}
+
+
+ {/*
+
+
+
+
+
+ */}
+
+
+ )})
+ }
+
+ {type === "documentation" ?
+
+
+ Search by
+
+
+
+
+
+ : null}
+
+ )
+ }
+
+ const CustomSearchBox = connectSearchBox(SearchBox)
+ const CustomAppHits = connectHits(AppHits)
+ const CustomWorkflowHits = connectHits(WorkflowHits)
+ const CustomDocHits = connectHits(DocHits)
+
+ return (
+
+ {
+ console.log("CLICKED")
+ }}>
+
+
+
+
+
+
+
+
+
+
+
+
+
+ )
+}
+
+export default SearchField;
diff --git a/frontend/src/components/WorkflowGrid.jsx b/frontend/src/components/WorkflowGrid.jsx
new file mode 100644
index 00000000..f639bce3
--- /dev/null
+++ b/frontend/src/components/WorkflowGrid.jsx
@@ -0,0 +1,360 @@
+import React, { useEffect, useState } from 'react';
+
+import { useTheme } from '@material-ui/core/styles';
+import {Link} from 'react-router-dom';
+
+import { Search as SearchIcon, CloudQueue as CloudQueueIcon, Code as CodeIcon } from '@material-ui/icons';
+
+import algoliasearch from 'algoliasearch/lite';
+import { InstantSearch, Configure, connectSearchBox, connectHits } from 'react-instantsearch-dom';
+import {
+ Grid,
+ Paper,
+ TextField,
+ ButtonBase,
+ InputAdornment,
+ Typography,
+ Button,
+ Tooltip,
+ Zoom,
+ Chip,
+} from '@material-ui/core';
+
+import WorkflowPaper from "../components/WorkflowPaper.jsx"
+
+const searchClient = algoliasearch("JNSS5CFDZZ", "db08e40265e2941b9a7d8f644b6e5240")
+const AppGrid = props => {
+ const { maxRows, showName, showSuggestion, isMobile, globalUrl, parsedXs } = props
+
+ const isCloud =
+ window.location.host === "localhost:3002" ||
+ window.location.host === "shuffler.io";
+
+ const rowHandler = maxRows === undefined || maxRows === null ? 50 : maxRows
+ const xs = parsedXs === undefined || parsedXs === null ? isMobile ? 6 : 2 : parsedXs
+ const theme = useTheme();
+ //const [apps, setApps] = React.useState([]);
+ //const [filteredApps, setFilteredApps] = React.useState([]);
+ const [formMail, setFormMail] = React.useState("");
+ const [message, setMessage] = React.useState("");
+ const [formMessage, setFormMessage] = React.useState("");
+ const [usecases, setUsecases] = React.useState([]);
+
+ const buttonStyle = {borderRadius: 30, height: 50, width: 220, margin: isMobile ? "15px auto 15px auto" : 20, fontSize: 18,}
+
+ const innerColor = "rgba(255,255,255,0.65)"
+ const borderRadius = 3
+ window.title = "Shuffle | Workflows | Discover your use-case"
+
+ const submitContact = (email, message) => {
+ const data = {
+ "firstname": "",
+ "lastname": "",
+ "title": "",
+ "companyname": "",
+ "email": email,
+ "phone": "",
+ "message": message,
+ }
+
+ const errorMessage = "Something went wrong. Please contact frikky@shuffler.io directly."
+
+ fetch(globalUrl+"/api/v1/contact", {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify(data),
+ })
+ .then(response => response.json())
+ .then(response => {
+ if (response.success === true) {
+ setFormMessage(response.reason)
+ //alert.info("Thanks for submitting!")
+ } else {
+ setFormMessage(errorMessage)
+ }
+
+ setFormMail("")
+ setMessage("")
+ })
+ .catch(error => {
+ setFormMessage(errorMessage)
+ console.log(error)
+ });
+ }
+
+ const handleKeysetting = (categorydata, workflows) => {
+ console.log("Workflows: ", workflows)
+ //workflows[0].category = ["detect"]
+ //workflows[0].usecase_ids = ["Correlate tickets"]
+
+ if (workflows !== undefined && workflows !== null) {
+ const newcategories = []
+ for (var key in categorydata) {
+ var category = categorydata[key]
+ category.matches = []
+
+ for (var subcategorykey in category.list) {
+ var subcategory = category.list[subcategorykey]
+ subcategory.matches = []
+
+ for (var workflowkey in workflows) {
+ const workflow = workflows[workflowkey]
+
+ if (workflow.usecase_ids !== undefined && workflow.usecase_ids !== null) {
+ for (var usecasekey in workflow.usecase_ids) {
+ if (workflow.usecase_ids[usecasekey].toLowerCase() === subcategory.name.toLowerCase()) {
+ console.log("Got match: ", workflow.usecase_ids[usecasekey])
+
+ category.matches.push({
+ "workflow": workflow.id,
+ "category": subcategory.name,
+ })
+ subcategory.matches.push(workflow.id)
+ break
+ }
+ }
+ }
+
+ if (subcategory.matches.length > 0) {
+ break
+ }
+ }
+ }
+
+ newcategories.push(category)
+ }
+
+ console.log("Categories: ", newcategories)
+ setUsecases(newcategories)
+ } else {
+ for (var key in categorydata) {
+ categorydata[key].matches = []
+ }
+ setUsecases(categorydata)
+ }
+ }
+
+ const fetchUsecases = (workflows) => {
+ fetch(globalUrl + "/api/v1/workflows/usecases", {
+ method: "GET",
+ headers: {
+ "Content-Type": "application/json",
+ Accept: "application/json",
+ },
+ credentials: "include",
+ })
+ .then((response) => {
+ if (response.status !== 200) {
+ console.log("Status not 200 for usecases");
+ }
+
+ return response.json();
+ })
+ .then((responseJson) => {
+ if (responseJson.success !== false) {
+ console.log("Usecases: ", responseJson)
+ //handleKeysetting(responseJson, workflows)
+ }
+ })
+ .catch((error) => {
+ //alert.error("ERROR: " + error.toString());
+ console.log("ERROR: " + error.toString());
+ });
+ };
+
+ useEffect(() => {
+ fetchUsecases()
+ }, [])
+
+ // value={currentRefinement}
+ const SearchBox = ({currentRefinement, refine, isSearchStalled} ) => {
+ 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)
+ }
+ }
+ }, [])
+
+ return (
+
+ )
+ }
+
+ const paperAppContainer = {
+ display: "flex",
+ flexWrap: "wrap",
+ alignContent: "space-between",
+ marginTop: 5,
+ }
+
+ var workflowDelay = -50
+ const Hits = ({ hits }) => {
+ const [mouseHoverIndex, setMouseHoverIndex] = useState(-1)
+ var counted = 0
+
+
+ return (
+
+ {hits.map((data, index) => {
+ workflowDelay += 50
+
+ return (
+
+
+
+
+
+ )
+ })}
+
+ )
+ }
+
+ const CustomSearchBox = connectSearchBox(SearchBox)
+ const CustomHits = connectHits(Hits)
+
+ return (
+
+
+
+
+
+
+ {usecases !== null && usecases !== undefined && usecases.length > 0 ?
+
+ {usecases.map((usecase, index) => {
+ console.log(usecase)
+ return (
+ {
+ console.log("Clicked!")
+ //addFilter(usecase.name.slice(3,usecase.name.length))
+ }}
+ variant="outlined"
+ color="primary"
+ />
+ )
+ })}
+
+ : null}
+
+
+ {showSuggestion === true ?
+
+
+ Can't find what you're looking for?
+
+
+ setFormMail(e.target.value)}
+ />
+ setMessage(e.target.value)}
+ />
+
+
+
{formMessage}
+
+ : null
+ }
+
+
+
+ Search by
+
+
+
+
+
+
+ )
+}
+
+export default AppGrid;
diff --git a/frontend/src/components/WorkflowPaper.jsx b/frontend/src/components/WorkflowPaper.jsx
index 5d3b68cb..21a9d7ea 100644
--- a/frontend/src/components/WorkflowPaper.jsx
+++ b/frontend/src/components/WorkflowPaper.jsx
@@ -91,6 +91,11 @@ const WorkflowPaper = (props) => {
}
//console.log("IMG: ", data)
+ var parsedUrl = `workflows/${data.objectID}`
+
+ if (data.__queryID !== undefined && data.__queryID !== null) {
+ parsedUrl += `?queryID=${data.__queryID}`
+ }
return (
@@ -134,7 +139,7 @@ const WorkflowPaper = (props) => {
}}
>
{parsedName}
diff --git a/frontend/src/views/Search.jsx b/frontend/src/views/Search.jsx
new file mode 100644
index 00000000..8ee8a186
--- /dev/null
+++ b/frontend/src/views/Search.jsx
@@ -0,0 +1,180 @@
+import React, { useState, useEffect } from "react";
+
+import theme from '../theme';
+import {isMobile} from "react-device-detect";
+import AppGrid from "../components/AppGrid.jsx"
+import WorkflowGrid from "../components/WorkflowGrid.jsx"
+import CreatorGrid from "../components/CreatorGrid.jsx"
+import DocsGrid from "../components/DocsGrid.jsx"
+import { useNavigate, Link } from "react-router-dom";
+
+import {
+ Tabs,
+ Paper,
+ Tab,
+} from "@material-ui/core";
+
+import {
+ Business as BusinessIcon,
+ Apps as AppsIcon,
+ Polymer as PolymerIcon,
+ EmojiObjects as EmojiObjectsIcon,
+ Description as DescriptionIcon,
+} from "@material-ui/icons";
+
+
+const bodyDivStyle = {
+ margin: "auto",
+ maxWidth: 1024,
+ scrollX: "hidden",
+ overflowX: "hidden",
+}
+
+const boxStyle = {
+ color: "white",
+ flex: "1",
+ marginLeft: 10,
+ marginRight: 10,
+ paddingLeft: 30,
+ paddingRight: 30,
+ paddingBottom: 30,
+ paddingTop: 30,
+ display: "flex",
+ flexDirection: "column",
+ overflowX: "hidden",
+ minHeight: 400,
+}
+
+// Should be different if logged in :|
+const Search = (props) => {
+ const { globalUrl, isLoaded, serverside, userdata } = props;
+ let navigate = useNavigate();
+
+ const [curTab, setCurTab] = useState(0);
+ const iconStyle = { marginRight: 10 };
+
+ const views = {
+ 0: "apps",
+ 1: "workflows",
+ 2: "docs",
+ 3: "creators",
+ }
+
+ const setConfig = (event, inputValue) => {
+ const newValue = parseInt(inputValue)
+ console.log("NEW: ", newValue)
+
+ setCurTab(newValue)
+ if (newValue === 0) {
+ document.title = "Shuffle - search - apps";
+ } else if (newValue === 1) {
+ document.title = "Shuffle - search - workflows";
+ } else if (newValue === 2) {
+ document.title = "Shuffle - search - documentation";
+ } else if (newValue === 3) {
+ document.title = "Shuffle - search - creators";
+ } else {
+ document.title = "Shuffle - search";
+ }
+
+
+ const urlSearchParams = new URLSearchParams(window.location.search)
+ const params = Object.fromEntries(urlSearchParams.entries())
+ const foundQuery = params["q"]
+ var extraQ = ""
+ if (foundQuery !== null && foundQuery !== undefined) {
+ extraQ = "&q="+foundQuery
+ }
+
+ navigate(`/search?tab=${views[newValue]}`+extraQ)
+ }
+
+ useEffect(() => {
+ if (serverside !== true && window.location.search !== undefined && window.location.search !== null) {
+ const urlSearchParams = new URLSearchParams(window.location.search)
+ const params = Object.fromEntries(urlSearchParams.entries())
+ const foundTab = params["tab"]
+ if (foundTab !== null && foundTab !== undefined) {
+ for (var key in Object.keys(views)) {
+ const value = views[key]
+ console.log(key, value)
+ if (value === foundTab) {
+ setConfig("", key)
+ break
+ }
+ }
+ }
+ }
+ }, [])
+
+ if (isLoaded === false) {
+ return null
+ }
+
+
+ // Random names for type & autoComplete. Didn't research :^)
+ const landingpageDataBrowser =
+
+
+
+
+ Apps
+
+ />
+
+ Workflows
+
+ />
+
+ Docs
+
+ />
+
+ Creators
+
+ />
+
+ {curTab === 0 ?
+
+ :
+ curTab === 1 ?
+
+ :
+ curTab === 2 ?
+
+ :
+ curTab === 3 ?
+
+ :
+ null}
+
+
+
+ const loadedCheck = isLoaded ?
+
+
{landingpageDataBrowser}
+
+ :
+
+
+
+ // #1f2023?
+ return(
+
+ {loadedCheck}
+
+ )
+}
+
+export default Search;