diff --git a/frontend/src/components/AppFramework.jsx b/frontend/src/components/AppFramework.jsx new file mode 100644 index 00000000..e6be25f1 --- /dev/null +++ b/frontend/src/components/AppFramework.jsx @@ -0,0 +1,2192 @@ +import React, { useState, useEffect } from 'react'; + +import CytoscapeComponent from 'react-cytoscapejs'; +import frameworkStyle from '../frameworkStyle.jsx'; +import { v4 as uuidv4 } from "uuid"; +import theme from '../theme.jsx'; +import { useAlert } from "react-alert"; + +import AppSearch from '../components/Appsearch.jsx'; +import PaperComponent from "../components/PaperComponent.jsx" +import { usecaseTypes } from "../components/UsecaseSearch.jsx" +import SuggestedWorkflows from "../components/SuggestedWorkflows.jsx" +import { securityFramework} from "../components/LandingpageUsecases.jsx"; + +import { + Paper, + Typography, + Divider, + IconButton, + Badge, + CircularProgress, + Tooltip, + Dialog, + Chip, + Avatar, +} from "@material-ui/core"; + +import { + Button +} from '@material-ui/core'; + +import { + Close as CloseIcon, + Delete as DeleteIcon, +} from "@material-ui/icons"; + +import * as edgehandles from "cytoscape-edgehandles"; +import * as cytoscape from "cytoscape"; + +cytoscape.use(edgehandles); + + +const svgSize = "40px" +const parsedDatatypeImages = { + "SIEM": encodeURI(`data:image/svg+xml;utf-8,`), + + "CASES": encodeURI(`data:image/svg+xml;utf-8,`), + + "EDR & AV": encodeURI(`data:image/svg+xml;utf-8,`), + + "INTEL": encodeURI(`data:image/svg+xml;utf-8,`), + + "COMMS": encodeURI(`data:image/svg+xml;utf-8,`), + + "NETWORK": encodeURI(`data:image/svg+xml;utf-8,`), + + "INTEL": encodeURI(`data:image/svg+xml;utf-8,`), + + "ASSETS": encodeURI(`data:image/svg+xml;utf-8,`), + + "IAM": encodeURI(`data:image/svg+xml;utf-8,`), +} + +export const usecases = { + "None": { + "manual": [], + "automated": [], + }, + "Phishing": { + "manual": [], + "automated": [ + { + "source": "BOTTOM_LEFT", + "target": "COMMS", + "description": "Email received", + "human": false, + }, + { + "source": "COMMS", + "target": "SHUFFLE", + "human": false, + }, + { + "source": "SHUFFLE", + "target": "CASES", + "human": false, + }, + { + "source": "INTEL", + "target": "SHUFFLE", + "human": false, + }, + { + "source": "ASSETS", + "target": "SHUFFLE", + "human": false, + }, + { + "source": "SIEM", + "target": "SHUFFLE", + "human": false, + }, + { + "source": "SHUFFLE", + "target": "INTEL", + "human": false, + }, + { + "source": "SHUFFLE", + "target": "COMMS", + "human": false, + }, + { + "source": "CASES", + "target": "EDR & AV", + "human": true, + }, + ]}, + + "Exploits": { + "manual": [], + "automated": [ + { + "source": "TOP_LEFT", + "target": "NETWORK", + "description": "Exploit", + "human": false, + }, + { + "source": "NETWORK", + "target": "SIEM", + "description": "WAF alert", + "human": false, + }, + { + "source": "SIEM", + "target": "SHUFFLE", + "human": false, + }, + { + "source": "SHUFFLE", + "target": "CASES", + "human": false, + }, + { + "source": "SHUFFLE", + "target": "COMMS", + "human": false, + }, + { + "source": "INTEL", + "target": "SHUFFLE", + "human": false, + }, + { + "source": "ASSETS", + "target": "SHUFFLE", + "human": false, + }, + { + "source": "IAM", + "target": "SHUFFLE", + "human": false, + }, + { + "source": "CASES", + "target": "EDR & AV", + "human": true, + }, + ] + }, + "SIEM alerts": { + "manual": [], + "automated": [ + { + "source": "TOP_LEFT", + "target": "SIEM", + "description": "Syslog", + "human": false, + }, + { + "source": "SIEM", + "target": "SHUFFLE", + "description": "Alerts", + "human": false, + }, + { + "source": "SHUFFLE", + "target": "INTEL", + "description": "Enrich", + "human": false, + }, + { + "source": "INTEL", + "target": "SHUFFLE", + "human": false, + }, + { + "source": "IAM", + "target": "SHUFFLE", + "human": false, + "description": "enrich", + }, + { + "source": "SHUFFLE", + "target": "IAM", + "human": false, + }, + { + "source": "SHUFFLE", + "target": "CASES", + "human": false, + }, + { + "source": "SHUFFLE", + "target": "COMMS", + "human": false, + }, + { + "source": "SHUFFLE", + "target": "EDR & AV", + "human": false, + }, + { + "source": "SHUFFLE", + "target": "EDR & AV", + "human": true, + }, + ] + }, + "New Detections": { + "manual": [], + "automated": [ + { + "source": "TOP_LEFT", + "target": "SIEM", + "description": "Hypothesis", + "human": true, + }, + { + "source": "SIEM", + "target": "SIEM", + "description": "Create rule", + "human": true, + }, + { + "source": "SIEM", + "target": "NETWORK", + "description": "Create rule", + "human": true, + }, + { + "source": "NETWORK", + "target": "EDR & AV", + "description": "Create rule", + "human": true, + }, + { + "source": "SIEM", + "target": "SHUFFLE", + "description": "Send alert", + "human": false, + }, + { + "source": "NETWORK", + "target": "SHUFFLE", + "description": "Send alert", + "human": false, + }, + { + "source": "EDR & AV", + "target": "SHUFFLE", + "description": "Send alert", + "human": false, + }, + { + "source": "INTEL", + "target": "SHUFFLE", + "description": "Enrich IOCs", + "human": false, + }, + { + "source": "ASSETS", + "target": "SHUFFLE", + "description": "Enrich hostnames etc.", + "human": false, + }, + { + "source": "SHUFFLE", + "target": "CASES", + "description": "Create enriched alert", + "human": false, + }, + ] + }, + "Vulnerabilities": { + "manual": [], + "automated": [ + { + "source": "TOP_RIGHT", + "target": "ASSETS", + "description": "New vuln", + "human": false, + }, + { + "source": "ASSETS", + "target": "SHUFFLE", + "description": "Get vuln", + "human": false, + }, + { + "source": "SHUFFLE", + "target": "CASES", + "description": "Raise ticket", + "human": false, + }, + { + "source": "SHUFFLE", + "target": "COMMS", + "description": "Notify owner", + "human": false, + }, + { + "source": "COMMS", + "target": "SHUFFLE", + "description": "", + "human": true, + }, + { + "source": "SHUFFLE", + "target": "ASSETS", + "description": "Auto-patch", + "human": false, + }, + ] + }, + "Approvals": { + "manual": [], + "automated": [ + { + "source": "TOP_LEFT", + "target": "CASES", + "description": "New inquiry", + "human": false, + }, + { + "source": "CASES", + "target": "SHUFFLE", + "description": "Get tickets", + "human": false, + }, + { + "source": "SHUFFLE", + "target": "COMMS", + "description": "Ask for approval", + "human": false, + }, + { + "source": "COMMS", + "target": "SHUFFLE", + "human": true, + }, + { + "source": "SHUFFLE", + "target": "ASSETS", + "description": "Add to user", + "human": false, + }, + { + "source": "SHUFFLE", + "target": "IAM", + "description": "Approve access", + "human": false, + }, + { + "source": "SHUFFLE", + "target": "CASES", + "human": false, + }, + ] + }, + "Enrichment": { + "manual": [], + "automated": [ + { + "source": "TOP_LEFT", + "target": "CASES", + "description": "Case updated", + "human": false, + }, + { + "source": "SHUFFLE", + "target": "CASES", + "description": "Get and enrich ticket", + "human": false, + }, + { + "source": "IAM", + "target": "SHUFFLE", + "description": "Get access rights", + "human": false, + }, + { + "source": "ASSETS", + "target": "SHUFFLE", + "description": "Get relevant assets", + "human": false, + }, + { + "source": "INTEL", + "target": "SHUFFLE", + "description": "Get relevant IPs", + "human": false, + }, + { + "source": "COMMS", + "target": "SHUFFLE", + "description": "Find relevant mails & chats", + "human": false, + }, + { + "source": "EDR & AV", + "target": "SHUFFLE", + "description": "Find incidents for host", + "human": false, + }, + { + "source": "SIEM", + "target": "SHUFFLE", + "description": "Find info about hostname and user", + "human": false, + }, + { + "source": "SHUFFLE", + "target": "SHUFFLE", + "description": "Format info", + "human": false, + }, + { + "source": "SHUFFLE", + "target": "CASES", + "description": "", + "human": false, + }, + ] + }, + "Ransomware": { + "manual": [], + "automated": [ + { + "source": "BOTTOM_LEFT", + "target": "EDR & AV", + "description": "EDR & AV alert", + "human": false, + }, + { + "source": "EDR & AV", + "target": "SHUFFLE", + "description": "", + "human": false, + }, + { + "source": "SHUFFLE", + "target": "EDR & AV", + "human": false, + "description": "isolate", + }, + { + "source": "SHUFFLE", + "target": "IAM", + "human": false, + "description": "Block access", + }, + { + "source": "SHUFFLE", + "target": "COMMS", + "description": "Notify oncall and affected user", + "human": false, + }, + { + "source": "SHUFFLE", + "target": "CASES", + "description": "Create enriched alert", + "human": false, + }, + { + "source": "SHUFFLE", + "target": "CASES", + "human": false, + }, + { + "source": "CASES", + "target": "EDR & AV", + "description": "Validate alert", + "human": true, + }, + ] + }, + "Draw": { + } +} + +const AppFramework = (props) => { + const { globalUrl, isLoaded, showOptions, selectedOption, rolling, frameworkData, setFrameworkData, size, inputUsecase, isLoggedIn, color, discoveryWrapper, setDiscoveryWrapper, userdata, apps, inputUsecases, setInputUsecases } = props; + const [cy, setCy] = React.useState() + const [edgesStarted, setEdgesStarted] = React.useState(false) + const [graphDone, setGraphDone] = React.useState(false) + const [cyDone, setCyDone] = React.useState(false) + const [discoveryData, setDiscoveryData] = React.useState({}) + const [selectionOpen, setSelectionOpen] = React.useState(true) + const [frameworkSuggestions, setFrameworkSuggestions] = React.useState([]) + const [newSelectedApp, setNewSelectedApp] = React.useState({}) + const [defaultSearch, setDefaultSearch] = React.useState("") + const [animationStarted, setAnimationStarted] = React.useState(false) + const [paperTitle, setPaperTitle] = React.useState("") + const [changedApp, setChangedApp] = React.useState("") + + + const [usecaseType, setUsecaseType] = React.useState(0) + const [selectedUsecase, setSelectedUsecase] = React.useState(selectedOption !== undefined ? selectedOption : "Phishing") + + const [injectedApps, setInjectedApps] = React.useState([]) + + const scale = size === undefined ? 1 : size > 5 ? 3 : size + + const alert = useAlert() + + + const handleLoadNextSuggestion = (frameworkData) => { + console.log("Should check for next apps to load from App suggestion model") + //fetch(globalUrl + "/api/v1/workflows/usecases", { + //credentials: "include", + //cors: "no-cors", + + const max_suggestions = 5 + const max_per_category = 2 + const priorities = { + "edr": 1, + "siem": 2, + "cases": 3, + "intel": 4, + "comms": 5, + } + + // Based on apps recommended from repo https://github.com/Shuffle/app-recommender + //fetch("http://localhost:8080/app_recommendations", { + fetch("https://europe-west2-shuffler.cloudfunctions.net/app_recommendations", { + method: "POST", + headers: { + "Content-Type": "application/json", + Accept: "application/json", + }, + body: JSON.stringify(frameworkData), + }) + .then((response) => { + //if (response.status !== 200) { + // console.log("Status not 200 for framework!"); + //} + + return response.json(); + }) + .then((responseJson) => { + // Loop the response dict + var suggestions = [] + var suggestion_cnt = 0 + var category_count = {} + for (const [key, value] of Object.entries(responseJson)) { + if (suggestion_cnt >= max_suggestions) { + break + } + + if (category_count[key] === undefined) { + category_count[key] = 1 + } else { + category_count[key] += 1 + } + + if (category_count[key] > max_per_category) { + continue + } + + //console.log("Found: " + key + " with value of len: " + value.length) + + //suggestion_cnt += value.slice(0,1).length + suggestions.push(value.slice(0,1)) + } + + setInjectedApps(suggestions) + }) + .catch((error) => { + console.log("Recommendation error: ", error); + }) + } + + const showRecommendations = (changed, frameworkData) => { + console.log("Inside recommendation loader") + setChangedApp(changed) + handleLoadNextSuggestion(frameworkData) + + // Alternative changed + // This is for secondary values like email = comms + var alternativeChanged = changed + if (changed == "COMMS") { + alternativeChanged = "email" + } + + // FIX: + // 0. Get workflows loaded in from usecasesearch + // 1. Search through workflow templates for matching app types + // 2. Validate if template is already in use~ (workflows with same tools) + // 3. Generate the workflow(s) - PS: Fix new workflow templates + // 4. Moving on! + + // How can we load templates? UsecaseSearch? + var showusecases = [] + //const foundusecase = usecaseTypes.find(data => data.name.toLowerCase() === defaultSearch.toLowerCase()) + for (var key in usecaseTypes) { + for (var subkey in usecaseTypes[key].value) { + const usecase = usecaseTypes[key].value[subkey] + + if (usecase.active === false) { + continue + } + + var potential = false + var matches = [] + for (var itemtype in usecase.items) { + var apptype = usecase.items[itemtype].app_type.toLowerCase() + if (apptype.toLowerCase() === "email" || apptype.toLowerCase() === "comms" || apptype === "communication") { + apptype = "Comms" + } + //console.log("OLD: ", changed, "USECASE: ", apptype) + + //console.log("APptype, changed, framework: ", apptype.toLowerCase(), alternativeChanged.toLowerCase(), changed.toLowerCase(), frameworkData) + if (changed.toLowerCase() === apptype.toLowerCase() || changed.toLowerCase().includes(apptype.toLowerCase()) || alternativeChanged.toLowerCase() === apptype.toLowerCase() || alternativeChanged.toLowerCase().includes(apptype.toLowerCase())) { + potential = true + console.log("Potential: !", apptype) + + if (frameworkData[apptype] !== undefined && frameworkData[apptype].name !== undefined && frameworkData[apptype].name !== null && frameworkData[apptype].name.length > 0) { + usecase.items[itemtype].app = frameworkData[apptype] + } + + matches.push(usecase.items[itemtype]) + } else { + // Check if the type is done in frameworkData + if (frameworkData[apptype] !== undefined) { + //console.log("NOT UNDEFINED: ", frameworkData[apptype]) + if (frameworkData[apptype].name !== undefined && frameworkData[apptype].name !== null && frameworkData[apptype].name.length > 0) { + //console.log("FOUND: ", frameworkData[apptype]) + usecase.items[itemtype].app = frameworkData[apptype] + + //console.log("Real app!") + matches.push(usecase.items[itemtype]) + } + + //if (frameworkData[apptype] !== undefined) { + } else { + console.log("UNDEFINED APP (bad name?): ", apptype) + } + } + } + + // Adds to list if it's all matching and unhandled + if (potential) { + // Check finished usecases. + if (inputUsecases !== undefined && setInputUsecases !== undefined && usecase.usecase_references !== undefined && usecase.usecase_references.length > 0) { + var foundUsecase = false + for (var usecaseKey in inputUsecases) { + const usecaseCategory = inputUsecases[usecaseKey] + for (var subUsecaseKey in usecaseCategory.list) { + const loopUsecase = usecaseCategory.list[subUsecaseKey] + if (loopUsecase.matches === undefined || loopUsecase.matches === null || loopUsecase.matches.length === 0) { + //console.log("No matches - continuing") + continue + } + + if (usecase.usecase_references.includes(loopUsecase.name)) { + foundUsecase = true + break + } + } + + if (foundUsecase) { + break + } + } + + if (!foundUsecase) { + console.log("Usecase NOT found!") + } else { + console.log("FOUND usecase existing in ", usecase.usecase_references) + continue + } + } else { + console.log("No usecase to try to match it to (usecase.usecase_references in UsecaseSearch)") + } + + console.log("Usecase: ", usecase) + if (matches.length === usecase.items.length) { + usecase.color = "#c51152" + usecase.type = usecaseTypes[key].name + showusecases.push(usecase) + } + } + } + } + + // FIXME: Check if a usecase has already been handled + console.log("") + console.log("GOT USECASES: ", showusecases) + + // FIXME: Just showing one usecase at a time for now + if (showusecases.length > 0) { + setFrameworkSuggestions(showusecases.slice(0,1)) + } + } + + useEffect(() => { + console.log("DISCWRAP CHANG: ", discoveryWrapper) + if (discoveryWrapper === undefined || discoveryWrapper.id === "SHUFFLE" || discoveryWrapper.id === undefined || cy === undefined) { + setDiscoveryData({}) + + if (cy !== undefined) { + cy.nodes().unselect() + } + + return + } + + + // Find the node and click it? + //setTimeout(() => { + const nodes = cy.nodes().jsons() + for (var key in nodes) { + const node = nodes[key] + var newSearchName = discoveryWrapper.id.valueOf() + if (newSearchName === "EMAIL") { + newSearchName = "COMMS" + } + + if (newSearchName === "ERADICATION" || newSearchName === "ENDPOINT") { + newSearchName = "EDR & AV" + } + + if (node.data.id === newSearchName) { + const tmpnode = cy.getElementById(node.data.id) + if (tmpnode !== undefined) { + tmpnode.select() + } + + setDefaultSearch(discoveryWrapper.id) + setPaperTitle(discoveryWrapper.id) + } + } + //}, 50,) + //setDiscoveryData(discoveryWrapper) + }, [discoveryWrapper]) + + const setUsecaseItem = (inputUsecase) => { + var parsedUsecase = inputUsecase + const edges = cy.edges().jsons() + + parsedUsecase.process = [] + for (var key in edges) { + const edge = edges[key] + console.log("Edge: ", edge) + parsedUsecase.process.push(edge.data) + } + + fetch(globalUrl + "/api/v1/workflows/usecases", { + method: "POST", + headers: { + "Content-Type": "application/json", + Accept: "application/json", + }, + body: JSON.stringify(parsedUsecase), + credentials: "include", + }) + .then((response) => { + if (response.status !== 200) { + console.log("Status not 200 for framework!"); + } + + return response.json(); + }) + .then((responseJson) => { + if (responseJson.success === false) { + if (responseJson.reason !== undefined) { + alert.error("Failed updating: " + responseJson.reason) + } else { + alert.error("Failed to update framework for your org.") + } + } else { + alert.info("Updated usecase.") + } + }) + .catch((error) => { + alert.error(error.toString()); + //setFrameworkLoaded(true) + }) + } + + const activateApp = (appid) => { + fetch(globalUrl+"/api/v1/apps/"+appid+"/activate", { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + }, + credentials: "include", + }) + .then((response) => { + if (response.status !== 200) { + console.log("Failed to activate") + } + + return response.json() + }) + .then((responseJson) => { + if (responseJson.success === false) { + alert.error("Failed to activate the app") + } else { + //alert.success("App activated for your organization! Refresh the page to use the app.") + } + }) + .catch(error => { + //alert.error(error.toString()) + console.log("Activate app error: ", error.toString()) + }); + } + + const setFrameworkItem = (data) => { + console.log("Setting framework item: ", data, isCloud) + if (!isCloud) { + activateApp(data.id) + } + + fetch(globalUrl + "/api/v1/apps/frameworkConfiguration", { + method: "POST", + headers: { + "Content-Type": "application/json", + Accept: "application/json", + }, + body: JSON.stringify(data), + credentials: "include", + }) + .then((response) => { + if (response.status !== 200) { + console.log("Status not 200 for framework!"); + } + + return response.json(); + }) + .then((responseJson) => { + if (responseJson.success === false) { + if (responseJson.reason !== undefined) { + alert.error("Failed updating: " + responseJson.reason) + } else { + alert.error("Failed to update framework for your org.") + + } + } + + //setFrameworkLoaded(true) + //setFrameworkData(responseJson) + }) + .catch((error) => { + alert.error(error.toString()); + //setFrameworkLoaded(true) + }) + } + + useEffect(() => { + if (!window.location.pathname.includes("usecases")) { + handleLoadNextSuggestion(frameworkData) + } + }, []) + + useEffect(() => { + console.log("New selected app: ", newSelectedApp, discoveryData) + if (newSelectedApp.objectID === undefined || newSelectedApp.objectID === undefined || newSelectedApp.objectID.length === 0) { + return + } + + //if (paperTitle.length > 0) { + // console.log("No papertitle (parent button)") + + // cy.elements().unselect() + // return + //} + + if (discoveryData.id === undefined) { + console.log("No discoverydata (parent button)") + + cy.elements().unselect() + return + } + + const submitValue = { + "type": discoveryData.id, + "name": newSelectedApp.name, + "id": newSelectedApp.objectID, + "large_image": newSelectedApp.image_url, + "description": newSelectedApp.description === undefined ? "" : newSelectedApp.description, + } + + + const foundelement = cy.getElementById(discoveryData.id) + if (foundelement !== undefined && foundelement !== null) { + foundelement.data("large_image", newSelectedApp.image_url) + foundelement.data("margin_x", "0px") + foundelement.data("margin_y", "0px") + foundelement.data("text_margin_y", `${60*scale}px`) + foundelement.data("width", `${85*scale}px`) + foundelement.data("height", `${85*scale}px`) + } + + if (setFrameworkData !== undefined) { + // Find discoveryData.id + var keys = [] + for (const [key, value] of Object.entries(frameworkData)) { + if (key.toLowerCase() === discoveryData.id.toLowerCase()) { + keys.push(key) + } + } + + if (keys.length === 0) { + console.log("Failed to find: ", discoveryData.id, " IN ", frameworkData) + } else { + for (var key in keys) { + frameworkData[keys[key]] = submitValue + } + + console.log("Frameworkdata: ", frameworkData) + setFrameworkData(frameworkData) + + if (discoveryData.large_image !== undefined && discoveryData.large_image !== null && discoveryData.large_image.includes("storage.googleapis.com")) { + showRecommendations(discoveryData.id, frameworkData) + } else { + console.log("Skipping recommendations during unselect") + } + } + } + + setFrameworkItem(submitValue) + cy.elements().unselect() + + }, [newSelectedApp]) + + + const isCloud = window.location.host === "localhost:3002" || window.location.host === "shuffler.io"; + + + const imgSize = 50; + var parsedFrameworkData = frameworkData === undefined ? {} : frameworkData + + // Awful mapping to make sure all access is always there + if (frameworkData !== undefined) { + if (frameworkData.cases !== undefined) { + if (frameworkData.cases.large_image === undefined && frameworkData.cases.large_image === null || frameworkData.cases.large_image === "") { + frameworkData.cases = {} + } + + parsedFrameworkData.Cases = frameworkData.cases + } else { + parsedFrameworkData.Cases = {} + } + + if (frameworkData.siem !== undefined) { + if (frameworkData.siem.large_image === undefined && frameworkData.siem.large_image === null || frameworkData.siem.large_image === "") { + frameworkData.siem = {} + } + + parsedFrameworkData.SIEM = frameworkData.siem + } else { + parsedFrameworkData.SIEM = {} + } + + if (frameworkData.assets !== undefined) { + if (frameworkData.assets.large_image === undefined && frameworkData.assets.large_image === null || frameworkData.assets.large_image === "") { + frameworkData.assets = {} + } + + parsedFrameworkData.Assets = frameworkData.assets + } else { + parsedFrameworkData.Assets = {} + } + + if (frameworkData.intel !== undefined) { + if (frameworkData.intel.large_image === undefined && frameworkData.intel.large_image === null || frameworkData.intel.large_image === "") { + frameworkData.intel = {} + } + + parsedFrameworkData.Intel = frameworkData.intel + } else { + parsedFrameworkData.Intel= {} + } + + if (frameworkData.communication !== undefined) { + if (frameworkData.communication.large_image === undefined && frameworkData.communication.large_image === null || frameworkData.communication.large_image === "") { + frameworkData.communication = {} + } + + parsedFrameworkData.Comms = frameworkData.communication + } else { + parsedFrameworkData.Comms = {} + } + + if (frameworkData.network !== undefined) { + if (frameworkData.network.large_image === undefined && frameworkData.network.large_image === null || frameworkData.network.large_image === "") { + frameworkData.network = {} + } + + parsedFrameworkData.Network = frameworkData.network + } else { + parsedFrameworkData.Network = {} + } + + if (frameworkData.iam !== undefined) { + if (frameworkData.iam.large_image === undefined && frameworkData.iam.large_image === null || frameworkData.iam.large_image === "") { + frameworkData.iam = {} + } + + parsedFrameworkData.IAM = frameworkData.iam + } else { + parsedFrameworkData.IAM = {} + } + + if (frameworkData.edr !== undefined) { + if (frameworkData.edr.large_image === undefined && frameworkData.edr.large_image === null || frameworkData.edr.large_image === "") { + frameworkData.edr = {} + } + + parsedFrameworkData["EDR & AV"] = frameworkData.edr + } else { + parsedFrameworkData["EDR & AV"] = {} + } + + } else { + //console.log("No frameworkdata for org! Setting default") + parsedFrameworkData["Cases"] = {} + parsedFrameworkData["SIEM"] = {} + parsedFrameworkData["Assets"] = {} + parsedFrameworkData["IAM"] = {} + parsedFrameworkData["Intel"] = {} + parsedFrameworkData["Comms"] = {} + parsedFrameworkData["Network"] = {} + parsedFrameworkData["EDR & AV"] = {} + } + + //console.log("Framework - update? ", parsedFrameworkData) + + // 0 = automated, 1 = manual + + const elements = [] + const surfaceColor = "#27292D" + + + const changeUsecase = (value, type) => { + //console.log("Value: ", value) + if (value === "Draw" && !edgesStarted) { + setEdgesStarted(true) + cy.edgehandles({ + handleNodes: (el) => { + if (el.isNode() && + !el.data("isButton") && + !el.data("isDescriptor") && + el.data("type") !== "COMMENT") { + return true + } + + return false + }, + preview: true, + toggleOffOnLeave: true, + loopAllowed: function (node) { + return false; + }, + }) + + } + + setSelectedUsecase(value) + + const edges = cy.edges() + if (edges !== undefined) { + const allEdges = edges.jsons() + for (var key in allEdges) { + const newedge = allEdges[key] + const foundelement = cy.getElementById(newedge.data.id) + if (foundelement !== undefined && foundelement !== null) { + foundelement.remove() + } + } + } + + var found = false + + var parsedType = type === 1 ? "manual" : "automated" + if (usecases === undefined || usecases === null) { + return + } + + const newedges = usecases[value][parsedType] + for (var key in newedges) { + newedges[key].label = parseInt(key)+1 + + if (newedges[key].description !== undefined && newedges[key].description !== null && newedges[key].description.length > 0) { + newedges[key].label = (parseInt(key)+1)+" "+newedges[key].description + } + + cy.add({ + group: "edges", + data: newedges[key], + }) + } + + setGraphDone(true) + } + + const animationDuration = 150; + const nodeAddDuration = 2500; + + if (cy !== undefined && cyDone && !animationStarted) { + const foundelement = cy.getElementById("CASES") + + if (foundelement !== undefined && foundelement !== null) { + setAnimationStarted(true) + + var parsedStyle = { + "ghost": "yes", + "border-color": "#f86a3e", + "border-width": "10px", + "border-opacity": ".7", + } + + var parsedStyle2 = { + "ghost": "yes", + "border-width": "7px", + } + + /* + // Animation + for (var i = 0; i < 10; i++) { + foundelement.animate( + { + style: parsedStyle, + }, + { + duration: nodeAddDuration, + } + ).delay(1000) + + foundelement.animate( + { + style: parsedStyle2, + }, + { + duration: nodeAddDuration, + } + ) + } + */ + } + } + + const onNodeAdded = (event) => { + //if (event.target.data("animate") === true) { + //} + } + + const onNodeHover = (event) => { + const nodedata = event.target.data(); + + + const cytoscapeElement = document.getElementById("cytoscape_view") + if (cytoscapeElement !== undefined && cytoscapeElement !== null) { + cytoscapeElement.style.cursor = "pointer" + } + + var parsedStyle = { + "border-width": "7px", + "border-opacity": ".7", + } + + //if (nodedata.type !== "COMMENT") { + // parsedStyle.color = "white"; + //} + + if (event.target !== undefined && event.target !== null) { + event.target.animate( + { + style: parsedStyle, + }, + { + duration: animationDuration, + } + ); + } + } + + const onNodeHoverOut = (event) => { + const nodedata = event.target.data(); + //console.log("Node OUT: ", nodedata) + + const cytoscapeElement = document.getElementById("cytoscape_view") + if (cytoscapeElement !== undefined && cytoscapeElement !== null) { + cytoscapeElement.style.cursor = "default" + } + + var parsedStyle = { + "border-width": "3px", + //"cursor": "default", + } + + event.target.animate( + { + style: parsedStyle, + }, + { + duration: animationDuration, + } + ) + } + + const onNodeUnselect = (event) => { + var data = event.target.data(); + console.log("UNSELECT: ", data) + + var parsedStyle = { + "border-width": "10px", + "border-opacity": ".7", + "border-color": "#7fe57f", + } + + // Some error here? + if (event.target !== undefined && event.target !== null) { + event.target.animate( + { + style: parsedStyle, + }, + { + duration: animationDuration, + } + ) + + setTimeout(() => { + event.target.animate( + { + style: { + "border-width": "3px", + }, + }, + { + duration: animationDuration, + } + ) + }, 2500) + } + + //setDiscoveryData({}) + if (setDiscoveryWrapper !== undefined) { + setDiscoveryWrapper({}) + } + + setSelectionOpen(false) + setDefaultSearch("") + setPaperTitle("") + + //setDiscoveryData({}) + } + + const onNodeSelect = (event) => { + var data = event.target.data(); + console.log("Node: ", data) + if (data.id === "SHUFFLE") { + event.target.unselect() + return + } + + if (data.label === "EDR & AV") { + data.label = "ERADICATION" + } + + setDiscoveryData(data) + setSelectionOpen(true) + setNewSelectedApp({}) + + setDefaultSearch(data.label.charAt(0).toUpperCase()+(data.label.substring(1)).toLowerCase()) + } + + const onEdgeAdded = (event) => { + //event.target.data("human", false) + } + + const onEdgeSelect = (event) => { + console.log(event) + + if (event.target.data("human") === undefined || event.target.data("human") === false) { + event.target.data("human", true) + } else if (event.target.data("human") === true) { + event.target.remove() + return + //event.target.data("human", false) + } + + console.log("Edge selected!", event.target.data()) + event.target.unselect() + } + + if (graphDone && cyDone === false) { + cy.fit(null, 200); + cy.on("select", "edge", (e) => onEdgeSelect(e)); + cy.on("add", "edge", (e) => onEdgeAdded(e)); + + cy.on("select", "node", (e) => { + onNodeSelect(e) + }) + cy.on("unselect", "node", (e) => { + onNodeUnselect(e) + }) + + cy.on("mouseover", "node", (e) => {onNodeHover(e)}) + cy.on("mouseout", "node", (e) => onNodeHoverOut(e)); + cy.on("add", "node", (e) => onNodeAdded(e)); + + setCyDone(true) + } + + if (cy !== undefined && cy.elements().length === 0 && parsedFrameworkData !== undefined) { + //'background-image': 'data(small_image)', + const shiftradius = 115*scale + const baselocationX = 285*scale + const baselocationY = 50*scale + const shiftmodifier = 3*scale + //const svgSize = `${40*scale}px` + //const svgSize = `${40}px` + + const foundMiddleImage = userdata !== undefined && userdata !== null && userdata.active_org !== undefined && userdata.active_org.image !== undefined && userdata.active_org.image !== null && userdata.active_org.image !== "" ? userdata.active_org.image : '/images/Shuffle_logo.png' + + + const fontSize = `${12*scale}px` + const defaultSize = `${85*scale}px` + const iconSize = `${45*scale}px` + const textMarginDefault = `${14*scale}px` + const textMarginImage = `${60*scale}px` + const nodes = [ + { + group: "nodes", + data: { + boxwidth: defaultSize, + boxheight: defaultSize, + font_size: fontSize, + is_valid: true, + isValid: true, + errors: [], + name: parsedFrameworkData.Cases.name === undefined ? "" : parsedFrameworkData.Cases.name, + description: parsedFrameworkData.Cases.description === undefined ? "" : parsedFrameworkData.Cases.description, + app_id: parsedFrameworkData.Cases.id === undefined ? "" : parsedFrameworkData.Cases.id, + text_margin_y: parsedFrameworkData.Cases.large_image === undefined ? textMarginDefault : textMarginImage, + margin_x: parsedFrameworkData.Cases.large_image === undefined ? `${32*scale}px` : "0px", + margin_y: parsedFrameworkData.Cases.large_image === undefined ? `${19*scale}px` : `0px`, + width: parsedFrameworkData.Cases.large_image === undefined ? iconSize : defaultSize, + height: parsedFrameworkData.Cases.large_image === undefined ? iconSize : defaultSize, + large_image: parsedFrameworkData.Cases.large_image === undefined ? parsedDatatypeImages["CASES"] : parsedFrameworkData.Cases.large_image, + + label: securityFramework[0].text.toUpperCase(), + id: securityFramework[0].text.toUpperCase(), + animate: true, + }, + renderedPosition: { + x: baselocationX, + y: baselocationY, + } + }, + { + group: "nodes", + data: { + font_size: fontSize, + boxwidth: defaultSize, + boxheight: defaultSize, + is_valid: true, + isValid: true, + errors: [], + name: parsedFrameworkData.IAM.name === undefined ? "" : parsedFrameworkData.IAM.name, + description: parsedFrameworkData.IAM.description === undefined ? "" : parsedFrameworkData.IAM.description, + app_id: parsedFrameworkData.IAM.id === undefined ? "" : parsedFrameworkData.IAM.id, + text_margin_y: parsedFrameworkData.IAM.large_image === undefined ? textMarginDefault : textMarginImage, + margin_x: parsedFrameworkData.IAM.large_image === undefined ? `${32*scale}px` : "0px", + margin_y: parsedFrameworkData.IAM.large_image === undefined ? `${19*scale}px` : `0px`, + width: parsedFrameworkData.IAM.large_image === undefined ? iconSize : defaultSize, + height: parsedFrameworkData.IAM.large_image === undefined ? iconSize : defaultSize, + large_image: parsedFrameworkData.IAM.large_image === undefined ? parsedDatatypeImages["IAM"] : parsedFrameworkData.IAM.large_image, + + label: securityFramework[3].text.toUpperCase(), + id: securityFramework[3].text.toUpperCase(), + animate: false, + }, + renderedPosition: { + x: baselocationX+shiftradius+(shiftradius/shiftmodifier), + y: baselocationY+shiftradius-(shiftradius/shiftmodifier), + } + }, + { + group: "nodes", + data: { + font_size: fontSize, + boxwidth: defaultSize, + boxheight: defaultSize, + is_valid: true, + isValid: true, + errors: [], + name: parsedFrameworkData.Assets.name === undefined ? "" : parsedFrameworkData.Assets.name, + description: parsedFrameworkData.Assets.description === undefined ? "" : parsedFrameworkData.Assets.description, + app_id: parsedFrameworkData.Assets.id === undefined ? "" : parsedFrameworkData.Assets.id, + text_margin_y: parsedFrameworkData.Assets.large_image === undefined ? textMarginDefault : textMarginImage, + margin_x: parsedFrameworkData.Assets.large_image === undefined ? `${32*scale}px` : "0px", + margin_y: parsedFrameworkData.Assets.large_image === undefined ? `${19*scale}px` : `0px`, + width: parsedFrameworkData.Assets.large_image === undefined ? iconSize : defaultSize, + height: parsedFrameworkData.Assets.large_image === undefined ? iconSize : defaultSize, + large_image: parsedFrameworkData.Assets.large_image === undefined ? parsedDatatypeImages["ASSETS"] : parsedFrameworkData.Assets.large_image, + + label: securityFramework[2].text.toUpperCase(), + id: securityFramework[2].text.toUpperCase(), + animate: false, + }, + renderedPosition: { + x: baselocationX+shiftradius*2, + y: baselocationY+shiftradius*2, + } + }, + { + group: "nodes", + data: { + font_size: fontSize, + boxwidth: defaultSize, + boxheight: defaultSize, + is_valid: true, + isValid: true, + errors: [], + name: parsedFrameworkData.Intel.name === undefined ? "" : parsedFrameworkData.Intel.name, + description: parsedFrameworkData.Intel.description === undefined ? "" : parsedFrameworkData.Intel.description, + app_id: parsedFrameworkData.Intel.id === undefined ? "" : parsedFrameworkData.Intel.id, + text_margin_y: parsedFrameworkData.Intel.large_image === undefined ? textMarginDefault : textMarginImage, + margin_x: parsedFrameworkData.Intel.large_image === undefined ? `${32*scale}px` : "0px", + margin_y: parsedFrameworkData.Intel.large_image === undefined ? `${19*scale}px` : `0px`, + width: parsedFrameworkData.Intel.large_image === undefined ? iconSize : defaultSize, + height: parsedFrameworkData.Intel.large_image === undefined ? iconSize : defaultSize, + large_image: parsedFrameworkData.Intel.large_image === undefined ? parsedDatatypeImages["INTEL"] : parsedFrameworkData.Intel.large_image, + + label: securityFramework[4].text.toUpperCase(), + id: securityFramework[4].text.toUpperCase(), + animate: false, + }, + renderedPosition: { + x: baselocationX+shiftradius+(shiftradius/shiftmodifier), + y: baselocationY+shiftradius*3+(shiftradius/shiftmodifier), + } + }, + { + group: "nodes", + data: { + font_size: fontSize, + boxwidth: defaultSize, + boxheight: defaultSize, + is_valid: true, + isValid: true, + errors: [], + name: parsedFrameworkData.Comms.name === undefined ? "" : parsedFrameworkData.Comms.name, + description: parsedFrameworkData.Comms.description === undefined ? "" : parsedFrameworkData.Comms.description, + app_id: parsedFrameworkData.Comms.id === undefined ? "" : parsedFrameworkData.Comms.id, + text_margin_y: parsedFrameworkData.Comms.large_image === undefined ? textMarginDefault : textMarginImage, + margin_x: parsedFrameworkData.Comms.large_image === undefined ? `${32*scale}px` : "0px", + margin_y: parsedFrameworkData.Comms.large_image === undefined ? `${19*scale}px` : `0px`, + width: parsedFrameworkData.Comms.large_image === undefined ? iconSize : defaultSize, + height: parsedFrameworkData.Comms.large_image === undefined ? iconSize : defaultSize, + large_image: parsedFrameworkData.Comms.large_image === undefined ? parsedDatatypeImages["COMMS"] : parsedFrameworkData.Comms.large_image, + + label: securityFramework[5].text.toUpperCase(), + id: securityFramework[5].text.toUpperCase(), + animate: false, + }, + renderedPosition: { + x: baselocationX, + y: baselocationY+shiftradius*4, + } + }, + { + group: "nodes", + data: { + font_size: fontSize, + boxwidth: defaultSize, + boxheight: defaultSize, + is_valid: true, + isValid: true, + errors: [], + name: parsedFrameworkData["EDR & AV"].name === undefined ? "" : parsedFrameworkData["EDR & AV"].name, + description: parsedFrameworkData["EDR & AV"].description === undefined ? "" : parsedFrameworkData["EDR & AV"].description, + app_id: parsedFrameworkData["EDR & AV"].id === undefined ? "" : parsedFrameworkData["EDR & AV"].id, + text_margin_y: parsedFrameworkData["EDR & AV"].large_image === undefined ? textMarginDefault : textMarginImage, + margin_x: parsedFrameworkData["EDR & AV"].large_image === undefined ? `${32*scale}px` : "0px", + margin_y: parsedFrameworkData["EDR & AV"].large_image === undefined ? `${19*scale}px` : `0px`, + width: parsedFrameworkData["EDR & AV"].large_image === undefined ? iconSize : defaultSize, + height: parsedFrameworkData["EDR & AV"].large_image === undefined ? iconSize : defaultSize, + large_image: parsedFrameworkData["EDR & AV"].large_image === undefined ? parsedDatatypeImages["EDR & AV"] : parsedFrameworkData["EDR & AV"].large_image, + + label: securityFramework[7].text.toUpperCase(), + id: securityFramework[7].text.toUpperCase(), + animate: false, + }, + renderedPosition: { + x: baselocationX-shiftradius-(shiftradius/shiftmodifier), + y: baselocationY+shiftradius*3+(shiftradius/shiftmodifier), + } + }, + { + group: "nodes", + data: { + font_size: fontSize, + boxwidth: defaultSize, + boxheight: defaultSize, + is_valid: true, + isValid: true, + errors: [], + name: parsedFrameworkData.Network.name === undefined ? "" : parsedFrameworkData.Network.name, + description: parsedFrameworkData.Network.description === undefined ? "" : parsedFrameworkData.Network.description, + app_id: parsedFrameworkData.Network.id === undefined ? "" : parsedFrameworkData.Network.id, + text_margin_y: parsedFrameworkData.Network.large_image === undefined ? textMarginDefault : textMarginImage, + margin_x: parsedFrameworkData.Network.large_image === undefined ? `${32*scale}px` : "0px", + margin_y: parsedFrameworkData.Network.large_image === undefined ? `${19*scale}px` : `0px`, + width: parsedFrameworkData.Network.large_image === undefined ? iconSize : defaultSize, + height: parsedFrameworkData.Network.large_image === undefined ? iconSize : defaultSize, + large_image: parsedFrameworkData.Network.large_image === undefined ? parsedDatatypeImages["NETWORK"] : parsedFrameworkData.Network.large_image, + label: securityFramework[6].text.toUpperCase(), + id: securityFramework[6].text.toUpperCase(), + animate: false, + }, + renderedPosition: { + x: baselocationX-shiftradius*2, + y: baselocationY+shiftradius*2, + } + }, + { + group: "nodes", + data: { + font_size: fontSize, + boxwidth: defaultSize, + boxheight: defaultSize, + is_valid: true, + isValid: true, + errors: [], + name: parsedFrameworkData.SIEM.name === undefined ? "" : parsedFrameworkData.SIEM.name, + description: parsedFrameworkData.SIEM.description === undefined ? "" : parsedFrameworkData.SIEM.description, + app_id: parsedFrameworkData.SIEM.id === undefined ? "" : parsedFrameworkData.SIEM.id, + text_margin_y: parsedFrameworkData.SIEM.large_image === undefined ? textMarginDefault : textMarginImage, + margin_x: parsedFrameworkData.SIEM.large_image === undefined ? `${32*scale}px` : "0px", + margin_y: parsedFrameworkData.SIEM.large_image === undefined ? `${19*scale}px` : `0px`, + width: parsedFrameworkData.SIEM.large_image === undefined ? iconSize : defaultSize, + height: parsedFrameworkData.SIEM.large_image === undefined ? iconSize : defaultSize, + large_image: parsedFrameworkData.SIEM.large_image === undefined ? parsedDatatypeImages["SIEM"] : parsedFrameworkData.SIEM.large_image, + label: securityFramework[1].text.toUpperCase(), + id: securityFramework[1].text.toUpperCase(), + animate: false, + }, + renderedPosition: { + x: baselocationX-shiftradius-(shiftradius/shiftmodifier), + y: baselocationY+shiftradius-(shiftradius/shiftmodifier), + } + }, + ] + + // Middlenode + nodes.push({ + group: "nodes", + data: { + font_size: fontSize, + width: defaultSize, + height: defaultSize, + id: "SHUFFLE", + is_valid: true, + isValid: true, + errors: [], + middle_node: true, + large_image: foundMiddleImage, + }, + renderedPosition: { + x: baselocationX, + y: baselocationY+shiftradius*2, + } + }) + + // Extra nodes + nodes.push({ + group: "nodes", + data: { + is_valid: true, + isValid: true, + errors: [], + large_image: encodeURI(`data:image/svg+xml;utf-8, + , + `), + id: "TOP_LEFT", + invisible: true, + }, + renderedPosition: { + x: baselocationX-shiftradius*2.5, + y: baselocationY-50, + } + }) + nodes.push({ + group: "nodes", + data: { + is_valid: true, + isValid: true, + errors: [], + large_image: encodeURI(`data:image/svg+xml;utf-8, + , + `), + id: "BOTTOM_LEFT", + invisible: true, + }, + renderedPosition: { + x: baselocationX-shiftradius*2.5-10, + y: baselocationY+shiftradius*4-10, + } + }) + nodes.push({ + group: "nodes", + data: { + is_valid: true, + isValid: true, + errors: [], + large_image: encodeURI(`data:image/svg+xml;utf-8, + , + `), + id: "BOTTOM_RIGHT", + invisible: true, + }, + renderedPosition: { + x: baselocationX+shiftradius*2+50, + y: baselocationY+shiftradius*4+50, + } + }) + nodes.push({ + group: "nodes", + data: { + is_valid: true, + isValid: true, + errors: [], + large_image: encodeURI(`data:image/svg+xml;utf-8, + , + `), + id: "TOP_RIGHT", + invisible: true, + }, + renderedPosition: { + x: baselocationX+shiftradius*2, + y: baselocationY-150, + } + }) + + //console.log("NODES: " , nodes) + for (var key in nodes) { + cy.add(nodes[key]).lock() + } + + changeUsecase(selectedUsecase, usecaseType) + + if (inputUsecase !== undefined && inputUsecase !== null) { + for (var key in inputUsecase.process) { + if (inputUsecase.process[key].source === "" || inputUsecase.process[key].target === "") { + continue + } + + inputUsecase.process[key].label = parseInt(key)+1 + inputUsecase.process[key].id = uuidv4(); + + cy.add({ + group: "edges", + data: inputUsecase.process[key], + }) + } + //changeUsecase(inputUsecase, 0) + //setSelectedUsecase(value) + } + } + + if (selectedOption !== undefined && selectedUsecase !== selectedOption) { + setSelectedUsecase(selectedOption) + changeUsecase(selectedOption, usecaseType) + } + + const UsecaseHandler = (props) => { + const { data, index, diff } = props + + const leftImage = data.left_image !== undefined ? parsedDatatypeImages[data.left_image.toUpperCase()] : undefined + const rightImage = data.right_image !== undefined ? parsedDatatypeImages[data.right_image.toUpperCase()] : undefined + + if (leftImage === undefined) { + console.log("LEFT MISSING: ", leftImage) + } + + if (rightImage === undefined) { + console.log("Right MISSING: ", rightImage) + } + + const parsedRightImage = + const parsedRightText =
{data.right_text}
+ + const parsedLeftImage = + const parsedLeftText =
{data.left_text}
+ + var svgIcon = + + + + + + + + + if (data.direction === "right") { + svgIcon = + + + + + } else if (data.direction === "left") { + svgIcon = + + + + + + } + + //{"usecase_directional_arrow_"+index} + // + const handleHover = () => { + //console.log("Hover IN: ", data.automated) + + const allEdges = cy.edges().jsons() + for (var key in allEdges) { + const newedge = allEdges[key] + const foundelement = cy.getElementById(newedge.data.id) + if (foundelement !== undefined && foundelement !== null) { + foundelement.remove() + } + } + + //const newedges = usecases[value][parsedType] + for (var key in data.automated) { + data.automated[key].label = parseInt(key)+1 + + if (data.automated[key].description !== undefined && data.automated[key].description !== null && data.automated[key].description.length > 0) { + data.automated[key].label = (parseInt(key)+1)+" "+data.automated[key].description + } + + cy.add({ + group: "edges", + data: data.automated[key], + }) + } + } + + const handleHoverOut = () => { + //console.log("Hover out: ", data.automated) + //const allEdges = cy.edges().jsons() + //for (var key in allEdges) { + // const newedge = allEdges[key] + // const foundelement = cy.getElementById(newedge.data.id) + // if (foundelement !== undefined && foundelement !== null) { + // foundelement.remove() + // } + //} + } + + const bgColor = color === undefined || color === null || color.length === 0 ? theme.palette.surfaceColor : color + console.log("BGCOLOR: ", bgColor) + + return ( + + + {data.name} + +
+
+ {parsedLeftImage} + {parsedLeftText} +
+
+ {svgIcon} +
+
+ {parsedRightImage} + {parsedRightText} +
+
+
+ + ) + } + + const selectedUsecases = [ + { + "name": "Sync tickets", + "description": "Do cool shit weeeee - this just ensure they are syncing", + "left_text": "Cases", + "right_text": "Cases", + "left_image": "Cases", + "right_image": "Cases", + "direction": "both", + "manual": [], + "automated": [ + { + "source": "CASES", + "target": "SHUFFLE", + "description": "Get cases", + "human": false, + }, + { + "source": "SHUFFLE", + "target": "CASES", + "description": "Update cases", + "human": false, + }, + ] + }, + { + "name": "Forward alerts with enrichment", + "description": "Do cool shit weeeee - this just ensure they are syncing", + "left_text": "EDR & AV", + "right_text": "Cases", + "left_image": "EDR & AV", + "right_image": "Cases", + "direction": "right", + "manual": [], + "automated": [ + { + "source": "BOTTOM_LEFT", + "target": "COMMS", + "description": "Email received", + "human": false, + }, + ] + } + ] + + const getNodeName = (category) => { + if (category === "email") { + category = "COMMS" + } else if (category === "edr") { + category = "EDR & AV" + } + + return category.toUpperCase() + } + + //autounselectify={true} + var usecasediff = -100 + const bgColor = color === undefined || color === null || color.length === 0 ? theme.palette.surfaceColor : color + + return ( +
+
+ + +
+ + {injectedApps.map((apps, appindex) => { + var categoryTop = 100 + var categoryLeft = 100 + + const category = apps[0].category + if (category === "edr") { + categoryTop = 355 + categoryLeft = 50 + } else if (category === "siem") { + categoryTop = 95 + categoryLeft = 50 + } else if (category === "cases") { + categoryTop = 65 + categoryLeft = 175 + } else if (category === "intel") { + categoryTop = 355 + categoryLeft = 315 + } else if (category === "comms") { + categoryTop = 415 + categoryLeft = 175 + } else if (category === "iam") { + categoryTop = 95 + categoryLeft = 315 + } else { + return null + } + + return ( +
+ {apps.map((app, appIndex) => { + return ( + } + key={app.id} + label={""} + variant="contained" + style={{}} + onClick={() => { + console.log("Add app to framework and remove category: ", app) + app.objectID = app.id + app.type = app.category + app.large_image = app.image_url + + const nodename = getNodeName(app.category) + + + const foundelement = cy.getElementById(nodename) + console.log("FOUND: ", foundelement) + if (foundelement !== undefined && foundelement !== null) { + foundelement.data("large_image", app.image_url) + foundelement.data("margin_x", "0px") + foundelement.data("margin_y", "0px") + foundelement.data("text_margin_y", `${60*scale}px`) + foundelement.data("width", `${85*scale}px`) + foundelement.data("height", `${85*scale}px`) + } + + if (setFrameworkData !== undefined) { + console.log("Setting frameworkdata") + // Find discoveryData.id + var keys = [] + for (const [key, value] of Object.entries(frameworkData)) { + if (key.toLowerCase() === app.category.toLowerCase()) { + keys.push(key) + } + } + + if (keys.length === 0) { + console.log("Failed to find: ", app.category, " IN ", frameworkData) + } else { + console.log("In else for keys: ", frameworkData, keys) + for (var key in keys) { + frameworkData[keys[key]] = app + } + + setFrameworkData(frameworkData) + showRecommendations(app.category, frameworkData) + } + } + + setFrameworkItem(app) + }} + /> + ) + })} +
+ ) + })} + + {showOptions === false ? null : +
+ {Object.keys(usecases).map((data, index) => { + return( + + ) + })} +
+ } + {/* + Object.getOwnPropertyNames(discoveryData).length > 0 ? +
+ + Use-cases + + {selectedUsecases.map((data, index) => { + usecasediff += 175 + + return ( + + ) + })} +
+ : null*/} + + { + Object.getOwnPropertyNames(discoveryData).length > 0 ? + + {paperTitle.length > 0 ? + + + {paperTitle} + + + + : null} + + { + //cy.elements().unselectify(); + cy.elements().unselect() + + e.preventDefault(); + setDiscoveryData({}) + setDefaultSearch("") + setPaperTitle("") + }} + > + + + + + { + e.preventDefault(); + setDiscoveryData({ + "id": discoveryData.id, + "label": discoveryData.label, + "name": "" + }) + + setNewSelectedApp({ + "animate": false, + "app_id": "", + "boxheight": "66.3px", + "boxwidth": "66.3px", + "description": "", + "errors": [], + "font_size": "9.36px", + "height": "66.3px", + "id": "", + "isValid": true, + "is_valid": true, + "label": "SIEM", + "large_image": "asd", + "margin_x": "0px", + "margin_y": "0px", + "name": "", + "text_margin_y": "46.800000000000004px", + "width": "66.3px", + "objectID": "remove", + }) + + setSelectionOpen(true) + setDefaultSearch("") + + //handleLoadNextSuggestion(frameworkData) + setInjectedApps([]) + + const foundelement = cy.getElementById(discoveryData.id) + if (foundelement !== undefined && foundelement !== null) { + console.log("element: ", foundelement) + console.log("DISC: ", discoveryData) + foundelement.data("large_image", parsedDatatypeImages[discoveryData.id.toUpperCase()]) + foundelement.data("text_margin_y", "14px") + foundelement.data("margin_x", "32px") + foundelement.data("margin_y", "19x") + foundelement.data("width", "45px") + foundelement.data("height", "45px") + } + + setTimeout(() => { + setDiscoveryData({}) + setNewSelectedApp({}) + }, 1000) + }} + > + + + +
+ {discoveryData.name !== undefined && discoveryData.name !== null && discoveryData.name.length > 0 ? +
+ + {discoveryData.id} 0 ? newSelectedApp.image_url : discoveryData.large_image} style={{height: 40, width: 40, margin: "auto",}}/> +
+ : + {discoveryData.id} + } + + {discoveryData.name !== undefined && discoveryData.name !== null && discoveryData.name.length > 0 ? + discoveryData.name + : + newSelectedApp.name !== undefined && newSelectedApp.name !== null && newSelectedApp.name.length > 0 ? + newSelectedApp.name + : + `Find your ${discoveryData.label} app!` + } + +
+
+ {discoveryData !== undefined && discoveryData.name !== undefined && discoveryData.name !== null && discoveryData.name.length > 0 ? + + + {discoveryData.description} + + {/*isCloud && defaultSearch !== undefined && defaultSearch.length > 0 ? + {< + newSelectedApp={newSelectedApp} + setNewSelectedApp={setNewSelectedApp} + defaultSearch={defaultSearch} + />} + : + null + */} + + : + selectionOpen + ? + + + Click an app below to select it + + + : + + } +
+
+ {selectionOpen ? + + : null} +
+
+ : null + } + { + // FIXME: There's something specific loading when + // you do the first hover of a node. Why is this different? + //console.log("CY: ", incy) + setCy(incy) + }} + /> + {isCloud && inputUsecase !== undefined && inputUsecase !== null && isLoggedIn === true && Object.keys(inputUsecase).length > 0 ? + + : null} +
+ ) +} + +export default AppFramework; diff --git a/frontend/src/components/AppGrid.jsx b/frontend/src/components/AppGrid.jsx new file mode 100644 index 00000000..af7ddae5 --- /dev/null +++ b/frontend/src/components/AppGrid.jsx @@ -0,0 +1,378 @@ +import React, {useEffect, useState} from 'react'; + +import ReactGA from 'react-ga4'; +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, connectHitInsights } from 'react-instantsearch-dom'; + +import aa from 'search-insights' + +import { + Zoom, + Grid, + Paper, + TextField, + ButtonBase, + InputAdornment, + Typography, + Button, + Tooltip +} from '@material-ui/core'; + +const searchClient = algoliasearch("JNSS5CFDZZ", "db08e40265e2941b9a7d8f644b6e5240") +//const searchClient = algoliasearch("L55H18ZINA", "a19be455e7e75ee8f20a93d26b9fc6d6") +const AppGrid = props => { + const { maxRows, showName, showSuggestion, isMobile, globalUrl, parsedXs, userdata } = 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 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 | Apps | Find and integrate any app" + + 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 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 ( +
+ + + + ), + }} + autoComplete='off' + type="search" + color="primary" + defaultValue={currentRefinement} + placeholder="Find Apps..." + id="shuffle_search_field" + onChange={(event) => { + refine(event.currentTarget.value) + }} + limit={5} + /> + {/*isSearchStalled ? 'My search is stalled' : ''*/} + + ) + } + + var workflowDelay = -50 + const Hits = ({ hits, insights }) => { + const [mouseHoverIndex, setMouseHoverIndex] = useState(-1) + var counted = 0 + + //console.log(hits) + //var curhits = hits + //if (hits.length > 0 && defaultApps.length === 0) { + // setDefaultApps(hits) + //} + + //const [defaultApps, setDefaultApps] = React.useState([]) + //console.log(hits) + //if (hits.length > 0 && hits.length !== innerHits.length) { + // setInnerHits(hits) + //} + + console.log("In appgrid") + + return ( + + {hits.map((data, index) => { + + workflowDelay += 50 + + const paperStyle = { + backgroundColor: index === mouseHoverIndex ? "rgba(255,255,255,0.8)" : theme.palette.inputColor, + color: index === mouseHoverIndex ? theme.palette.inputColor : "rgba(255,255,255,0.8)", + border: `1px solid ${innerColor}`, + padding: 15, + cursor: "pointer", + position: "relative", + minHeight: 116, + } + + if (counted === 12/xs*rowHandler) { + return null + } + + counted += 1 + var parsedname = "" + for (var key = 0; key < data.name.length; key++) { + var character = data.name.charAt(key) + if (character === character.toUpperCase()) { + //console.log(data.name[key], data.name[key+1]) + if (data.name.charAt(key+1) !== undefined && data.name.charAt(key+1) === data.name.charAt(key+1).toUpperCase()) { + } else { + parsedname += " " + } + } + + parsedname += character + } + + parsedname = (parsedname.charAt(0).toUpperCase()+parsedname.substring(1)).replaceAll("_", " ") + const appUrl = isCloud ? `/apps/${data.objectID}?queryID=${data.__queryID}` : `https://shuffler.io/apps/${data.objectID}?queryID=${data.__queryID}` + return ( + + + + { + setMouseHoverIndex(index) + /* + ReactGA.event({ + category: "app_grid_view", + action: `search_bar_click`, + label: "", + }) + */ + }} onMouseOut={() => { + setMouseHoverIndex(-1) + }} onClick={() => { + if (isCloud) { + ReactGA.event({ + category: "app_grid_view", + action: `app_${parsedname}_${data.id}_click`, + label: "", + }) + } + + //const searchClient = algoliasearch("L55H18ZINA", "a19be455e7e75ee8f20a93d26b9fc6d6") + console.log(searchClient) + aa('init', { + appId: searchClient.appId, + apiKey: searchClient.transporter.queryParameters["x-algolia-api-key"] + }) + + const timestamp = new Date().getTime() + aa('sendEvents', [ + { + eventType: 'click', + eventName: 'Product Clicked', + index: 'appsearch', + objectIDs: [data.objectID], + timestamp: timestamp, + queryID: data.__queryID, + positions: [data.__position], + userToken: userdata === undefined || userdata === null || userdata.id === undefined ? "unauthenticated" : userdata.id, + } + ]) + + }}> + + {data.name} + +
+ {index === mouseHoverIndex || showName === true ? + parsedname + : + null + } + {data.generated ? + + {data.invalid ? + + : + + } + + : + + + + } + + + + + ) + })} + + ) + } + + const CustomSearchBox = connectSearchBox(SearchBox) + const CustomHits = connectHits(Hits) + //const CustomHits = connectHitInsights(aa)(Hits) + const selectButtonStyle = { + minWidth: 150, + maxWidth: 150, + minHeight: 50, + } + + return ( +
+ {/* +
+ +
+ */} +
+ +
+ +
+ + +
+ {showSuggestion === true ? +
+ + Can't find what you're looking for? + +
+ setFormMail(e.target.value)} + /> + setMessage(e.target.value)} + /> +
+ + {formMessage} +
+ : null + } + + + + Search by + + + Algolia logo + + +
+
+ ) +} + +export default AppGrid; diff --git a/frontend/src/components/Appsearch.jsx b/frontend/src/components/Appsearch.jsx new file mode 100644 index 00000000..9da69a70 --- /dev/null +++ b/frontend/src/components/Appsearch.jsx @@ -0,0 +1,291 @@ +import React, { useState, useEffect } from 'react'; +import ReactGA from 'react-ga4'; +import { useTheme } from '@material-ui/core/styles'; +import {Link} from 'react-router-dom'; +import { useAlert } from "react-alert"; +import { Search as SearchIcon, CloudQueue as CloudQueueIcon, Code as CodeIcon } from '@material-ui/icons'; + +//import algoliasearch from 'algoliasearch/lite'; +import algoliasearch from 'algoliasearch'; +import { InstantSearch, connectSearchBox, connectHits } from 'react-instantsearch-dom'; +import { Grid, Paper, TextField, ButtonBase, InputAdornment, Typography, Button, Tooltip} from '@material-ui/core'; +import aa from 'search-insights' +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} = props + + const isCloud = window.location.host === "localhost:3002" || window.location.host === "shuffler.io"; + const alert = useAlert(); + const rowHandler = maxRows === undefined || maxRows === null ? 50 : maxRows + const xs = parsedXs === undefined || parsedXs === null ? 12 : 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 [selectedApp, setSelectedApp] = 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 | Apps | Find and integration any app" + + const setUserSpecialzedApp = (user, data) => { + // var data = newfields] + console.log("data value", data) + const appData = {"user_id":user,"specialized_apps":[{}]} + console.log("User Check for appdata:", user) + appData["specialized_apps"][0]["name"] = data["name"] + appData["specialized_apps"][0]["image"] = data["image_url"] + appData["specialized_apps"][0]["category"] = data["categories"].toString() + console.log("AppData:",appData) + console.log("setActionImageList",setActionImageList) + console.log("actionImageList",actionImageList) + + const finalData = actionImageList.concat(appData["specialized_apps"]) + appData["specialized_apps"]=finalData + fetch(globalUrl + "/api/v1/users/updateuser", { + method: "PUT", + headers: { + "Content-Type": "application/json", + Accept: "application/json", + }, + body: JSON.stringify(appData), + credentials: "include", + }) + .then((response) => { + if (response.status !== 200) { + console.log("Status not 200 for set creator :O!"); + } + alert.success("Sucessfully updated specialzed app.") + return response.json(); + }) + .then((responseJson) => { + if (!responseJson.success && responseJson.reason !== undefined) { + alert.error("Failed updating user: " + responseJson.reason); + } + }) + .catch((error) => { + console.log(error); + }); + }; + 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) + }); + } + + // value={currentRefinement} + const SearchBox = ({currentRefinement, refine, isSearchStalled} ) => { + useEffect(() => { + //console.log("FIRST LOAD ONLY? RUN REFINEMENT: !", currentRefinement) + if (defaultSearch !== undefined && defaultSearch !== null) { + refine(defaultSearch) + } + }, []) + + return ( +
+ + + + ), + }} + autoComplete='on' + type="search" + color="primary" + defaultValue={defaultSearch} + placeholder={`Find ${defaultSearch} Apps...`} + id="shuffle_workflow_search_field" + onChange={(event) => { + refine(event.currentTarget.value) + }} + limit={5} + /> + {/*isSearchStalled ? 'My search is stalled' : ''*/} + + ) + //value={currentRefinement} + } + + const Hits = ({ hits }) => { + const [mouseHoverIndex, setMouseHoverIndex] = useState(-1) + var counted = 0 + + return ( + + {hits.map((data, index) => { + const paperStyle = { + backgroundColor: index === mouseHoverIndex ? "rgba(255,255,255,0.8)" : theme.palette.inputColor, + 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: "100%", + minHeight: 37, + maxHeight: 52, + } + + if (counted === 12/xs*rowHandler) { + return null + } + + counted += 1 + var parsedname = data.name.valueOf() + //for (var key = 0; key < data.name.length; key++) { + // var character = data.name.charAt(key) + // if (character === character.toUpperCase()) { + // //console.log(data.name[key], data.name[key+1]) + // if (data.name.charAt(key+1) !== undefined && data.name.charAt(key+1) === data.name.charAt(key+1).toUpperCase()) { + // } else { + // parsedname += " " + // } + // } + + // parsedname += character + //} + + parsedname = (parsedname.charAt(0).toUpperCase()+parsedname.substring(1)).replaceAll("_", " ") + + return ( + { + setMouseHoverIndex(index) + /* + ReactGA.event({ + category: "app_grid_view", + action: `search_bar_click`, + label: "", + }) + */ + }} onMouseOut={() => { + setMouseHoverIndex(-1) + }} onClick={() => { + if(isCreatorPage === true){ + console.log("data:",data) + console.log("userdata.id",userdata.id) + console.log("is creator", isCreatorPage) + if (setNewSelectedApp !== undefined) { + // setUserSpecialzedApp = data + setUserSpecialzedApp(userdata.id, data) + //setActionImageList(userdata.id, data) + } + } + if (setNewSelectedApp !== undefined) { + setNewSelectedApp(data) + } + + if (isCloud) { + ReactGA.event({ + category: "app_search", + action: `app_${parsedname}_${data.id}_personalize_click`, + label: "", + }) + } + + const queryID = "" + + if (queryID !== undefined && queryID !== null) { + try { + aa('init', { + appId: searchClient.appId, + apiKey: searchClient.transporter.headers["x-algolia-api-key"] + }) + + const timestamp = new Date().getTime() + aa('sendEvents', [ + { + eventType: 'conversion', + eventName: 'App Framework Activation', + index: 'appsearch', + objectIDs: [data.objectID], + timestamp: timestamp, + queryID: queryID, + userToken: userdata === undefined || userdata === null || userdata.id === undefined ? "unauthenticated" : userdata.id, + } + ]) + } catch (e) { + console.log("Failed algolia search update: ", e) + } + } + }}> +
+ {data.name} + + {parsedname} + +
+
+ ) + })} +
+ ) + } + + const InputHits = ConfiguredHits === undefined ? Hits : ConfiguredHits + const CustomSearchBox = connectSearchBox(SearchBox) + const CustomHits = connectHits(InputHits) + + return ( +
+ + {/* showSearch === false ? null : +
+ +
+ */} +
+ +
+ +
+
+ ) +} + +export default Appsearch; diff --git a/frontend/src/components/AppsearchPopout.jsx b/frontend/src/components/AppsearchPopout.jsx new file mode 100644 index 00000000..25ea719c --- /dev/null +++ b/frontend/src/components/AppsearchPopout.jsx @@ -0,0 +1,190 @@ +import React, { useState, useEffect } from 'react'; + +import theme from '../theme.jsx'; +import AppSearch from './Appsearch.jsx'; + +import { + Paper, + Typography, + Divider, + IconButton, + Badge, + CircularProgress, + Tooltip, + Button, +} from "@material-ui/core"; + +import { + Close as CloseIcon, + Delete as DeleteIcon, +} from "@material-ui/icons"; + +const AppSearchPopout = (props) => { + const { + cy, + paperTitle, + setPaperTitle, + newSelectedApp, + setNewSelectedApp, + selectionOpen, + setSelectionOpen, + discoveryData, + setDiscoveryData, + userdata, + } = props; + + const [defaultSearch, setDefaultSearch] = React.useState(paperTitle !== undefined ? paperTitle : "") + + if (selectionOpen !== true) { + return null + } + + // + return ( + + {paperTitle !== undefined && paperTitle.length > 0 ? + + + {paperTitle} + + + + : null} + + { + //cy.elements().unselectify(); + if (cy !== undefined) { + cy.elements().unselect() + } + + e.preventDefault(); + setSelectionOpen(false) + }} + > + + + + {/* {/*Causes errors in Cytoscape. Removing for now.} + + { + e.preventDefault(); + setDiscoveryData({ + "id": discoveryData.id, + "label": discoveryData.label, + "name": "" + }) + setNewSelectedApp({ + "image_url": "", + "name": "", + "id": "", + "objectID": "remove", + }) + setSelectionOpen(true) + setDefaultSearch("") + + const foundelement = cy.getElementById(discoveryData.id) + if (foundelement !== undefined && foundelement !== null) { + console.log("element: ", foundelement) + foundelement.data("large_image", discoveryData.large_image) + foundelement.data("text_margin_y", "14px") + foundelement.data("margin_x", "32px") + foundelement.data("margin_y", "19x") + foundelement.data("width", "45px") + foundelement.data("height", "45px") + } + + setTimeout(() => { + setDiscoveryData({}) + setNewSelectedApp({}) + }, 1000) + }} + > + + + + */} +
+ {discoveryData.name !== undefined && discoveryData.name !== null && discoveryData.name.length > 0 ? +
+ + {discoveryData.id} 0 ? newSelectedApp.image_url : discoveryData.large_image} style={{height: 40, width: 40, margin: "auto",}}/> +
+ : + {discoveryData.id} + } + + {discoveryData.name !== undefined && discoveryData.name !== null && discoveryData.name.length > 0 ? + discoveryData.name + : + newSelectedApp.name !== undefined && newSelectedApp.name !== null && newSelectedApp.name.length > 0 ? + newSelectedApp.name + : + `No ${discoveryData.label} app chosen` + } + +
+
+ {discoveryData !== undefined && discoveryData.name !== undefined && discoveryData.name !== null && discoveryData.name.length > 0 ? + + + {discoveryData.description} + + {/*isCloud && defaultSearch !== undefined && defaultSearch.length > 0 ? + {< + newSelectedApp={newSelectedApp} + setNewSelectedApp={setNewSelectedApp} + defaultSearch={defaultSearch} + />} + : + null + */} + + : + selectionOpen + ? + + + Click an app below to select it + + + : + + } +
+
+ {selectionOpen ? + + : null} +
+
+ ) +} + +export default AppSearchPopout; diff --git a/frontend/src/components/AuthenticationItem.jsx b/frontend/src/components/AuthenticationItem.jsx new file mode 100644 index 00000000..eef9806c --- /dev/null +++ b/frontend/src/components/AuthenticationItem.jsx @@ -0,0 +1,278 @@ +import React, { useState, useEffect } from "react"; + +import theme from '../theme.jsx'; +import { useAlert } from "react-alert"; +import { + Tooltip, + IconButton, + ListItem, + ListItemText, + FormGroup, + FormControl, + InputLabel, + FormLabel, + FormControlLabel, + Select, + MenuItem, + Grid, + Paper, + Typography, + TextField, + Zoom, +} from "@material-ui/core"; + +import { + Edit as EditIcon, + Delete as DeleteIcon, + SelectAll as SelectAllIcon, +} from "@material-ui/icons"; + +const AuthenticationItem = (props) => { + const { data, index, globalUrl, getAppAuthentication } = props + + const [selectedAuthentication, setSelectedAuthentication] = React.useState({}) + const [selectedAuthenticationModalOpen, setSelectedAuthenticationModalOpen] = React.useState(false); + const [authenticationFields, setAuthenticationFields] = React.useState([]); + + const alert = useAlert(); + var bgColor = "#27292d"; + if (index % 2 === 0) { + bgColor = "#1f2023"; + } + + //console.log("Auth data: ", data) + if (data.type === "oauth2") { + data.fields = [ + { + key: "url", + value: "Secret. Replaced during app execution!", + }, + { + key: "client_id", + value: "Secret. Replaced during app execution!", + }, + { + key: "client_secret", + value: "Secret. Replaced during app execution!", + }, + { + key: "scope", + value: "Secret. Replaced during app execution!", + }, + ]; + } + + const deleteAuthentication = (data) => { + alert.info("Deleting auth " + data.label); + + // Just use this one? + const url = globalUrl + "/api/v1/apps/authentication/" + data.id; + console.log("URL: ", url); + fetch(url, { + method: "DELETE", + credentials: "include", + headers: { + "Content-Type": "application/json", + }, + }) + .then((response) => + response.json().then((responseJson) => { + console.log("RESP: ", responseJson); + if (responseJson["success"] === false) { + alert.error("Failed deleting auth"); + } else { + // Need to wait because query in ES is too fast + setTimeout(() => { + getAppAuthentication(); + }, 1000); + //alert.success("Successfully deleted authentication!") + } + }) + ) + .catch((error) => { + console.log("Error in userdata: ", error); + }); + } + + const editAuthenticationConfig = (id) => { + const data = { + id: id, + action: "assign_everywhere", + }; + const url = globalUrl + "/api/v1/apps/authentication/" + id + "/config"; + + fetch(url, { + mode: "cors", + method: "POST", + body: JSON.stringify(data), + credentials: "include", + crossDomain: true, + withCredentials: true, + headers: { + "Content-Type": "application/json; charset=utf-8", + }, + }) + .then((response) => + response.json().then((responseJson) => { + if (responseJson["success"] === false) { + alert.error("Failed overwriting appauth in workflows"); + } else { + alert.success("Successfully updated auth everywhere!"); + //setSelectedUserModalOpen(false); + setTimeout(() => { + getAppAuthentication(); + }, 1000); + } + }) + ) + .catch((error) => { + alert.error("Err: " + error.toString()); + }); + }; + + const updateAppAuthentication = (field) => { + setSelectedAuthenticationModalOpen(true); + setSelectedAuthentication(field); + //{selectedAuthentication.fields.map((data, index) => { + var newfields = []; + for (var key in field.fields) { + newfields.push({ + key: field.fields[key].key, + value: "", + }); + } + setAuthenticationFields(newfields); + } + + return ( + + + style={{ minWidth: 75, maxWidth: 75 }} + /> + + + {/* + + */} + + {/* + + */} + { + return data.key; + }) + .join(", ") + } + style={{ + minWidth: 125, + maxWidth: 125, + overflow: "hidden", + }} + /> + + + { + updateAppAuthentication(data); + }} + > + + + {data.defined ? ( + + { + editAuthenticationConfig(data.id); + }} + > + + + + ) : ( + + {}} + > + + + + )} + { + deleteAuthentication(data); + }} + > + + + + + ) + } + +export default AuthenticationItem diff --git a/frontend/src/components/AuthenticationNormal.jsx b/frontend/src/components/AuthenticationNormal.jsx new file mode 100644 index 00000000..5d3a4fd3 --- /dev/null +++ b/frontend/src/components/AuthenticationNormal.jsx @@ -0,0 +1,366 @@ +import React, { useState, useEffect } from "react"; +import theme from '../theme.jsx'; +import { v4 as uuidv4 } from "uuid"; + + +import { + Button, + Divider, + Select, + MenuItem, + TextField, + DialogActions, + DialogTitle, + DialogContent, + Typography, +} from "@material-ui/core"; + +import { + LockOpen as LockOpenIcon, +} from "@material-ui/icons"; + +const AuthenticationData = (props) => { + const { + globalUrl, + saveWorkflow, + selectedApp, + workflow, + selectedAction, + authenticationType, + getAppAuthentication, + appAuthentication, + setSelectedAction, + setAuthenticationModalOpen, + isCloud, + } = props; + + const setNewAppAuth = (appAuthData) => { + console.log("DAta: ", appAuthData); + fetch(globalUrl + "/api/v1/apps/authentication", { + method: "PUT", + headers: { + "Content-Type": "application/json", + Accept: "application/json", + }, + body: JSON.stringify(appAuthData), + credentials: "include", + }) + .then((response) => { + if (response.status !== 200) { + console.log("Status not 200 for setting app auth :O!"); + } + + return response.json(); + }) + .then((responseJson) => { + if (!responseJson.success) { + alert.error("Failed to set app auth: " + responseJson.reason); + } else { + if (getAppAuthentication !== undefined) { + getAppAuthentication() + } + + if (setAuthenticationModalOpen !== undefined) { + setAuthenticationModalOpen(false) + } + + // Needs a refresh with the new authentication.. + //alert.success("Successfully saved new app auth") + } + }) + .catch((error) => { + //alert.error(error.toString()); + console.log("New auth error: ", error.toString()); + }); + } + + const [authenticationOption, setAuthenticationOptions] = React.useState({ + app: JSON.parse(JSON.stringify(selectedApp)), + fields: {}, + label: "", + usage: [ + { + workflow_id: workflow.id, + }, + ], + id: uuidv4(), + active: true, + }); + + if ( + selectedApp.authentication === undefined || + selectedApp.authentication.parameters === null || + selectedApp.authentication.parameters === undefined || + selectedApp.authentication.parameters.length === 0 + ) { + return ( + + + {selectedApp.name} does not require authentication + + + ); + } + + authenticationOption.app.actions = []; + + for (var key in selectedApp.authentication.parameters) { + if ( + authenticationOption.fields[ + selectedApp.authentication.parameters[key].name + ] === undefined + ) { + authenticationOption.fields[ + selectedApp.authentication.parameters[key].name + ] = ""; + } + } + + const handleSubmitCheck = () => { + console.log("NEW AUTH: ", authenticationOption); + if (authenticationOption.label.length === 0) { + authenticationOption.label = `Auth for ${selectedApp.name}`; + } + + // Automatically mapping fields that already exist (predefined). + // Warning if fields are NOT filled + for (var key in selectedApp.authentication.parameters) { + if ( + authenticationOption.fields[ + selectedApp.authentication.parameters[key].name + ].length === 0 + ) { + if ( + selectedApp.authentication.parameters[key].value !== undefined && + selectedApp.authentication.parameters[key].value !== null && + selectedApp.authentication.parameters[key].value.length > 0 + ) { + authenticationOption.fields[ + selectedApp.authentication.parameters[key].name + ] = selectedApp.authentication.parameters[key].value; + } else { + if ( + selectedApp.authentication.parameters[key].schema.type === "bool" + ) { + authenticationOption.fields[ + selectedApp.authentication.parameters[key].name + ] = "false"; + } else { + alert.info( + "Field " + + selectedApp.authentication.parameters[key].name + + " can't be empty" + ); + return; + } + } + } + } + + console.log("Action: ", selectedAction); + selectedAction.authentication_id = authenticationOption.id; + selectedAction.selectedAuthentication = authenticationOption; + if ( + selectedAction.authentication === undefined || + selectedAction.authentication === null + ) { + selectedAction.authentication = [authenticationOption]; + } else { + selectedAction.authentication.push(authenticationOption); + } + + setSelectedAction(selectedAction); + + var newAuthOption = JSON.parse(JSON.stringify(authenticationOption)); + var newFields = []; + for (const key in newAuthOption.fields) { + const value = newAuthOption.fields[key]; + newFields.push({ + key: key, + value: value, + }); + } + + console.log("FIELDS: ", newFields); + newAuthOption.fields = newFields; + setNewAppAuth(newAuthOption); + + //if (configureWorkflowModalOpen) { + // setSelectedAction({}); + //} + + //setUpdate(authenticationOption.id); + }; + + if ( + authenticationOption.label === null || + authenticationOption.label === undefined + ) { + authenticationOption.label = selectedApp.name + " authentication"; + } + + return ( +
+ +
+ Authentication for {selectedApp.name} +
+
+ + + What is app authentication? + +
+ These are required fields for authenticating with {selectedApp.name} +
+ Name - what is this used for? + { + authenticationOption.label = event.target.value; + }} + /> + +
+ {selectedApp.authentication.parameters.map((data, index) => { + return ( +
+ + {data.name} + + {data.schema !== undefined && + data.schema !== null && + data.schema.type === "bool" ? ( + + ) : ( + { + authenticationOption.fields[data.name] = + event.target.value; + }} + /> + )} +
+ ); + })} + + + + + +
+ ); +}; + +export default AuthenticationData diff --git a/frontend/src/components/Branding.jsx b/frontend/src/components/Branding.jsx new file mode 100644 index 00000000..40254bbb --- /dev/null +++ b/frontend/src/components/Branding.jsx @@ -0,0 +1,83 @@ +import React, { useState, useEffect } from "react"; +import ReactGA from 'react-ga4'; +import theme from "../theme.jsx"; + +import { useTheme } from "@material-ui/core/styles"; +import { + Paper, + Typography, + Divider, + Button, + Grid, + Card, +} from "@material-ui/core"; + +import { useAlert } from "react-alert"; + +const Branding = (props) => { + const { globalUrl, userdata, serverside, billingInfo, stripeKey, selectedOrganization, handleGetOrg, } = props; + const alert = useAlert(); + const [publishingInfo, setPublishingInfo] = useState(""); + + // Should enable / disable org branding + const handleChangePublishing = () => { + console.log("Handle change publishing"); + } + + const isOrganizationReady = () => { + // A simple checklist to ensure the button shows up properly + if (selectedOrganization.name === selectedOrganization.org) { + return false; + } + + if (selectedOrganization.large_image === "" || selectedOrganization.large_image === theme.palette.defaultImage) { + return false; + } + + return true + } + + return ( +
+ + Branding + + + You can customize your organization's branding by uploading a logo, changing the color scheme and a lot more. + + + +

+ Creator Network +

+
+
+ + + By changing publishing settings, you agree to our Terms of Service, and acknowledge that your organization's non-sensitive data will be turned into a creator account. Support: support@shuffler.io + + + + + {publishingInfo} + + +
+
+
+ ) +} + +export default Branding; diff --git a/frontend/src/components/CacheView.jsx b/frontend/src/components/CacheView.jsx new file mode 100644 index 00000000..873f9f7e --- /dev/null +++ b/frontend/src/components/CacheView.jsx @@ -0,0 +1,427 @@ +import React, { useState, useEffect } from "react"; +import theme from "../theme.jsx"; +import { + Tooltip, + Divider, + TextField, + Button, + Tabs, + Tab, + Grid, + List, + ListItem, + ListItemText, + IconButton, + Dialog, + DialogTitle, + DialogActions, +} from "@material-ui/core"; +import { useAlert } from "react-alert"; + +import { + Edit as EditIcon, + FileCopy as FileCopyIcon, + SelectAll as SelectAllIcon, + OpenInNew as OpenInNewIcon, + CloudDownload as CloudDownloadIcon, + Description as DescriptionIcon, + Polymer as PolymerIcon, + CheckCircle as CheckCircleIcon, + Close as CloseIcon, + Apps as AppsIcon, + Image as ImageIcon, + Delete as DeleteIcon, + Cached as CachedIcon, + AccessibilityNew as AccessibilityNewIcon, + Lock as LockIcon, + Eco as EcoIcon, + Schedule as ScheduleIcon, + Cloud as CloudIcon, + Business as BusinessIcon, + Visibility as VisibilityIcon, + VisibilityOff as VisibilityOffIcon, +} from "@material-ui/icons"; + +const CacheView = (props) => { + const { globalUrl, userdata, serverside, orgId } = props; + const [orgCache, setOrgCache] = React.useState(""); + const [listCache, setListCache] = React.useState([]); + const [addCache, setAddCache] = React.useState(""); + const [modalOpen, setModalOpen] = React.useState(false); + const [key, setKey]= React.useState(""); + const [value, setValue]= React.useState(""); + const [cacheInput, setCacheInput]= React.useState(""); + const [cacheCursor, setCacheCursor]= React.useState(""); + + const alert = useAlert(); + useEffect(() => { + listOrgCache(orgId); + console.log("orgid", orgId); + }, []); + + const listOrgCache = (orgId) => { + fetch(globalUrl + `/api/v1/orgs/${orgId}/list_cache`, { + method: "GET", + headers: { + "Content-Type": "application/json", + Accept: "application/json", + }, + credentials: "include", + }) + .then((response) => { + if (response.status !== 200) { + console.log("Status not 200 for apps :O!"); + return; + } + + return response.json(); + }) + .then((responseJson) => { + if (responseJson.success === true) { + setListCache(responseJson.keys); + } + + if (responseJson.cursor !== undefined && responseJson.cursor !== null && responseJson.cursor !== "") { + setCacheCursor(responseJson.cursor); + } + }) + .catch((error) => { + alert.error(error.toString()); + }); + }; + + // const getCacheList = (orgId) => { + // fetch(`${globalUrl}/api/v1/orgs/${orgId}/get_cache`, { + // method: "GET", + // headers: { + // "Content-Type": "application/json", + // Accept: "application/json", + // }, + // credentials: "include", + // }) + // .then((response) => { + // if (response.status !== 200) { + // console.log("Status not 200 for WORKFLOW EXECUTION :O!"); + // } + + + // return response.json(); + // }) + // .then((responseJson) => { + // if (responseJson.success !== false) { + // console.log("Found cache: ", responseJson) + // setListCache(responseJson) + // } else { + // console.log("Couldn't find the creator profile (rerun?): ", responseJson) + // // If the current user is any of the Shuffle Creators + // // AND the workflow doesn't have an owner: allow editing. + // // else: Allow suggestions? + // //console.log("User: ", userdata) + // //if (rerun !== true) { + // // getUserProfile(userdata.id, true) + // //} + // } + // }) + // .catch((error) => { + // console.log("Get userprofile error: ", error); + // }) + // } + + + const deleteCache = (orgId, key) => { + alert.info("Attempting to delete Cache"); + fetch(globalUrl + `/api/v1/orgs/${orgId}/cache/${key}`, { + method: "DELETE", + headers: { + Accept: "application/json", + }, + credentials: "include", + }) + .then((response) => { + if (response.status === 200) { + alert.success("Successfully deleted Cache"); + setTimeout(() => { + listOrgCache(orgId); + }, 1000); + } else { + alert.error("Failed deleting Cache. Does it still exist?"); + } + }) + .catch((error) => { + alert.error(error.toString()); + }); + }; + + const addOrgCache = (orgId) => { + const cache={key:key,value:value}; + setCacheInput([cache]); + console.log("cache input:",cacheInput) + + fetch(globalUrl + `/api/v1/orgs/${orgId}/set_cache`, { + + method: "POST", + body: JSON.stringify(cache), + headers: { + "Content-Type": "application/json", + Accept: "application/json", + }, + credentials: "include", + }) + .then((response) => { + if (response.status !== 200) { + console.log("Status not 200 for apps :O!"); + return; + } + + return response.json(); + }) + .then((responseJson) => { + setAddCache(responseJson); + alert.success("New Cache Added Successfully!"); + listOrgCache(orgId); + setModalOpen(false); + }) + .catch((error) => { + alert.error(error.toString()); + }); + }; + + const modalView = ( + { + setModalOpen(false); + }} + PaperProps={{ + style: { + backgroundColor: theme.palette.surfaceColor, + color: "white", + minWidth: "800px", + minHeight: "320px", + }, + }} + > + + + Add Cache + + +
+ Key + setKey(e.target.value)} + /> +
+
+ Value + setValue(e.target.value)} + /> +
+ + + + +
+ ); + + return ( + +
+ {modalView} +
+

Shuffle Datastore

+ + Datastore is a key-value store for storing data that can be used cross-workflow.  + + Learn more + + +
+ + + + + + + + + + + {listCache === undefined || listCache === null + ? null + : listCache.map((data, index) => { + var bgColor = "#27292d"; + if (index % 2 === 0) { + bgColor = "#1f2023"; + } + + return ( + + + + + + {/* + + { + + }} + > + + + + */} + + + { + deleteCache(orgId, data.key); + //deleteFile(orgId); + }} + > + + + + + + /> + + ); + })} + +
+ + ); +} +export default CacheView; diff --git a/frontend/src/components/Countries.jsx b/frontend/src/components/Countries.jsx new file mode 100644 index 00000000..2cb9c853 --- /dev/null +++ b/frontend/src/components/Countries.jsx @@ -0,0 +1,434 @@ +const countries = [ + { code: 'GB', label: 'United Kingdom', phone: '44' }, + { + code: 'US', + label: 'United States', + phone: '1', + suggested: true, + }, + { code: 'IN', label: 'India', phone: '91' }, + { code: 'AD', label: 'Andorra', phone: '376' }, + { + code: 'AE', + label: 'United Arab Emirates', + phone: '971', + }, + { code: 'AF', label: 'Afghanistan', phone: '93' }, + { + code: 'AG', + label: 'Antigua and Barbuda', + phone: '1-268', + }, + { code: 'AI', label: 'Anguilla', phone: '1-264' }, + { code: 'AL', label: 'Albania', phone: '355' }, + { code: 'AM', label: 'Armenia', phone: '374' }, + { code: 'AO', label: 'Angola', phone: '244' }, + { code: 'AQ', label: 'Antarctica', phone: '672' }, + { code: 'AR', label: 'Argentina', phone: '54' }, + { code: 'AS', label: 'American Samoa', phone: '1-684' }, + { code: 'AT', label: 'Austria', phone: '43' }, + { + code: 'AU', + label: 'Australia', + phone: '61', + suggested: true, + }, + { code: 'AW', label: 'Aruba', phone: '297' }, + { code: 'AX', label: 'Alland Islands', phone: '358' }, + { code: 'AZ', label: 'Azerbaijan', phone: '994' }, + { + code: 'BA', + label: 'Bosnia and Herzegovina', + phone: '387', + }, + { code: 'BB', label: 'Barbados', phone: '1-246' }, + { code: 'BD', label: 'Bangladesh', phone: '880' }, + { code: 'BE', label: 'Belgium', phone: '32' }, + { code: 'BF', label: 'Burkina Faso', phone: '226' }, + { code: 'BG', label: 'Bulgaria', phone: '359' }, + { code: 'BH', label: 'Bahrain', phone: '973' }, + { code: 'BI', label: 'Burundi', phone: '257' }, + { code: 'BJ', label: 'Benin', phone: '229' }, + { code: 'BL', label: 'Saint Barthelemy', phone: '590' }, + { code: 'BM', label: 'Bermuda', phone: '1-441' }, + { code: 'BN', label: 'Brunei Darussalam', phone: '673' }, + { code: 'BO', label: 'Bolivia', phone: '591' }, + { code: 'BR', label: 'Brazil', phone: '55' }, + { code: 'BS', label: 'Bahamas', phone: '1-242' }, + { code: 'BT', label: 'Bhutan', phone: '975' }, + { code: 'BV', label: 'Bouvet Island', phone: '47' }, + { code: 'BW', label: 'Botswana', phone: '267' }, + { code: 'BY', label: 'Belarus', phone: '375' }, + { code: 'BZ', label: 'Belize', phone: '501' }, + { + code: 'CA', + label: 'Canada', + phone: '1', + suggested: true, + }, + { + code: 'CC', + label: 'Cocos (Keeling) Islands', + phone: '61', + }, + { + code: 'CD', + label: 'Congo, Democratic Republic of the', + phone: '243', + }, + { + code: 'CF', + label: 'Central African Republic', + phone: '236', + }, + { + code: 'CG', + label: 'Congo, Republic of the', + phone: '242', + }, + { code: 'CH', label: 'Switzerland', phone: '41' }, + { code: 'CI', label: "Cote d'Ivoire", phone: '225' }, + { code: 'CK', label: 'Cook Islands', phone: '682' }, + { code: 'CL', label: 'Chile', phone: '56' }, + { code: 'CM', label: 'Cameroon', phone: '237' }, + { code: 'CN', label: 'China', phone: '86' }, + { code: 'CO', label: 'Colombia', phone: '57' }, + { code: 'CR', label: 'Costa Rica', phone: '506' }, + { code: 'CU', label: 'Cuba', phone: '53' }, + { code: 'CV', label: 'Cape Verde', phone: '238' }, + { code: 'CW', label: 'Curacao', phone: '599' }, + { code: 'CX', label: 'Christmas Island', phone: '61' }, + { code: 'CY', label: 'Cyprus', phone: '357' }, + { code: 'CZ', label: 'Czech Republic', phone: '420' }, + { + code: 'DE', + label: 'Germany', + phone: '49', + suggested: true, + }, + { code: 'DJ', label: 'Djibouti', phone: '253' }, + { code: 'DK', label: 'Denmark', phone: '45' }, + { code: 'DM', label: 'Dominica', phone: '1-767' }, + { + code: 'DO', + label: 'Dominican Republic', + phone: '1-809', + }, + { code: 'DZ', label: 'Algeria', phone: '213' }, + { code: 'EC', label: 'Ecuador', phone: '593' }, + { code: 'EE', label: 'Estonia', phone: '372' }, + { code: 'EG', label: 'Egypt', phone: '20' }, + { code: 'EH', label: 'Western Sahara', phone: '212' }, + { code: 'ER', label: 'Eritrea', phone: '291' }, + { code: 'ES', label: 'Spain', phone: '34' }, + { code: 'ET', label: 'Ethiopia', phone: '251' }, + { code: 'FI', label: 'Finland', phone: '358' }, + { code: 'FJ', label: 'Fiji', phone: '679' }, + { + code: 'FK', + label: 'Falkland Islands (Malvinas)', + phone: '500', + }, + { + code: 'FM', + label: 'Micronesia, Federated States of', + phone: '691', + }, + { code: 'FO', label: 'Faroe Islands', phone: '298' }, + { + code: 'FR', + label: 'France', + phone: '33', + suggested: true, + }, + { code: 'GA', label: 'Gabon', phone: '241' }, + { code: 'GB', label: 'United Kingdom', phone: '44' }, + { code: 'GD', label: 'Grenada', phone: '1-473' }, + { code: 'GE', label: 'Georgia', phone: '995' }, + { code: 'GF', label: 'French Guiana', phone: '594' }, + { code: 'GG', label: 'Guernsey', phone: '44' }, + { code: 'GH', label: 'Ghana', phone: '233' }, + { code: 'GI', label: 'Gibraltar', phone: '350' }, + { code: 'GL', label: 'Greenland', phone: '299' }, + { code: 'GM', label: 'Gambia', phone: '220' }, + { code: 'GN', label: 'Guinea', phone: '224' }, + { code: 'GP', label: 'Guadeloupe', phone: '590' }, + { code: 'GQ', label: 'Equatorial Guinea', phone: '240' }, + { code: 'GR', label: 'Greece', phone: '30' }, + { + code: 'GS', + label: 'South Georgia and the South Sandwich Islands', + phone: '500', + }, + { code: 'GT', label: 'Guatemala', phone: '502' }, + { code: 'GU', label: 'Guam', phone: '1-671' }, + { code: 'GW', label: 'Guinea-Bissau', phone: '245' }, + { code: 'GY', label: 'Guyana', phone: '592' }, + { code: 'HK', label: 'Hong Kong', phone: '852' }, + { + code: 'HM', + label: 'Heard Island and McDonald Islands', + phone: '672', + }, + { code: 'HN', label: 'Honduras', phone: '504' }, + { code: 'HR', label: 'Croatia', phone: '385' }, + { code: 'HT', label: 'Haiti', phone: '509' }, + { code: 'HU', label: 'Hungary', phone: '36' }, + { code: 'ID', label: 'Indonesia', phone: '62' }, + { code: 'IE', label: 'Ireland', phone: '353' }, + { code: 'IL', label: 'Israel', phone: '972' }, + { code: 'IM', label: 'Isle of Man', phone: '44' }, + { code: 'IN', label: 'India', phone: '91' }, + { + code: 'IO', + label: 'British Indian Ocean Territory', + phone: '246', + }, + { code: 'IQ', label: 'Iraq', phone: '964' }, + { + code: 'IR', + label: 'Iran, Islamic Republic of', + phone: '98', + }, + { code: 'IS', label: 'Iceland', phone: '354' }, + { code: 'IT', label: 'Italy', phone: '39' }, + { code: 'JE', label: 'Jersey', phone: '44' }, + { code: 'JM', label: 'Jamaica', phone: '1-876' }, + { code: 'JO', label: 'Jordan', phone: '962' }, + { + code: 'JP', + label: 'Japan', + phone: '81', + suggested: true, + }, + { code: 'KE', label: 'Kenya', phone: '254' }, + { code: 'KG', label: 'Kyrgyzstan', phone: '996' }, + { code: 'KH', label: 'Cambodia', phone: '855' }, + { code: 'KI', label: 'Kiribati', phone: '686' }, + { code: 'KM', label: 'Comoros', phone: '269' }, + { + code: 'KN', + label: 'Saint Kitts and Nevis', + phone: '1-869', + }, + { + code: 'KP', + label: "Korea, Democratic People's Republic of", + phone: '850', + }, + { code: 'KR', label: 'Korea, Republic of', phone: '82' }, + { code: 'KW', label: 'Kuwait', phone: '965' }, + { code: 'KY', label: 'Cayman Islands', phone: '1-345' }, + { code: 'KZ', label: 'Kazakhstan', phone: '7' }, + { + code: 'LA', + label: "Lao People's Democratic Republic", + phone: '856', + }, + { code: 'LB', label: 'Lebanon', phone: '961' }, + { code: 'LC', label: 'Saint Lucia', phone: '1-758' }, + { code: 'LI', label: 'Liechtenstein', phone: '423' }, + { code: 'LK', label: 'Sri Lanka', phone: '94' }, + { code: 'LR', label: 'Liberia', phone: '231' }, + { code: 'LS', label: 'Lesotho', phone: '266' }, + { code: 'LT', label: 'Lithuania', phone: '370' }, + { code: 'LU', label: 'Luxembourg', phone: '352' }, + { code: 'LV', label: 'Latvia', phone: '371' }, + { code: 'LY', label: 'Libya', phone: '218' }, + { code: 'MA', label: 'Morocco', phone: '212' }, + { code: 'MC', label: 'Monaco', phone: '377' }, + { + code: 'MD', + label: 'Moldova, Republic of', + phone: '373', + }, + { code: 'ME', label: 'Montenegro', phone: '382' }, + { + code: 'MF', + label: 'Saint Martin (French part)', + phone: '590', + }, + { code: 'MG', label: 'Madagascar', phone: '261' }, + { code: 'MH', label: 'Marshall Islands', phone: '692' }, + { + code: 'MK', + label: 'Macedonia, the Former Yugoslav Republic of', + phone: '389', + }, + { code: 'ML', label: 'Mali', phone: '223' }, + { code: 'MM', label: 'Myanmar', phone: '95' }, + { code: 'MN', label: 'Mongolia', phone: '976' }, + { code: 'MO', label: 'Macao', phone: '853' }, + { + code: 'MP', + label: 'Northern Mariana Islands', + phone: '1-670', + }, + { code: 'MQ', label: 'Martinique', phone: '596' }, + { code: 'MR', label: 'Mauritania', phone: '222' }, + { code: 'MS', label: 'Montserrat', phone: '1-664' }, + { code: 'MT', label: 'Malta', phone: '356' }, + { code: 'MU', label: 'Mauritius', phone: '230' }, + { code: 'MV', label: 'Maldives', phone: '960' }, + { code: 'MW', label: 'Malawi', phone: '265' }, + { code: 'MX', label: 'Mexico', phone: '52' }, + { code: 'MY', label: 'Malaysia', phone: '60' }, + { code: 'MZ', label: 'Mozambique', phone: '258' }, + { code: 'NA', label: 'Namibia', phone: '264' }, + { code: 'NC', label: 'New Caledonia', phone: '687' }, + { code: 'NE', label: 'Niger', phone: '227' }, + { code: 'NF', label: 'Norfolk Island', phone: '672' }, + { code: 'NG', label: 'Nigeria', phone: '234' }, + { code: 'NI', label: 'Nicaragua', phone: '505' }, + { code: 'NL', label: 'Netherlands', phone: '31' }, + { code: 'NO', label: 'Norway', phone: '47' }, + { code: 'NP', label: 'Nepal', phone: '977' }, + { code: 'NR', label: 'Nauru', phone: '674' }, + { code: 'NU', label: 'Niue', phone: '683' }, + { code: 'NZ', label: 'New Zealand', phone: '64' }, + { code: 'OM', label: 'Oman', phone: '968' }, + { code: 'PA', label: 'Panama', phone: '507' }, + { code: 'PE', label: 'Peru', phone: '51' }, + { code: 'PF', label: 'French Polynesia', phone: '689' }, + { code: 'PG', label: 'Papua New Guinea', phone: '675' }, + { code: 'PH', label: 'Philippines', phone: '63' }, + { code: 'PK', label: 'Pakistan', phone: '92' }, + { code: 'PL', label: 'Poland', phone: '48' }, + { + code: 'PM', + label: 'Saint Pierre and Miquelon', + phone: '508', + }, + { code: 'PN', label: 'Pitcairn', phone: '870' }, + { code: 'PR', label: 'Puerto Rico', phone: '1' }, + { + code: 'PS', + label: 'Palestine, State of', + phone: '970', + }, + { code: 'PT', label: 'Portugal', phone: '351' }, + { code: 'PW', label: 'Palau', phone: '680' }, + { code: 'PY', label: 'Paraguay', phone: '595' }, + { code: 'QA', label: 'Qatar', phone: '974' }, + { code: 'RE', label: 'Reunion', phone: '262' }, + { code: 'RO', label: 'Romania', phone: '40' }, + { code: 'RS', label: 'Serbia', phone: '381' }, + { code: 'RU', label: 'Russian Federation', phone: '7' }, + { code: 'RW', label: 'Rwanda', phone: '250' }, + { code: 'SA', label: 'Saudi Arabia', phone: '966' }, + { code: 'SB', label: 'Solomon Islands', phone: '677' }, + { code: 'SC', label: 'Seychelles', phone: '248' }, + { code: 'SD', label: 'Sudan', phone: '249' }, + { code: 'SE', label: 'Sweden', phone: '46' }, + { code: 'SG', label: 'Singapore', phone: '65' }, + { code: 'SH', label: 'Saint Helena', phone: '290' }, + { code: 'SI', label: 'Slovenia', phone: '386' }, + { + code: 'SJ', + label: 'Svalbard and Jan Mayen', + phone: '47', + }, + { code: 'SK', label: 'Slovakia', phone: '421' }, + { code: 'SL', label: 'Sierra Leone', phone: '232' }, + { code: 'SM', label: 'San Marino', phone: '378' }, + { code: 'SN', label: 'Senegal', phone: '221' }, + { code: 'SO', label: 'Somalia', phone: '252' }, + { code: 'SR', label: 'Suriname', phone: '597' }, + { code: 'SS', label: 'South Sudan', phone: '211' }, + { + code: 'ST', + label: 'Sao Tome and Principe', + phone: '239', + }, + { code: 'SV', label: 'El Salvador', phone: '503' }, + { + code: 'SX', + label: 'Sint Maarten (Dutch part)', + phone: '1-721', + }, + { + code: 'SY', + label: 'Syrian Arab Republic', + phone: '963', + }, + { code: 'SZ', label: 'Swaziland', phone: '268' }, + { + code: 'TC', + label: 'Turks and Caicos Islands', + phone: '1-649', + }, + { code: 'TD', label: 'Chad', phone: '235' }, + { + code: 'TF', + label: 'French Southern Territories', + phone: '262', + }, + { code: 'TG', label: 'Togo', phone: '228' }, + { code: 'TH', label: 'Thailand', phone: '66' }, + { code: 'TJ', label: 'Tajikistan', phone: '992' }, + { code: 'TK', label: 'Tokelau', phone: '690' }, + { code: 'TL', label: 'Timor-Leste', phone: '670' }, + { code: 'TM', label: 'Turkmenistan', phone: '993' }, + { code: 'TN', label: 'Tunisia', phone: '216' }, + { code: 'TO', label: 'Tonga', phone: '676' }, + { code: 'TR', label: 'Turkey', phone: '90' }, + { + code: 'TT', + label: 'Trinidad and Tobago', + phone: '1-868', + }, + { code: 'TV', label: 'Tuvalu', phone: '688' }, + { + code: 'TW', + label: 'Taiwan, Province of China', + phone: '886', + }, + { + code: 'TZ', + label: 'United Republic of Tanzania', + phone: '255', + }, + { code: 'UA', label: 'Ukraine', phone: '380' }, + { code: 'UG', label: 'Uganda', phone: '256' }, + { + code: 'US', + label: 'United States', + phone: '1', + suggested: true, + }, + { code: 'UY', label: 'Uruguay', phone: '598' }, + { code: 'UZ', label: 'Uzbekistan', phone: '998' }, + { + code: 'VA', + label: 'Holy See (Vatican City State)', + phone: '379', + }, + { + code: 'VC', + label: 'Saint Vincent and the Grenadines', + phone: '1-784', + }, + { code: 'VE', label: 'Venezuela', phone: '58' }, + { + code: 'VG', + label: 'British Virgin Islands', + phone: '1-284', + }, + { + code: 'VI', + label: 'US Virgin Islands', + phone: '1-340', + }, + { code: 'VN', label: 'Vietnam', phone: '84' }, + { code: 'VU', label: 'Vanuatu', phone: '678' }, + { code: 'WF', label: 'Wallis and Futuna', phone: '681' }, + { code: 'WS', label: 'Samoa', phone: '685' }, + { code: 'XK', label: 'Kosovo', phone: '383' }, + { code: 'YE', label: 'Yemen', phone: '967' }, + { code: 'YT', label: 'Mayotte', phone: '262' }, + { code: 'ZA', label: 'South Africa', phone: '27' }, + { code: 'ZM', label: 'Zambia', phone: '260' }, + { code: 'ZW', label: 'Zimbabwe', phone: '263' }, +]; + +export default countries diff --git a/frontend/src/components/CreatorGrid.jsx b/frontend/src/components/CreatorGrid.jsx new file mode 100644 index 00000000..45e1961f --- /dev/null +++ b/frontend/src/components/CreatorGrid.jsx @@ -0,0 +1,306 @@ +import React, { useEffect, useState } from 'react'; + +import ReactGA from 'react-ga4'; +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, + Card, + Box, + CardContent, + IconButton, + Zoom, + CardMedia, + CardActionArea, +} from '@material-ui/core'; + +import { + Avatar, + AvatarGroup, +} from "@mui/material" + +import { + SkipNext as SkipNextIcon, + SkipPrevious as SkipPreviousIcon, + PlayArrow as PlayArrowIcon, + VerifiedUser as VerifiedUserIcon, +} from "@material-ui/icons"; + + +const searchClient = algoliasearch("JNSS5CFDZZ", "db08e40265e2941b9a7d8f644b6e5240") +const CreatorGrid = props => { + const { maxRows, showName, showSuggestion, isMobile, globalUrl, parsedXs } = props + const rowHandler = maxRows === undefined || maxRows === null ? 50 : maxRows + const xs = parsedXs === undefined || parsedXs === null ? isMobile ? 6 : 4 : 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 buttonStyle = {borderRadius: 30, height: 50, width: 220, margin: isMobile ? "15px auto 15px auto" : 20, fontSize: 18,} + + const isCloud = + window.location.host === "localhost:3002" || + window.location.host === "shuffler.io"; + + 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) + }); + } + + // 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) { + refine(foundQuery) + } + } + }, []) + + return ( +
+ + + + ), + }} + autoComplete='off' + type="search" + color="primary" + value={currentRefinement} + placeholder="Find Creators..." + id="shuffle_search_field" + onChange={(event) => { + refine(event.currentTarget.value) + }} + /> + {/*isSearchStalled ? 'My search is stalled' : ''*/} + + ) + } + + const paperAppContainer = { + display: "flex", + flexWrap: "wrap", + alignContent: "space-between", + marginTop: 5, + } + + const Hits = ({ hits }) => { + const [mouseHoverIndex, setMouseHoverIndex] = useState(-1) + var counted = 0 + + return ( + + {hits.map((data, index) => { + if (counted === 12/xs*rowHandler) { + return null + } + + counted += 1 + const creatorUrl = !isCloud ? `https://shuffler.io/creators/${data.username}` : `/creators/${data.username}` + + return ( + + + + + + +
+ {"Creator + + @{data.username} + + + {data.verified === true ? + + + + : + null + } + +
+ + {data.apps === undefined || data.apps === null ? 0 : data.apps} apps {data.workflows === null || data.workflows === undefined ? 0 : data.workflows} workflows + + {data.specialized_apps !== undefined && data.specialized_apps !== null && data.specialized_apps.length > 0 ? + + {data.specialized_apps.map((app, index) => { + // Putting all this in secondary of ListItemText looked weird. + return ( +
{ + console.log("Click") + //navigate("/apps/"+app.id) + }} + > + + + +
+ ) + })} +
+ : + null} +
+
+
+
+
+
+ ) + })} +
+ ) + } + + const CustomSearchBox = connectSearchBox(SearchBox) + const CustomHits = connectHits(Hits) + + return ( +
+ + +
+ +
+ +
+ {showSuggestion === true ? +
+ + Can't find what you're looking for? + +
+ setFormMail(e.target.value)} + /> + setMessage(e.target.value)} + /> +
+ + {formMessage} +
+ : null + } +
+ ) +} + +export default CreatorGrid; diff --git a/frontend/src/components/DocsGrid.jsx b/frontend/src/components/DocsGrid.jsx new file mode 100644 index 00000000..ea20cd0e --- /dev/null +++ b/frontend/src/components/DocsGrid.jsx @@ -0,0 +1,356 @@ +import React, {useEffect, useState} from 'react'; + +import ReactGA from 'react-ga4'; +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 aa from 'search-insights' + +import algoliasearch from 'algoliasearch/lite'; +import { InstantSearch, Configure, connectSearchBox, connectHits } from 'react-instantsearch-dom'; +import { + Zoom, + Grid, + Paper, + TextField, + Avatar, + ButtonBase, + InputAdornment, + Typography, + Button, + Tooltip, + List, + ListItem, + ListItemAvatar, + ListItemText, +} from '@material-ui/core'; + +import {Close as CloseIcon, Folder as FolderIcon, Polymer as PolymerIcon, LibraryBooks as LibraryBooksIcon} from '@material-ui/icons' + +const searchClient = algoliasearch("JNSS5CFDZZ", "db08e40265e2941b9a7d8f644b6e5240") +const DocsGrid = props => { + const { maxRows, showName, showSuggestion, isMobile, globalUrl, parsedXs, userdata, } = props + 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 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 | Apps | Find and integrate any app" + + 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 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 ( +
+ + + + ), + }} + autoComplete='off' + type="search" + color="primary" + defaultValue={currentRefinement} + placeholder="Search our Documentation..." + id="shuffle_search_field" + onChange={(event) => { + refine(event.currentTarget.value) + }} + limit={5} + /> + {/*isSearchStalled ? 'My search is stalled' : ''*/} + + ) + } + + var workflowDelay = -50 + const Hits = ({ hits }) => { + const [mouseHoverIndex, setMouseHoverIndex] = useState(-1) + //console.log(hits) + //var curhits = hits + //if (hits.length > 0 && defaultApps.length === 0) { + // setDefaultApps(hits) + //} + + //const [defaultApps, setDefaultApps] = React.useState([]) + //console.log(hits) + //if (hits.length > 0 && hits.length !== innerHits.length) { + // setInnerHits(hits) + //} + + var counted = 0 + return ( + + {hits.map((data, index) => { + workflowDelay += 50 + + const innerlistitemStyle = { + width: "100%", + 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%", + } + + if (counted >= 12/xs*rowHandler) { + return null + } + + counted += 1 + + var name = data.name === undefined ? + data.filename.charAt(0).toUpperCase() + data.filename.slice(1).replaceAll("_", " ") + " - " + data.title : + (data.name.charAt(0).toUpperCase()+data.name.slice(1)).replaceAll("_", " ") + + if (name.length > 96) { + name = name.slice(0, 96)+"..." + } + + //const secondaryText = data.data !== undefined ? data.data.slice(0, 100)+"..." : "" + const secondaryText = data.data !== undefined ? data.data.slice(0, 100)+"..." : "" + const baseImage = + const avatar = data.image_url === undefined ? + baseImage + : + + + var parsedUrl = data.urlpath !== undefined ? data.urlpath : "" + parsedUrl += `?queryID=${data.__queryID}` + + return ( + + { + aa('init', { + appId: searchClient.appId, + apiKey: searchClient.transporter.queryParameters["x-algolia-api-key"] + }) + + const timestamp = new Date().getTime() + aa('sendEvents', [ + { + eventType: 'click', + eventName: 'Product Clicked Appgrid', + index: 'documentation', + objectIDs: [data.objectID], + timestamp: timestamp, + queryID: data.__queryID, + positions: [data.__position], + userToken: userdata === undefined || userdata === null || userdata.id === undefined ? "unauthenticated" : userdata.id, + } + ]) + + console.log("CLICK") + }}> + { + setMouseHoverIndex(index) + }}> + + {avatar} + + + {/* + + + + + + */} + + + + ) + })} + + ) + } + + const CustomSearchBox = connectSearchBox(SearchBox) + const CustomHits = connectHits(Hits) + const selectButtonStyle = { + minWidth: 150, + maxWidth: 150, + minHeight: 50, + } + + return ( +
+ {/* +
+ +
+ */} +
+ +
+ +
+ + +
+ {showSuggestion === true ? +
+ + Can't find what you're looking for? + +
+ setFormMail(e.target.value)} + /> + setMessage(e.target.value)} + /> +
+ + {formMessage} +
+ : null + } + + + + Search by + + + Algolia logo + + +
+
+ ) +} + +export default DocsGrid; diff --git a/frontend/src/components/EditWorkflow.jsx b/frontend/src/components/EditWorkflow.jsx new file mode 100644 index 00000000..22a9d9ea --- /dev/null +++ b/frontend/src/components/EditWorkflow.jsx @@ -0,0 +1,516 @@ +import React, { useEffect, useContext } from "react"; +import theme from '../theme.jsx'; +import { isMobile } from "react-device-detect" +import ChipInput from "material-ui-chip-input"; +import UsecaseSearch from "../components/UsecaseSearch.jsx" + +import { + Badge, + Avatar, + Grid, + InputLabel, + Select, + ListSubheader, + Paper, + Tooltip, + Divider, + Button, + TextField, + IconButton, + Menu, + MenuItem, + FormControlLabel, + Chip, + Switch, + Typography, + Zoom, + CircularProgress, + Dialog, + DialogTitle, + DialogActions, + DialogContent, + OutlinedInput, + Checkbox, + ListItemText, + Radio, + RadioGroup, + FormControl, + FormLabel, + +} from "@material-ui/core"; + +import { + ExpandLess as ExpandLessIcon, + ExpandMore as ExpandMoreIcon, + Publish as PublishIcon, + OpenInNew as OpenInNewIcon, +} from "@material-ui/icons"; + +const EditWorkflow = (props) => { + const { globalUrl, workflow, setWorkflow, modalOpen, setModalOpen, showUpload, usecases, setNewWorkflow, appFramework, isEditing, userdata, } = props + + const [submitLoading, setSubmitLoading] = React.useState(false); + const [showMoreClicked, setShowMoreClicked] = React.useState(false); + const [innerWorkflow, setInnerWorkflow] = React.useState(workflow) + const [_, setUpdate] = React.useState(""); // Used for rendering, don't remove + const [newWorkflowTags, setNewWorkflowTags] = React.useState(workflow.tags !== undefined && workflow.tags !== null ? JSON.parse(JSON.stringify(workflow.tags)) : []) + const [selectedUsecases, setSelectedUsecases] = React.useState(workflow.usecase_ids !== undefined && workflow.usecase_ids !== null ? JSON.parse(JSON.stringify(workflow.usecase_ids)) : []); + const [foundWorkflowId, setFoundWorkflowId] = React.useState("") + const [name, setName] = React.useState(workflow.name !== undefined ? workflow.name : "") + const [description, setDescription] = React.useState(workflow.description !== undefined ? workflow.description : "") + + + // Gets the generated workflow + const getGeneratedWorkflow = (workflow_id) => { + fetch(globalUrl + "/api/v1/workflows/" + workflow_id, { + method: "GET", + headers: { + "Content-Type": "application/json", + Accept: "application/json", + }, + credentials: "include", + }) + .then((response) => { + if (response.status !== 200) { + console.log("Status not 200 when getting workflow"); + } + + return response.json(); + }) + .then((responseJson) => { + if (responseJson.id === workflow_id) { + console.log("GOT WORKFLOW: ", responseJson) + if (name === "") { + innerWorkflow.name = responseJson.name + setName(responseJson.name) + } + + if (description === "") { + innerWorkflow.description = responseJson.description + setDescription(description) + } + + if (newWorkflowTags === []) { + innerWorkflow.tags = responseJson.tags + setNewWorkflowTags(responseJson.tags) + } + + if (selectedUsecases === []) { + selectedUsecases = responseJson.usecase_ids + } + + innerWorkflow.id = responseJson.id + innerWorkflow.blogpost = responseJson.blogpost + innerWorkflow.actions = responseJson.actions + innerWorkflow.triggers = responseJson.triggers + innerWorkflow.branches = responseJson.branches + innerWorkflow.comments = responseJson.comments + innerWorkflow.workflow_variables = responseJson.workflow_variables + innerWorkflow.execution_variables = responseJson.execution_variables + + + setInnerWorkflow(innerWorkflow) + setUpdate(Math.random()) + } + }) + .catch((error) => { + //alert.error(error.toString()); + console.log("Get workflow error: ", error.toString()); + }) + } + + if (foundWorkflowId.length > 0) { + getGeneratedWorkflow(foundWorkflowId) + + setFoundWorkflowId("") + } else { + } + + if (modalOpen !== true) { + return null + } + + const newWorkflow = isEditing === true ? false : true + + var upload = ""; + var total_count = 0 + + return ( + { + setModalOpen(false); + }} + PaperProps={{ + style: { + backgroundColor: theme.palette.surfaceColor, + color: "white", + minWidth: isMobile ? "90%" : 550, + maxWidth: isMobile ? "90%" : 550, + minHeight: 400, + //minWidth: isMobile ? "90%" : newWorkflow === true ? 1000 : 550, + //maxWidth: isMobile ? "90%" : newWorkflow === true ? 1000 : 550, + }, + }} + > + +
+
+
+ + {newWorkflow ? "New" : "Editing"} workflow + + {newWorkflow === true ? null : +
+ + + + + +
+ } +
+ + Workflows can be built from scratch, or from templates. Usecases can help you discover next steps, and you can search for them directly. Learn more + + {showUpload === true ? +
+ + + +
+ : null} +
+ {/*newWorkflow === true ? +
+ + Use a Template + + + Start your workflow from our templating system. This uses publied workflows from our Creators to generate full Usecases or parts of your Workflow. + +
+ : null*/} +
+
+ + +
+ { + setName(event.target.value) + }} + InputProps={{ + style: { + color: "white", + }, + }} + color="primary" + placeholder="Name" + required + margin="dense" + defaultValue={innerWorkflow.name} + label="Name" + autoFocus + fullWidth + /> + { + setDescription(event.target.value) + }} + InputProps={{ + style: { + color: "white", + }, + }} + maxRows={4} + color="primary" + defaultValue={innerWorkflow.description} + placeholder="Description" + multiline + label="Description" + margin="dense" + fullWidth + /> +
+ { + newWorkflowTags.push(chip); + setNewWorkflowTags(newWorkflowTags); + }} + onDelete={(chip, index) => { + newWorkflowTags.splice(index, 1); + setNewWorkflowTags(newWorkflowTags); + }} + /> + {usecases !== null && usecases !== undefined && usecases.length > 0 ? + + Usecases + + + : null} +
+ + {showMoreClicked === true ? + + + + Status + { + console.log("Data: ", e.target.value) + + innerWorkflow.workflow_type = e.target.value + setInnerWorkflow(innerWorkflow) + }} + > + } label="Test" /> + } label="Production" /> + + + +
+ + + Type + { + console.log("Data: ", e.target.value) + + innerWorkflow.workflow_type = e.target.value + setInnerWorkflow(innerWorkflow) + }} + > + } label="Trigger" /> + } label="Subflow" /> + } label="Standalone" /> + + + + + + { + innerWorkflow.blogpost = event.target.value + setInnerWorkflow(innerWorkflow) + }} + InputProps={{ + style: { + color: "white", + }, + }} + color="primary" + defaultValue={innerWorkflow.blogpost} + placeholder="A blogpost or other reference for how this work workflow was built, and what it's for." + rows="1" + label="blogpost" + margin="dense" + fullWidth + /> + { + innerWorkflow.video = event.target.value + setInnerWorkflow(innerWorkflow) + }} + InputProps={{ + style: { + color: "white", + }, + }} + color="primary" + defaultValue={innerWorkflow.video} + placeholder="A youtube or loom link to the video" + rows="1" + label="Video" + margin="dense" + fullWidth + /> + { + innerWorkflow.default_return_value = event.target.value + setInnerWorkflow(innerWorkflow) + }} + InputProps={{ + style: { + color: "white", + }, + }} + color="primary" + defaultValue={innerWorkflow.default_return_value} + placeholder="Default return value (used for Subflows if the subflow fails)" + rows="3" + multiline + label="Default return value" + margin="dense" + fullWidth + /> + + : null} + + { + setShowMoreClicked(!showMoreClicked); + }} + > + {showMoreClicked ? : } + + +
+ {/*newWorkflow === true ? +
+ +
+ : null*/} + + + + + + +
+ ) +} + +export default EditWorkflow; diff --git a/frontend/src/components/ExtraApps.jsx b/frontend/src/components/ExtraApps.jsx new file mode 100644 index 00000000..c854a7ba --- /dev/null +++ b/frontend/src/components/ExtraApps.jsx @@ -0,0 +1,27 @@ +// Move this to the backend to be loaded in? +const extraApps = [{ + "name": "Cases", + "description": "Allows use of other Case Management apps without knowing how to use them.", + "app_version": "1.0.0", + "app_name": "Cases", + "type": "ACTION", + "large_image": encodeURI('data:image/svg+xml;utf-8,'), + "template": true, + "actions": [{ + "name": "Create Alert", + "description": "Create a ticket", + "parameters": [{ + "name": "id", + }, + { + "name": "name", + }, + { + "name": "description", + "multiline": true, + }, + ], + }], +}] + +export default extraApps diff --git a/frontend/src/components/Files.jsx b/frontend/src/components/Files.jsx new file mode 100644 index 00000000..a3f4e520 --- /dev/null +++ b/frontend/src/components/Files.jsx @@ -0,0 +1,809 @@ +import React, { useState, useEffect } from "react"; + +import { + IconButton, + List, + ListItem, + ListItemText, + ListItemAvatar, + ListItemSecondaryAction, + Tooltip, + Button, + FormControl, + InputLabel, + TextField, + Divider, + Select, + MenuItem, +} from "@material-ui/core"; + +import { + OpenInNew as OpenInNewIcon, + Edit as EditIcon, + CloudDownload as CloudDownloadIcon, + Delete as DeleteIcon, + FileCopy as FileCopyIcon, + Cached as CachedIcon, + Publish as PublishIcon, + Clear as ClearIcon, + Add as AddIcon, +} from "@material-ui/icons"; + +import { useAlert } from "react-alert"; +import Dropzone from "../components/Dropzone.jsx"; +import CodeEditor from "../components/ShuffleCodeEditor.jsx"; +import theme from "../theme.jsx"; + +const Files = (props) => { + const { globalUrl, userdata, serverside, selectedOrganization, isCloud, } = props; + + const [files, setFiles] = React.useState([]); + const [selectedNamespace, setSelectedNamespace] = React.useState("default"); + const [openFileId, setOpenFileId] = React.useState(false); + const [fileNamespaces, setFileNamespaces] = React.useState([]); + const [fileContent, setFileContent] = React.useState(""); + const [openEditor, setOpenEditor] = React.useState(false); + const [renderTextBox, setRenderTextBox] = React.useState(false); + + const alert = useAlert(); + const allowedFileTypes = ["txt", "py", "yaml", "yml","json", "html", "js", "csv", "log"] + var upload = ""; + + const handleKeyDown = (event) => { + if (event.key === 'Enter') { + + console.log('do validate') + console.log("new namespace name->",event.target.value); + fileNamespaces.push(event.target.value); + setSelectedNamespace(event.target.value); + setRenderTextBox(false); + } + + if (event.key === 'Escape'){ // not working for some reasons + console.log('escape pressed') + setRenderTextBox(false); + } + + } + + const runUpdateText = (text) =>{ + fetch(`${globalUrl}/api/v1/files/${openFileId}/edit`, { + method: "PUT", + headers: { + "Content-Type": "application/json", + Accept: "application/json", + }, + body:text, + credentials: "include", + }).then((response) => { + if (response.status !== 200) { + console.log("Can't update file"); + } + return response.json(); + }) + //console.log(text); + } + + const getFiles = () => { + fetch(globalUrl + "/api/v1/files", { + method: "GET", + headers: { + "Content-Type": "application/json", + Accept: "application/json", + }, + credentials: "include", + }) + .then((response) => { + if (response.status !== 200) { + console.log("Status not 200 for apps :O!"); + return; + } + + return response.json(); + }) + .then((responseJson) => { + if (responseJson.files !== undefined && responseJson.files !== null) { + setFiles(responseJson.files); + } else { + setFiles([]); + } + + if (responseJson.namespaces !== undefined && responseJson.namespaces !== null) { + setFileNamespaces(responseJson.namespaces); + } + }) + .catch((error) => { + alert.error(error.toString()); + }); + }; + + useEffect(() => { + getFiles(); + }, []); + + const deleteFile = (file) => { + fetch(globalUrl + "/api/v1/files/" + file.id, { + method: "DELETE", + headers: { + "Content-Type": "application/json", + Accept: "application/json", + }, + credentials: "include", + }) + .then((response) => { + if (response.status !== 200) { + console.log("Status not 200 for file delete :O!"); + } + + return response.json(); + }) + .then((responseJson) => { + if (responseJson.success) { + alert.info("Successfully deleted file " + file.name); + } else if ( + responseJson.reason !== undefined && + responseJson.reason !== null + ) { + alert.error("Failed to delete file: " + responseJson.reason); + } + setTimeout(() => { + getFiles(); + }, 1500); + + console.log(responseJson); + }) + .catch((error) => { + alert.error(error.toString()); + }); + }; + + const readFileData = (file) => { + fetch(globalUrl + "/api/v1/files/" + file.id + "/content", { + method: "GET", + headers: { + "Content-Type": "application/json", + Accept: "application/json", + }, + credentials: "include", + }) + .then((response) => { + if (response.status !== 200) { + console.log("Status not 200 for file :O!"); + return ""; + } + return response.text(); + }) + .then((respdata) => { + // console.log("respdata ->", respdata); + // console.log("respdata type ->", typeof(respdata)); + + if (respdata.length === 0) { + alert.error("Failed getting file. Is it deleted?"); + return; + } + return respdata + }) + .then((responseData) => { + + setFileContent(responseData); + //console.log("filecontent state ",fileContent); + }) + .catch((error) => { + alert.error(error.toString()); + }); + }; + + const downloadFile = (file) => { + fetch(globalUrl + "/api/v1/files/" + file.id + "/content", { + method: "GET", + credentials: "include", + }) + .then((response) => { + if (response.status !== 200) { + console.log("Status not 200 for apps :O!"); + return ""; + } + + console.log("Resp: ", response) + + return response.blob() + }) + .then((respdata) => { + if (respdata.length === 0) { + alert.error("Failed getting file. Is it deleted?"); + return; + } + + var blob = new Blob([respdata], { + type: "application/octet-stream", + }); + + var url = URL.createObjectURL(blob); + var link = document.createElement("a"); + link.setAttribute("href", url); + link.setAttribute("download", `${file.filename}`); + var event = document.createEvent("MouseEvents"); + event.initMouseEvent( + "click", + true, + true, + window, + 1, + 0, + 0, + 0, + 0, + false, + false, + false, + false, + 0, + null + ); + link.dispatchEvent(event); + + //return response.json() + }) + .then((responseJson) => { + //console.log(responseJson) + //setSchedules(responseJson) + }) + .catch((error) => { + alert.error(error.toString()); + }); + }; + + const handleCreateFile = (filename, file) => { + var data = { + filename: filename, + org_id: selectedOrganization.id, + workflow_id: "global", + }; + + if ( + selectedNamespace !== undefined && + selectedNamespace !== null && + selectedNamespace.length > 0 && + selectedNamespace !== "default" + ) { + data.namespace = selectedNamespace; + } + + fetch(globalUrl + "/api/v1/files/create", { + method: "POST", + headers: { + "Content-Type": "application/json", + Accept: "application/json", + }, + credentials: "include", + body: JSON.stringify(data), + }) + .then((response) => { + if (response.status !== 200) { + console.log("Status not 200 for apps :O!"); + return; + } + + return response.json(); + }) + .then((responseJson) => { + //console.log("RESP: ", responseJson) + if (responseJson.success === true) { + handleFileUpload(responseJson.id, file); + } else { + alert.error("Failed to upload file ", filename); + } + }) + .catch((error) => { + alert.error("Failed to upload file ", filename); + console.log(error.toString()); + }); + }; + + + + const handleFileUpload = (file_id, file) => { + //console.log("FILE: ", file_id, file) + fetch(`${globalUrl}/api/v1/files/${file_id}/upload`, { + method: "POST", + credentials: "include", + body: file, + }) + .then((response) => { + if (response.status !== 200 && response.status !== 201) { + console.log("Status not 200 for apps :O!"); + alert.error("File was created, but failed to upload."); + return; + } + + return response.json(); + }) + .then((responseJson) => { + //console.log("RESPONSE: ", responseJson) + //setFiles(responseJson) + }) + .catch((error) => { + alert.error(error.toString()); + }); + }; + + const uploadFiles = (files) => { + for (var key in files) { + try { + const filename = files[key].name; + var filedata = new FormData(); + filedata.append("shuffle_file", files[key]); + + if (typeof files[key] === "object") { + handleCreateFile(filename, filedata); + } + + /* + reader.addEventListener('load', (e) => { + var data = e.target.result; + setIsDropzone(false) + console.log(filename) + console.log(data) + console.log(files[key]) + }) + reader.readAsText(files[key]) + */ + } catch (e) { + console.log("Error in dropzone: ", e); + } + } + + setTimeout(() => { + getFiles(); + }, 2500); + }; + + const uploadFile = (e) => { + const isDropzone = + e.dataTransfer === undefined ? false : e.dataTransfer.files.length > 0; + const files = isDropzone ? e.dataTransfer.files : e.target.files; + + //const reader = new FileReader(); + //alert.info("Starting fileupload") + uploadFiles(files); + }; + + return ( + 1366 ? 1366 : 1200, + margin: "auto", + padding: 20, + }} + onDrop={uploadFile} + > +
+
+

Files

+ + Files from Workflows.{" "} + + Learn more + + +
+ + {/* */} + (upload = ref)} + onChange={(event) => { + //const file = event.target.value + //const fileObject = URL.createObjectURL(actualFile) + //setFile(fileObject) + //const files = event.target.files[0] + uploadFiles(event.target.files); + }} + /> + + + {fileNamespaces !== undefined && + fileNamespaces !== null && + fileNamespaces.length > 1 ? ( + + File Category + + + ) : null} +
+ {renderTextBox ? + + + + + : + + + } + {renderTextBox && { + handleKeyDown(event); + }} + InputProps={{ + style: { + color: "white", + }, + }} + color="primary" + placeholder="File category name" + required + margin="dense" + defaultValue={""} + autoFocus + />}
+ + + + + + + + + + + + + + + + {files === undefined || files === null || files.length === 0 ? null : + files.map((file, index) => { + if (file.namespace === "") { + file.namespace = "default"; + } + + if (file.namespace !== selectedNamespace) { + return null; + } + + var bgColor = "#27292d"; + if (index % 2 === 0) { + bgColor = "#1f2023"; + } + + const filenamesplit = file.filename.split(".") + const iseditable = file.filesize < 2000000 && file.status === "active" && allowedFileTypes.includes(filenamesplit[filenamesplit.length-1]) + + return ( + + + + + + + ) : ( + + + + + + + + + + ) + } + style={{ + minWidth: 100, + maxWidth: 100, + overflow: "hidden", + }} + /> + + + + + + + { + setOpenEditor(true) + setOpenFileId(file.id) + readFileData(file) + }} + > + + + + + + + { + downloadFile(file); + }} + > + + + + + + { + const elementName = "copy_element_shuffle"; + var copyText = + document.getElementById(elementName); + if ( + copyText !== null && + copyText !== undefined + ) { + const clipboard = navigator.clipboard; + if (clipboard === undefined) { + alert.error( + "Can only copy over HTTPS (port 3443)" + ); + return; + } + + navigator.clipboard.writeText(file.id); + copyText.select(); + copyText.setSelectionRange( + 0, + 99999 + ); /* For mobile devices */ + + /* Copy the text inside the text field */ + document.execCommand("copy"); + + alert.info(file.id + " copied to clipboard"); + } + }} + > + + + + + + { + deleteFile(file); + }} + > + + + + + + style={{ + minWidth: 250, + maxWidth: 250, + // overflow: "hidden", + }} + /> + + ); + }) + } + +
+
+ ) +} + +export default Files; diff --git a/frontend/src/components/FrameworkData.jsx b/frontend/src/components/FrameworkData.jsx new file mode 100644 index 00000000..456ea24f --- /dev/null +++ b/frontend/src/components/FrameworkData.jsx @@ -0,0 +1,28 @@ +const data = { + "Cases": { + "large_image": "data:image/png;base64,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", + }, + "IAM": { + "large_image": undefined, + }, + "SIEM": { + "large_image": "data:image/png;base64,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", + }, + "Assets": { + "large_image": "data:image/png;base64,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", + }, + "Intel": { + "large_image": "data:image/jpg;base64,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", + }, + "Comms": { + "large_image": "data:image/jpeg;base64,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", + }, + "Network": { + "large_image": "data:image/png;base64,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", + }, + "EDR & AV": { + "large_image": undefined, + }, +} + +export default data; diff --git a/frontend/src/components/Header.jsx b/frontend/src/components/Header.jsx new file mode 100644 index 00000000..975df6b7 --- /dev/null +++ b/frontend/src/components/Header.jsx @@ -0,0 +1,1028 @@ +import React, {useState} from 'react'; +import {BrowserView, MobileView} from "react-device-detect"; +import { useTheme } from '@material-ui/core/styles'; + +import {Link} from 'react-router-dom'; +import ReactGA from 'react-ga4'; + +import { + Paper, + Typography, + Badge, + Tooltip, + List, + ListItem, + Avatar, + Menu, + MenuItem, + Select, + Button, + Grid, + IconButton, + Divider, + LinearProgress, +} from '@material-ui/core' + +import { + MeetingRoom as MeetingRoomIcon, + HelpOutline as HelpOutlineIcon, + Settings as SettingsIcon, + Notifications as NotificationsIcon, + Home as HomeIcon, + Polymer as PolymerIcon, + Apps as AppsIcon, + Description as DescriptionIcon, + EmojiObjects as EmojiObjectsIcon, + Business as BusinessIcon, +} from '@material-ui/icons'; + +import { + Analytics as AnalyticsIcon, + Lightbulb as LightbulbIcon, +} from "@mui/icons-material"; + +import { useAlert } from "react-alert"; + +import SearchField from '../components/Searchfield.jsx' +const hoverColor = "#f85a3e" +const hoverOutColor = "#e8eaf6" + +const Header = props => { +const { globalUrl, setNotifications, notifications, isLoggedIn, removeCookie, homePage, userdata, serverside, } = props; + const theme = useTheme(); + const alert = useAlert() + + + const [HomeHoverColor, setHomeHoverColor] = useState(hoverOutColor); + const [SoarHoverColor, setSoarHoverColor] = useState(hoverOutColor); + const [LoginHoverColor, setLoginHoverColor] = useState(hoverOutColor); + const [DocsHoverColor, setDocsHoverColor] = useState(hoverOutColor); + const [HelpHoverColor, setHelpHoverColor] = useState(hoverOutColor); + const [anchorEl, setAnchorEl] = React.useState(null); + const [anchorElAvatar, setAnchorElAvatar] = React.useState(null); + const [subAnchorEl, setSubAnchorEl] = React.useState(null); + + + const handleClick = (event) => { + setAnchorEl(event.currentTarget); + }; + + const handleClose = () => { + setAnchorEl(null); + setAnchorElAvatar(null); + }; + + const hrefStyle = { + color: hoverOutColor, + textDecoration: "none", + } + + const isCloud = serverside === true || typeof window === 'undefined' ? true : window.location.host === "localhost:3002" || window.location.host === "shuffler.io"; + + const clearNotifications = () => { + // Don't really care about the logout + fetch(`${globalUrl}/api/v1/notifications/clear`, { + credentials: "include", + method: 'GET', + headers: { + 'Content-Type': 'application/json', + }, + }) + .then(function(response) { + if (response.status !== 200) { + console.log("Error in response") + } + + return response.json(); + }).then(function(responseJson) { + if (responseJson.success === true) { + setNotifications([]) + handleClose() + } else { + alert.error("Failed dismissing notifications. Please try again later.") + } + }) + .catch(error => { + console.log("error in notification dismissal: ", error) + //removeCookie("session_token", {path: "/"}) + }) + } + + const dismissNotification = (alert_id) => { + // Don't really care about the logout + fetch(`${globalUrl}/api/v1/notifications/${alert_id}/markasread`, { + credentials: "include", + method: 'GET', + headers: { + 'Content-Type': 'application/json', + }, + }) + .then(function(response) { + if (response.status !== 200) { + console.log("Error in response") + } + + return response.json(); + }).then(function(responseJson) { + if (responseJson.success === true) { + const newNotifications = notifications.filter(data => data.id !== alert_id) + console.log("NEW NOTIFICATIONS: ", newNotifications) + setNotifications(newNotifications) + } else { + alert.error("Failed dismissing notification. Please try again later.") + } + }) + .catch(error => { + console.log("error in notification dismissal: ", error) + //removeCookie("session_token", {path: "/"}) + }) + } + + // DEBUG HERE + const handleClickLogout = () => { + console.log("SHOULD LOG OUT") + + // Don't really care about the logout + fetch(globalUrl+"/api/v1/logout", { + credentials: "include", + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + }) + .then(() => { + // Log out anyway + removeCookie("session_token", {path: "/"}) + removeCookie("session_token", {path: "/"}) + removeCookie("session_token", {path: "/"}) + removeCookie("session_token", {path: "/"}) + window.location.pathname = "/" + }) + .catch(error => { + console.log(error) + }); + } + + // Rofl this is weird + const handleDocsHover = () => { + setDocsHoverColor(hoverColor) + } + + const handleDocsHoverOut = () => { + setDocsHoverColor(hoverOutColor) + } + + const handleHomeHover = () => { + setHomeHoverColor(hoverColor) + } + + const handleHelpHover = () => { + setHelpHoverColor(hoverColor) + } + + const handleHelpHoverOut = () => { + setHelpHoverColor(hoverOutColor) + } + + const handleSoarHover = () => { + setSoarHoverColor(hoverColor) + } + + const handleSoarHoverOut = () => { + setSoarHoverColor(hoverOutColor) + } + + const handleHomeHoverOut = () => { + setHomeHoverColor(hoverOutColor) + } + + const handleLoginHover = () => { + setLoginHoverColor(hoverColor) + } + + const handleLoginHoverOut = () => { + setLoginHoverColor(hoverOutColor) + } + + // Should be based on some path + const logoCheck = !homePage ? null : null + + const notificationWidth = 300 + const imagesize = 22; + const boxColor = "#86c142"; + const NotificationItem = (props) => { + const {data} = props + + var image = ""; + var orgName = ""; + var orgId = ""; + if (userdata.orgs !== undefined) { + const foundOrg = userdata.orgs.find((org) => org.id === data["org_id"]); + if (foundOrg !== undefined && foundOrg !== null) { + //position: "absolute", bottom: 5, right: -5, + const imageStyle = { + width: imagesize, + height: imagesize, + pointerEvents: "none", + marginLeft: data.creator_org !== undefined && data.creator_org.length > 0 ? 20 : 0, + borderRadius: 10, + border: foundOrg.id === userdata.active_org.id ? `3px solid ${boxColor}` : null, + cursor: "pointer", + marginRight: 10, + }; + + image = + foundOrg.image === "" ? ( + {foundOrg.name} + ) : ( + {foundOrg.name} {}} + /> + ); + + orgName = foundOrg.name; + orgId = foundOrg.id; + } + } + + return ( + + {/* + {new Date(data.updated_at).toISOString()} + */} + {data.reference_url !== undefined && data.reference_url !== null && data.reference_url.length > 0 ? + + + {data.title} + + + : + + {data.title} + + } + + {data.image !== undefined && data.image !== null && data.image.length > 0 ? + {data.title} + : + null + } + + {data.description} + + {/*data.tags !== undefined && data.tags !== null && data.tags.length > 0 ? + data.tags.map((tag, index) => { + return ( + { + }} + variant="outlined" + color="primary" + /> + ) + }) + : null */} +
+ {data.read === false ? + + : null} + +
{ + }} + > + {image} +
+
+
+
+ ) + } + + + + const notificationMenu = + + { + setAnchorEl(event.currentTarget); + }}> + + + + + { + handleClose() + }} + > + +
+ + Your Notifications ({notifications.length}) + + {notifications.length > 1 ? + + : null} +
+ + Notifications are made by Shuffle to help you discover issues or improvements. + +
+ {notifications.map((data, index) => { + return ( + + ) + })} +
+
+ + const handleClickChangeOrg = (orgId) => { + // Don't really care about the logout + //name: org.name, + //orgId = "asd" + const data = { + org_id: orgId, + } + + localStorage.setItem("globalUrl", "") + localStorage.setItem("getting_started_sidebar", "open"); + + fetch(`${globalUrl}/api/v1/orgs/${orgId}/change`, { + mode: 'cors', + credentials: 'include', + crossDomain: true, + method: 'POST', + body: JSON.stringify(data), + withCredentials: true, + headers: { + 'Content-Type': 'application/json; charset=utf-8', + }, + }) + .then(function(response) { + if (response.status !== 200) { + console.log("Error in response") + } + + return response.json(); + }).then(function(responseJson) { + if (responseJson.success === true) { + if (responseJson.region_url !== undefined && responseJson.region_url !== null && responseJson.region_url.length > 0) { + console.log("Region Change: ", responseJson.region_url) + localStorage.setItem("globalUrl", responseJson.region_url) + //globalUrl = responseJson.region_url + } + + setTimeout(() => { + window.location.reload() + }, 2000) + alert.success("Successfully changed active organization - refreshing!") + } else { + alert.error("Failed changing org: ", responseJson.reason) + } + }) + .catch(error => { + console.log("error changing: ", error) + //removeCookie("session_token", {path: "/"}) + }) + } + + const supportMenu = + + + + {}}> + Discord Community Join + + + + + + // Should be based on some path + const parsedAvatar = userdata.avatar !== undefined && userdata.avatar !== null && userdata.avatar.length > 0 ? userdata.avatar : "" + + const avatarMenu = + + { + setAnchorElAvatar(event.currentTarget); + }}> + + + { + handleClose() + }} + > + + { + handleClose(); + }} + > + Admin + + + + + { + handleClose(); + }} + > + About + + + {/* + + { + handleClose(); + }} + > + Get Started + + + */} + + { + handleClose(); + }} + > + Use Cases + + + + + { + handleClose() + }}> + Creator page + + + + { + handleClose() + }}> + Settings + + + + { + handleClickLogout() + event.preventDefault() + handleClose() + }}> +  Logout + + + + + const listItemStyle = { + textAlign: "center", + marginTop: "auto", + marginBottom: "auto", + } + + // Handle top bar or something + const loginTextBrowser = !isLoggedIn ? +
+
+ + + + + + + + + + + + + {/* + + + + + + */} + {isCloud ? + + + + + + : null} + {isCloud ? + + + + + + : + + + + + + } + + +
+
+ +
+
+ + + + + + + + {isCloud ? + + + + + + : null} + + {/* + + {supportMenu} + + */} + + {/* + + + + + + */} + +
+
+ : +
+
+
+ + + +
+ + + logo + {/* + + */} + + +
+ +
+ + +
+ {/* + + */} + Workflows +
+ +
+ + +
+ {/* + + */} + Apps +
+ +
+ {/* + + +
Dashboard
+ +
+ */} + + +
+ {/* + + */} + Docs +
+ +
+ {/* + + +
+ + Pricing +
+ +
+ */} + {/* + + +
Configure
+ +
+ */} +
+
+
+ +
+
+ + + {avatarMenu} + {notificationMenu} + {/*supportMenu*/} + {logoCheck} + + + {/* + + + + + + */} + + {/* + + + + + + */} + + {/*userdata.app_execution_limit !== 5000 && userdata.app_execution_limit !== 10000 ? + userdata === undefined || userdata.orgs === undefined || userdata.orgs === null || userdata.orgs.length > 1 ? null : + + + + + + : null*/} + + + + {userdata === undefined || userdata.orgs === undefined || userdata.orgs === null || userdata.orgs.length <= 1 ? + null + : + + + + } + + {userdata === undefined || userdata.app_execution_limit === undefined || userdata.app_execution_usage === undefined || userdata.app_execution_usage < 1000 ? + null + : + +
{ + if (window.drift !== undefined) { + window.drift.api.startInteraction({ interactionId: 326905 }) + } else { + console.log("Couldn't find drift in window.drift and not .drift-open-chat with querySelector: ", window.drift) + } + }}> + + + {(userdata.app_execution_usage/userdata.app_execution_limit*100).toFixed(0)}% + + + +
+
+ } + +
+
+
+
+ + const loginTextMobile = !isLoggedIn ? +
+ + + +
+ + + logo + {/**/} + + +
+ +
+ + +
+ About +
+ +
+ + + + + + + + + + +
+
+ : +
+
+ + + +
+ + + + + +
+ +
+ + + + + + + + + + +
+
+
+ + +
+ Logout +
+
+ {logoCheck} + +
+
+
+ + // + const loadedCheck = +
+ + {loginTextBrowser} + + + {loginTextMobile} + +
+ //
+ return ( +
+ {loadedCheck} +
+ ) +} + +export default Header; diff --git a/frontend/src/components/OrgHeader.jsx b/frontend/src/components/OrgHeader.jsx new file mode 100644 index 00000000..2f1b765e --- /dev/null +++ b/frontend/src/components/OrgHeader.jsx @@ -0,0 +1,272 @@ +import React, { useEffect } from "react"; +import { makeStyles } from "@material-ui/styles"; +import { useTheme } from "@material-ui/core/styles"; + +import Tooltip from "@material-ui/core/Tooltip"; +import Grid from "@material-ui/core/Grid"; +import Button from "@material-ui/core/Button"; +import TextField from "@material-ui/core/TextField"; +import Typography from "@material-ui/core/Typography"; +import { useAlert } from "react-alert"; +import IconButton from "@material-ui/core/IconButton"; +import ExpandLessIcon from "@material-ui/icons/ExpandLess"; +import ExpandMoreIcon from "@material-ui/icons/ExpandMore"; +import SaveIcon from "@material-ui/icons/Save"; + +const useStyles = makeStyles({ + notchedOutline: { + borderColor: "#f85a3e !important", + }, +}); + +const defaultImage = + "data:image/png;base64,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"; + +const OrgHeader = (props) => { + const { + userdata, + selectedOrganization, + setSelectedOrganization, + globalUrl, + isCloud, + adminTab, + handleEditOrg, + } = props; + + const theme = useTheme(); + const alert = useAlert(); + const classes = useStyles(); + + var upload = ""; + const defaultBranch = "master"; + const [orgName, setOrgName] = React.useState(selectedOrganization.name); + const [orgDescription, setOrgDescription] = React.useState( + selectedOrganization.description + ); + + + const [file, setFile] = React.useState(""); + const [fileBase64, setFileBase64] = React.useState( + selectedOrganization.image + ); + const [expanded, setExpanded] = React.useState(false); + + if (file !== "") { + const img = document.getElementById("logo"); + var canvas = document.createElement("canvas"); + canvas.width = 174; + canvas.height = 174; + var ctx = canvas.getContext("2d"); + + img.onload = function () { + // img, x, y, width, height + //ctx.drawImage(img, 174, 174) + //console.log("IMG natural: ", img.naturalWidth, img.naturalHeight) + //ctx.drawImage(img, 0, 0, 174, 174) + ctx.drawImage( + img, + 0, + 0, + img.width, + img.height, + 0, + 0, + canvas.width, + canvas.height + ); + + const canvasUrl = canvas.toDataURL(); + if (canvasUrl !== fileBase64) { + setFileBase64(canvasUrl); + selectedOrganization.image = canvasUrl; + setSelectedOrganization(selectedOrganization); + } + }; + } + + + var image = ""; + const editHeaderImage = (event) => { + const file = event.target.value; + const actualFile = event.target.files[0]; + const fileObject = URL.createObjectURL(actualFile); + setFile(fileObject); + }; + + //console.log("USER: ", userdata) + const orgSaveButton = ( + + + + ); + + var imageData = file.length > 0 ? file : fileBase64; + imageData = + imageData === undefined || imageData.length === 0 + ? defaultImage + : imageData; + const imageInfo = ( + + ); + + return ( +
+
+ +
0 + ? null + : "1px solid #f85a3e", + cursor: "pointer", + backgroundColor: + imageData !== undefined && imageData.length > 0 + ? null + : theme.palette.inputColor, + maxWidth: 174, + maxHeight: 174, + }} + onClick={() => { + upload.click(); + }} + > + (upload = ref)} + onChange={editHeaderImage} + /> + {imageInfo} +
+
+
+
+ Name + { + const invalid = ["#", ":", "."]; + for (var key in invalid) { + if (e.target.value.includes(invalid[key])) { + alert.error("Can't use " + invalid[key] + " in name"); + return; + } + } + + if (e.target.value.length > 100) { + alert.error("Choose a shorter name."); + return; + } + + setOrgName(e.target.value); + }} + color="primary" + InputProps={{ + style: { + color: "white", + height: "50px", + fontSize: "1em", + }, + classes: { + notchedOutline: classes.notchedOutline, + }, + }} + /> +
+ Description +
+ { + setOrgDescription(e.target.value); + }} + InputProps={{ + classes: { + notchedOutline: classes.notchedOutline, + }, + style: { + color: "white", + }, + }} + /> +
+ {orgSaveButton} +
+
+
+
+
+ ); +}; + +export default OrgHeader; diff --git a/frontend/src/components/PaperComponent.jsx b/frontend/src/components/PaperComponent.jsx new file mode 100644 index 00000000..76fb56ef --- /dev/null +++ b/frontend/src/components/PaperComponent.jsx @@ -0,0 +1,19 @@ +import React, {useState, useEffect, useLayoutEffect} from 'react'; + +import Draggable from "react-draggable"; +import { + Paper +} from "@material-ui/core"; + +const PaperComponent = (props) => { + return ( + + + + ) +} + +export default PaperComponent; diff --git a/frontend/src/components/Priorities.jsx b/frontend/src/components/Priorities.jsx new file mode 100644 index 00000000..88bea907 --- /dev/null +++ b/frontend/src/components/Priorities.jsx @@ -0,0 +1,91 @@ +import React, { useState, useEffect } from "react"; + +import theme from "../theme.jsx"; +import { + Paper, + Typography, + Divider, + Button, + Grid, + Card, + Switch, +} from "@material-ui/core"; + +import Priority from "../components/Priority.jsx"; +import { useAlert } from "react-alert"; + +const Priorities = (props) => { + const { globalUrl, userdata, serverside, billingInfo, stripeKey, checkLogin, } = props; + const [showDismissed, setShowDismissed] = React.useState(false); + const [showRead, setShowRead] = React.useState(false); + + if (userdata === undefined || userdata === null) { + return + } + + return ( +
+

Suggestions

+ + Suggestions are tasks identified by Shuffle to help you discover ways to protect your and customers' company. These range from simple configurations in Shuffle to Usecases you may have missed.  + + Learn more + + +
+ { + setShowDismissed(!showDismissed); + }} + />  Show dismissed + {userdata.priorities === null || userdata.priorities === undefined || userdata.priorities.length === 0 ? + + No Suggestions found + + : + userdata.priorities.map((priority, index) => { + if (showDismissed === false && priority.active === false) { + return null + } + + return ( + + ) + }) + } + +

Notifications

+ + Notifications help you find potential problems with your workflows and apps.  + + Learn more + + +
+ { + setShowRead(!showRead); + }} + />  Show read +
+ ) +} + +export default Priorities; diff --git a/frontend/src/components/Priority.jsx b/frontend/src/components/Priority.jsx new file mode 100644 index 00000000..0123b37d --- /dev/null +++ b/frontend/src/components/Priority.jsx @@ -0,0 +1,128 @@ +import React, { useState, useEffect } from "react"; + +import theme from "../theme.jsx"; +import { useNavigate, Link } from "react-router-dom"; +import { + Paper, + Typography, + Divider, + Button, + Grid, + Card, +} from "@material-ui/core"; + +// import magic wand icon from material ui icons +import { + AutoFixHigh as AutoFixHighIcon, + ArrowForward as ArrowForwardIcon, +} from '@mui/icons-material'; +import { useAlert } from "react-alert"; + +const Priority = (props) => { + const { globalUrl, userdata, serverside, priority, checkLogin, } = props; + + + let navigate = useNavigate(); + const changeRecommendation = (recommendation, action) => { + const data = { + action: action, + name: recommendation.name, + }; + + fetch(`${globalUrl}/api/v1/recommendations/modify`, { + mode: "cors", + method: "POST", + body: JSON.stringify(data), + credentials: "include", + crossDomain: true, + withCredentials: true, + headers: { + "Content-Type": "application/json; charset=utf-8", + }, + }) + .then((response) => { + if (response.status === 200) { + } else { + } + + return response.json(); + }) + .then((responseJson) => { + if (responseJson.success === true) { + if (checkLogin !== undefined) { + checkLogin() + } + } else { + if (responseJson.success === false && responseJson.reason !== undefined) { + alert.error("Failed change recommendation: ", responseJson.reason) + } else { + alert.error("Failed change recommendation"); + } + } + }) + .catch((error) => { + alert.info("Failed dismissing alert. Please contact support@shuffler.io if this persists."); + }); + } + + + return ( +
+
+ + {priority.type === "usecase" || priority.type == "apps" ? : null} + + {priority.name} + + + {priority.type === "usecase" && priority.description.includes("&") ? + + {priority.name} + + {priority.description.split("&")[0]} + + + {priority.description.split("&").length > 3 ? + + + {priority.name+"2"} + + {priority.description.split("&")[2]} + + + : null} + + + : + + {priority.description} + + } +
+
+ + {priority.active === true ? + + : null } +
+
+ ) +} + +export default Priority; diff --git a/frontend/src/components/RenderCytoscape.jsx b/frontend/src/components/RenderCytoscape.jsx new file mode 100644 index 00000000..c112b448 --- /dev/null +++ b/frontend/src/components/RenderCytoscape.jsx @@ -0,0 +1,157 @@ +import React, { useState, useEffect, useLayoutEffect } from "react"; +import * as cytoscape from "cytoscape"; +import CytoscapeComponent from "react-cytoscapejs"; +import cystyle from "../defaultCytoscapeStyle.jsx"; + +const surfaceColor = "#27292D"; +const CytoscapeWrapper = (props) => { + const { globalUrl, inworkflow } = props; + + const [elements, setElements] = useState([]); + const [workflow, setWorkflow] = useState(inworkflow); + const [cy, setCy] = React.useState(); + const bodyWidth = 200; + const bodyHeight = 150; + + const setupGraph = () => { + const actions = workflow.actions.map((action) => { + const node = {}; + node.position = action.position; + node.data = action; + + node.data._id = action["id"]; + node.data.type = "ACTION"; + node.isStartNode = action["id"] === workflow.start; + + var example = ""; + if ( + action.example !== undefined && + action.example !== null && + action.example.length > 0 + ) { + example = action.example; + } + + node.data.example = example; + return node; + }); + + const triggers = workflow.triggers.map((trigger) => { + const node = {}; + node.position = trigger.position; + node.data = trigger; + + node.data._id = trigger["id"]; + node.data.type = "TRIGGER"; + + return node; + }); + + // FIXME - tmp branch update + var insertedNodes = [].concat(actions, triggers); + const edges = workflow.branches.map((branch, index) => { + //workflow.branches[index].conditions = [{ + + const edge = {}; + var conditions = workflow.branches[index].conditions; + if (conditions === undefined || conditions === null) { + conditions = []; + } + + var label = ""; + if (conditions.length === 1) { + label = conditions.length + " condition"; + } else if (conditions.length > 1) { + label = conditions.length + " conditions"; + } + + edge.data = { + id: branch.id, + _id: branch.id, + source: branch.source_id, + target: branch.destination_id, + label: label, + conditions: conditions, + hasErrors: branch.has_errors, + }; + + // This is an attempt at prettier edges. The numbers are weird to work with. + /* + //http://manual.graphspace.org/projects/graphspace-python/en/latest/demos/edge-types.html + const sourcenode = actions.find(node => node.data._id === branch.source_id) + const destinationnode = actions.find(node => node.data._id === branch.destination_id) + if (sourcenode !== undefined && destinationnode !== undefined && branch.source_id !== branch.destination_id) { + //node.data._id = action["id"] + console.log("SOURCE: ", sourcenode.position) + console.log("DESTINATIONNODE: ", destinationnode.position) + + var opposite = true + if (sourcenode.position.x > destinationnode.position.x) { + opposite = false + } else { + opposite = true + } + + edge.style = { + 'control-point-distance': opposite ? ["25%", "-75%"] : ["-10%", "90%"], + 'control-point-weight': ['0.3', '0.7'], + } + } + */ + + return edge; + }); + + setWorkflow(workflow); + + // Verifies if a branch is valid and skips others + var newedges = []; + for (var key in edges) { + var item = edges[key]; + + const sourcecheck = insertedNodes.find( + (data) => data.data.id === item.data.source + ); + const destcheck = insertedNodes.find( + (data) => data.data.id === item.data.target + ); + if (sourcecheck === undefined || destcheck === undefined) { + continue; + } + + newedges.push(item); + } + + insertedNodes = insertedNodes.concat(newedges); + setElements(insertedNodes); + }; + + if (elements.length === 0) { + setupGraph(); + } + + return ( + { + // FIXME: There's something specific loading when + // you do the first hover of a node. Why is this different? + //console.log("CY: ", incy) + setCy(incy); + }} + /> + ); +}; + +export default CytoscapeWrapper; diff --git a/frontend/src/components/Searchfield.jsx b/frontend/src/components/Searchfield.jsx new file mode 100644 index 00000000..37e44bd5 --- /dev/null +++ b/frontend/src/components/Searchfield.jsx @@ -0,0 +1,652 @@ +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 aa from 'search-insights' +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 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 SearchField = props => { + const { serverside, userdata } = 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 ( +