first sync
Some checks failed
Deployment Verification / deploy-and-test (push) Failing after 29s

This commit is contained in:
2025-03-04 07:59:21 +01:00
parent 9cdcf486b6
commit 506716e703
1450 changed files with 577316 additions and 62 deletions

View File

@ -0,0 +1,586 @@
import React, { useState, useEffect } from "react";
import { Link, Route, Routes, BrowserRouter, useNavigate } from "react-router-dom";
import { CookiesProvider } from "react-cookie";
import { removeCookies, useCookies } from "react-cookie";
import Workflows from "./views/Workflows";
import GettingStarted from "./views/GettingStarted";
import AngularWorkflow from "./views/AngularWorkflow.jsx";
import Header from "./components/Header.jsx";
import theme from "./theme";
import Apps from "./views/Apps";
import AppCreator from "./views/AppCreator";
import Welcome from "./views/Welcome.jsx";
import Dashboard from "./views/Dashboard.jsx";
import DashboardView from "./views/DashboardViews.jsx";
import AdminSetup from "./views/AdminSetup";
import Admin from "./views/Admin";
import Docs from "./views/Docs.jsx";
//import Introduction from "./views/Introduction";
import SetAuthentication from "./views/SetAuthentication";
import SetAuthenticationSSO from "./views/SetAuthenticationSSO";
import Search from "./views/Search.jsx";
import RunWorkflow from "./views/RunWorkflow.jsx";
import LoginPage from "./views/LoginPage";
import SettingsPage from "./views/SettingsPage";
import KeepAlive from "./views/KeepAlive.jsx";
import { ThemeProvider } from "@mui/material/styles";
import CssBaseline from '@mui/material/CssBaseline';
import UpdateAuthentication from "./views/UpdateAuthentication.jsx";
import FrameworkWrapper from "./views/FrameworkWrapper.jsx";
import ScrollToTop from "./components/ScrollToTop";
import AlertTemplate from "./components/AlertTemplate";
import { useAlert, positions, Provider } from "react-alert";
import { isMobile } from "react-device-detect";
import { ToastContainer, toast } from 'react-toastify';
import 'react-toastify/dist/ReactToastify.css';
import Drift from "react-driftjs";
// Production - backend proxy forwarding in nginx
var globalUrl = window.location.origin;
// CORS used for testing purposes. Should only happen with specific port and http
if (window.location.port === "3000") {
globalUrl = "http://localhost:5001";
//globalUrl = "http://localhost:5002"
}
// Development on Github Codespaces
if (globalUrl.includes("app.github.dev")) {
//globalUrl = globalUrl.replace("3000", "5001")
globalUrl = "https://frikky-shuffle-5gvr4xx62w64-5001.preview.app.github.dev"
}
//console.log("global: ", globalUrl)
const App = (message, props) => {
const [userdata, setUserData] = useState({});
const [notifications, setNotifications] = useState([])
const [cookies, setCookie, removeCookie] = useCookies([])
const [isLoggedIn, setIsLoggedIn] = useState(false)
const [dataset, setDataset] = useState(false)
const [isLoaded, setIsLoaded] = useState(false)
const [curpath, setCurpath] = useState(typeof window === "undefined" || window.location === undefined ? "" : window.location.pathname)
useEffect(() => {
if (dataset === false) {
getUserNotifications();
checkLogin();
setDataset(true);
}
}, []);
if (
isLoaded &&
!isLoggedIn &&
!window.location.pathname.startsWith("/login") &&
!window.location.pathname.startsWith("/docs") &&
!window.location.pathname.startsWith("/support") &&
!window.location.pathname.startsWith("/detectionframework") &&
!window.location.pathname.startsWith("/appframework") &&
!window.location.pathname.startsWith("/adminsetup") &&
!window.location.pathname.startsWith("/usecases")
) {
window.location = "/login";
}
const getUserNotifications = () => {
fetch(`${globalUrl}/api/v1/users/notifications`, {
credentials: "include",
headers: {
"Content-Type": "application/json",
},
cors: "cors",
})
.then((response) => response.json())
.then((responseJson) => {
if (
responseJson.success === true &&
responseJson.notifications !== null &&
responseJson.notifications !== undefined &&
responseJson.notifications.length > 0
) {
//console.log("RESP: ", responseJson)
setNotifications(responseJson.notifications);
}
})
.catch((error) => {
console.log("Failed getting notifications for user: ", error);
});
};
const checkLogin = () => {
var baseurl = globalUrl;
fetch(`${globalUrl}/api/v1/getinfo`, {
credentials: "include",
headers: {
"Content-Type": "application/json",
},
})
.then((response) => response.json())
.then((responseJson) => {
var userInfo = {};
if (responseJson.success === true) {
//console.log("USER: ", responseJson);
userInfo = responseJson;
setIsLoggedIn(true);
//console.log("Cookies: ", cookies)
// Updating cookie every request
for (var key in responseJson["cookies"]) {
setCookie(
responseJson["cookies"][key].key,
responseJson["cookies"][key].value,
{ path: "/" }
);
}
}
// Handling Ethereum update
//console.log("USER: ", userInfo)
setUserData(userInfo);
setIsLoaded(true);
})
.catch((error) => {
setIsLoaded(true);
});
};
// Dumb for content load (per now), but good for making the site not suddenly reload parts (ajax thingies)
const options = {
timeout: 9000,
position: positions.BOTTOM_LEFT,
};
const handleFirstInteraction = (event) => {
console.log("First interaction: ", event)
}
const includedData =
<div
style={{
backgroundColor: theme.palette.backgroundColor,
color: "rgba(255, 255, 255, 0.65)",
minHeight: "100vh",
}}
>
<ScrollToTop
getUserNotifications={getUserNotifications}
curpath={curpath}
setCurpath={setCurpath}
/>
{!isLoaded ? null :
userdata.chat_disabled === true ? null :
<Drift
appId="zfk9i7w3yizf"
attributes={{
name: userdata.username === undefined || userdata.username === null ? "OSS user" : `OSS ${userdata.username}`,
}}
eventHandlers={[
{
event: "conversation:firstInteraction",
function: handleFirstInteraction
},
]}
/>
}
<Header
notifications={notifications}
setNotifications={setNotifications}
checkLogin={checkLogin}
cookies={cookies}
removeCookie={removeCookie}
isLoaded={isLoaded}
globalUrl={globalUrl}
setIsLoggedIn={setIsLoggedIn}
isLoggedIn={isLoggedIn}
userdata={userdata}
{...props}
/>
{/*
<div style={{ height: 60 }} />
*/}
<Routes>
<Route
exact
path="/login"
element={
<LoginPage
isLoggedIn={isLoggedIn}
setIsLoggedIn={setIsLoggedIn}
register={true}
isLoaded={isLoaded}
globalUrl={globalUrl}
setCookie={setCookie}
cookies={cookies}
checkLogin={checkLogin}
{...props}
/>
}
/>
<Route
exact
path="/admin"
element={
<Admin
userdata={userdata}
isLoggedIn={isLoggedIn}
setIsLoggedIn={setIsLoggedIn}
register={true}
isLoaded={isLoaded}
globalUrl={globalUrl}
setCookie={setCookie}
cookies={cookies}
checkLogin={checkLogin}
{...props}
/>
}
/>
<Route exact path="/search" element={<Search serverside={false} isLoaded={isLoaded} userdata={userdata} globalUrl={globalUrl} surfaceColor={theme.palette.surfaceColor} inputColor={theme.palette.inputColor} {...props} /> } />
<Route
exact
path="/admin/:key"
element={
<Admin
isLoggedIn={isLoggedIn}
setIsLoggedIn={setIsLoggedIn}
register={true}
isLoaded={isLoaded}
globalUrl={globalUrl}
setCookie={setCookie}
cookies={cookies}
{...props}
/>
}
/>
{userdata.id !== undefined ? (
<Route
exact
path="/settings"
element={
<SettingsPage
isLoaded={isLoaded}
setUserData={setUserData}
userdata={userdata}
globalUrl={globalUrl}
{...props}
/>
}
/>
) : null}
<Route
exact
path="/AdminSetup"
element={
<AdminSetup
isLoaded={isLoaded}
userdata={userdata}
globalUrl={globalUrl}
{...props}
/>
}
/>
<Route
exact
path="/detectionframework"
element={
<FrameworkWrapper
selectedOption={"Draw"}
showOptions={false}
isLoaded={isLoaded}
isLoggedIn={isLoggedIn}
globalUrl={globalUrl}
{...props}
/>
}
/>
<Route
exact
path="/app"
element={
<FrameworkWrapper
selectedOption={"Draw"}
showOptions={false}
isLoaded={isLoaded}
isLoggedIn={isLoggedIn}
globalUrl={globalUrl}
{...props}
/>
}
/>
<Route
exact
path="/usecases"
element={
<Dashboard
userdata={userdata}
isLoaded={isLoaded}
isLoggedIn={isLoggedIn}
globalUrl={globalUrl}
{...props}
/>
}
/>
<Route
exact
path="/apps/new"
element={
<AppCreator
isLoaded={isLoaded}
isLoggedIn={isLoggedIn}
globalUrl={globalUrl}
{...props}
/>
}
/>
<Route exact path="/apps/authentication" element={<UpdateAuthentication serverside={false} userdata={userdata} isLoggedIn={isLoggedIn} setIsLoggedIn={setIsLoggedIn} register={true} isLoaded={isLoaded} globalUrl={globalUrl} setCookie={setCookie} cookies={cookies} {...props} />} />
<Route
exact
path="/apps"
element={
<Apps
isLoaded={isLoaded}
isLoggedIn={isLoggedIn}
globalUrl={globalUrl}
userdata={userdata}
{...props}
/>
}
/>
<Route
exact
path="/apps/edit/:appid"
element={
<AppCreator
isLoaded={isLoaded}
isLoggedIn={isLoggedIn}
globalUrl={globalUrl}
{...props}
/>
}
/>
<Route
exact
path="/workflows"
element={
<Workflows
checkLogin={checkLogin}
cookies={cookies}
removeCookie={removeCookie}
isLoaded={isLoaded}
isLoggedIn={isLoggedIn}
globalUrl={globalUrl}
cookies={cookies}
userdata={userdata}
{...props}
/>
}
/>
<Route
exact
path="/getting-started"
element={
<GettingStarted
cookies={cookies}
removeCookie={removeCookie}
isLoaded={isLoaded}
isLoggedIn={isLoggedIn}
globalUrl={globalUrl}
cookies={cookies}
userdata={userdata}
{...props}
/>
}
/>
<Route
exact
path="/workflows/:key"
element={
<AngularWorkflow
alert={alert}
userdata={userdata}
globalUrl={globalUrl}
isLoaded={isLoaded}
isLoggedIn={isLoggedIn}
{...props}
/>
}
/>
<Route exact path="/workflows/:key/run" element={<RunWorkflow userdata={userdata} globalUrl={globalUrl} isLoaded={isLoaded} isLoggedIn={isLoggedIn} surfaceColor={theme.palette.surfaceColor} inputColor={theme.palette.inputColor}{...props} /> } />
<Route exact path="/workflows/:key/execute" element={<RunWorkflow userdata={userdata} globalUrl={globalUrl} isLoaded={isLoaded} isLoggedIn={isLoggedIn} surfaceColor={theme.palette.surfaceColor} inputColor={theme.palette.inputColor}{...props} /> } />
<Route
exact
path="/docs/:key"
element={
<Docs
isMobile={isMobile}
isLoaded={isLoaded}
globalUrl={globalUrl}
{...props}
/>
}
/>
<Route
exact
path="/docs"
element={
//navigate(`/docs/about`)
<Docs
isMobile={isMobile}
isLoaded={isLoaded}
globalUrl={globalUrl}
{...props}
/>
}
/>
<Route
exact
path="/support"
element={
//navigate(`/docs/about`)
<Docs
isMobile={isMobile}
isLoaded={isLoaded}
globalUrl={globalUrl}
{...props}
/>
}
/>
<Route
exact
path="/set_authentication"
element={
<SetAuthentication
userdata={userdata}
isLoggedIn={isLoggedIn}
setIsLoggedIn={setIsLoggedIn}
register={true}
isLoaded={isLoaded}
globalUrl={globalUrl}
setCookie={setCookie}
cookies={cookies}
{...props}
/>
}
/>
<Route
exact
path="/login_sso"
element={
<SetAuthenticationSSO
userdata={userdata}
isLoggedIn={isLoggedIn}
setIsLoggedIn={setIsLoggedIn}
register={true}
isLoaded={isLoaded}
globalUrl={globalUrl}
setCookie={setCookie}
cookies={cookies}
{...props}
/>
}
/>
<Route
exact
path="/keepalive"
element={
<KeepAlive
isLoggedIn={isLoggedIn}
setIsLoggedIn={setIsLoggedIn}
register={true}
isLoaded={isLoaded}
globalUrl={globalUrl}
setCookie={setCookie}
cookies={cookies}
{...props}
/>
}
/>
<Route
exact
path="/dashboards"
element={
<DashboardView
isLoaded={isLoaded}
isLoggedIn={isLoggedIn}
globalUrl={globalUrl}
{...props}
/>
}
/>
<Route
exact
path="/welcome"
element={
<Welcome
cookies={cookies}
removeCookie={removeCookie}
isLoaded={isLoaded}
isLoggedIn={isLoggedIn}
globalUrl={globalUrl}
cookies={cookies}
userdata={userdata}
checkLogin={checkLogin}
{...props}
/>
}
/>
<Route
exact
path="/"
element={
<LoginPage
isLoggedIn={isLoggedIn}
setIsLoggedIn={setIsLoggedIn}
register={true}
isLoaded={isLoaded}
globalUrl={globalUrl}
setCookie={setCookie}
cookies={cookies}
{...props}
/>
}
/>
</Routes>
</div>
return (
<ThemeProvider theme={theme}>
<CssBaseline />
<CookiesProvider>
<BrowserRouter>
<Provider template={AlertTemplate} {...options}>
{includedData}
</Provider>
</BrowserRouter>
<ToastContainer
position="bottom-center"
autoClose={5000}
hideProgressBar={false}
newestOnTop={false}
closeOnClick
rtl={false}
pauseOnFocusLoss
draggable
pauseOnHover
theme="dark"
/>
</CookiesProvider>
</ThemeProvider>
);
};
export default App;

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,6 @@
const data = [
{ name: "cloud", type: "cloud" },
{ name: "onprem", type: "onprem" },
];
export default data;

View File

@ -0,0 +1,29 @@
const Data = {
src: {
name: "Get Tickets",
description: "Get tickets",
outputparameters: [
{
name: "SymptomDescription",
schema: { type: "string" },
},
{ name: "DetailedDescription", schema: { type: "string" } },
{ name: "EventSource", schema: { type: "string" } },
],
},
dst: {
name: "Create alert",
description: "Create alert in TheHive",
inputparameters: [
{
name: "title",
required: true,
schema: { type: "string" },
},
{ name: "description", required: true, schema: { type: "string" } },
{ name: "source", required: true, schema: { type: "string" } },
],
},
};
export default Data;

View File

@ -0,0 +1,15 @@
const data = {
id: "8ccf0bec1fde018771ab685d2a40bd52",
info: {
url: "",
name: "testing",
description: "wut",
},
transforms: {},
actions: {},
type: "webhook",
status: "uninitialized",
running: false,
};
export default data;

View File

@ -0,0 +1,169 @@
const data = {
actions: [
{
app_name: "hello_world",
app_version: "1.0.0",
errors: null,
id: "70574332-da82-cf17-c723-75fa7b8493c2",
is_valid: true,
label: "hello_world",
environment: "onprem",
name: "hello_world",
parameters: null,
position: { x: 353.7438792397648, y: 260.6717930890377 },
priority: 0,
},
{
app_name: "hello_world",
app_version: "1.0.0",
errors: null,
id: "30522433-56ed-53c3-575d-766e282e1d3e",
is_valid: true,
label: "random_number",
environment: "cloud",
name: "random_number",
parameters: null,
position: { x: 458.30040774503794, y: 104.27580103487651 },
priority: 0,
},
{
app_name: "hello_world",
app_version: "1.0.0",
errors: null,
id: "5b7ac5b5-9514-02b9-ebe0-998c0843b104",
is_valid: false,
label: "hello_world_2",
environment: "onprem",
name: "hello_world",
parameters: null,
position: { x: 414.7256019053981, y: -140.46450482659628 },
priority: 0,
},
{
app_name: "hello_world",
app_version: "1.0.0",
errors: null,
id: "7e6e7a19-4636-cebc-91c4-052a3769a18b",
is_valid: true,
label: "hello_world_3",
environment: "cloud",
name: "hello_world",
parameters: null,
position: { x: 83.59752786243806, y: 50.232317715020734 },
priority: 0,
},
{
app_name: "hello_world",
app_version: "1.0.0",
errors: null,
id: "edbf927d-5a00-2405-28ed-47982cdf5110",
is_valid: true,
label: "hello_world_4",
environment: "cloud",
name: "hello_world",
parameters: null,
position: { x: -147.30681300186404, y: 89.16690830150289 },
priority: 0,
},
{
app_name: "hello_world",
app_version: "1.0.0",
errors: null,
id: "4844a855-1e2b-669d-fc72-5f398321ac5d",
is_valid: false,
label: "hello_world_5",
environment: "onprem",
name: "hello_world",
parameters: null,
position: { x: 130.24982593523967, y: 233.8325632286361 },
priority: 0,
},
{
app_name: "hello_world",
app_version: "1.0.0",
errors: null,
id: "6d1d3f8a-1ac9-3db4-0e0f-2fe32e9d3c09",
is_valid: true,
label: "hello_world_6",
environment: "cloud",
name: "hello_world",
parameters: null,
position: { x: 83.551088005629, y: -105.15867327274223 },
priority: 0,
},
{
app_name: "hello_world",
app_version: "1.0.0",
errors: null,
id: "469d8c2b-52ac-e397-9a29-becccd04aed8",
is_valid: true,
label: "hello_world_7",
environment: "cloud",
name: "hello_world",
parameters: null,
position: { x: 314.4987657226086, y: 10.167183586257954 },
priority: 0,
},
],
branches: [
{
destination_id: "30522433-56ed-53c3-575d-766e282e1d3e",
id: "4bcb9795-94e6-7d5f-2074-0d5b27784e0b",
source_id: "70574332-da82-cf17-c723-75fa7b8493c2",
},
{
destination_id: "5b7ac5b5-9514-02b9-ebe0-998c0843b104",
id: "fe0ab8e4-a535-61cd-3c09-8fd3d8e40769",
source_id: "30522433-56ed-53c3-575d-766e282e1d3e",
},
{
destination_id: "469d8c2b-52ac-e397-9a29-becccd04aed8",
id: "8b9ee9bc-b0ab-0bb6-af61-46d4594b2663",
source_id: "30522433-56ed-53c3-575d-766e282e1d3e",
},
{
destination_id: "6d1d3f8a-1ac9-3db4-0e0f-2fe32e9d3c09",
id: "c204d5ef-9cc1-d906-9988-86a624c57783",
source_id: "469d8c2b-52ac-e397-9a29-becccd04aed8",
},
{
destination_id: "6d1d3f8a-1ac9-3db4-0e0f-2fe32e9d3c09",
id: "1ffb3934-60ec-8f80-5cee-3ddc0a37fdb6",
source_id: "5b7ac5b5-9514-02b9-ebe0-998c0843b104",
},
{
destination_id: "edbf927d-5a00-2405-28ed-47982cdf5110",
id: "9c7fb048-9d0d-cb84-9ba0-be729af9b4d1",
source_id: "6d1d3f8a-1ac9-3db4-0e0f-2fe32e9d3c09",
},
{
destination_id: "edbf927d-5a00-2405-28ed-47982cdf5110",
id: "e3ab104e-fc8b-3af5-8daa-bfa57bcf9690",
source_id: "7e6e7a19-4636-cebc-91c4-052a3769a18b",
},
{
destination_id: "7e6e7a19-4636-cebc-91c4-052a3769a18b",
id: "b6626081-22dd-3af3-b899-480f60d886ca",
source_id: "30522433-56ed-53c3-575d-766e282e1d3e",
},
{
destination_id: "4844a855-1e2b-669d-fc72-5f398321ac5d",
id: "4275cf97-0447-bbda-0c80-ab20d389de1a",
source_id: "edbf927d-5a00-2405-28ed-47982cdf5110",
},
],
conditions: [],
triggers: [],
transforms: [],
description: "asd",
id: "2f299808-0f1b-4ae0-97fc-ac17483dfcf7",
id: "2f299808-0f1b-4ae0-97fc-ac17483dfcf7",
is_valid: true,
name: "test2",
start: "70574332-da82-cf17-c723-75fa7b8493c2",
owner: { username: "", id: "", orgs: "" },
execution_org: { name: "", org: "", users: null, id: "" },
workflow_variables: null,
};
export default data;

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 14 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 20 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 449 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 431 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

View File

@ -0,0 +1,26 @@
<svg width="200" height="200" xmlns="http://www.w3.org/2000/svg">
<!---->
<defs>
<linearGradient y2="0%" x2="100%" y1="0%" x1="0%" id="30c29011-a081-4741-b6bb-e06d8873e7b7" gradientTransform="rotate(25)">
<stop stop-color=" rgb(169, 37, 128)" offset="0%"/>
<stop stop-color=" rgb(247, 188, 0)" offset="100%"/>
</linearGradient>
</defs>
<!---->
<!---->
<!---->
<g>
<title>background</title>
<rect fill="none" id="canvas_background" height="202" width="202" y="-1" x="-1"/>
</g>
<g>
<title>Layer 1</title>
<g fill="url(#30c29011-a081-4741-b6bb-e06d8873e7b7)" transform="matrix(1.1414221157695137,0,0,1.1414221157695137,-10.343879888974278,-11.465698853151771) " id="1085c47b-b6bc-4e6d-b1f9-4301cfb34be7">
<switch transform="translate(2.6282968521118164,0) translate(0.0000033420565159758553,0) translate(-0.2776981592178345,0) translate(44.68110275268555,47.30940246582031) ">
<g id="svg_2">
<path id="svg_3" d="m89.141,35.617c-2.891,-12.765 -11.297,-21.212 -20.442,-25.253c11.371,10.018 21.846,29.405 8.814,47.069c-5.406,7.331 -16.217,10.228 -20.746,8.184c0,0 7.002,-1.487 11.357,-5.295c8.469,-8.017 11.299,-20.932 4.52,-32.673a25.839,25.839 0 0 0 -3.357,-4.575c-0.018,-0.021 -0.033,-0.042 -0.051,-0.062c-4.764,-5.683 -11.307,-9.075 -18.337,-10.116c-9.127,-1.715 -20.896,0.515 -29.71,8.666c-9.609,8.888 -12.721,20.391 -11.647,30.333c2.989,-14.858 14.542,-33.622 36.355,-31.171c9.053,1.019 16.965,8.932 17.461,13.877c0,0 -5.277,-8.281 -18.365,-8.281c-0.24,0.004 -0.747,0.024 -0.761,0.024l-0.167,0.01c-8.51,0.333 -16.783,4.784 -21.83,13.526a25.819,25.819 0 0 0 -2.284,5.195l-0.028,0.074c-2.539,6.968 -2.205,14.33 0.407,20.938c3.079,8.763 10.896,17.841 22.361,21.397c12.502,3.878 24.02,0.82 32.092,-5.079c-14.363,4.837 -36.389,4.216 -45.173,-15.899c-3.645,-8.35 -0.749,-19.159 3.287,-22.061c0,0 -4.534,8.71 2.012,20.044c4.482,7.484 12.617,12.658 22.949,12.658c0.498,0 4.488,-0.311 5.926,-0.633l0.08,-0.011c7.303,-1.286 13.512,-5.256 17.928,-10.822c6.05,-7.046 10.003,-18.356 7.349,-30.064z"/>
</g>
</switch>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 143 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 440 KiB

View File

@ -0,0 +1,3 @@
# resize: convert schedule.png -resize 100x100\> schedule100.png
# base64: - cat picture.png | base64 -w 0
# js insert: data:image/png;base64,<base64>

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

View File

@ -0,0 +1,427 @@
/*!
=========================================================
* Black Dashboard React v1.1.0
=========================================================
* Product Page: https://www.creative-tim.com/product/black-dashboard-react
* Copyright 2020 Creative Tim (https://www.creative-tim.com)
* Licensed under MIT (https://github.com/creativetimofficial/black-dashboard-react/blob/master/LICENSE.md)
* Coded by Creative Tim
=========================================================
* The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
*/
// ##############################
// // // Chart variables
// #############################
// chartExample1 and chartExample2 options
let chart1_2_options = {
maintainAspectRatio: false,
legend: {
display: false,
},
tooltips: {
backgroundColor: "#f5f5f5",
titleFontColor: "#333",
bodyFontColor: "#666",
bodySpacing: 4,
xPadding: 12,
mode: "nearest",
intersect: 0,
position: "nearest",
},
responsive: true,
scales: {
yAxes: [
{
barPercentage: 1.6,
gridLines: {
drawBorder: false,
color: "rgba(29,140,248,0.0)",
zeroLineColor: "transparent",
},
ticks: {
suggestedMin: 60,
suggestedMax: 125,
padding: 20,
fontColor: "#9a9a9a",
},
},
],
xAxes: [
{
barPercentage: 1.6,
gridLines: {
drawBorder: false,
color: "rgba(29,140,248,0.1)",
zeroLineColor: "transparent",
},
ticks: {
padding: 20,
fontColor: "#9a9a9a",
},
},
],
},
};
// #########################################
// // // used inside src/views/Dashboard.js
// #########################################
let chartExample1 = {
data1: (canvas) => {
let ctx = canvas.getContext("2d");
let gradientStroke = ctx.createLinearGradient(0, 230, 0, 50);
gradientStroke.addColorStop(1, "rgba(29,140,248,0.2)");
gradientStroke.addColorStop(0.4, "rgba(29,140,248,0.0)");
gradientStroke.addColorStop(0, "rgba(29,140,248,0)"); //blue colors
return {
labels: [
"JAN",
"FEB",
"MAR",
"APR",
"MAY",
"JUN",
"JUL",
"AUG",
"SEP",
"OCT",
"NOV",
"DEC",
],
datasets: [
{
label: "My First dataset",
fill: true,
backgroundColor: gradientStroke,
borderColor: "#1f8ef1",
borderWidth: 2,
borderDash: [],
borderDashOffset: 0.0,
pointBackgroundColor: "#1f8ef1",
pointBorderColor: "rgba(255,255,255,0)",
pointHoverBackgroundColor: "#1f8ef1",
pointBorderWidth: 20,
pointHoverRadius: 4,
pointHoverBorderWidth: 15,
pointRadius: 4,
data: [100, 70, 90, 70, 85, 60, 75, 60, 90, 80, 110, 100],
},
],
};
},
data2: (canvas) => {
let ctx = canvas.getContext("2d");
let gradientStroke = ctx.createLinearGradient(0, 230, 0, 50);
gradientStroke.addColorStop(1, "rgba(29,140,248,0.2)");
gradientStroke.addColorStop(0.4, "rgba(29,140,248,0.0)");
gradientStroke.addColorStop(0, "rgba(29,140,248,0)"); //blue colors
return {
labels: [
"JAN",
"FEB",
"MAR",
"APR",
"MAY",
"JUN",
"JUL",
"AUG",
"SEP",
"OCT",
"NOV",
"DEC",
],
datasets: [
{
label: "My First dataset",
fill: true,
backgroundColor: gradientStroke,
borderColor: "#1f8ef1",
borderWidth: 2,
borderDash: [],
borderDashOffset: 0.0,
pointBackgroundColor: "#1f8ef1",
pointBorderColor: "rgba(255,255,255,0)",
pointHoverBackgroundColor: "#1f8ef1",
pointBorderWidth: 20,
pointHoverRadius: 4,
pointHoverBorderWidth: 15,
pointRadius: 4,
data: [80, 120, 105, 110, 95, 105, 90, 100, 80, 95, 70, 120],
},
],
};
},
data3: (canvas) => {
let ctx = canvas.getContext("2d");
let gradientStroke = ctx.createLinearGradient(0, 230, 0, 50);
gradientStroke.addColorStop(1, "rgba(29,140,248,0.2)");
gradientStroke.addColorStop(0.4, "rgba(29,140,248,0.0)");
gradientStroke.addColorStop(0, "rgba(29,140,248,0)"); //blue colors
return {
labels: [
"JAN",
"FEB",
"MAR",
"APR",
"MAY",
"JUN",
"JUL",
"AUG",
"SEP",
"OCT",
"NOV",
"DEC",
],
datasets: [
{
label: "My First dataset",
fill: true,
backgroundColor: gradientStroke,
borderColor: "#1f8ef1",
borderWidth: 2,
borderDash: [],
borderDashOffset: 0.0,
pointBackgroundColor: "#1f8ef1",
pointBorderColor: "rgba(255,255,255,0)",
pointHoverBackgroundColor: "#1f8ef1",
pointBorderWidth: 20,
pointHoverRadius: 4,
pointHoverBorderWidth: 15,
pointRadius: 4,
data: [60, 80, 65, 130, 80, 105, 90, 130, 70, 115, 60, 130],
},
],
};
},
options: chart1_2_options,
};
// #########################################
// // // used inside src/views/Dashboard.js
// #########################################
let chartExample2 = {
data: (canvas) => {
let ctx = canvas.getContext("2d");
let gradientStroke = ctx.createLinearGradient(0, 230, 0, 50);
gradientStroke.addColorStop(1, "rgba(29,140,248,0.2)");
gradientStroke.addColorStop(0.4, "rgba(29,140,248,0.0)");
gradientStroke.addColorStop(0, "rgba(29,140,248,0)"); //blue colors
return {
labels: ["JUL", "AUG", "SEP", "OCT", "NOV", "DEC"],
datasets: [
{
label: "Data",
fill: true,
backgroundColor: gradientStroke,
borderColor: "#1f8ef1",
borderWidth: 2,
borderDash: [],
borderDashOffset: 0.0,
pointBackgroundColor: "#1f8ef1",
pointBorderColor: "rgba(255,255,255,0)",
pointHoverBackgroundColor: "#1f8ef1",
pointBorderWidth: 20,
pointHoverRadius: 4,
pointHoverBorderWidth: 15,
pointRadius: 4,
data: [80, 100, 70, 80, 120, 80],
},
],
};
},
options: chart1_2_options,
};
// #########################################
// // // used inside src/views/Dashboard.js
// #########################################
let chartExample3 = {
data: (canvas) => {
let ctx = canvas.getContext("2d");
let gradientStroke = ctx.createLinearGradient(0, 230, 0, 50);
gradientStroke.addColorStop(1, "rgba(72,72,176,0.1)");
gradientStroke.addColorStop(0.4, "rgba(72,72,176,0.0)");
gradientStroke.addColorStop(0, "rgba(119,52,169,0)"); //purple colors
return {
labels: ["USA", "GER", "AUS", "UK", "RO", "BR"],
datasets: [
{
label: "Countries",
fill: true,
backgroundColor: gradientStroke,
hoverBackgroundColor: gradientStroke,
borderColor: "#d048b6",
borderWidth: 2,
borderDash: [],
borderDashOffset: 0.0,
data: [53, 20, 10, 80, 100, 45],
},
],
};
},
options: {
maintainAspectRatio: false,
legend: {
display: false,
},
tooltips: {
backgroundColor: "#f5f5f5",
titleFontColor: "#333",
bodyFontColor: "#666",
bodySpacing: 4,
xPadding: 12,
mode: "nearest",
intersect: 0,
position: "nearest",
},
responsive: true,
scales: {
yAxes: [
{
gridLines: {
drawBorder: false,
color: "rgba(225,78,202,0.1)",
zeroLineColor: "transparent",
},
ticks: {
suggestedMin: 60,
suggestedMax: 120,
padding: 20,
fontColor: "#9e9e9e",
},
},
],
xAxes: [
{
gridLines: {
drawBorder: false,
color: "rgba(225,78,202,0.1)",
zeroLineColor: "transparent",
},
ticks: {
padding: 20,
fontColor: "#9e9e9e",
},
},
],
},
},
};
// #########################################
// // // used inside src/views/Dashboard.js
// #########################################
const chartExample4 = {
data: (canvas) => {
let ctx = canvas.getContext("2d");
let gradientStroke = ctx.createLinearGradient(0, 230, 0, 50);
gradientStroke.addColorStop(1, "rgba(66,134,121,0.15)");
gradientStroke.addColorStop(0.4, "rgba(66,134,121,0.0)"); //green colors
gradientStroke.addColorStop(0, "rgba(66,134,121,0)"); //green colors
return {
labels: ["JUL", "AUG", "SEP", "OCT", "NOV"],
datasets: [
{
label: "My First dataset",
fill: true,
backgroundColor: gradientStroke,
borderColor: "#00d6b4",
borderWidth: 2,
borderDash: [],
borderDashOffset: 0.0,
pointBackgroundColor: "#00d6b4",
pointBorderColor: "rgba(255,255,255,0)",
pointHoverBackgroundColor: "#00d6b4",
pointBorderWidth: 20,
pointHoverRadius: 4,
pointHoverBorderWidth: 15,
pointRadius: 4,
data: [90, 27, 60, 12, 80],
},
],
};
},
options: {
maintainAspectRatio: false,
legend: {
display: false,
},
tooltips: {
backgroundColor: "#f5f5f5",
titleFontColor: "#333",
bodyFontColor: "#666",
bodySpacing: 4,
xPadding: 12,
mode: "nearest",
intersect: 0,
position: "nearest",
},
responsive: true,
scales: {
yAxes: [
{
barPercentage: 1.6,
gridLines: {
drawBorder: false,
color: "rgba(29,140,248,0.0)",
zeroLineColor: "transparent",
},
ticks: {
suggestedMin: 50,
suggestedMax: 125,
padding: 20,
fontColor: "#9e9e9e",
},
},
],
xAxes: [
{
barPercentage: 1.6,
gridLines: {
drawBorder: false,
color: "rgba(0,242,195,0.1)",
zeroLineColor: "transparent",
},
ticks: {
padding: 20,
fontColor: "#9e9e9e",
},
},
],
},
},
};
module.exports = {
chartExample1, // in src/views/Dashboard.js
chartExample2, // in src/views/Dashboard.js
chartExample3, // in src/views/Dashboard.js
chartExample4, // in src/views/Dashboard.js
};

View File

@ -0,0 +1,19 @@
import React, { useEffect } from "react";
const Popup = (props) => {
const { data } = props;
const popupStyle = {
position: "fixed",
width: "300px",
height: "50px",
backgroundColor: "black",
color: "white",
};
const popupData = <div>HEY</div>;
return <div>{popupData}</div>;
};
export default Popup;

View File

@ -0,0 +1,53 @@
import React from "react";
import {
Info as InfoIcon,
Check as CheckIcon,
ErrorOutline as ErrorOutlineIcon,
Close as CloseIcon,
} from "@mui/icons-material";
import {
Typography
} from "@mui/material";
const alertStyle = {
backgroundColor: "rgba(0,0,0,0.9)",
color: "white",
padding: 15,
textTransform: "uppercase",
borderRadius: "3px",
display: "flex",
justifyContent: "space-between",
alignItems: "center",
boxShadow: "0px 2px 2px 2px rgba(0, 0, 0, 0.03)",
width: 300,
boxSizing: "border-box",
zIndex: 100001,
overflow: "hidden",
};
const buttonStyle = {
marginLeft: "20px",
border: "none",
backgroundColor: "transparent",
cursor: "pointer",
color: "#FFFFFF",
};
const AlertTemplate = ({ message, options, style, close }) => {
return (
<div style={{ ...alertStyle, ...style }}>
{options.type === "info" && <InfoIcon style={{ color: "white" }} />}
{options.type === "success" && <CheckIcon style={{ color: "green" }} />}
{options.type === "error" && (
<ErrorOutlineIcon style={{ color: "red" }} />
)}
<Typography style={{ marginLeft: 15, flex: 2 }}>{message}</Typography>
<button onClick={close} style={buttonStyle}>
<CloseIcon />
</button>
</div>
);
};
export default AlertTemplate;

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,385 @@
import React, {useEffect, useState} from 'react';
import theme from '../theme.jsx';
import ReactGA from 'react-ga4';
import {Link} from 'react-router-dom';
import { removeQuery } from '../components/ScrollToTop.jsx';
import {
Search as SearchIcon,
CloudQueue as CloudQueueIcon,
Code as CodeIcon
} from '@mui/icons-material';
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 '@mui/material';
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 [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)
//toast("Thanks for submitting!")
} else {
setFormMessage(errorMessage)
}
setFormMail("")
setMessage("")
})
.catch(error => {
setFormMessage(errorMessage)
console.log(error)
});
}
const SearchBox = ({currentRefinement, refine, isSearchStalled} ) => {
var defaultSearch = ""
//useEffect(() => {
if (window !== undefined && window.location !== undefined && window.location.search !== undefined && window.location.search !== null) {
const urlSearchParams = new URLSearchParams(window.location.search)
const params = Object.fromEntries(urlSearchParams.entries())
const foundQuery = params["q"]
if (foundQuery !== null && foundQuery !== undefined) {
console.log("Got query: ", foundQuery)
refine(foundQuery)
defaultSearch = foundQuery
}
}
//}, [])
return (
<form noValidate action="" role="search">
<TextField
defaultValue={defaultSearch}
fullWidth
style={{backgroundColor: theme.palette.inputColor, borderRadius: borderRadius, margin: 10, width: "100%",}}
InputProps={{
style:{
color: "white",
fontSize: "1em",
height: 50,
},
startAdornment: (
<InputAdornment position="start">
<SearchIcon style={{marginLeft: 5}}/>
</InputAdornment>
),
}}
autoComplete='off'
type="search"
color="primary"
placeholder="Find Apps..."
id="shuffle_search_field"
onChange={(event) => {
// Remove "q" from URL
removeQuery("q")
refine(event.currentTarget.value)
}}
limit={5}
/>
{/*isSearchStalled ? 'My search is stalled' : ''*/}
</form>
)
}
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)
//}
return (
<Grid container spacing={2}>
{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 (
<Zoom key={index} in={true} style={{ transitionDelay: `${workflowDelay}ms` }}>
<Grid item xs={xs} key={index}>
<a href={appUrl} rel="noopener noreferrer" target="_blank" style={{textDecoration: "none", color: "#f85a3e"}}>
<Paper elevation={0} style={paperStyle} onMouseOver={() => {
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,
}
])
}}>
<ButtonBase style={{padding: 5, borderRadius: 3, minHeight: 100, minWidth: 100,}}>
<img alt={data.name} src={data.image_url} style={{width: "100%", maxWidth: 100, minWidth: 100, minHeight: 100, maxHeight: 100, display: "block", margin: "0 auto"}} />
</ButtonBase>
<div/>
{index === mouseHoverIndex || showName === true ?
parsedname
:
null
}
{data.generated ?
<Tooltip title={"Created with App editor"} style={{marginTop: "28px", width: "100%"}} aria-label={data.name}>
{data.invalid ?
<CloudQueueIcon style={{position: "absolute", top: 1, left: 3, height: 16, width: 16, color: theme.palette.primary.main }}/>
:
<CloudQueueIcon style={{position: "absolute", top: 1, left: 3, height: 16, width: 16, color: "rgba(255,255,255,0.95)",}}/>
}
</Tooltip>
:
<Tooltip title={"Created with python (custom app)"} style={{marginTop: "28px", width: "100%"}} aria-label={data.name}>
<CodeIcon style={{position: "absolute", top: 1, left: 3, height: 16, width: 16, color: "rgba(255,255,255,0.95)",}}/>
</Tooltip>
}
</Paper>
</a>
</Grid>
</Zoom>
)
})}
</Grid>
)
}
const CustomSearchBox = connectSearchBox(SearchBox)
const CustomHits = connectHits(Hits)
//const CustomHits = connectHitInsights(aa)(Hits)
const selectButtonStyle = {
minWidth: 150,
maxWidth: 150,
minHeight: 50,
}
return (
<div style={{width: "100%", textAlign: "center", position: "relative", height: "100%", display: "flex"}}>
{/*
<div style={{padding: 10, }}>
<Button
style={selectButtonStyle}
variant="outlined"
onClick={() => {
const searchField = document.createElement("shuffle_search_field")
console.log("Field: ", searchField)
if (searchField !== null & searchField !== undefined) {
console.log("Set field.")
searchField.value = "WHAT WABALABA"
searchField.setAttribute("value", "WHAT WABALABA")
}
}}
>
Cases
</Button>
</div>
*/}
<div style={{width: "100%", position: "relative", height: "100%",}}>
<InstantSearch searchClient={searchClient} indexName="appsearch">
<div style={{maxWidth: 450, margin: "auto", marginTop: 15, marginBottom: 15, }}>
<CustomSearchBox />
</div>
<CustomHits hitsPerPage={5}/>
<Configure clickAnalytics />
</InstantSearch>
{showSuggestion === true ?
<div style={{paddingTop: 0, maxWidth: isMobile ? "100%" : "60%", margin: "auto"}}>
<Typography variant="h6" style={{color: "white", marginTop: 50,}}>
Can't find what you're looking for?
</Typography>
<div style={{flex: "1", display: "flex", flexDirection: "row", textAlign: "center",}}>
<TextField
required
style={{flex: "1", marginRight: "15px", backgroundColor: theme.palette.inputColor}}
InputProps={{
style:{
color: "#ffffff",
},
}}
color="primary"
fullWidth={true}
placeholder="Email (optional)"
type="email"
id="email-handler"
autoComplete="email"
margin="normal"
variant="outlined"
onChange={e => setFormMail(e.target.value)}
/>
<TextField
required
style={{flex: "1", backgroundColor: theme.palette.inputColor}}
InputProps={{
style:{
color: "#ffffff",
},
}}
color="primary"
fullWidth={true}
placeholder="What apps do you want to see?"
type=""
id="standard-required"
margin="normal"
variant="outlined"
autoComplete="off"
onChange={e => setMessage(e.target.value)}
/>
</div>
<Button
variant="contained"
color="primary"
style={buttonStyle}
disabled={message.length === 0}
onClick={() => {
submitContact(formMail, message)
}}
>
Submit
</Button>
<Typography style={{color: "white"}} variant="body2">{formMessage}</Typography>
</div>
: null
}
<span style={{position: "absolute", display: "flex", textAlign: "right", float: "right", right: 0, bottom: isMobile?"":120, }}>
<Typography variant="body2" color="textSecondary" style={{}}>
Search by
</Typography>
<a rel="noopener noreferrer" href="https://www.algolia.com/" target="_blank" style={{textDecoration: "none", color: "white"}}>
<img src={"/images/logo-algolia-nebula-blue-full.svg"} alt="Algolia logo" style={{height: 17, marginLeft: 5, marginTop: 3,}} />
</a>
</span>
</div>
</div>
)
}
export default AppGrid;

View File

@ -0,0 +1,403 @@
import React, { useState, useEffect, useRef } from "react";
import theme from '../theme.jsx';
import ReactGA from 'react-ga4';
import { useNavigate, Link } from 'react-router-dom';
import { Search as SearchIcon, CloudQueue as CloudQueueIcon, Code as CodeIcon, Close as CloseIcon, Folder as FolderIcon, LibraryBooks as LibraryBooksIcon } from '@mui/icons-material';
import aa from 'search-insights'
import DeleteIcon from '@mui/icons-material/Delete';
import ShowChartIcon from '@mui/icons-material/ShowChart';
import ExploreIcon from '@mui/icons-material/Explore';
import LightbulbIcon from "@mui/icons-material/Lightbulb";
import NewReleasesIcon from "@mui/icons-material/NewReleases";
import ExtensionIcon from "@mui/icons-material/Extension";
import EmailIcon from "@mui/icons-material/Email";
import FingerprintIcon from '@mui/icons-material/Fingerprint';
import AppSearch from "../components/Appsearch.jsx";
import { toast } from 'react-toastify';
import {
Zoom,
Grid,
Paper,
TextField,
Collapse,
IconButton,
Avatar,
ButtonBase,
InputAdornment,
Typography,
Button,
Tooltip,
List,
ListItem,
ListItemAvatar,
ListItemText,
} from '@mui/material';
const AppSearchButtons = (props) => {
const { userdata, globalUrl, appFramework, defaultSearch, finishedApps, onNodeSelect, setDiscoveryData, appName, AppImage, setDefaultSearch, discoveryData } = props
const ref = useRef()
const [moreButton, setMoreButton] = useState(false);
let navigate = useNavigate();
const sizing = moreButton ? 510 : 480;
const buttonWidth = 450;
const buttonMargin = 10;
const bottomButtonStyle = {
borderRadius: 200,
marginTop: moreButton ? 44 : "",
height: 51,
width: 510,
fontSize: 16,
// background: "linear-gradient(89.83deg, #FF8444 0.13%, #F2643B 99.84%)",
background: "linear-gradient(90deg, #F86744 0%, #F34475 100%)",
padding: "16px 24px",
// top: 20,
// margin: "auto",
textTransform: 'capitalize',
itemAlign: "center",
// marginTop: 25
// marginLeft: "65px",
};
const buttonStyle = {
flex: 1,
width: 224,
padding: 25,
margin: buttonMargin,
color: "var(--White-text, #F1F1F1)",
fontWeight: 400,
fontSize: 17,
background: "rgba(33, 33, 33, 1)",
textTransform: 'capitalize',
border: "1px solid rgba(33, 33, 33, 1)",
borderRadius: 8,
marginRight: 8,
};
const isCloud = window.location.host === "localhost:3002" || window.location.host === "shuffler.io";
const mouseOver = (e) => {
e.target.style.border = "1px solid #f85a3e";
}
const mouseOut = (e) => {
e.target.style.border = "1px solid rgb(33, 33, 33)";
}
return (
<Grid item xs={11} style={{ display: "flex" }}>
{/*<FormLabel style={{ color: "#B9B9BA" }}>Find your integrations!</FormLabel>*/}
{/* <div style={{ display: "flex", width: 510, height:100 }}>
<Button
disabled={finishedApps.includes("CASES")}
variant={
defaultSearch === "CASES" ? "contained" : "outlined"
}
color="secondary"
style={{
flex: 1,
width: "100%",
padding: 25,
margin: buttonMargin,
fontSize: 18,
color: "var(--White-text, #F1F1F1)",
fontWeight: 400,
background: "rgba(33, 33, 33, 1)",
borderRadius: 8,
textTransform: 'capitalize',
border: "1px solid rgba(33, 33, 33, 1)" ,
}}
onMouseOver={mouseOver}
onMouseOut={mouseOut}
// startIcon = {defaultSearch === "CASES" ? newSelectedApp.image_url : <LightbulbIcon/>}
onClick={(event) => {
onNodeSelect("CASES");
setDefaultSearch(discoveryData.label)
}}
>
{appFramework === undefined || appFramework.cases === undefined || appFramework.cases.large_image === undefined ||
appFramework === null || appFramework.cases === null || appFramework.cases.large_image === null || appFramework.cases.large_image.length === 0 ?
<div style={{width: 40, border: "1px solid rgba(33, 33, 33, 1) !importent", height: 40, borderRadius: 9999, backgroundColor: "#2F2F2F", textAlign:"center"}}>
<LightbulbIcon style={{ marginTop: 6 }} />
</div>
: <img style={{ marginRight: 8, width: 40, height: 40, flexShrink: 0, borderRadius: 40, }} src={AppImage} />}
<div style={{marginLeft: 8, }}>
<Typography style={{display:"flex",border:"none"}} >Case Management</Typography>
{appFramework === undefined || appFramework.cases === undefined || appFramework.cases.name === undefined ||
appFramework === null || appFramework.cases === null || appFramework.cases.name === null || appFramework.cases.name.length === 0 ?
"":<Typography style={{fontSize: 12, textAlign:"left", color:"var(--label-grey-text, #9E9E9E)" }} >{AppName}</Typography>}
</div>
</Button>
</div>
<div style={{ display: "flex", width: 510, height: 100 }}>
<Button
disabled={finishedApps.includes("SIEM")}
variant={
defaultSearch === "SIEM" ? "contained" : "outlined"
}
style={buttonStyle}
// startIcon={<SearchIcon />}
onMouseOver={mouseOver}
onMouseOut={mouseOut}
color="secondary"
onClick={(event) => {
onNodeSelect("SIEM");
setDefaultSearch(discoveryData.label)
}}
>
{appFramework === undefined || appFramework.siem === undefined || appFramework.siem.large_image === undefined ||
appFramework === null || appFramework.siem === null || appFramework.siem.large_image === null || appFramework.siem.large_image.length === 0 ?
<div style={{width: 40, border: "1px solid rgba(33, 33, 33, 1) !importent", height: 40, borderRadius: 9999, backgroundColor: "#2F2F2F", textAlign:"center"}}>
<SearchIcon style={{ marginTop: 6 }} />
</div>
: <img style={{ marginRight: 8, width: 40, height: 40, flexShrink: 0, borderRadius: 40, }} src={appFramework.siem.large_image} />}
<div style={{marginLeft: 8,}}>
<Typography style={{display:"flex",}}>SIEM</Typography>
{appFramework === undefined || appFramework.siem === undefined || appFramework.siem.name === undefined ||
appFramework === null || appFramework.siem === null || appFramework.siem.name === null || appFramework.siem.name.length === 0 ?
"":<Typography style={{fontSize: 12, textAlign:"left", color:"var(--label-grey-text, #9E9E9E)" }} >{appFramework.siem.name.split('_').join(' ')}</Typography>}
</div>
</Button>
<Button
disabled={
finishedApps.includes("EDR & AV") ||
finishedApps.includes("ERADICATION")
}
onMouseOver={mouseOver}
onMouseOut={mouseOut}
variant={
defaultSearch === "Eradication" ? "contained" : "outlined"
}
style={buttonStyle}
// startIcon={<NewReleasesIcon />}
color="secondary"
onClick={(event) => {
onNodeSelect("ERADICATION");
}}
>
{appFramework === undefined || appFramework.edr === undefined || appFramework.edr.large_image === undefined ||
appFramework === null || appFramework.edr === null || appFramework.edr.large_image === null || appFramework.edr.large_image.length === 0 ?
<div style={{width: 40, border: "1px solid rgba(33, 33, 33, 1) !importent", height: 40, borderRadius: 9999, backgroundColor: "#2F2F2F", textAlign:"center"}}>
<NewReleasesIcon style={{marginTop: 8 }} />
</div>
: <img style={{ marginRight: 8, width: 40, height: 40, flexShrink: 0, borderRadius: 40, }} src={appFramework.edr.large_image} />}
<div style={{marginLeft: 8,}}>
<Typography style={{display:"flex",}}>Endpoint</Typography>
{appFramework === undefined || appFramework.edr === undefined || appFramework.edr.name === undefined ||
appFramework === null || appFramework.edr === null || appFramework.edr.name === null || appFramework.edr.name.length === 0 ?
"":<Typography style={{fontSize: 12, textAlign:"left", color:"var(--label-grey-text, #9E9E9E)" }} >{appFramework.edr.name.split('_').join(' ')}</Typography>}
</div>
</Button>
</div>
<div style={{ display: "flex", width: 510, height: 100 }}>
<Button
disabled={finishedApps.includes("INTEL")}
variant={
defaultSearch === "INTEL" ? "contained" : "outlined"
}
onMouseOver={mouseOver}
onMouseOut={mouseOut}
style={buttonStyle}
// startIcon={<ExtensionIcon />}
color="secondary"
onClick={(event) => {
onNodeSelect("INTEL");
}}
>
{appFramework === undefined || appFramework.intel === undefined || appFramework.intel.large_image === undefined ||
appFramework === null || appFramework.intel === null || appFramework.intel.large_image === null || appFramework.intel.large_image.length === 0 ?
<div style={{width: 40, border: "1px solid rgba(33, 33, 33, 1) !importent", height: 40, borderRadius: 9999, backgroundColor: "#2F2F2F", textAlign:"center"}}>
<ExtensionIcon style={{ marginTop: 8 }} />
</div>
: <img style={{ marginRight: 8, width: 40, height: 40, flexShrink: 0, borderRadius: 40, }} src={appFramework.intel.large_image} />}
<div style={{marginLeft: 8,}}>
<Typography style={{display:"flex",}}>Intel</Typography>
{appFramework === undefined || appFramework.intel === undefined || appFramework.intel.name === undefined ||
appFramework === null || appFramework.intel === null || appFramework.intel.name === null || appFramework.intel.name.length === 0 ?
"":<Typography style={{fontSize: 12, textAlign:"left", color:"var(--label-grey-text, #9E9E9E)" }} >{appFramework.intel.name.split('_').join(' ')}</Typography>}
</div>
</Button>
<Button
disabled={
finishedApps.includes("COMMS") ||
finishedApps.includes("EMAIL")
}
variant={
defaultSearch === "EMAIL" ? "contained" : "outlined"
}
onMouseOver={mouseOver}
onMouseOut={mouseOut}
style={buttonStyle}
// startIcon={<EmailIcon />}
color="secondary"
onClick={(event) => {
onNodeSelect("EMAIL");
}}
>
{appFramework === undefined || appFramework.communication === undefined || appFramework.communication.large_image === undefined ||
appFramework === null || appFramework.communication === null || appFramework.communication.large_image === null || appFramework.communication.large_image.length === 0 ?
<div style={{width: 40, border: "1px solid rgba(33, 33, 33, 1) !importent", height: 40, borderRadius: 9999, backgroundColor: "#2F2F2F", textAlign:"center"}}>
<EmailIcon style={{ marginTop: 8 }} />
</div>
: <img style={{ marginRight: 8, width: 40, height: 40, flexShrink: 0, borderRadius: 40, }} src={appFramework.communication.large_image} />}
<div style={{marginLeft: 8,}}>
<Typography style={{display:"flex",}}>Email</Typography>
{appFramework === undefined || appFramework.communication === undefined || appFramework.communication.name === undefined ||
appFramework === null || appFramework.communication === null || appFramework.communication.name === null || appFramework.communication.name.length === 0 ?
"":<Typography style={{fontSize: 12, textAlign:"left", color:"var(--label-grey-text, #9E9E9E)" }} >{appFramework.communication.name.split('_').join(' ')}</Typography>}
</div>
</Button>
</div>
{moreButton ? (
<div style={{ display: "flex", width: 510, height: 100, marginBottom: 20 }}>
<Button
disabled={finishedApps.includes("NETWORK")}
variant={
defaultSearch === "NETWORK" ? "contained" : "outlined"
}
onMouseOver={mouseOver}
onMouseOut={mouseOut}
style={buttonStyle}
// startIcon={<ExtensionIcon />}
color="secondary"
onClick={(event) => {
onNodeSelect("NETWORK");
}}
>
{appFramework === undefined || appFramework.network === undefined || appFramework.network.large_image === undefined ||
appFramework === null || appFramework.network === null || appFramework.network.large_image === null || appFramework.network.large_image.length === 0 ?
<div style={{width: 40, border: "1px solid rgba(33, 33, 33, 1) !importent", height: 40, borderRadius: 9999, backgroundColor: "#2F2F2F", textAlign:"center"}}>
<ShowChartIcon style={{ marginTop: 8 }} />
</div>
: <img style={{ marginRight: 8, width: 40, height: 40, flexShrink: 0, borderRadius: 40, }} src={appFramework.network.large_image} />}
<div style={{marginLeft: 8,}}>
<Typography style={{display:"flex",}}>Network</Typography>
{appFramework === undefined || appFramework.network === undefined || appFramework.network.name === undefined ||
appFramework === null || appFramework.network === null || appFramework.network.name === null || appFramework.network.name.length === 0 ?
"":<Typography style={{fontSize: 10, textAlign:"left", color:"var(--label-grey-text, #9E9E9E)" }} >{appFramework.network.name}</Typography>}
</div>
</Button>
<Button
disabled={
finishedApps.includes("ASSETS")
}
variant={
defaultSearch === "ASSETS" ? "contained" : "outlined"
}
onMouseOver={mouseOver}
onMouseOut={mouseOut}
style={buttonStyle}
// startIcon={<EmailIcon />}
color="secondary"
onClick={(event) => {
onNodeSelect("ASSETS");
}}
>
{appFramework === undefined || appFramework.assets === undefined || appFramework.assets.large_image === undefined ||
appFramework === null || appFramework.assets === null || appFramework.assets.large_image === null || appFramework.assets.large_image.length === 0 ?
<div style={{width: 40, border: "1px solid rgba(33, 33, 33, 1) !importent", height: 40, borderRadius: 9999, backgroundColor: "#2F2F2F", textAlign:"center"}}>
<ExploreIcon style={{ marginTop: 8 }} />
</div>
: <img style={{ marginRight: 8, width: 40, height: 40, flexShrink: 0, borderRadius: 40, }} src={appFramework.assets.large_image} />}
<div style={{marginLeft: 8,}}>
<Typography style={{display:"flex",}}>Assets</Typography>
{appFramework === undefined || appFramework.assets === undefined || appFramework.assets.name === undefined ||
appFramework === null || appFramework.assets === null || appFramework.assets.name === null || appFramework.assets.name.length === 0 ?
"":<Typography style={{fontSize: 10, textAlign:"left", color:"var(--label-grey-text, #9E9E9E)" }} >{appFramework.assets.name}</Typography>}
</div>
</Button>
<Button
disabled={
finishedApps.includes("IAM")
}
variant={
defaultSearch === "IAM" ? "contained" : "outlined"
}
onMouseOver={mouseOver}
onMouseOut={mouseOut}
style={buttonStyle}
// startIcon={<EmailIcon />}
color="secondary"
onClick={(event) => {
onNodeSelect("IAM");
}}
>
{appFramework === undefined || appFramework.iam === undefined || appFramework.iam.large_image === undefined ||
appFramework === null || appFramework.iam === null || appFramework.iam.large_image === null || appFramework.iam.large_image.length === 0 ?
<div style={{width: 40, border: "1px solid rgba(33, 33, 33, 1) !importent", height: 40, borderRadius: 9999, backgroundColor: "#2F2F2F", textAlign:"center"}}>
<FingerprintIcon style={{ marginTop: 8 }} />
</div>
: <img style={{ marginRight: 8, width: 40, height: 40, flexShrink: 0, borderRadius: 40, }} src={appFramework.iam.large_image} />}
<div style={{marginLeft: 8,}}>
<Typography style={{display:"flex",}}>IAM</Typography>
{appFramework === undefined || appFramework.iam === undefined || appFramework.iam.name === undefined ||
appFramework === null || appFramework.iam === null || appFramework.iam.name === null || appFramework.iam.name.length === 0 ?
"":<Typography style={{fontSize: 8, textAlign:"left", color:"var(--label-grey-text, #9E9E9E)" }} >{appFramework.iam.name}</Typography>}
</div>
</Button>
</div>
)
:
<div style={{ display: "flex", width: 510, paddingLeft: 165, }}>
<Button
style={{ color: "#f86a3e", textTransform: 'capitalize', border: 2, backgroundColor: "var(--Background-color, #1A1A1A)" }}
className="btn btn-primary"
onClick={(event) => {
setMoreButton(true);
}}
>
<Typography style={{ textDecorationLine: 'underline', }}>
See more Categories
</Typography>
</Button>
</div>} */}
<div style={{ display: "flex", width: 510, height: 64, borderRadius: 8, background: "var(--Container, #212121)" }}
>
<div
onMouseOver={mouseOver}
onMouseOut={mouseOut}
disabled={finishedApps.includes("CASES")}
variant={
defaultSearch === "CASES" ? "contained" : "outlined"
}
color="secondary"
style={{
flex: 1,
width: "100%",
margin: buttonMargin,
fontSize: 18,
color: "var(--White-text, #F1F1F1)",
fontWeight: 400,
background: "rgba(33, 33, 33, 1)",
borderRadius: 8,
textTransform: 'capitalize',
}}
// startIcon = {defaultSearch === "CASES" ? newSelectedApp.image_url : <LightbulbIcon/>}
onClick={(event) => {
onNodeSelect("CASES");
setDefaultSearch(discoveryData.label)
}}
>
<div style={{marginLeft: 20, display:"flex", textAlign:"center", alignItems:"center", marginLeft: 100, width: 320, marginRight: "auto" }}>
{AppImage === undefined || AppImage === undefined ||
AppImage === null || AppImage === null || AppImage.length === 0 ?
<div style={{ width: 40, height: 40, borderRadius: 9999, backgroundColor: "#2F2F2F", textAlign: "center" }}>
<LightbulbIcon style={{ marginTop: 6 }} />
</div>
: <img style={{ marginRight: 8, width: 40, height: 40, flexShrink: 0, borderRadius: 40, }} src={AppImage} />}
<div style={{ marginLeft: 8, }}>
<Typography style={{ display: "flex", border: "none" }} >Case Management</Typography>
{appName === undefined || appName === undefined ||
appName === null || appName === null || appName.length === 0 ?
""
:
<Typography style={{ fontSize: 12, textAlign: "left", color: "var(--label-grey-text, #9E9E9E)" }} >{appName}</Typography>}
</div>
</div>
</div>
</div>
</Grid>
)
}
export default AppSearchButtons

View File

@ -0,0 +1,433 @@
import React, { useState, useEffect, useRef } from "react";
import theme from '../theme.jsx';
import ReactGA from 'react-ga4';
import { useNavigate, Link } from 'react-router-dom';
import { Search as SearchIcon, CloudQueue as CloudQueueIcon, Code as CodeIcon, Close as CloseIcon, Folder as FolderIcon, LibraryBooks as LibraryBooksIcon } from '@mui/icons-material';
import aa from 'search-insights'
import DeleteIcon from '@mui/icons-material/Delete';
import ShowChartIcon from '@mui/icons-material/ShowChart';
import ExploreIcon from '@mui/icons-material/Explore';
import LightbulbIcon from "@mui/icons-material/Lightbulb";
import NewReleasesIcon from "@mui/icons-material/NewReleases";
import ExtensionIcon from "@mui/icons-material/Extension";
import EmailIcon from "@mui/icons-material/Email";
import FingerprintIcon from '@mui/icons-material/Fingerprint';
import AppSearch from "../components/Appsearch.jsx";
import AppSearchButtons from "../components/AppSearchButtons.jsx";
import { toast } from 'react-toastify';
import {
Zoom,
Grid,
Paper,
TextField,
Collapse,
IconButton,
Avatar,
ButtonBase,
InputAdornment,
Typography,
Button,
Tooltip,
List,
ListItem,
ListItemAvatar,
ListItemText,
} from '@mui/material';
const AppSelection = props => {
const {
userdata,
globalUrl,
appFramework,
setActiveStep,
defaultSearch,
setDefaultSearch,
checkLogin,
} = props;
const [discoveryData, setDiscoveryData] = React.useState({})
const [selectionOpen, setSelectionOpen] = React.useState(false)
const [newSelectedApp, setNewSelectedApp] = React.useState({})
const [finishedApps, setFinishedApps] = React.useState([])
const [appButtons, setAppButtons] = useState([])
const [apps, setApps] = useState([])
const [appName, setAppName] = React.useState();
const [moreButton, setMoreButton] = useState(false);
// const [mouseHoverIndex, setMouseHoverIndex] = useState(-1)
const ref = useRef()
let navigate = useNavigate();
const isCloud = window.location.host === "localhost:3002" || window.location.host === "shuffler.io";
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!");
}
if (checkLogin !== undefined) {
checkLogin()
}
return response.json();
})
.then((responseJson) => {
if (responseJson.success === false) {
if (responseJson.reason !== undefined) {
toast("Failed updating: " + responseJson.reason)
} else {
toast("Failed to update framework for your org.")
}
}
//setFrameworkLoaded(true)
//setFrameworkData(responseJson)
})
.catch((error) => {
if (checkLogin !== undefined) {
checkLogin()
}
toast(error.toString());
//setFrameworkLoaded(true)
})
}
const GetApps = (data) => {
console.log("Setting framework item: ", data, isCloud)
// if (!isCloud) {
// activateApp(data.id)
// }
fetch(globalUrl + "/api/v1/apps/frameworkConfiguration", {
method: "GET",
headers: {
"Content-Type": "application/json",
Accept: "application/json",
},
body: JSON.stringify(data),
credentials: "include",
})
.then((responseJson) => {
if (responseJson === null) {
console.log("null-response from server")
const pretend_apps = [{
"description": "TBD",
"id": "TBD",
"large_image": "",
"name": "TBD",
"type": "TBD"
}]
setApps(pretend_apps)
return
}
if (responseJson.success === false) {
console.log("error loading apps: ", responseJson)
return
}
setApps(responseJson);
})
.catch((error) => {
console.log("App loading error: " + error.toString());
})
}
const onNodeSelect = (label) => {
// if (setDiscoveryWrapper !== undefined) {
// setDiscoveryWrapper({ id: label });
// }
if (isCloud) {
ReactGA.event({
category: "welcome",
action: `click_${label}`,
label: "",
});
}
setDiscoveryData(label)
setSelectionOpen(true)
setNewSelectedApp({})
setDefaultSearch(label.charAt(0).toUpperCase() + (label.substring(1)).toLowerCase())
};
useEffect(() => {
var tempApps = []
if (tempApps.length === 0) {
const tempApps =
[{
"description": newSelectedApp.description,
"id": newSelectedApp.objectID,
"large_image": newSelectedApp.image_url,
"name": newSelectedApp.name,
"type": discoveryData
},
//{
// // description: newSelectedApp.siem.description,
// id: newSelectedApp.siem.objectID,
// large_image: newSelectedApp.siem.image_url,
// name: newSelectedApp.siem.name,
// type: discoveryData.siem
// },{
// // description: newSelectedApp.edr.description,
// id: newSelectedApp.edr.objectID,
// large_image: newSelectedApp.edr.image_url,
// name: newSelectedApp.edr.name,
// type: discoveryData.edr
// }
]
setAppButtons(tempApps)
GetApps()
}
}, [])
useEffect(() => {
if (newSelectedApp.objectID === undefined || newSelectedApp.objectID === undefined || newSelectedApp.objectID.length === 0) {
return
}
const submitNewApp = {
description: newSelectedApp.description,
id: newSelectedApp.objectID,
large_image: newSelectedApp.image_url,
name: newSelectedApp.name,
type: discoveryData
}
if (discoveryData === "CASES") {
appFramework.cases = submitNewApp
}
else if (discoveryData === "SIEM") {
appFramework.siem = submitNewApp
}
else if (discoveryData === "ERADICATION") {
appFramework.edr = submitNewApp
}
else if (discoveryData === "INTEL") {
appFramework.intel = submitNewApp
}
else if (discoveryData === "EMAIL") {
appFramework.communication = submitNewApp
}
else if (discoveryData === "NETWORK") {
appFramework.network = submitNewApp
}
else if (discoveryData === "ASSETS") {
appFramework.assets = submitNewApp
}
else if (discoveryData === "IAM") {
appFramework.iam = submitNewApp
}
setFrameworkItem(submitNewApp);
setSelectionOpen(false);
console.log("Selected app changed (effect)");
}, [newSelectedApp]);
const sizing = moreButton ? 510 : 480;
const buttonWidth = 450;
const buttonMargin = 10;
const bottomButtonStyle = {
borderRadius: 200,
marginTop: moreButton ? 44 : "",
height: 51,
width: 510,
fontSize: 16,
// background: "linear-gradient(89.83deg, #FF8444 0.13%, #F2643B 99.84%)",
background: "linear-gradient(90deg, #F86744 0%, #F34475 100%)",
padding: "16px 24px",
// top: 20,
// margin: "auto",
textTransform: 'capitalize',
itemAlign: "center",
// marginTop: 25
// marginLeft: "65px",
};
const buttonStyle = {
flex: 1,
width: 224,
padding: 25,
margin: buttonMargin,
color: "var(--White-text, #F1F1F1)",
fontWeight: 400,
fontSize: 17,
background: "rgba(33, 33, 33, 1)",
textTransform: 'capitalize',
border: "1px solid rgba(33, 33, 33, 1)",
borderRadius: 8,
marginRight: 8,
};
// console.log("appFramework",appFramework.cases.name)
return (
<Collapse in={true}>
<div
style={{
minHeight: sizing,
maxHeight: sizing,
marginTop: 10,
width: 500,
}}
>
{selectionOpen ? (
<div
style={{
width: 319,
height: 395,
flexShrink: 0,
marginLeft: 70,
marginTop: 68,
position: "absolute",
zIndex: 100,
borderRadius: 6,
border: "1px solid var(--Container-Stroke, #494949)",
background: "var(--Container, #212121)",
boxShadow: "8px 8px 32px 24px rgba(0, 0, 0, 0.16)",
}}
>
<div style={{ display: "flex" }}>
<div style={{ display: "flex", textAlign: "center", textTransform: "capitalize" }}>
<Typography style={{ padding: 16, color: "#FFFFFF", textTransform: "capitalize" }}> {discoveryData} </Typography>
</div>
<div style={{ display: "flex" }}>
<Tooltip
title="Close"
placement="top"
style={{ zIndex: 10011 }}
>
<IconButton
style={{
flex: 1,
// width: 224,
marginLeft: discoveryData === ('ERADICATION') ? 120 : 177,
width: "100%",
marginBottom: 23,
fontSize: 16,
background: "rgba(33, 33, 33, 1)",
borderColor: "rgba(33, 33, 33, 1)",
borderRadius: 8,
}}
onClick={() => {
setSelectionOpen(false)
}}
>
<CloseIcon style={{ width: 16 }} />
</IconButton>
</Tooltip>
<Tooltip
title="Delete app"
placement="bottom"
style={{ zIndex: 10011 }}
>
<IconButton
style={{ zIndex: 12501, position: "absolute", top: 32, right: 16 }}
onClick={(e) => {
e.preventDefault();
setSelectionOpen(false)
setDefaultSearch("")
const submitDeletedApp = {
"description": "",
"id": "remove",
"name": "",
"type": discoveryData
}
setFrameworkItem(submitDeletedApp)
setNewSelectedApp({})
setTimeout(() => {
setDiscoveryData({})
setFrameworkItem(submitDeletedApp)
setNewSelectedApp({})
}, 1000)
//setAppName(discoveryData.cases.name)
}}
>
<DeleteIcon style={{ color: "white", height: 15, width: 15, }} />
</IconButton>
</Tooltip>
</div>
</div>
<div
style={{ width: "100%", border: "1px #494949 solid" }}
/>
<AppSearch
defaultSearch={defaultSearch}
newSelectedApp={newSelectedApp}
setNewSelectedApp={setNewSelectedApp}
userdata={userdata}
// cy={cy}
/>
</div>
) : null}
<Typography
variant="h4"
style={{
marginLeft: 8,
marginTop: 40,
marginRight: 30,
marginBottom: 0,
}}
color="rgba(241, 241, 241, 1)"
>
Find your apps
</Typography>
<Typography
variant="body2"
style={{
marginLeft: 8,
marginTop: 10,
marginRight: 30,
marginBottom: 40,
}}
color="rgba(158, 158, 158, 1)"
>
Select the apps you work with and we will connect them for you.
</Typography>
{appButtons.map((appData, index) => {
const appName = appData.name
const AppImage = appData.large_image
const appType = appData.type
return (
<AppSearchButtons
appFramework={appFramework}
appName={appName}
appType = {appType}
AppImage={AppImage}
defaultSearch={defaultSearch}
finishedApps={finishedApps}
onNodeSelect={onNodeSelect}
discoveryData={discoveryData}
setDiscoveryData={setDiscoveryData}
setDefaultSearch={setDefaultSearch}
apps={apps}
/>
)
})}
</div>
<div style={{ flexDirection: "row", }}>
<Button variant="contained" type="submit" fullWidth style={bottomButtonStyle} onClick={() => {
navigate("/welcome?tab=3")
setActiveStep(2)
}}>
Continue
</Button>
</div>
</Collapse>
)
}
export default AppSelection;

View File

@ -0,0 +1,220 @@
import React, { useState, useEffect } from 'react';
import ReactGA from 'react-ga4';
import theme from '../theme.jsx';
import {Link} from 'react-router-dom';
import { Search as SearchIcon, CloudQueue as CloudQueueIcon, Code as CodeIcon } from '@mui/icons-material';
import { toast } from 'react-toastify';
//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 '@mui/material';
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, setUserSpecialzedApp } = 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 ? 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"
// value={currentRefinement}
const SearchBox = ({currentRefinement, refine, isSearchStalled} ) => {
useEffect(() => {
//console.log("FIRST LOAD ONLY? RUN REFINEMENT: !", currentRefinement)
if (defaultSearch !== undefined && defaultSearch !== null) {
refine(defaultSearch)
}
}, [])
return (
<form noValidate action="" role="search">
<TextField
fullWidth
style={{backgroundColor: "#2F2F2F", borderRadius: borderRadius, width: "100%",}}
InputProps={{
style:{
color: "white",
fontSize: "1em",
height: 50,
},
endAdornment: (
<InputAdornment position="start">
<SearchIcon style={{marginLeft: 5}}/>
</InputAdornment>
),
}}
autoComplete='on'
type="search"
color="primary"
defaultValue={defaultSearch}
// placeholder={`Find ${defaultSearch} Apps...`}
placeholder= {defaultSearch ? `${defaultSearch}` : "Search Cases "}
id="shuffle_workflow_search_field"
onChange={(event) => {
refine(event.currentTarget.value)
}}
limit={5}
/>
{/*isSearchStalled ? 'My search is stalled' : ''*/}
</form>
)
//value={currentRefinement}
}
const Hits = ({ hits }) => {
const [mouseHoverIndex, setMouseHoverIndex] = useState(-1)
var counted = 0
return (
<Grid container spacing={0} style={{border: "1px solid rgba(255,255,255,0.2)", maxHeight: 250, minHeight: 250, overflowY: "auto", overflowX: "hidden", }}>
{hits.map((data, index) => {
const paperStyle = {
backgroundColor: index === mouseHoverIndex ? "rgba(255,255,255,0.8)" : "#2F2F2F",
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 (
<Paper key={index} elevation={0} style={paperStyle} onMouseOver={() => {
setMouseHoverIndex(index)
/*
ReactGA.event({
category: "app_grid_view",
action: `search_bar_click`,
label: "",
})
*/
}} onMouseOut={() => {
setMouseHoverIndex(-1)
}} onClick={() => {
if(isCreatorPage === true){
if (setNewSelectedApp !== undefined && setUserSpecialzedApp !== undefined) {
setUserSpecialzedApp(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)
}
}
}}>
<div style={{display: "flex"}}>
<img alt={data.name} src={data.image_url} style={{width: "100%", maxWidth: 30, minWidth: 30, minHeight: 30, borderRadius: 40, maxHeight: 30, display: "block", }} />
<Typography variant="body1" style={{marginTop: 2, marginLeft: 10, }}>
{parsedname}
</Typography>
</div>
</Paper>
)
})}
</Grid>
)
}
const InputHits = ConfiguredHits === undefined ? Hits : ConfiguredHits
const CustomSearchBox = connectSearchBox(SearchBox)
const CustomHits = connectHits(InputHits)
return (
<div style={{width: 287, height: 295, padding: "16px 16px 267px 16px", alignItems: "center", gap: 138,}}>
<InstantSearch searchClient={searchClient} indexName="appsearch">
{/* showSearch === false ? null :
<div style={{maxWidth: 450, margin: "auto", }}>
<CustomSearchBox />
</div>
*/}
<div style={{maxWidth: 450, margin: "auto", }}>
<CustomSearchBox />
</div>
<CustomHits hitsPerPage={5}/>
</InstantSearch>
</div>
)
}
export default Appsearch;

View File

@ -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 "@mui/material";
import {
Close as CloseIcon,
Delete as DeleteIcon,
} from "@mui/icons-material";
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
}
// <Paper style={{width: 275, maxHeight: 400, zIndex: 100000, padding: 25, paddingRight: 35, backgroundColor: theme.palette.surfaceColor, border: "1px solid rgba(255,255,255,0.2)", position: "absolute", top: -15, left: 50, }}>
return (
<Paper style={{minWidth: 275, width: 275, minHeight: 400, maxHeight: 400, zIndex: 100000, padding: 25, paddingRight: 35, backgroundColor: theme.palette.surfaceColor, border: "1px solid rgba(255,255,255,0.2)", position: "absolute", top: -15, left: 50, }}>
{paperTitle !== undefined && paperTitle.length > 0 ?
<span>
<Typography variant="h6" style={{textAlign: "center"}}>
{paperTitle}
</Typography>
<Divider style={{marginTop: 5, marginBottom: 5 }} />
</span>
: null}
<Tooltip
title="Close window"
placement="top"
style={{ zIndex: 10011 }}
>
<IconButton
style={{ zIndex: 12501, position: "absolute", top: 10, right: 10}}
onClick={(e) => {
//cy.elements().unselectify();
if (cy !== undefined) {
cy.elements().unselect()
}
e.preventDefault();
setSelectionOpen(false)
}}
>
<CloseIcon style={{ color: "white", height: 15, width: 15, }} />
</IconButton>
</Tooltip>
{/* {/*Causes errors in Cytoscape. Removing for now.}
<Tooltip
title="Unselect app"
placement="top"
style={{ zIndex: 10011 }}
>
<IconButton
style={{ zIndex: 12501, position: "absolute", top: 32, right: 10}}
onClick={(e) => {
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)
}}
>
<DeleteIcon style={{ color: "white", height: 15, width: 15, }} />
</IconButton>
</Tooltip>
*/}
<div style={{display: "flex"}}>
{discoveryData.name !== undefined && discoveryData.name !== null && discoveryData.name.length > 0 ?
<div style={{border: "1px solid rgba(255,255,255,0.2)", borderRadius: 25, height: 40, width: 40, textAlign: "center", overflow: "hidden",}}>
<img alt={discoveryData.id} src={newSelectedApp.image_url !== undefined && newSelectedApp.image_url !== null && newSelectedApp.image_url.length > 0 ? newSelectedApp.image_url : discoveryData.large_image} style={{height: 40, width: 40, margin: "auto",}}/>
</div>
:
<img alt={discoveryData.id} src={discoveryData.large_image} style={{height: 40,}}/>
}
<Typography variant="body1" style={{marginLeft: 10, marginTop: 6}}>
{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`
}
</Typography>
</div>
<div>
{discoveryData !== undefined && discoveryData.name !== undefined && discoveryData.name !== null && discoveryData.name.length > 0 ?
<span>
<Typography variant="body2" color="textSecondary" style={{marginTop: 10, marginBottom: 10, maxHeight: 75, overflowY: "auto", overflowX: "hidden", }}>
{discoveryData.description}
</Typography>
{/*isCloud && defaultSearch !== undefined && defaultSearch.length > 0 ?
{<
newSelectedApp={newSelectedApp}
setNewSelectedApp={setNewSelectedApp}
defaultSearch={defaultSearch}
/>}
:
null
*/}
</span>
:
selectionOpen
?
<span>
<Typography variant="body2" color="textSecondary" style={{marginTop: 10}}>
Click an app below to select it
</Typography>
</span>
:
<Button
variant="contained"
color="primary"
style={{marginTop: 10, }}
onClick={() => {
setSelectionOpen(true)
setDefaultSearch(discoveryData.label)
}}
>
Choose {discoveryData.label} app
</Button>
}
</div>
<div style={{marginTop: 10}}>
{selectionOpen ?
<AppSearch
defaultSearch={defaultSearch}
newSelectedApp={newSelectedApp}
setNewSelectedApp={setNewSelectedApp}
userdata={userdata}
/>
: null}
</div>
</Paper>
)
}
export default AppSearchPopout;

View File

@ -0,0 +1,278 @@
import React, { useState, useEffect } from "react";
import theme from '../theme.jsx';
import { toast } from 'react-toastify';
import {
Tooltip,
IconButton,
ListItem,
ListItemText,
FormGroup,
FormControl,
InputLabel,
FormLabel,
FormControlLabel,
Select,
MenuItem,
Grid,
Paper,
Typography,
Zoom,
} from "@mui/material";
import {
Edit as EditIcon,
Delete as DeleteIcon,
SelectAll as SelectAllIcon,
} from "@mui/icons-material";
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) => {
toast("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) {
toast("Failed deleting auth");
} else {
// Need to wait because query in ES is too fast
setTimeout(() => {
getAppAuthentication();
}, 1000);
//toast("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) {
toast("Failed overwriting appauth in workflows");
} else {
toast("Successfully updated auth everywhere!");
//setSelectedUserModalOpen(false);
setTimeout(() => {
getAppAuthentication();
}, 1000);
}
})
)
.catch((error) => {
toast("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 (
<ListItem key={index} style={{ backgroundColor: bgColor }}>
<ListItemText
primary=<img
alt=""
src={data.app.large_image}
style={{
maxWidth: 50,
borderRadius: theme.palette.borderRadius,
}}
/>
style={{ minWidth: 75, maxWidth: 75 }}
/>
<ListItemText
primary={data.label}
style={{
minWidth: 225,
maxWidth: 225,
overflow: "hidden",
}}
/>
<ListItemText
primary={data.app.name}
style={{ minWidth: 175, maxWidth: 175, marginLeft: 10 }}
/>
{/*
<ListItemText
primary={data.defined === false ? "No" : "Yes"}
style={{ minWidth: 100, maxWidth: 100, }}
/>
*/}
<ListItemText
primary={
data.workflow_count === null ? 0 : data.workflow_count
}
style={{
minWidth: 100,
maxWidth: 100,
textAlign: "center",
overflow: "hidden",
}}
/>
{/*
<ListItemText
primary={data.node_count}
style={{
minWidth: 110,
maxWidth: 110,
textAlign: "center",
overflow: "hidden",
}}
/>
*/}
<ListItemText
primary={
data.fields === null || data.fields === undefined
? ""
: data.fields
.map((data) => {
return data.key;
})
.join(", ")
}
style={{
minWidth: 125,
maxWidth: 125,
overflow: "hidden",
}}
/>
<ListItemText
style={{
maxWidth: 230,
minWidth: 230,
overflow: "hidden",
}}
primary={new Date(data.created * 1000).toISOString()}
/>
<ListItemText>
<IconButton
onClick={() => {
updateAppAuthentication(data);
}}
>
<EditIcon color="primary" />
</IconButton>
{data.defined ? (
<Tooltip
color="primary"
title="Set in EVERY workflow"
placement="top"
>
<IconButton
style={{ marginRight: 10 }}
disabled={data.defined === false}
onClick={() => {
editAuthenticationConfig(data.id);
}}
>
<SelectAllIcon
color={data.defined ? "primary" : "secondary"}
/>
</IconButton>
</Tooltip>
) : (
<Tooltip
color="primary"
title="Must edit before you can set in all workflows"
placement="top"
>
<IconButton
style={{ marginRight: 10 }}
onClick={() => {}}
>
<SelectAllIcon
color={data.defined ? "primary" : "secondary"}
/>
</IconButton>
</Tooltip>
)}
<IconButton
onClick={() => {
deleteAuthentication(data);
}}
>
<DeleteIcon color="primary" />
</IconButton>
</ListItemText>
</ListItem>
)
}
export default AuthenticationItem

View File

@ -0,0 +1,366 @@
import React, { useState, useEffect } from "react";
import theme from '../theme.jsx';
import { v4 as uuidv4 } from "uuid";
import { toast } from 'react-toastify';
import {
Button,
Divider,
Select,
MenuItem,
TextField,
DialogActions,
DialogTitle,
DialogContent,
Typography,
} from "@mui/material";
import {
LockOpen as LockOpenIcon,
} from "@mui/icons-material";
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) {
toast("Failed to set app auth: " + responseJson.reason);
} else {
if (getAppAuthentication !== undefined) {
getAppAuthentication()
}
if (setAuthenticationModalOpen !== undefined) {
setAuthenticationModalOpen(false)
}
// Needs a refresh with the new authentication..
//toast("Successfully saved new app auth")
}
})
.catch((error) => {
//toast(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 (
<DialogContent style={{ textAlign: "center", marginTop: 50 }}>
<Typography variant="h4" id="draggable-dialog-title" style={{cursor: "move",}}>
{selectedApp.name} does not require authentication
</Typography>
</DialogContent>
);
}
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 {
toast(
"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 (
<div>
<DialogTitle id="draggable-dialog-title" style={{cursor: "move",}}>
<div style={{ color: "white" }}>
Authentication for {selectedApp.name}
</div>
</DialogTitle>
<DialogContent>
<a
target="_blank"
rel="noopener noreferrer"
href="https://shuffler.io/docs/apps#authentication"
style={{ textDecoration: "none", color: "#f85a3e" }}
>
What is app authentication?
</a>
<div />
These are required fields for authenticating with {selectedApp.name}
<div style={{ marginTop: 15 }} />
<b>Name - what is this used for?</b>
<TextField
style={{
backgroundColor: theme.palette.inputColor,
borderRadius: theme.palette.borderRadius,
}}
InputProps={{
style: {
color: "white",
marginLeft: "5px",
maxWidth: "95%",
height: 50,
fontSize: "1em",
},
}}
fullWidth
color="primary"
placeholder={"Auth july 2020"}
defaultValue={`Auth for ${selectedApp.name}`}
onChange={(event) => {
authenticationOption.label = event.target.value;
}}
/>
<Divider
style={{
marginTop: 15,
marginBottom: 15,
backgroundColor: "rgb(91, 96, 100)",
}}
/>
<div />
{selectedApp.authentication.parameters.map((data, index) => {
return (
<div key={index} style={{ marginTop: 10 }}>
<LockOpenIcon style={{ marginRight: 10 }} />
<b>{data.name}</b>
{data.schema !== undefined &&
data.schema !== null &&
data.schema.type === "bool" ? (
<Select
MenuProps={{
disableScrollLock: true,
}}
SelectDisplayProps={{
style: {
marginLeft: 10,
},
}}
defaultValue={"false"}
fullWidth
onChange={(e) => {
console.log("Value: ", e.target.value);
authenticationOption.fields[data.name] = e.target.value;
}}
style={{
backgroundColor: theme.palette.surfaceColor,
color: "white",
height: 50,
}}
>
<MenuItem
key={"false"}
style={{
backgroundColor: theme.palette.inputColor,
color: "white",
}}
value={"false"}
>
false
</MenuItem>
<MenuItem
key={"true"}
style={{
backgroundColor: theme.palette.inputColor,
color: "white",
}}
value={"true"}
>
true
</MenuItem>
</Select>
) : (
<TextField
style={{
backgroundColor: theme.palette.inputColor,
borderRadius: theme.palette.borderRadius,
}}
InputProps={{
style: {
color: "white",
marginLeft: "5px",
maxWidth: "95%",
height: 50,
fontSize: "1em",
},
}}
fullWidth
type={
data.example !== undefined && data.example.includes("***")
? "password"
: "text"
}
color="primary"
defaultValue={
data.value !== undefined && data.value !== null
? data.value
: ""
}
placeholder={data.example}
onChange={(event) => {
authenticationOption.fields[data.name] =
event.target.value;
}}
/>
)}
</div>
);
})}
</DialogContent>
<DialogActions>
<Button
style={{ borderRadius: "0px" }}
onClick={() => {
setAuthenticationModalOpen(false);
}}
color="primary"
>
Cancel
</Button>
<Button
style={{ borderRadius: "0px" }}
onClick={() => {
setAuthenticationOptions(authenticationOption);
handleSubmitCheck();
}}
color="primary"
>
Submit
</Button>
</DialogActions>
</div>
);
};
export default AuthenticationData

View File

@ -0,0 +1,469 @@
import React, { useState, useEffect } from "react";
import theme from '../theme.jsx';
import { v4 as uuidv4 } from "uuid";
import { toast } from 'react-toastify';
import {
Divider,
MenuItem,
Button,
Dialog,
DialogTitle,
DialogActions,
DialogContent,
Textfield,
TextField,
Typography,
Select,
IconButton,
} from "@mui/material";
import {
LockOpen as LockOpenIcon,
Close as CloseIcon,
} from "@mui/icons-material";
import PaperComponent from "../components/PaperComponent.jsx"
import { useParams, useNavigate, Link } from "react-router-dom";
const AuthenticationData = (props) => {
const {
globalUrl,
selectedApp,
getAppAuthentication,
authenticationModalOpen,
setAuthenticationModalOpen,
configureWorkflowModalOpen,
workflow,
setUpdate,
selectedAction,
setSelectedAction,
isLoggedIn,
authFieldsOnly,
} = props
//const alert = useAlert()
let navigate = useNavigate();
const [submitSuccessful, setSubmitSuccessful] = useState(false)
const [authenticationOption, setAuthenticationOptions] = React.useState({
app: JSON.parse(JSON.stringify(selectedApp)),
fields: {},
label: "",
usage: [
{
workflow_id: workflow === undefined ? "" : workflow.id,
},
],
id: uuidv4(),
active: true,
});
useEffect(() => {
if (isLoggedIn === false && authFieldsOnly !== true) {
navigate(`/login?view=${window.location.pathname}&message=Log in to authenticate this app`)
}
}, [])
const setNewAppAuth = (appAuthData) => {
var headers = {
"Content-Type": "application/json",
"Accept": "application/json",
}
// Find org_id and authorization from queries and add to headers
if (window.location.search !== "") {
const params = new URLSearchParams(window.location.search)
const org_id = params.get("org_id")
const authorization = params.get("authorization")
if (org_id !== null && authorization !== null) {
headers["Org-Id"] = org_id
headers["Authorization"] = "Bearer " + authorization
}
}
fetch(globalUrl + "/api/v1/apps/authentication", {
method: "PUT",
headers: headers,
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) {
if (responseJson.reason === undefined) {
toast("Failed to set app auth. Are you logged in?")
} else {
toast("Failed to set app auth: " + responseJson.reason);
}
} else {
setSubmitSuccessful(true)
if (getAppAuthentication !== undefined) {
getAppAuthentication(true, false);
}
if (setAuthenticationModalOpen !== undefined) {
setAuthenticationModalOpen(false)
}
}
})
.catch((error) => {
//toast(error.toString());
console.log("New auth error: ", error.toString());
});
};
if (selectedApp.authentication === undefined || selectedApp.authentication.parameters === null ||
selectedApp.authentication.parameters === undefined || selectedApp.authentication.parameters.length === 0) {
return (
<DialogContent style={{ textAlign: "center", marginTop: 50 }}>
<Typography variant="h4" id="draggable-dialog-title" style={{ cursor: "move", }}>
{selectedApp.name} does not require authentication
</Typography>
</DialogContent>
);
}
authenticationOption.app.actions = [];
for (let paramkey in selectedApp.authentication.parameters) {
if (
authenticationOption.fields[
selectedApp.authentication.parameters[paramkey].name
] === undefined
) {
authenticationOption.fields[
selectedApp.authentication.parameters[paramkey].name
] = "";
}
}
const handleSubmitCheck = () => {
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 (let paramkey in selectedApp.authentication.parameters) {
if (
authenticationOption.fields[
selectedApp.authentication.parameters[paramkey].name
].length === 0
) {
if (
selectedApp.authentication.parameters[paramkey].value !== undefined &&
selectedApp.authentication.parameters[paramkey].value !== null &&
selectedApp.authentication.parameters[paramkey].value.length > 0
) {
authenticationOption.fields[
selectedApp.authentication.parameters[paramkey].name
] = selectedApp.authentication.parameters[paramkey].value;
} else {
if (
selectedApp.authentication.parameters[paramkey].schema.type === "bool"
) {
authenticationOption.fields[
selectedApp.authentication.parameters[paramkey].name
] = "false";
} else {
toast(
"Field " +
selectedApp.authentication.parameters[paramkey].name +
" can't be empty"
);
return;
}
}
}
}
console.log("Action: ", selectedAction);
if (selectedAction !== undefined) {
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 = [];
console.log("Fields: ", newAuthOption.fields)
for (let authkey in newAuthOption.fields) {
const value = newAuthOption.fields[authkey];
newFields.push({
"key": authkey,
"value": value,
});
}
newAuthOption.fields = newFields;
setNewAppAuth(newAuthOption);
if (configureWorkflowModalOpen === true) {
setSelectedAction({});
}
if (setUpdate !== undefined) {
setUpdate(authenticationOption.id);
}
};
if (authenticationOption.label === null || authenticationOption.label === undefined) {
authenticationOption.label = selectedApp.name + " authentication";
}
const authenticationParameters = selectedApp.authentication.parameters.map((data, index) => {
return (
<div key={index} style={{ marginTop: 10 }}>
<div style={{display: "flex"}}>
<LockOpenIcon style={{ marginRight: 10, }} />
<Typography variant="body1" style={{}}>
{data.name.replace("_basic", "", -1).replace("_", " ", -1)}
</Typography>
</div>
{data.schema !== undefined &&
data.schema !== null &&
data.schema.type === "bool" ? (
<Select
MenuProps={{
disableScrollLock: true,
}}
SelectDisplayProps={{
style: {
marginLeft: 10,
},
}}
defaultValue={"false"}
fullWidth
onChange={(e) => {
console.log("Value: ", e.target.value);
authenticationOption.fields[data.name] = e.target.value;
}}
style={{
backgroundColor: theme.palette.surfaceColor,
color: "white",
height: 50,
}}
>
<MenuItem
key={"false"}
style={{
backgroundColor: theme.palette.inputColor,
color: "white",
}}
value={"false"}
>
false
</MenuItem>
<MenuItem
key={"true"}
style={{
backgroundColor: theme.palette.inputColor,
color: "white",
}}
value={"true"}
>
true
</MenuItem>
</Select>
) : (
<TextField
style={{
backgroundColor: theme.palette.inputColor,
borderRadius: theme.palette.borderRadius,
}}
InputProps={{
style: {
color: "white",
fontSize: "1em",
},
disableUnderline: true,
}}
fullWidth
type={
data.example !== undefined && data.example.includes("***")
? "password"
: "text"
}
color="primary"
defaultValue={
data.value !== undefined && data.value !== null
? data.value
: ""
}
placeholder={data.example}
onChange={(event) => {
authenticationOption.fields[data.name] =
event.target.value;
}}
/>
)}
</div>
);
})
const authenticationButtons = <span>
<Button
style={{ borderRadius: theme.palette.borderRadius, marginTop: authFieldsOnly ? 20 : 0 }}
onClick={() => {
setAuthenticationOptions(authenticationOption);
handleSubmitCheck();
}}
variant={"contained"}
disabled={submitSuccessful}
color="primary"
>
Submit
</Button>
{authFieldsOnly === true ? null :
<Button
style={{ borderRadius: 0 }}
onClick={() => {
setAuthenticationModalOpen(false);
}}
color="primary"
>
Cancel
</Button>
}
</span>
// Check if only the auth items should show
if (authFieldsOnly === true) {
return (
<div>
{submitSuccessful === true ?
<Typography variant="h6" style={{ marginTop: 10 }}>
App succesfully configured! You may close this window.
</Typography>
:
<span>
{authenticationParameters}
{authenticationButtons}
</span>
}
</div>
)
}
return (
<Dialog
PaperComponent={PaperComponent}
aria-labelledby="draggable-dialog-title"
hideBackdrop={true}
disableEnforceFocus={true}
disableBackdropClick={true}
style={{ pointerEvents: "none" }}
open={authenticationModalOpen}
onClose={() => {
//if (configureWorkflowModalOpen) {
// setSelectedAction({});
//}
setAuthenticationModalOpen(false);
}}
PaperProps={{
style: {
pointerEvents: "auto",
color: "white",
minWidth: 600,
minHeight: 600,
maxHeight: 600,
padding: 15,
overflow: "hidden",
zIndex: 10012,
border: theme.palette.defaultBorder,
},
}}
>
<IconButton
style={{
zIndex: 5000,
position: "absolute",
top: 14,
right: 18,
color: "grey",
}}
onClick={() => {
setAuthenticationModalOpen(false);
if (configureWorkflowModalOpen === true) {
setSelectedAction({});
}
}}
>
<CloseIcon />
</IconButton>
<DialogTitle id="draggable-dialog-title" style={{ cursor: "move", }}>
<div style={{ color: "white" }}>
Authentication for {selectedApp.name}
</div>
</DialogTitle>
<DialogContent>
<a
target="_blank"
rel="noopener noreferrer"
href="https://shuffler.io/docs/apps#authentication"
style={{ textDecoration: "none", color: "#f85a3e" }}
>
What is app authentication?
</a>
<div />
These are required fields for authenticating with {selectedApp.name}
<div style={{ marginTop: 15 }} />
<b>Name - what is this used for?</b>
<TextField
style={{
backgroundColor: theme.palette.inputColor,
borderRadius: theme.palette.borderRadius,
}}
InputProps={{
style: {
color: "white",
fontSize: "1em",
},
}}
fullWidth
color="primary"
placeholder={"Auth july 2020"}
defaultValue={`Auth for ${selectedApp.name}`}
onChange={(event) => {
authenticationOption.label = event.target.value;
}}
/>
<Divider
style={{
marginTop: 15,
marginBottom: 15,
backgroundColor: "rgb(91, 96, 100)",
}}
/>
<div />
{authenticationParameters}
</DialogContent>
<DialogActions>
{authenticationButtons}
</DialogActions>
</Dialog>
);
};
export default AuthenticationData;

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,280 @@
import React, { useState, useEffect } from 'react';
import classNames from "classnames";
import theme from '../theme.jsx';
import {
Tooltip,
TextField,
IconButton,
Button,
Typography,
Grid,
Paper,
Chip,
Checkbox,
} from "@mui/material";
import {
BarChart,
RadialBarChart,
RadialAreaChart,
RadialAxis,
StackedBarSeries,
TooltipArea,
ChartTooltip,
TooltipTemplate,
RadialAreaSeries,
RadialPointSeries,
RadialArea,
RadialLine,
TreeMap,
TreeMapSeries,
TreeMapLabel,
TreeMapRect,
Line,
LineChart,
LineSeries,
LinearYAxis,
LinearXAxis,
LinearYAxisTickSeries,
LinearXAxisTickSeries,
Area,
AreaChart,
AreaSeries,
AreaSparklineChart,
PointSeries,
GridlineSeries,
Gridline,
Stripes,
Gradient,
GradientStop,
LinearXAxisTickLabel,
} from 'reaviz';
const LineChartWrapper = ({keys, inputname, height, width}) => {
const [hovered, setHovered] = useState("");
const inputdata = keys.data === undefined ? keys : keys.data
return (
<div style={{color: "white", border: "1px solid rgba(255,255,255,0.3)", borderRadius: theme.palette.borderRadius, padding: 30, marginTop: 15, }}>
<Typography variant="h6" style={{marginBotton: 15, }}>
{inputname}
</Typography>
<BarChart
width={"100%"}
height={height}
data={inputdata}
gridlines={
<GridlineSeries line={<Gridline direction="all" />} />
}
/>
</div>
)
}
const AppStats = (defaultprops) => {
const { globalUrl, selectedOrganization, userdata, } = defaultprops;
const [keys, setKeys] = useState([])
const [searches, setSearches] = useState([]);
const [clickData, setClickData] = useState(undefined);
const [conversionData, setConversionData] = useState(undefined);
const [statistics, setStatistics] = useState(undefined);
const [appRuns, setAppruns] = useState(undefined);
const [workflowRuns, setWorkflowRuns] = useState(undefined);
const [subflowRuns, setSubflowRuns] = useState(undefined);
const handleDataSetting = (inputdata, grouping) => {
if (inputdata === undefined || inputdata === null) {
return
}
const dailyStats = inputdata.daily_statistics
if (dailyStats === undefined || dailyStats === null) {
return
}
console.log("Looking at daily data: ", inputdata)
var appRuns = {
"key": "App Runs",
"data": []
}
var workflowRuns = {
"key": "Workflow Runs (includes subflows)",
"data": []
}
var subflowRuns = {
"key": "Subflow Runs",
"data": []
}
for (let key in dailyStats) {
// Always skips first one as it has accumulated data in it
if (key === 0) {
continue
}
const item = dailyStats[key]
if (item["date"] === undefined) {
console.log("No date: ", item)
continue
}
// Check if app_executions key in item
if (item["app_executions"] !== undefined && item["app_executions"] !== null) {
appRuns["data"].push({
key: new Date(item["date"]),
data: item["app_executions"]
})
}
// Check if workflow_executions key in item
if (item["workflow_executions"] !== undefined && item["workflow_executions"] !== null) {
workflowRuns["data"].push({
key: new Date(item["date"]),
data: item["workflow_executions"]
})
}
if (item["subflow_executions"] !== undefined && item["subflow_executions"] !== null) {
subflowRuns["data"].push({
key: new Date(item["date"]),
data: item["subflow_executions"]
})
}
}
// Adds data for today
console.log("Inputdata: ", inputdata)
if (inputdata["daily_app_executions"] !== undefined && inputdata["daily_app_executions"] !== null) {
appRuns["data"].push({
key: new Date(),
data: inputdata["daily_app_executions"]
})
}
if (inputdata["daily_workflow_executions"] !== undefined && inputdata["daily_workflow_executions"] !== null) {
workflowRuns["data"].push({
key: new Date(),
data: inputdata["daily_workflow_executions"]
})
}
if (inputdata["daily_subflow_executions"] !== undefined && inputdata["daily_subflow_executions"] !== null) {
subflowRuns["data"].push({
key: new Date(),
data: inputdata["daily_subflow_executions"]
})
}
setSubflowRuns(subflowRuns)
setWorkflowRuns(workflowRuns)
setAppruns(appRuns)
}
const getStats = () => {
fetch(`${globalUrl}/api/v1/orgs/${selectedOrganization.id}/stats`, {
method: "GET",
headers: {
"Content-Type": "application/json",
Accept: "application/json",
},
credentials: "include",
})
.then((response) => {
if (response.status !== 200) {
console.log("Status not 200 for workflows :O!: ", response.status);
return;
}
return response.json();
})
.then((responseJson) => {
if (responseJson["success"] === false) {
return
}
setStatistics(responseJson)
handleDataSetting(responseJson, "day")
})
.catch((error) => {
console.log("error: ", error)
});
}
useEffect(() => {
getStats()
}, [])
const paperStyle = {
textAlign: "center",
padding: 40,
margin: 5,
backgroundColor: theme.palette.surfaceColor,
maxWidth: 300,
}
const data = (
<div className="content" style={{width: "100%", margin: "auto", }}>
<Typography variant="body1" style={{margin: "auto", marginLeft: 10, marginBottom: 20, }}>
All Stat widgets are monthly and gathered from <a
href={`${globalUrl}/api/v1/orgs/${selectedOrganization.id}/stats`}
target="_blank"
style={{ textDecoration: "none", color: "#f85a3e",}}
>Your Organization Statistics. </a>
This is a feature to help give you more insight into Shuffle, and will be populating over time.
</Typography>
{statistics !== undefined ?
<div style={{display: "flex", textAlign: "center",}}>
<Paper style={paperStyle}>
<Typography variant="h4">
{statistics.monthly_workflow_executions}
</Typography>
<Typography variant="h6">
Workflow Runs
</Typography>
</Paper>
<Paper style={paperStyle}>
<Typography variant="h4">
{statistics.monthly_app_executions}
</Typography>
<Typography variant="h6">
App Runs
</Typography>
</Paper>
</div>
: null}
{appRuns === undefined ?
null
:
<LineChartWrapper keys={appRuns} height={300} width={"100%"} inputname={"Daily App Runs"}/>
}
{workflowRuns === undefined ?
null
:
<LineChartWrapper keys={workflowRuns} height={300} width={"100%"} inputname={"Daily Workflow Runs (including subflows)"}/>
}
{subflowRuns === undefined ?
null
:
<LineChartWrapper keys={subflowRuns} height={300} width={"100%"} inputname={"Subflow Runs"}/>
}
</div>
)
const dataWrapper = (
<div style={{ maxWidth: 1366, margin: "auto" }}>{data}</div>
);
return dataWrapper;
}
export default AppStats;

View File

@ -0,0 +1,166 @@
import React, { useState, useEffect } from "react";
import ReactGA from 'react-ga4';
import theme from "../theme.jsx";
import { ToastContainer, toast } from "react-toastify"
import {
Paper,
Typography,
Divider,
Button,
Grid,
Card,
} from "@mui/material";
//import { useAlert
const Branding = (props) => {
const { globalUrl, userdata, serverside, billingInfo, stripeKey, selectedOrganization, handleGetOrg, } = props;
//const alert = useAlert();
const [publishingInfo, setPublishingInfo] = useState("");
const [publishRequirements, setPublishRequirements] = useState([])
const handleEditOrg = (joinStatus) => {
const data = {
"org_id": selectedOrganization.id,
"creator_config": joinStatus,
};
const url = globalUrl + `/api/v1/orgs/${selectedOrganization.id}`;
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) {
toast("Failed updating org: ", responseJson.reason);
} else {
if (joinStatus == "join") {
setPublishingInfo("Your organization is now part of the Creator Incentive Program. You can now create and publish content to your organization's page. You can also create a creator account to manage your organization's content.")
} else {
setPublishingInfo("Your organization is no longer part of the Creator Incentive Program. You can still create a creator account to manage your organization's content.")
}
handleGetOrg(selectedOrganization.id);
}
})
)
.catch((error) => {
toast("Err: " + error.toString());
});
};
// Should enable / disable org branding
const handleChangePublishing = () => {
console.log("Handle change publishing");
if (selectedOrganization.creator_id == "") {
handleEditOrg("join")
} else {
handleEditOrg("leave")
}
}
const isOrganizationReady = () => {
console.log("Is organization ready?")
// A simple checklist to ensure the button shows up properly
if (selectedOrganization.name === selectedOrganization.org) {
const comment = "Change the name of your organization"
if (!publishRequirements.includes(comment)) {
setPublishRequirements([...publishRequirements, comment])
}
return false;
}
// Check if it's a suborg
if (selectedOrganization.creator_org !== "") {
const comment = "Child orgs can't become creators"
if (!publishRequirements.includes(comment)) {
setPublishRequirements([...publishRequirements, comment])
}
return false;
}
if (selectedOrganization.large_image === "" || selectedOrganization.large_image === theme.palette.defaultImage) {
const comment = "Add a logo for your organization"
if (!publishRequirements.includes(comment)) {
setPublishRequirements([...publishRequirements, comment])
}
return false;
}
return true
}
return (
<div>
<h2>
Branding
</h2>
<Typography variant="body1" color="textSecondary" style={{ marginTop: 20, marginBottom: 10 }}>
You can customize your organization's branding by uploading a logo, changing the color scheme and a lot more.
</Typography>
<Divider style={{marginTop: 50, marginBottom: 50, }} />
<h2>
Creator Incentive Program
</h2>
<div style={{ display: "flex", width: 900, }}>
<div>
<span>
<Typography variant="body1" color="textSecondary">
By changing publishing settings, you agree to our <a href="/docs/terms_of_service" target="_blank" style={{ textDecoration: "none", color: "#f86a3e"}}>Terms of Service</a>, and acknowledge that your organization's non-sensitive data will be added as a <a target="_blank" style={{ textDecoration: "none", color: "#f86a3e"}} href="https://shuffler.io/creators">creator account</a>. None of your existing workflows, apps, or other stored data will be published. Any admin in your organization can manage the creator configuration. Becoming a creator organization is reversible.<div/>Support: <a href="mailto:support@shuffler.io"target="_blank" style={{ textDecoration: "none", color: "#f86a3e"}}>support@shuffler.io</a>
</Typography>
{selectedOrganization.creator_id == "" ?
<Typography variant="h6" color="textSecondary" style={{ marginTop: 20, marginBottom: 10, color: "grey", }}>
&nbsp;
</Typography>
:
<Typography variant="h6" color="textSecondary" style={{ marginTop: 20, marginBottom: 10, color: "grey", }}>
<a href={`/creators/${selectedOrganization.creator_id}`} target="_blank" style={{ textDecoration: "none", color: "#f86a3e"}}>Modify your creator organization</a>
</Typography>
}
<Button
style={{ height: 40, marginTop: 10, width: 300, }}
variant={selectedOrganization.creator_id == "" ? "contained" : "outlined"}
color={selectedOrganization.creator_id == "" ? "primary" : "secondary"}
disabled={!isOrganizationReady()}
onClick={() => {
handleChangePublishing();
}}
>
{selectedOrganization.creator_id == "" ? "Join" : "Leave"} Creators
</Button>
<Typography variant="body1" color="textSecondary" style={{ marginTop: 20, marginBottom: 10, color: "white", }}>
{publishingInfo}
</Typography>
<Typography variant="body1" color="textSecondary" style={{ marginTop: 20, marginBottom: 10, color: "grey", }}>
{publishRequirements.map((item) => {
return (
<div>
Required: {item}
</div>
)
})}
</Typography>
</span>
</div>
</div>
</div>
)
}
export default Branding;

View File

@ -0,0 +1,504 @@
import React, { useState, useEffect } from "react";
import theme from "../theme.jsx";
import { toast } from 'react-toastify';
import {
Tooltip,
Divider,
TextField,
Button,
Tabs,
Tab,
Grid,
List,
ListItem,
ListItemText,
IconButton,
Dialog,
DialogTitle,
DialogActions,
} from "@mui/material";
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 "@mui/icons-material";
const scrollStyle1 = {
height: 100,
width: 225,
overflow: "hidden",
position: "relative",
}
const scrollStyle2 = {
position: "absolute",
top: 0,
left: 0,
bottom: "-20px",
right: "-20px",
overflow: "scroll",
}
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 [editedCache, setEditedCache] = 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 [dataValue, setDataValue] = React.useState({});
const [editCache, setEditCache] = React.useState(false);
const [show, setShow] = useState({});
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) => {
toast(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) => {
toast("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) {
toast("Successfully deleted Cache");
setTimeout(() => {
listOrgCache(orgId);
}, 1000);
} else {
toast("Failed deleting Cache. Does it still exist?");
}
})
.catch((error) => {
toast(error.toString());
});
};
const editOrgCache = (orgId) => {
const cache = { key: dataValue.key , value: value };
setCacheInput([cache]);
console.log("cache:", cache)
console.log("cache input: ", cacheInput)
fetch(globalUrl + `/api/v1/orgs/${orgId}/set_cache`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Accept: "application/json",
},
credentials: "include",
body: JSON.stringify(cache),
})
.then((response) => {
if (response.status !== 200) {
console.log("Status not 200 for Cache :O!");
return;
}
return response.json();
})
.then((responseJson) => {
setAddCache(responseJson);
toast("Cache Edited Successfully!");
listOrgCache(orgId);
setModalOpen(false);
})
.catch((error) => {
toast(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);
toast("New Cache Added Successfully!");
listOrgCache(orgId);
setModalOpen(false);
})
.catch((error) => {
toast(error.toString());
});
};
const modalView = (
// console.log("key:", dataValue.key),
//console.log("value:",dataValue.value),
<Dialog
open={modalOpen}
onClose={() => {
setModalOpen(false);
}}
PaperProps={{
style: {
backgroundColor: theme.palette.surfaceColor,
color: "white",
minWidth: "800px",
minHeight: "320px",
},
}}
>
<DialogTitle>
<span style={{ color: "white" }}>
{ editCache ? "Edit Cache" : "Add Cache" }
</span>
</DialogTitle>
<div style={{ paddingLeft: "30px", paddingRight: '30px' }}>
Key
<TextField
color="primary"
style={{ backgroundColor: theme.palette.inputColor }}
autoFocus
InputProps={{
style: {
height: "50px",
color: "white",
fontSize: "1em",
},
}}
required
fullWidth={true}
autoComplete="Key"
placeholder="abc"
id="keyfield"
margin="normal"
variant="outlined"
value={editCache ? dataValue.key : key}
onChange={(e) => setKey(e.target.value)}
/>
</div>
<div style={{ paddingLeft: "30px", paddingRight: '30px' }}>
Value
<TextField
color="primary"
style={{ backgroundColor: theme.palette.inputColor }}
InputProps={{
style: {
height: "50px",
color: "white",
fontSize: "1em",
},
}}
required
fullWidth={true}
autoComplete="Value"
placeholder="123"
id="Valuefield"
margin="normal"
variant="outlined"
defaultValue={editCache ? dataValue.value : ""}
onChange={(e) => setValue(e.target.value)}
/>
</div>
<DialogActions style={{ paddingLeft: "30px", paddingRight: '30px' }}>
<Button
style={{ borderRadius: "0px" }}
onClick={() => setModalOpen(false)}
color="primary"
>
Cancel
</Button>
<Button
variant="contained"
style={{ borderRadius: "0px" }}
onClick={() => {
{editCache ? editOrgCache(orgId) : addOrgCache(orgId)}
}}
color="primary"
>
{editCache ? "Edit":"Submit"}
</Button>
</DialogActions>
</Dialog>
);
return (
<div>
{modalView}
<div style={{ marginTop: 20, marginBottom: 20 }}>
<h2 style={{ display: "inline" }}>Shuffle Datastore</h2>
<span style={{ marginLeft: 25 }}>
Datastore is a key-value store for storing data that can be used cross-workflow.&nbsp;
<a
target="_blank"
rel="noopener noreferrer"
href="/docs/organizations#datastore"
style={{ textDecoration: "none", color: "#f85a3e" }}
>
Learn more
</a>
</span>
</div>
<Button
style={{}}
variant="contained"
color="primary"
onClick={() =>{
setEditCache(false)
setModalOpen(true)
}
}
>
Add Cache
</Button>
<Button
style={{ marginLeft: 5, marginRight: 15 }}
variant="contained"
color="primary"
onClick={() => listOrgCache(orgId)}
>
<CachedIcon />
</Button>
<Divider
style={{
marginTop: 20,
marginBottom: 20,
}}
/>
<List>
<ListItem>
<ListItemText
primary="Key"
// style={{ minWidth: 150, maxWidth: 150 }}
/>
<ListItemText
primary="value"
// style={{ minWidth: 150, maxWidth: 150 }}
/>
<ListItemText
primary="Updated"
// style={{ minWidth: 150, maxWidth: 150 }}
/>
<ListItemText
primary="Actions"
// style={{ minWidth: 150, maxWidth: 150 }}
/>
</ListItem>
{listCache === undefined || listCache === null
? null
: listCache.map((data, index) => {
var bgColor = "#27292d";
if (index % 2 === 0) {
bgColor = "#1f2023";
}
return (
<ListItem key={index} style={{ backgroundColor: bgColor }}>
<ListItemText
style={{
maxWidth: 225,
minWidth: 225,
overflow: "hidden",
}}
primary={data.key}
/>
<div style={scrollStyle1}>
<ListItemText
// style={{
// maxWidth: 225,
// maxHeight: 150,
// // overflow: "hidden",
// paddingLeft: "52px",
// overflow: "scroll",
// }}
style={scrollStyle2}
// style={{ maxWidth: 100, minWidth: 100 }}
// onMouseOver={() =>
// setShow((prevState) => ({ ...prevState, [data.value]: true }))
// }
// onMouseLeave={() =>
// setShow((prevState) => ({ ...prevState, [data.value]: false }))
// }
//primary={show[data.value] ? data.value : `${data.value.substring(0, 5)}...`}
primary={data.value}
/>
</div>
<ListItemText
style={{
maxWidth: 225,
minWidth: 225,
overflow: "hidden",
marginLeft: "42px",
}}
primary={new Date(data.edited * 1000).toISOString()}
/>
<ListItemText
style={{
minWidth: 250,
maxWidth: 250,
overflow: "hidden",
paddingLeft: "155px",
}}
primary=<span style={{ display: "inline" }}>
<Tooltip
title="Edit"
style={{}}
aria-label={"Edit"}
>
<span>
<IconButton
style={{ padding: "6px" }}
onClick={() => {
setEditCache(true)
setDataValue({"key":data.key,"value":data.value})
setModalOpen(true)
}}
>
<EditIcon
style={{ color: "white" }}
/>
</IconButton>
</span>
</Tooltip>
<Tooltip
title={"Delete Cache"}
style={{ marginLeft: 15, }}
aria-label={"Delete"}
>
<span>
<IconButton
style={{ padding: "6px" }}
onClick={() => {
deleteCache(orgId, data.key);
//deleteFile(orgId);
}}
>
<DeleteIcon
style={{ color: "white" }}
/>
</IconButton>
</span>
</Tooltip>
</span>
/>
</ListItem>
);
})}
</List>
</div>
);
}
export default CacheView;

File diff suppressed because it is too large Load Diff

View File

@ -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

View File

@ -0,0 +1,304 @@
import React, { useEffect, useState } from 'react';
import ReactGA from 'react-ga4';
import {Link} from 'react-router-dom';
import theme from '../theme.jsx';
import { removeQuery } from '../components/ScrollToTop.jsx';
import {
SkipNext as SkipNextIcon,
SkipPrevious as SkipPreviousIcon,
PlayArrow as PlayArrowIcon,
VerifiedUser as VerifiedUserIcon,
Search as SearchIcon, CloudQueue as CloudQueueIcon, Code as CodeIcon } from '@mui/icons-material';
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 '@mui/material';
import {
Avatar,
AvatarGroup,
} from "@mui/material"
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 [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)
//toast("Thanks for submitting!")
} else {
setFormMessage(errorMessage)
}
setFormMail("")
setMessage("")
})
.catch(error => {
setFormMessage(errorMessage)
console.log(error)
});
}
// value={currentRefinement}
const SearchBox = ({currentRefinement, refine, isSearchStalled} ) => {
var defaultSearch = ""
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)
defaultSearch = foundQuery
}
}
return (
<form noValidate action="" role="search">
<TextField
defaultValue={defaultSearch}
fullWidth
style={{backgroundColor: theme.palette.inputColor, borderRadius: borderRadius, margin: 10, width: "100%",}}
InputProps={{
style:{
color: "white",
fontSize: "1em",
height: 50,
},
startAdornment: (
<InputAdornment position="start">
<SearchIcon style={{marginLeft: 5}}/>
</InputAdornment>
),
}}
autoComplete='off'
type="search"
color="primary"
placeholder="Find Creators..."
id="shuffle_search_field"
onChange={(event) => {
removeQuery("q")
refine(event.currentTarget.value)
}}
/>
{/*isSearchStalled ? 'My search is stalled' : ''*/}
</form>
)
}
const paperAppContainer = {
display: "flex",
flexWrap: "wrap",
alignContent: "space-between",
marginTop: 5,
}
const Hits = ({ hits }) => {
const [mouseHoverIndex, setMouseHoverIndex] = useState(-1)
var counted = 0
return (
<Grid container spacing={4} style={paperAppContainer}>
{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 (
<Zoom key={index} in={true} style={{}}>
<Grid item xs={xs} style={{ padding: "12px 10px 12px 10px", }}>
<Card style={{border: "1px solid rgba(255,255,255,0.3)", minHeight: 177, maxHeight: 177,}}>
<a href={creatorUrl} rel="noopener noreferrer" target={isCloud ? "" : "_blank"} style={{textDecoration: "none", color: "inherit",}}>
<CardActionArea style={{padding: "5px 10px 5px 10px", minHeight: 177, maxHeight: 177,}}>
<CardContent sx={{ flex: '1 0 auto', minWidth: 160, maxWidth: 160, overflow: "hidden", padding: 0, }}>
<div style={{display: "flex"}}>
<img style={{height: 74, width: 74, borderRadius: 100, }} alt={"Creator profile of "+data.username} src={data.image} />
<Typography component="div" variant="body1" style={{marginTop: 20, marginLeft: 15, }}>
@{data.username}
</Typography>
<span style={{marginTop: "auto", marginBottom: "auto", marginLeft: 10, }}>
{data.verified === true ?
<Tooltip title="Verified and earning from Shuffle contributions" placement="top">
<VerifiedUserIcon style={{}}/>
</Tooltip>
:
null
}
</span>
</div>
<Typography variant="body1" color="textSecondary" style={{marginTop: 10, }}>
<b>{data.apps === undefined || data.apps === null ? 0 : data.apps}</b> apps <span style={{marginLeft: 15, }}/><b>{data.workflows === null || data.workflows === undefined ? 0 : data.workflows}</b> workflows
</Typography>
{data.specialized_apps !== undefined && data.specialized_apps !== null && data.specialized_apps.length > 0 ?
<AvatarGroup max={10} style={{flexDirection: "row", padding: 0, margin: 0, itemAlign: "left", textAlign: "left", marginTop: 3,}}>
{data.specialized_apps.map((app, index) => {
// Putting all this in secondary of ListItemText looked weird.
return (
<div
key={index}
style={{
height: 24,
width: 24,
filter: "brightness(0.6)",
cursor: "pointer",
}}
onClick={() => {
console.log("Click")
//navigate("/apps/"+app.id)
}}
>
<Tooltip color="primary" title={app.name} placement="bottom">
<Avatar alt={app.name} src={app.image} style={{width: 24, height: 24}}/>
</Tooltip>
</div>
)
})}
</AvatarGroup>
:
null}
</CardContent>
</CardActionArea>
</a>
</Card>
</Grid>
</Zoom>
)
})}
</Grid>
)
}
const CustomSearchBox = connectSearchBox(SearchBox)
const CustomHits = connectHits(Hits)
return (
<div style={{width: "100%", position: "relative", height: "100%",}}>
<InstantSearch searchClient={searchClient} indexName="creators">
<Configure clickAnalytics />
<div style={{maxWidth: 450, margin: "auto", marginTop: 15, marginBottom: 15, }}>
<CustomSearchBox />
</div>
<CustomHits hitsPerPage={100}/>
</InstantSearch>
{showSuggestion === true ?
<div style={{maxWidth: isMobile ? "100%" : "60%", margin: "auto", paddingTop: 50, textAlign: "center",}}>
<Typography variant="h6" style={{color: "white", marginTop: 50,}}>
Can't find what you're looking for?
</Typography>
<div style={{flex: "1", display: "flex", flexDirection: "row"}}>
<TextField
required
style={{flex: "1", marginRight: "15px", backgroundColor: theme.palette.inputColor}}
InputProps={{
style:{
color: "#ffffff",
},
}}
color="primary"
fullWidth={true}
placeholder="Email (optional)"
type="email"
id="email-handler"
autoComplete="email"
margin="normal"
variant="outlined"
onChange={e => setFormMail(e.target.value)}
/>
<TextField
required
style={{flex: "1", backgroundColor: theme.palette.inputColor}}
InputProps={{
style:{
color: "#ffffff",
},
}}
color="primary"
fullWidth={true}
placeholder="What are we missing?"
type=""
id="standard-required"
margin="normal"
variant="outlined"
autoComplete="off"
onChange={e => setMessage(e.target.value)}
/>
</div>
<Button
variant="contained"
color="primary"
style={buttonStyle}
disabled={message.length === 0}
onClick={() => {
submitContact(formMail, message)
}}
>
Submit
</Button>
<Typography style={{color: "white"}} variant="body2">{formMessage}</Typography>
</div>
: null
}
</div>
)
}
export default CreatorGrid;

View File

@ -0,0 +1,357 @@
import React, {useEffect, useState} from 'react';
import theme from '../theme.jsx';
import ReactGA from 'react-ga4';
import {Link} from 'react-router-dom';
import { removeQuery } from '../components/ScrollToTop.jsx';
import { Search as SearchIcon, CloudQueue as CloudQueueIcon, Code as CodeIcon, Close as CloseIcon, Folder as FolderIcon, LibraryBooks as LibraryBooksIcon } from '@mui/icons-material';
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 '@mui/material';
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 [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)
//toast("Thanks for submitting!")
} else {
setFormMessage(errorMessage)
}
setFormMail("")
setMessage("")
})
.catch(error => {
setFormMessage(errorMessage)
console.log(error)
});
}
const SearchBox = ({currentRefinement, refine, isSearchStalled} ) => {
var defaultSearch = ""
if (window !== undefined && window.location !== undefined && window.location.search !== undefined && window.location.search !== null) {
const urlSearchParams = new URLSearchParams(window.location.search)
const params = Object.fromEntries(urlSearchParams.entries())
const foundQuery = params["q"]
if (foundQuery !== null && foundQuery !== undefined) {
console.log("Got query: ", foundQuery)
refine(foundQuery)
defaultSearch = foundQuery
}
}
return (
<form noValidate action="" role="search">
<TextField
defaultValue={defaultSearch}
fullWidth
style={{backgroundColor: theme.palette.inputColor, borderRadius: borderRadius, margin: 10, width: "100%",}}
InputProps={{
style:{
color: "white",
fontSize: "1em",
height: 50,
},
startAdornment: (
<InputAdornment position="start">
<SearchIcon style={{marginLeft: 5}}/>
</InputAdornment>
),
}}
autoComplete='off'
type="search"
color="primary"
placeholder="Search our Documentation..."
id="shuffle_search_field"
onChange={(event) => {
removeQuery("q")
refine(event.currentTarget.value)
}}
limit={5}
/>
{/*isSearchStalled ? 'My search is stalled' : ''*/}
</form>
)
}
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 (
<List>
{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 = <CodeIcon/>
const avatar = data.image_url === undefined ?
baseImage
:
<Avatar
src={data.image_url}
variant="rounded"
/>
var parsedUrl = data.urlpath !== undefined ? data.urlpath : ""
parsedUrl += `?queryID=${data.__queryID}`
return (
<Zoom key={index} in={true} style={{ transitionDelay: `${workflowDelay}ms` }}>
<Link key={data.objectID} to={parsedUrl} style={{textDecoration: "none", color: "white",}} onClick={(event) => {
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")
}}>
<ListItem key={data.objectID} style={innerlistitemStyle} onMouseOver={() => {
setMouseHoverIndex(index)
}}>
<ListItemAvatar>
{avatar}
</ListItemAvatar>
<ListItemText
primary={name}
secondary={secondaryText}
/>
{/*
<ListItemSecondaryAction>
<IconButton edge="end" aria-label="delete">
<DeleteIcon />
</IconButton>
</ListItemSecondaryAction>
*/}
</ListItem>
</Link>
</Zoom>
)
})}
</List>
)
}
const CustomSearchBox = connectSearchBox(SearchBox)
const CustomHits = connectHits(Hits)
const selectButtonStyle = {
minWidth: 150,
maxWidth: 150,
minHeight: 50,
}
return (
<div style={{width: "100%", textAlign: "center", position: "relative", height: "100%", display: "flex"}}>
{/*
<div style={{padding: 10, }}>
<Button
style={selectButtonStyle}
variant="outlined"
onClick={() => {
const searchField = document.createElement("shuffle_search_field")
console.log("Field: ", searchField)
if (searchField !== null & searchField !== undefined) {
console.log("Set field.")
searchField.value = "WHAT WABALABA"
searchField.setAttribute("value", "WHAT WABALABA")
}
}}
>
Cases
</Button>
</div>
*/}
<div style={{width: "100%", position: "relative", height: "100%",}}>
<InstantSearch searchClient={searchClient} indexName="documentation">
<div style={{maxWidth: 450, margin: "auto", marginTop: 15, marginBottom: 15, }}>
<CustomSearchBox />
</div>
<Configure clickAnalytics />
<CustomHits hitsPerPage={5}/>
</InstantSearch>
{showSuggestion === true ?
<div style={{paddingTop: 0, maxWidth: isMobile ? "100%" : "60%", margin: "auto"}}>
<Typography variant="h6" style={{color: "white", marginTop: 50,}}>
Can't find what you're looking for?
</Typography>
<div style={{flex: "1", display: "flex", flexDirection: "row", textAlign: "center",}}>
<TextField
required
style={{flex: "1", marginRight: "15px", backgroundColor: theme.palette.inputColor}}
InputProps={{
style:{
color: "#ffffff",
},
}}
color="primary"
fullWidth={true}
placeholder="Email (optional)"
type="email"
id="email-handler"
autoComplete="email"
margin="normal"
variant="outlined"
onChange={e => setFormMail(e.target.value)}
/>
<TextField
required
style={{flex: "1", backgroundColor: theme.palette.inputColor}}
InputProps={{
style:{
color: "#ffffff",
},
}}
color="primary"
fullWidth={true}
placeholder="What are we missing?"
type=""
id="standard-required"
margin="normal"
variant="outlined"
autoComplete="off"
onChange={e => setMessage(e.target.value)}
/>
</div>
<Button
variant="contained"
color="primary"
style={buttonStyle}
disabled={message.length === 0}
onClick={() => {
submitContact(formMail, message)
}}
>
Submit
</Button>
<Typography style={{color: "white"}} variant="body2">{formMessage}</Typography>
</div>
: null
}
<span style={{position: "absolute", display: "flex", textAlign: "right", float: "right", right: 0, bottom: 120, }}>
<Typography variant="body2" color="textSecondary" style={{}}>
Search by
</Typography>
<a rel="noopener noreferrer" href="https://www.algolia.com/" target="_blank" style={{textDecoration: "none", color: "white"}}>
<img src={"/images/logo-algolia-nebula-blue-full.svg"} alt="Algolia logo" style={{height: 17, marginLeft: 5, marginTop: 3,}} />
</a>
</span>
</div>
</div>
)
}
export default DocsGrid;

View File

@ -0,0 +1,94 @@
import React, { useRef, useState } from "react";
import { useEffect } from "react";
import {
Backup as BackupIcon
} from "@mui/icons-material";
const dragOverStyle = {
backgroundColor: "rgba(0,0,0,0.8)",
border: "5px dashed white",
borderRadius: "8px",
width: "100%",
height: "100%",
position: "absolute",
overflow: "hidden",
zIndex: 100,
display: "flex",
alignItems: "center",
justifyContent: "center",
};
const Dropzone = ({ children, style, onDrop }) => {
const dropzoneRef = useRef(null);
const [dragging, setDragging] = useState(false);
let dragCounter = 0;
const handleDragOver = (e) => {
e.preventDefault();
e.stopPropagation();
};
const handleDragEnter = (e) => {
e.preventDefault();
e.stopPropagation();
dragCounter++;
if (e.dataTransfer.items && e.dataTransfer.items.length > 0)
setDragging(true);
};
const handleDragLeave = (e) => {
e.preventDefault();
e.stopPropagation();
dragCounter--;
if (dragCounter === 0) setDragging(false);
};
const handleDrop = (e) => {
e.preventDefault();
e.stopPropagation();
setDragging(false);
if (e.dataTransfer.files && e.dataTransfer.files.length > 0) {
onDrop(e);
e.dataTransfer.clearData();
dragCounter = 0;
}
};
useEffect(() => {
if (dropzoneRef === null || dropzoneRef === undefined || dropzoneRef.current === null || dropzoneRef.current === undefined) {
return
}
// Check if event listene exists for dropzoneRef.current
dropzoneRef.current.addEventListener("dragover", handleDragOver);
dropzoneRef.current.addEventListener("dragenter", handleDragEnter);
dropzoneRef.current.addEventListener("dragleave", handleDragLeave);
dropzoneRef.current.addEventListener("drop", handleDrop);
return () => {
if (dropzoneRef.current === null || dropzoneRef.current === undefined) {
return
}
dropzoneRef.current.removeEventListener("dragover", handleDragOver);
dropzoneRef.current.removeEventListener("dragenter", handleDragEnter);
dropzoneRef.current.removeEventListener("dragleave", handleDragLeave);
dropzoneRef.current.removeEventListener("drop", handleDrop);
};
}, [dropzoneRef]);
return (
<div ref={dropzoneRef} style={{ position: "relative", ...style }}>
{dragging && (
<div style={dragOverStyle}>
<BackupIcon fontSize="large" />
</div>
)}
{children}
</div>
);
};
export default Dropzone;

View File

@ -0,0 +1,601 @@
import React, { useEffect, useContext } from "react";
import theme from '../theme.jsx';
import { isMobile } from "react-device-detect"
import { MuiChipsInput } from "mui-chips-input";
import UsecaseSearch from "../components/UsecaseSearch.jsx"
import WorkflowGrid from "../components/WorkflowGrid.jsx"
import dayjs from 'dayjs';
import WorkflowTemplatePopup from "./WorkflowTemplatePopup.jsx";
import {
Badge,
Avatar,
Grid,
InputLabel,
Select,
ListSubheader,
Paper,
Tooltip,
Divider,
Button,
TextField,
IconButton,
Menu,
MenuItem,
FormControlLabel,
Chip,
Switch,
Typography,
Zoom,
CircularProgress,
Drawer,
Dialog,
DialogTitle,
DialogActions,
DialogContent,
OutlinedInput,
Checkbox,
ListItemText,
Radio,
RadioGroup,
FormControl,
FormLabel,
} from "@mui/material";
import {
DatePicker,
LocalizationProvider,
} from '@mui/x-date-pickers'
import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'
import {
ExpandLess as ExpandLessIcon,
ExpandMore as ExpandMoreIcon,
Publish as PublishIcon,
OpenInNew as OpenInNewIcon,
} from "@mui/icons-material";
const EditWorkflow = (props) => {
const { globalUrl, workflow, setWorkflow, modalOpen, setModalOpen, showUpload, usecases, setNewWorkflow, appFramework, isEditing, userdata, apps, } = props
const [_, setUpdate] = React.useState(""); // Used for rendering, don't remove
const [submitLoading, setSubmitLoading] = React.useState(false);
const [showMoreClicked, setShowMoreClicked] = React.useState(false);
const [innerWorkflow, setInnerWorkflow] = React.useState(workflow)
const [newWorkflowTags, setNewWorkflowTags] = React.useState(workflow.tags !== undefined && workflow.tags !== null ? JSON.parse(JSON.stringify(workflow.tags)) : [])
const [description, setDescription] = React.useState(workflow.description !== undefined ? workflow.description : "")
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 [dueDate, setDueDate] = React.useState(workflow.due_date !== undefined && workflow.due_date !== null && workflow.due_date !== 0 ? dayjs(workflow.due_date*1000) : dayjs().subtract(1, 'day'))
// 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) => {
//toast(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
const priority = userdata === undefined || userdata === null ? null : userdata.priorities.find(prio => prio.type === "usecase" && prio.active === true)
console.log("PRIO: ", priority)
var upload = "";
var total_count = 0
return (
<Drawer
open={modalOpen}
onClose={() => {
setModalOpen(false);
}}
PaperProps={{
style: {
color: "white",
minWidth: isMobile ? "90%" : 650,
maxWidth: isMobile ? "90%" : 650,
minHeight: 400,
paddingTop: 25,
paddingLeft: 50,
//minWidth: isMobile ? "90%" : newWorkflow === true ? 1000 : 550,
//maxWidth: isMobile ? "90%" : newWorkflow === true ? 1000 : 550,
},
}}
>
<DialogTitle style={{padding: 30, paddingBottom: 0, zIndex: 1000,}}>
<div style={{display: "flex"}}>
<div style={{flex: 1, color: "rgba(255,255,255,0.9)" }}>
<div style={{display: "flex"}}>
<Typography variant="h4" style={{flex: 9, }}>
{newWorkflow ? "New" : "Editing"} workflow
</Typography>
{newWorkflow === true ? null :
<div style={{ marginLeft: 5, flex: 1 }}>
<Tooltip title="Open Workflow Form for 'normal' users">
<a
rel="noopener noreferrer"
href={`/workflows/${workflow.id}/run`}
target="_blank"
style={{
textDecoration: "none",
color: "#f85a3e",
marginLeft: 5,
marginTop: 10,
}}
>
<OpenInNewIcon />
</a>
</Tooltip>
</div>
}
</div>
<Typography variant="body2" color="textSecondary" style={{marginTop: 20, maxWidth: 440,}}>
Workflows can be built from scratch, or from templates. <a href="/usecases" rel="noopener noreferrer" target="_blank" style={{ textDecoration: "none", color: "#f86a3e" }}>Usecases</a> can help you discover next steps, and you can <a href="/search?tab=workflows" rel="noopener noreferrer" target="_blank" style={{ textDecoration: "none", color: "#f86a3e" }}>search</a> for them directly. <a href="/docs/workflows" rel="noopener noreferrer" target="_blank" style={{ textDecoration: "none", color: "#f86a3e" }}>Learn more</a>
</Typography>
{showUpload === true ?
<div style={{ float: "right" }}>
<Tooltip color="primary" title={"Import manually"} placement="top">
<Button
color="primary"
style={{}}
variant="text"
onClick={() => upload.click()}
>
<PublishIcon />
</Button>
</Tooltip>
</div>
: null}
</div>
{/*newWorkflow === true ?
<div style={{flex: 1, marginLeft: 45, }}>
<Typography variant="h6">
Use a Template
</Typography>
<Typography variant="body2" color="textSecondary" style={{maxWidth: 440,}}>
Start your workflow from our templating system. This uses publied workflows from our <a href="/creators" rel="noopener noreferrer" target="_blank" style={{ textDecoration: "none", color: "#f86a3e"}}>Creators</a> to generate full Usecases or parts of your Workflow.
</Typography>
</div>
: null*/}
</div>
</DialogTitle>
<FormControl>
<DialogContent style={{paddingTop: 10, display: "flex", minHeight: 300, zIndex: 1001, }}>
<div style={{minWidth: newWorkflow ? 500 : 550, maxWidth: newWorkflow ? 450 : 500, }}>
<TextField
onChange={(event) => {
setName(event.target.value)
}}
InputProps={{
style: {
color: "white",
},
}}
color="primary"
placeholder="Name"
required
margin="dense"
defaultValue={innerWorkflow.name}
label="Name"
autoFocus
fullWidth
/>
<div style={{display: "flex", }}>
<TextField
onBlur={(event) => {
setDescription(event.target.value)
}}
InputProps={{
style: {
color: "white",
},
}}
maxRows={4}
color="primary"
defaultValue={innerWorkflow.description}
placeholder="Description"
multiline
label="Description"
margin="dense"
fullWidth
/>
</div>
<div style={{display: "flex", marginTop: 10, }}>
<MuiChipsInput
style={{ flex: 1, maxHeight: 40, }}
InputProps={{
style: {
color: "white",
},
}}
placeholder="Tags"
color="primary"
fullWidth
value={newWorkflowTags}
onChange={(chip) => {
console.log("Chip: ", chip)
//newWorkflowTags.push(chip);
setNewWorkflowTags(chip);
}}
onAdd={(chip) => {
newWorkflowTags.push(chip);
setNewWorkflowTags(newWorkflowTags);
}}
onDelete={(chip, index) => {
console.log("Deleting: ", chip, index)
newWorkflowTags.splice(index, 1);
setNewWorkflowTags(newWorkflowTags);
setUpdate(Math.random());
}}
/>
{usecases !== null && usecases !== undefined && usecases.length > 0 ?
<FormControl style={{flex: 1, marginLeft: 5, }}>
<InputLabel htmlFor="grouped-select-usecase">Usecases</InputLabel>
<Select
defaultValue=""
id="grouped-select"
label="Matching Usecase"
multiple
value={selectedUsecases}
renderValue={(selected) => selected.join(', ')}
onChange={(event) => {
console.log("Changed: ", event)
}}
>
<MenuItem value="">
<em>None</em>
</MenuItem>
{usecases.map((usecase, index) => {
//console.log(usecase)
return (
<span key={index}>
<ListSubheader
style={{color: usecase.color}}
>
{usecase.name}
</ListSubheader>
{usecase.list.map((subcase, subindex) => {
//console.log(subcase)
total_count += 1
return (
<MenuItem key={subindex} value={total_count} onClick={(event) => {
if (selectedUsecases.includes(subcase.name)) {
const itemIndex = selectedUsecases.indexOf(subcase.name)
if (itemIndex > -1) {
selectedUsecases.splice(itemIndex, 1)
}
} else {
selectedUsecases.push(subcase.name)
}
setUpdate(Math.random());
setSelectedUsecases(selectedUsecases)
}}>
<Checkbox style={{color: selectedUsecases.includes(subcase.name) ? usecase.color : theme.palette.inputColor}} checked={selectedUsecases.includes(subcase.name)} />
<ListItemText primary={subcase.name} />
</MenuItem>
)
})}
</span>
)
})}
</Select>
</FormControl>
: null}
</div>
{showMoreClicked === true ?
<span style={{marginTop: 25, }}>
<div style={{display: "flex"}}>
<FormControl style={{marginTop: 15, }}>
<FormLabel id="demo-row-radio-buttons-group-label">Status</FormLabel>
<RadioGroup
row
aria-labelledby="demo-row-radio-buttons-group-label"
name="row-radio-buttons-group"
defaultValue={innerWorkflow.status}
onChange={(e) => {
console.log("Data: ", e.target.value)
innerWorkflow.workflow_type = e.target.value
setInnerWorkflow(innerWorkflow)
}}
>
<FormControlLabel value="test" control={<Radio />} label="Test" />
<FormControlLabel value="production" control={<Radio />} label="Production" />
</RadioGroup>
</FormControl>
<LocalizationProvider dateAdapter={AdapterDayjs}>
<DatePicker
sx={{
marginTop: 3,
marginLeft: 3,
}}
value={dueDate}
label="Due Date"
format="YYYY-MM-DD"
onChange={(newValue) => {
setDueDate(newValue)
}}
/>
</LocalizationProvider>
</div>
<div />
<FormControl style={{marginTop: 15, }}>
<FormLabel id="demo-row-radio-buttons-group-label">Type</FormLabel>
<RadioGroup
row
aria-labelledby="demo-row-radio-buttons-group-label"
name="row-radio-buttons-group"
defaultValue={innerWorkflow.workflow_type}
onChange={(e) => {
console.log("Data: ", e.target.value)
innerWorkflow.workflow_type = e.target.value
setInnerWorkflow(innerWorkflow)
}}
>
<FormControlLabel value="trigger" control={<Radio />} label="Trigger" />
<FormControlLabel value="subflow" control={<Radio />} label="Subflow" />
<FormControlLabel value="standalone" control={<Radio />} label="Standalone" />
</RadioGroup>
</FormControl>
<TextField
onBlur={(event) => {
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
/>
<TextField
onBlur={(event) => {
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
/>
<TextField
onBlur={(event) => {
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
/>
</span>
: null}
<Tooltip color="primary" title={"Add more details"} placement="top">
<IconButton
style={{ color: "white", margin: "auto", marginTop: 10, textAlign: "center", width: 50,}}
onClick={() => {
setShowMoreClicked(!showMoreClicked);
}}
>
{showMoreClicked ? <ExpandLessIcon /> : <ExpandMoreIcon/>}
</IconButton>
</Tooltip>
</div>
</DialogContent>
<DialogActions style={{paddingRight: 100, }}>
<Button
style={{}}
onClick={() => {
if (setNewWorkflow !== undefined) {
setWorkflow({})
}
setModalOpen(false)
}}
color="primary"
>
Cancel
</Button>
<Button
variant="contained"
style={{}}
disabled={name.length === 0 || submitLoading === true}
onClick={() => {
setSubmitLoading(true)
innerWorkflow.name = name
innerWorkflow.description = description
if (newWorkflowTags.length > 0) {
innerWorkflow.tags = newWorkflowTags
}
if (selectedUsecases.length > 0) {
innerWorkflow.usecase_ids = selectedUsecases
}
if (dueDate > 0) {
innerWorkflow.due_date = new Date(`${dueDate["$y"]}-${dueDate["$M"]+1}-${dueDate["$D"]}`).getTime()/1000
}
if (setNewWorkflow !== undefined) {
setNewWorkflow(
innerWorkflow.name,
innerWorkflow.description,
innerWorkflow.tags,
innerWorkflow.default_return_value,
innerWorkflow,
newWorkflow,
innerWorkflow.usecase_ids,
innerWorkflow.blogpost,
innerWorkflow.status,
)
setWorkflow({})
} else {
setWorkflow(innerWorkflow)
console.log("editing workflow: ", innerWorkflow)
}
setSubmitLoading(true)
// If new workflow, don't close it
if (isEditing) {
setModalOpen(false)
}
}}
color="primary"
>
{submitLoading ? <CircularProgress color="secondary" /> : "Done"}
</Button>
</DialogActions>
{newWorkflow === true ?
<span style={{marginTop: 30, }}>
<Typography variant="h6" style={{marginLeft: 30, paddingBottom: 0, }}>
Relevant Workflows
</Typography>
{priority === null || priority === undefined ? null :
<div style={{marginLeft: 30, }}>
<WorkflowTemplatePopup
userdata={userdata}
globalUrl={globalUrl}
srcapp={priority.description.split("&").length > 2 ? priority.description.split("&")[0] : ""}
img1={priority.description.split("&").length > 2 ? priority.description.split("&")[1] : ""}
dstapp={priority.description.split("&").length > 3 ? priority.description.split("&")[2] : ""}
img2={priority.description.split("&").length > 3 ? priority.description.split("&")[3] : ""}
title={priority.name}
description={priority.description.split("&").length > 4 ? priority.description.split("&")[4] : ""}
apps={apps}
/>
</div>
}
</span>
: null}
{newWorkflow === true && name.length > 2 ?
<div style={{marginLeft: 30, }}>
<WorkflowGrid
maxRows={1}
globalUrl={globalUrl}
showSuggestions={false}
isMobile={isMobile}
userdata={userdata}
inputsearch={name+description+newWorkflowTags.join(" ")}
parsedXs={6}
alternativeView={false}
onlyResults={true}
/>
</div>
: null}
</FormControl>
</Drawer>
)
}
export default EditWorkflow;

View File

@ -0,0 +1,382 @@
import React, { useState, useEffect } from "react";
import ReactGA from 'react-ga4';
import 'react-alice-carousel/lib/alice-carousel.css';
import TrendingFlatIcon from '@mui/icons-material/TrendingFlat';
import theme from '../theme.jsx';
import CheckBoxSharpIcon from '@mui/icons-material/CheckBoxSharp';
import { findSpecificApp } from "../components/AppFramework.jsx"
import {
Checkbox,
Button,
Collapse,
IconButton,
FormGroup,
FormControl,
InputLabel,
FormLabel,
FormControlLabel,
Select,
MenuItem,
Grid,
Paper,
Typography,
TextField,
Zoom,
List,
ListItem,
ListItemText,
Divider,
Tooltip,
Chip,
ButtonGroup,
Dialog,
DialogTitle,
DialogActions,
DialogContent,
} from "@mui/material";
import { useNavigate, Link } from "react-router-dom";
import WorkflowTemplatePopup from "../components/WorkflowTemplatePopup.jsx";
const ExploreWorkflow = (props) => {
const { userdata, globalUrl, appFramework } = props
const [activeUsecases, setActiveUsecases] = useState(0);
const [modalOpen, setModalOpen] = React.useState(false);
const [suggestedUsecases, setSuggestedUsecases] = useState([])
const [usecasesSet, setUsecasesSet] = useState(false)
const [apps, setApps] = useState([])
const sizing = 475
let navigate = useNavigate();
const imagestyle = {
height: 40,
borderRadius: 40,
//border: "2px solid rgba(255,255,255,0.3)",
}
const loadApps = () => {
fetch(`${globalUrl}/api/v1/apps`, {
method: "GET",
headers: {
"Content-Type": "application/json",
Accept: "application/json",
},
credentials: "include",
})
.then((response) => {
return response.json();
})
.then((responseJson) => {
if (responseJson === null) {
console.log("null-response from server")
const pretend_apps = [{
"name": "TBD",
"app_name": "TBD",
"app_version": "TBD",
"description": "TBD",
"version": "TBD",
"large_image": "",
}]
setApps(pretend_apps)
return
}
if (responseJson.success === false) {
console.log("error loading apps: ", responseJson)
return
}
setApps(responseJson);
})
.catch((error) => {
console.log("App loading error: " + error.toString());
})
}
// Find priorities in userdata.priorities and check if the item.type === "usecase"
// If so, set the item.isActive to true
if (usecasesSet === false && userdata.priorities !== undefined && userdata.priorities !== null && userdata.priorities.length > 0 && suggestedUsecases.length === 0) {
var tmpUsecases = []
for (let i = 0; i < userdata.priorities.length; i++) {
if (userdata.priorities[i].type !== "usecase" || userdata.priorities[i].active === false) {
continue
}
const descsplit = userdata.priorities[i].description.split("&")
if (descsplit.length === 5) {
console.log("descsplit: ", descsplit)
if (descsplit[1] === "") {
const item = findSpecificApp(appFramework, descsplit[0])
console.log("item: ", item)
if (item !== null) {
descsplit[1] = item.large_image
}
}
if (descsplit[3] === "") {
const item = findSpecificApp(appFramework, descsplit[2])
console.log("item: ", item)
if (item !== null) {
descsplit[3] = item.large_image
}
}
console.log("descsplit: ", descsplit)
userdata.priorities[i].description = descsplit.join("&")
}
tmpUsecases.push(userdata.priorities[i])
}
console.log("USECASES: ", tmpUsecases)
if (tmpUsecases.length === 0) {
console.log("Add some random ones, as everything is done")
const comms = findSpecificApp(appFramework, "communication")
const cases = findSpecificApp(appFramework, "cases")
const edr = findSpecificApp(appFramework, "edr")
const siem = findSpecificApp(appFramework, "siem")
tmpUsecases = [{
"name": "Suggested Usecase: Email management",
"description": comms.name+"&"+comms.large_image+"&"+cases.name+"&"+cases.large_image,
"type": "usecase",
"url": "/usecases?selected_object=Email management",
"severity": 0,
"active": false,
},{
"name": "Suggested Usecase: EDR to ticket",
"description": edr.name+"&"+edr.large_image+"&"+cases.name+"&"+cases.large_image,
"type": "usecase",
"url": "/usecases?selected_object=EDR to ticket",
"severity": 0,
"active": false,
},{
"name": "Suggested Usecase: SIEM to ticket",
"description": siem.name+"&"+siem.large_image+"&"+cases.name+"&"+cases.large_image,
"type": "usecase",
"url": "/usecases?selected_object=SIEM to ticket",
"severity": 0,
"active": false,
}
]
}
setSuggestedUsecases(tmpUsecases)
setUsecasesSet(true)
loadApps()
}
const modalView = (
// console.log("key:", dataValue.key),
//console.log("value:",dataValue.value),
<Dialog
open={modalOpen}
onClose={() => {
setModalOpen(false);
}}
PaperProps={{
style: {
backgroundColor: theme.palette.surfaceColor,
color: "white",
minWidth: "800px",
minHeight: "320px",
},
}}
>
<DialogTitle style={{}}>
<div style={{ color: "white", display: "flex", alignItems: "center", justifyContent: "center" }}>
<CheckBoxSharpIcon sx={{ borderRadius: 4, color: "rgba(255, 132, 68, 1)" }} style={{ width: 24 }} />
<span style={{ marginLeft: 8, color: "rgba(255, 132, 68, 1)", fontSize: 16, width: 60 }}>Sign Up</span>
<div style={{ borderTop: "1px solid rgba(255, 132, 68, 1)", width: 85, marginLeft: 8, marginRight: 8 }} />
<CheckBoxSharpIcon sx={{ borderRadius: 4, color: "rgba(255, 132, 68, 1)" }} style={{ width: 24 }} />
<span style={{ marginLeft: 8, color: "rgba(255, 132, 68, 1)", fontSize: 16, width: 60 }}>Setup</span>
<div style={{ borderTop: "1px solid rgba(255, 132, 68, 1)", width: 85, marginRight: 8 }} />
<CheckBoxSharpIcon sx={{ borderRadius: 4, color: "rgba(255, 132, 68, 1)" }} style={{ width: 24 }} />
<span style={{ marginLeft: 8, color: "rgba(255, 132, 68, 1)", fontSize: 16, width: 60 }}>Explore</span>
</div>
</DialogTitle>
<Typography style={{ fontSize: 16, width: 252, marginLeft: 167 }}>
Heres a recommended workflow:
</Typography>
{/* <div style={{ marginTop: 0, maxWidth: 700, minWidth: 700, margin: "auto", minHeight: sizing, maxHeight: sizing, }}>
<div style={{ marginTop: 0, }}>
<div className="thumbs" style={{ display: "flex" }}>
<Tooltip title={"Previous usecase"}>
<IconButton
style={{
// backgroundColor: thumbIndex === 0 ? "inherit" : "white",
zIndex: 5000,
minHeight: 50,
maxHeight: 50,
color: "grey",
marginTop: 150,
borderRadius: 50,
border: "1px solid rgba(255,255,255,0.3)",
}}
onClick={() => {
slidePrev()
}}
>
<ArrowBackIosNewIcon />
</IconButton>
</Tooltip>
<div style={{ minWidth: 554, maxWidth: 554, borderRadius: theme.palette.borderRadius, }}>
<AliceCarousel
style={{ backgroundColor: theme.palette.surfaceColor, minHeight: 750, maxHeight: 750, }}
items={formattedCarousel}
activeIndex={thumbIndex}
infiniteLoop
mouseTracking={false}
responsive={responsive}
// activeIndex={activeIndex}
controlsStrategy="responsive"
autoPlay={false}
infinite={true}
animationType="fadeout"
animationDuration={800}
disableButtonsControls
/>
</div>
<Tooltip title={"Next usecase"}>
<IconButton
style={{
backgroundColor: thumbIndex === usecaseButtons.length - 1 ? "inherit" : "white",
zIndex: 5000,
minHeight: 50,
maxHeight: 50,
color: "grey",
marginTop: 150,
borderRadius: 50,
border: "1px solid rgba(255,255,255,0.3)",
}}
onClick={() => {
slideNext()
}}
>
<ArrowForwardIosIcon />
</IconButton>
</Tooltip>
</div>
</div>
</div> */}
<DialogActions style={{ paddingLeft: "30px", paddingRight: '30px' }}>
<Button
style={{ borderRadius: "0px" }}
onClick={() => setModalOpen(false)}
color="primary"
>
Cancel
</Button>
<Button
variant="contained"
style={{ borderRadius: "0px" }}
onClick={() => {
console.log("hello")
}}
color="primary"
>
Submit
</Button>
</DialogActions>
</Dialog>
);
return (
<div style={{ marginTop: 0, margin: "auto", minHeight: sizing, maxHeight: sizing, }}>
{modalView}
<Typography variant="h4" style={{ marginLeft: 8, marginTop: 40, marginRight: 30, marginBottom: 0, }} color="rgba(241, 241, 241, 1)">
Start using workflows
</Typography>
<Typography variant="body2" style={{ marginLeft: 8, marginTop: 10, marginRight: 30, marginBottom: 40, }} color="rgba(158, 158, 158, 1)">
Based on what you selected workflows, here are our recommendations! You will see more of these later.
</Typography>
<div style={{ marginTop: 0, }}>
<div className="thumbs" style={{ display: "flex" }}>
<div style={{ minWidth: 554, maxWidth: 554, borderRadius: theme.palette.borderRadius, }}>
<Grid item xs={11} style={{}}>
{suggestedUsecases.length === 0 && usecasesSet ?
<Typography variant="h6" style={{ marginTop: 30, marginBottom: 50, }} color="rgba(158, 158, 158, 1)">
All Workflows are already added for your current apps!
</Typography>
:
suggestedUsecases.map((priority, index) => {
const srcapp = priority.description.split("&")[0]
var image1 = priority.description.split("&")[1]
var image2 = ""
var dstapp = ""
if (priority.description.split("&").length > 3) {
dstapp = priority.description.split("&")[2]
image2 = priority.description.split("&")[3]
}
const name = priority.name.replace("Suggested Usecase: ", "")
var description = ""
if (priority.description.split("&").length > 4) {
description = priority.description[4]
}
// FIXME: Should have a proper description
description = ""
return (
<WorkflowTemplatePopup
userdata={userdata}
globalUrl={globalUrl}
img1={image1}
srcapp={srcapp}
img2={image2}
dstapp={dstapp}
title={name}
description={description}
apps={apps}
/>
)
})}
</Grid>
<div>
<div style={{ marginTop: 32 }}>
<Typography variant="body2" style={{ fontSize: 16, marginTop: 24 }} color="rgba(158, 158, 158, 1)">
<Button variant="contained" type="submit"
fullWidth style={{
borderRadius: 200,
height: 51,
width: 464,
fontSize: 16,
padding: "16px 24px",
margin: "auto",
itemAlign: "center",
background: activeUsecases === 0 ? "rgba(47, 47, 47, 1)" : "linear-gradient(90deg, #F86744 0%, #F34475 100%)",
color: activeUsecases === 0? "rgba(158, 158, 158, 1)" : "rgba(241, 241, 241, 1)",
border: activeUsecases === 0 ? "1px solid rgba(158, 158, 158, 1)" : "none",
}}
onClick={() => {
navigate("/workflows?message="+activeUsecases+" workflows added")
}}>
Continue to workflows
</Button>
</Typography>
</div>
<Typography variant="body2" style={{ fontSize: 16, marginTop: 24 }} color="rgba(158, 158, 158, 1)">
<Link style={{ color: "#f86a3e", marginLeft: 145 }} to="/usecases" className="btn btn-primary">
Explore usecases
</Link>
</Typography>
</div>
</div>
</div>
</div>
</div>
)
}
export default ExploreWorkflow

View File

@ -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,<svg fill="rgb(248,90,62)" width="${svgSize}" height="${svgSize}" viewBox="0 0 ${svgSize} ${svgSize}" version="1.1" xmlns="http://www.w3.org/2000/svg"><path d="M15.6408 8.39233H18.0922V10.0287H15.6408V8.39233ZM0.115234 8.39233H2.56663V10.0287H0.115234V8.39233ZM9.92083 0.21051V2.66506H8.28656V0.21051H9.92083ZM3.31839 2.25596L5.05889 4.00687L3.89856 5.16051L2.15807 3.42596L3.31839 2.25596ZM13.1485 3.99869L14.8808 2.25596L16.0493 3.42596L14.3088 5.16051L13.1485 3.99869ZM9.10369 4.30142C10.404 4.30142 11.651 4.81863 12.5705 5.73926C13.4899 6.65989 14.0065 7.90854 14.0065 9.21051C14.0065 11.0269 13.0178 12.6141 11.5551 13.4651V14.9378C11.5551 15.1548 11.469 15.3629 11.3158 15.5163C11.1625 15.6698 10.9547 15.756 10.738 15.756H7.46943C7.25271 15.756 7.04487 15.6698 6.89163 15.5163C6.73839 15.3629 6.6523 15.1548 6.6523 14.9378V13.4651C5.18963 12.6141 4.2009 11.0269 4.2009 9.21051C4.2009 7.90854 4.71744 6.65989 5.63689 5.73926C6.55635 4.81863 7.80339 4.30142 9.10369 4.30142ZM10.738 16.5741V17.3923C10.738 17.6093 10.6519 17.8174 10.4986 17.9709C10.3454 18.1243 10.1375 18.2105 9.92083 18.2105H8.28656C8.06984 18.2105 7.862 18.1243 7.70876 17.9709C7.55552 17.8174 7.46943 17.6093 7.46943 17.3923V16.5741H10.738ZM8.28656 14.1196H9.92083V12.3769C11.3345 12.0169 12.3722 10.7323 12.3722 9.21051C12.3722 8.34253 12.0279 7.5101 11.4149 6.89634C10.8019 6.28259 9.97056 5.93778 9.10369 5.93778C8.23683 5.93778 7.40546 6.28259 6.79249 6.89634C6.17953 7.5101 5.83516 8.34253 5.83516 9.21051C5.83516 10.7323 6.87292 12.0169 8.28656 12.3769V14.1196Z" /></svg>'),
"template": true,
"actions": [{
"name": "Create Alert",
"description": "Create a ticket",
"parameters": [{
"name": "id",
},
{
"name": "name",
},
{
"name": "description",
"multiline": true,
},
],
}],
}]
export default extraApps

View File

@ -0,0 +1,23 @@
import React, {useState} from 'react';
const FAQItem = (props) => {
const { question, answer } = props
const [isExpanded, setIsExpanded] = useState(false)
return (
<Paper onClick={() => {
setIsExpanded(!isExpanded)
}}>
<Typography variant="body1">
{question}
</Typography>
<Typography variant="body2" color="textSecondary">
{answer}
</Typography>
</Paper>
)
}
export default FAQItem;

View File

@ -0,0 +1,810 @@
import React, { useState, useEffect } from "react";
import { toast } from 'react-toastify';
import {
IconButton,
List,
ListItem,
ListItemText,
ListItemAvatar,
ListItemSecondaryAction,
Tooltip,
Button,
FormControl,
InputLabel,
TextField,
Divider,
Select,
MenuItem,
} from "@mui/material";
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 "@mui/icons-material";
//import { useAlert
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) => {
toast(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) {
toast("Successfully deleted file " + file.name);
} else if (
responseJson.reason !== undefined &&
responseJson.reason !== null
) {
toast("Failed to delete file: " + responseJson.reason);
}
setTimeout(() => {
getFiles();
}, 1500);
console.log(responseJson);
})
.catch((error) => {
toast(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) {
toast("Failed getting file. Is it deleted?");
return;
}
return respdata
})
.then((responseData) => {
setFileContent(responseData);
//console.log("filecontent state ",fileContent);
})
.catch((error) => {
toast(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) {
toast("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) => {
toast(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 {
toast("Failed to upload file ", filename);
}
})
.catch((error) => {
toast("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!");
toast("File was created, but failed to upload.");
return;
}
return response.json();
})
.then((responseJson) => {
//console.log("RESPONSE: ", responseJson)
//setFiles(responseJson)
})
.catch((error) => {
toast(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();
//toast("Starting fileupload")
uploadFiles(files);
};
return (
<Dropzone
style={{
maxWidth: window.innerWidth > 1366 ? 1366 : 1200,
margin: "auto",
padding: 20,
}}
onDrop={uploadFile}
>
<div>
<div style={{ marginTop: 20, marginBottom: 20 }}>
<h2 style={{ display: "inline" }}>Files</h2>
<span style={{ marginLeft: 25 }}>
Files from Workflows.{" "}
<a
target="_blank"
rel="noopener noreferrer"
href="https://shuffler.io/docs/organizations#files"
style={{ textDecoration: "none", color: "#f85a3e" }}
>
Learn more
</a>
</span>
</div>
<Button
color="primary"
variant="contained"
onClick={() => {
upload.click();
}}
>
<PublishIcon /> Upload files
</Button>
{/* <FileCategoryInput
isSet={renderTextBox} /> */}
<input
hidden
type="file"
multiple
ref={(ref) => (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);
}}
/>
<Button
style={{ marginLeft: 5, marginRight: 15 }}
variant="contained"
color="primary"
onClick={() => getFiles()}
>
<CachedIcon />
</Button>
{fileNamespaces !== undefined &&
fileNamespaces !== null &&
fileNamespaces.length > 1 ? (
<FormControl style={{ minWidth: 150, maxWidth: 150 }}>
<InputLabel id="input-namespace-label">File Category</InputLabel>
<Select
labelId="input-namespace-select-label"
id="input-namespace-select-id"
style={{
color: "white",
minWidth: 150,
maxWidth: 150,
float: "right",
}}
value={selectedNamespace}
onChange={(event) => {
console.log("CHANGE NAMESPACE: ", event.target);
setSelectedNamespace(event.target.value);
}}
>
{fileNamespaces.map((data, index) => {
return (
<MenuItem
key={index}
value={data}
style={{ color: "white" }}
>
{data}
</MenuItem>
);
})}
</Select>
</FormControl>
) : null}
<div style={{display: "inline-flex", position:"relative"}}>
{renderTextBox ?
<Tooltip title={"Close"} style={{}} aria-label={""}>
<Button
style={{ marginLeft: 5, marginRight: 15 }}
color="primary"
onClick={() => {
setRenderTextBox(false);
console.log(" close clicked")
}}
>
<ClearIcon/>
</Button>
</Tooltip>
:
<Tooltip title={"Add new file category"} style={{}} aria-label={""}>
<Button
style={{ marginLeft: 5, marginRight: 15 }}
color="primary"
onClick={() => {
setRenderTextBox(true);
}}
>
<AddIcon/>
</Button>
</Tooltip> }
{renderTextBox && <TextField
onKeyPress={(event)=>{
handleKeyDown(event);
}}
InputProps={{
style: {
color: "white",
},
}}
color="primary"
placeholder="File category name"
required
margin="dense"
defaultValue={""}
autoFocus
/>}</div>
<CodeEditor
isCloud={isCloud}
expansionModalOpen={openEditor}
setExpansionModalOpen={setOpenEditor}
setcodedata = {setFileContent}
codedata={fileContent}
isFileEditor = {true}
key = {fileContent} //https://reactjs.org/docs/reconciliation.html#recursing-on-children
runUpdateText = {runUpdateText}
/>
<Divider
style={{
marginTop: 20,
marginBottom: 20,
backgroundColor: theme.palette.inputColor,
}}
/>
<List>
<ListItem>
<ListItemText
primary="Updated"
style={{ maxWidth: 225, minWidth: 225 }}
/>
<ListItemText
primary="Name"
style={{
maxWidth: 150,
minWidth: 150,
overflow: "hidden",
marginLeft: 10,
}}
/>
<ListItemText
primary="Workflow"
style={{ maxWidth: 100, minWidth: 100, overflow: "hidden" }}
/>
<ListItemText
primary="Md5"
style={{ minWidth: 300, maxWidth: 300, overflow: "hidden" }}
/>
<ListItemText
primary="Status"
style={{ minWidth: 75, maxWidth: 75, marginLeft: 10 }}
/>
<ListItemText
primary="Filesize"
style={{ minWidth: 125, maxWidth: 125 }}
/>
<ListItemText primary="Actions" />
</ListItem>
{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 (
<ListItem
key={index}
style={{
backgroundColor: bgColor,
maxHeight: 100,
overflow: "hidden",
}}
>
<ListItemText
style={{
maxWidth: 225,
minWidth: 225,
overflow: "hidden",
}}
primary={new Date(file.updated_at * 1000).toISOString()}
/>
<ListItemText
style={{
maxWidth: 150,
minWidth: 150,
overflow: "hidden",
marginLeft: 10,
}}
primary={file.filename}
/>
<ListItemText
primary={
file.workflow_id === "global" ? (
<IconButton
disabled={file.workflow_id === "global"}
>
<OpenInNewIcon
style={{
color:
file.workflow_id !== "global"
? "white"
: "grey",
}}
/>
</IconButton>
) : (
<Tooltip
title={"Go to workflow"}
style={{}}
aria-label={"Download"}
>
<span>
<a
rel="noopener noreferrer"
style={{
textDecoration: "none",
color: "#f85a3e",
}}
href={`/workflows/${file.workflow_id}`}
target="_blank"
>
<IconButton
disabled={file.workflow_id === "global"}
>
<OpenInNewIcon
style={{
color:
file.workflow_id !== "global"
? "white"
: "grey",
}}
/>
</IconButton>
</a>
</span>
</Tooltip>
)
}
style={{
minWidth: 100,
maxWidth: 100,
overflow: "hidden",
}}
/>
<ListItemText
primary={file.md5_sum}
style={{
minWidth: 300,
maxWidth: 300,
overflow: "hidden",
}}
/>
<ListItemText
primary={file.status}
style={{
minWidth: 75,
maxWidth: 75,
overflow: "hidden",
marginLeft: 10,
}}
/>
<ListItemText
primary={file.filesize}
style={{
minWidth: 125,
maxWidth: 125,
overflow: "hidden",
}}
/>
<ListItemText
primary=<span style={{ display:"inline"}}>
<Tooltip
title={`Edit File (${allowedFileTypes.join(", ")}). Max size 2MB`}
style={{}}
aria-label={"Edit"}
>
<span>
<IconButton
disabled={!iseditable}
style = {{padding: "6px"}}
onClick={() => {
setOpenEditor(true)
setOpenFileId(file.id)
readFileData(file)
}}
>
<EditIcon
style={{color: iseditable ? "white" : "grey",}}
/>
</IconButton>
</span>
</Tooltip>
<Tooltip
title={"Download file"}
style={{}}
aria-label={"Download"}
>
<span>
<IconButton
style = {{padding: "6px"}}
disabled={file.status !== "active"}
onClick={() => {
downloadFile(file);
}}
>
<CloudDownloadIcon
style={{
color:
file.status === "active"
? "white"
: "grey",
}}
/>
</IconButton>
</span>
</Tooltip>
<Tooltip
title={"Copy file ID"}
style={{}}
aria-label={"copy"}
>
<IconButton
style = {{padding: "6px"}}
onClick={() => {
const elementName = "copy_element_shuffle";
var copyText =
document.getElementById(elementName);
if (
copyText !== null &&
copyText !== undefined
) {
const clipboard = navigator.clipboard;
if (clipboard === undefined) {
toast(
"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");
toast(file.id + " copied to clipboard");
}
}}
>
<FileCopyIcon style={{ color: "white" }} />
</IconButton>
</Tooltip>
<Tooltip
title={"Delete file"}
style={{marginLeft: 15, }}
aria-label={"Delete"}
>
<span>
<IconButton
disabled={file.status !== "active"}
style = {{padding: "6px"}}
onClick={() => {
deleteFile(file);
}}
>
<DeleteIcon
style={{
color:
file.status === "active"
? "white"
: "grey",
}}
/>
</IconButton>
</span>
</Tooltip>
</span>
style={{
minWidth: 250,
maxWidth: 250,
// overflow: "hidden",
}}
/>
</ListItem>
);
})
}
</List>
</div>
</Dropzone>
)
}
export default Files;

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,245 @@
import React, { useState, useEffect } from 'react';
import {isMobile} from "react-device-detect";
import AppFramework, { usecases } from "../components/AppFramework.jsx";
import {Link} from 'react-router-dom';
import ReactGA from 'react-ga4';
import { Button, LinearProgress, Typography } from '@mui/material';
export const securityFramework = [
{
image: <path d="M15.6408 8.39233H18.0922V10.0287H15.6408V8.39233ZM0.115234 8.39233H2.56663V10.0287H0.115234V8.39233ZM9.92083 0.21051V2.66506H8.28656V0.21051H9.92083ZM3.31839 2.25596L5.05889 4.00687L3.89856 5.16051L2.15807 3.42596L3.31839 2.25596ZM13.1485 3.99869L14.8808 2.25596L16.0493 3.42596L14.3088 5.16051L13.1485 3.99869ZM9.10369 4.30142C10.404 4.30142 11.651 4.81863 12.5705 5.73926C13.4899 6.65989 14.0065 7.90854 14.0065 9.21051C14.0065 11.0269 13.0178 12.6141 11.5551 13.4651V14.9378C11.5551 15.1548 11.469 15.3629 11.3158 15.5163C11.1625 15.6698 10.9547 15.756 10.738 15.756H7.46943C7.25271 15.756 7.04487 15.6698 6.89163 15.5163C6.73839 15.3629 6.6523 15.1548 6.6523 14.9378V13.4651C5.18963 12.6141 4.2009 11.0269 4.2009 9.21051C4.2009 7.90854 4.71744 6.65989 5.63689 5.73926C6.55635 4.81863 7.80339 4.30142 9.10369 4.30142ZM10.738 16.5741V17.3923C10.738 17.6093 10.6519 17.8174 10.4986 17.9709C10.3454 18.1243 10.1375 18.2105 9.92083 18.2105H8.28656C8.06984 18.2105 7.862 18.1243 7.70876 17.9709C7.55552 17.8174 7.46943 17.6093 7.46943 17.3923V16.5741H10.738ZM8.28656 14.1196H9.92083V12.3769C11.3345 12.0169 12.3722 10.7323 12.3722 9.21051C12.3722 8.34253 12.0279 7.5101 11.4149 6.89634C10.8019 6.28259 9.97056 5.93778 9.10369 5.93778C8.23683 5.93778 7.40546 6.28259 6.79249 6.89634C6.17953 7.5101 5.83516 8.34253 5.83516 9.21051C5.83516 10.7323 6.87292 12.0169 8.28656 12.3769V14.1196Z" />,
text: "Cases",
description: "Case management"
},
{
image:
<path d="M6.93767 0C8.71083 0 10.4114 0.704386 11.6652 1.9582C12.919 3.21202 13.6234 4.91255 13.6234 6.68571C13.6234 8.34171 13.0165 9.864 12.0188 11.0366L12.2965 11.3143H13.1091L18.252 16.4571L16.7091 18L11.5662 12.8571V12.0446L11.2885 11.7669C10.116 12.7646 8.59367 13.3714 6.93767 13.3714C5.16451 13.3714 3.46397 12.667 2.21015 11.4132C0.956339 10.1594 0.251953 8.45888 0.251953 6.68571C0.251953 4.91255 0.956339 3.21202 2.21015 1.9582C3.46397 0.704386 5.16451 0 6.93767 0ZM6.93767 2.05714C4.36624 2.05714 2.3091 4.11429 2.3091 6.68571C2.3091 9.25714 4.36624 11.3143 6.93767 11.3143C9.5091 11.3143 11.5662 9.25714 11.5662 6.68571C11.5662 4.11429 9.5091 2.05714 6.93767 2.05714Z" />,
text: "SIEM",
description: "Case management"
},
{
image:
<path d="M11.223 10.971L3.85195 14.4L7.28095 7.029L14.652 3.6L11.223 10.971ZM9.25195 0C8.07006 0 6.89973 0.232792 5.8078 0.685084C4.71587 1.13738 3.72372 1.80031 2.88799 2.63604C1.20016 4.32387 0.251953 6.61305 0.251953 9C0.251953 11.3869 1.20016 13.6761 2.88799 15.364C3.72372 16.1997 4.71587 16.8626 5.8078 17.3149C6.89973 17.7672 8.07006 18 9.25195 18C11.6389 18 13.9281 17.0518 15.6159 15.364C17.3037 13.6761 18.252 11.3869 18.252 9C18.252 7.8181 18.0192 6.64778 17.5669 5.55585C17.1146 4.46392 16.4516 3.47177 15.6159 2.63604C14.7802 1.80031 13.788 1.13738 12.6961 0.685084C11.6042 0.232792 10.4338 0 9.25195 0ZM9.25195 8.01C8.98939 8.01 8.73758 8.1143 8.55192 8.29996C8.36626 8.48563 8.26195 8.73744 8.26195 9C8.26195 9.26256 8.36626 9.51437 8.55192 9.70004C8.73758 9.8857 8.98939 9.99 9.25195 9.99C9.51452 9.99 9.76633 9.8857 9.95199 9.70004C10.1376 9.51437 10.242 9.26256 10.242 9C10.242 8.73744 10.1376 8.48563 9.95199 8.29996C9.76633 8.1143 9.51452 8.01 9.25195 8.01Z" />,
text: "Assets",
description: "Case management"
},
{
image:
<path d="M13.3318 2.223C13.2598 2.223 13.1878 2.205 13.1248 2.169C11.3968 1.278 9.90284 0.9 8.11184 0.9C6.32984 0.9 4.63784 1.323 3.09884 2.169C2.88284 2.286 2.61284 2.205 2.48684 1.989C2.36984 1.773 2.45084 1.494 2.66684 1.377C4.34084 0.468 6.17684 0 8.11184 0C10.0288 0 11.7028 0.423 13.5388 1.368C13.7638 1.485 13.8448 1.755 13.7278 1.971C13.6468 2.133 13.4938 2.223 13.3318 2.223ZM0.452843 6.948C0.362843 6.948 0.272843 6.921 0.191843 6.867C-0.015157 6.723 -0.0601571 6.444 0.0838429 6.237C0.974843 4.977 2.10884 3.987 3.45884 3.294C6.28484 1.836 9.90284 1.827 12.7378 3.285C14.0878 3.978 15.2218 4.959 16.1128 6.21C16.2568 6.408 16.2118 6.696 16.0048 6.84C15.7978 6.984 15.5188 6.939 15.3748 6.732C14.5648 5.598 13.5388 4.707 12.3238 4.086C9.74084 2.763 6.43784 2.763 3.86384 4.095C2.63984 4.725 1.61384 5.625 0.803843 6.759C0.731843 6.885 0.596843 6.948 0.452843 6.948ZM6.07784 17.811C5.96084 17.811 5.84384 17.766 5.76284 17.676C4.97984 16.893 4.55684 16.389 3.95384 15.3C3.33284 14.193 3.00884 12.843 3.00884 11.394C3.00884 8.721 5.29484 6.543 8.10284 6.543C10.9108 6.543 13.1968 8.721 13.1968 11.394C13.1968 11.646 12.9988 11.844 12.7468 11.844C12.4948 11.844 12.2968 11.646 12.2968 11.394C12.2968 9.216 10.4158 7.443 8.10284 7.443C5.78984 7.443 3.90884 9.216 3.90884 11.394C3.90884 12.69 4.19684 13.887 4.74584 14.859C5.32184 15.894 5.71784 16.335 6.41084 17.037C6.58184 17.217 6.58184 17.496 6.41084 17.676C6.31184 17.766 6.19484 17.811 6.07784 17.811ZM12.5308 16.146C11.4598 16.146 10.5148 15.876 9.74084 15.345C8.39984 14.436 7.59884 12.96 7.59884 11.394C7.59884 11.142 7.79684 10.944 8.04884 10.944C8.30084 10.944 8.49884 11.142 8.49884 11.394C8.49884 12.663 9.14684 13.86 10.2448 14.598C10.8838 15.03 11.6308 15.237 12.5308 15.237C12.7468 15.237 13.1068 15.21 13.4668 15.147C13.7098 15.102 13.9438 15.264 13.9888 15.516C14.0338 15.759 13.8718 15.993 13.6198 16.038C13.1068 16.137 12.6568 16.146 12.5308 16.146ZM10.7218 18C10.6858 18 10.6408 17.991 10.6048 17.982C9.17384 17.586 8.23784 17.055 7.25684 16.092C5.99684 14.841 5.30384 13.176 5.30384 11.394C5.30384 9.936 6.54584 8.748 8.07584 8.748C9.60584 8.748 10.8478 9.936 10.8478 11.394C10.8478 12.357 11.6848 13.14 12.7198 13.14C13.7548 13.14 14.5918 12.357 14.5918 11.394C14.5918 8.001 11.6668 5.247 8.06684 5.247C5.51084 5.247 3.17084 6.669 2.11784 8.874C1.76684 9.603 1.58684 10.458 1.58684 11.394C1.58684 12.096 1.64984 13.203 2.18984 14.643C2.27984 14.877 2.16284 15.138 1.92884 15.219C1.69484 15.309 1.43384 15.183 1.35284 14.958C0.911843 13.779 0.695843 12.609 0.695843 11.394C0.695843 10.314 0.902843 9.333 1.30784 8.478C2.50484 5.967 5.15984 4.338 8.06684 4.338C12.1618 4.338 15.4918 7.497 15.4918 11.385C15.4918 12.843 14.2498 14.031 12.7198 14.031C11.1898 14.031 9.94784 12.843 9.94784 11.385C9.94784 10.422 9.11084 9.639 8.07584 9.639C7.04084 9.639 6.20384 10.422 6.20384 11.385C6.20384 12.924 6.79784 14.364 7.88684 15.444C8.74184 16.29 9.56084 16.758 10.8298 17.109C11.0728 17.172 11.2078 17.424 11.1448 17.658C11.0998 17.865 10.9108 18 10.7218 18Z" />,
text: "IAM",
description: "Case management"
},
{
image: <path d="M16.1091 8.57143H14.8234V5.14286C14.8234 4.19143 14.052 3.42857 13.1091 3.42857H9.68052V2.14286C9.68052 1.57454 9.45476 1.02949 9.0529 0.627628C8.65103 0.225765 8.10599 0 7.53767 0C6.96935 0 6.4243 0.225765 6.02244 0.627628C5.62057 1.02949 5.39481 1.57454 5.39481 2.14286V3.42857H1.96624C1.51158 3.42857 1.07555 3.60918 0.754056 3.93067C0.432565 4.25216 0.251953 4.6882 0.251953 5.14286V8.4H1.53767C2.82338 8.4 3.85195 9.42857 3.85195 10.7143C3.85195 12 2.82338 13.0286 1.53767 13.0286H0.251953V16.2857C0.251953 16.7404 0.432565 17.1764 0.754056 17.4979C1.07555 17.8194 1.51158 18 1.96624 18H5.22338V16.7143C5.22338 15.4286 6.25195 14.4 7.53767 14.4C8.82338 14.4 9.85195 15.4286 9.85195 16.7143V18H13.1091C13.5638 18 13.9998 17.8194 14.3213 17.4979C14.6428 17.1764 14.8234 16.7404 14.8234 16.2857V12.8571H16.1091C16.6774 12.8571 17.2225 12.6314 17.6243 12.2295C18.0262 11.8277 18.252 11.2826 18.252 10.7143C18.252 10.146 18.0262 9.60092 17.6243 9.19906C17.2225 8.79719 16.6774 8.57143 16.1091 8.57143Z" />,
text: "Intel",
description: "Case management"
},
{
image:
<path d="M9.89516 7.71433H8.60945V5.1429H9.89516V7.71433ZM9.89516 10.2858H8.60945V9.00004H9.89516V10.2858ZM14.3952 2.57147H4.10944C3.76845 2.57147 3.44143 2.70693 3.20031 2.94805C2.95919 3.18917 2.82373 3.51619 2.82373 3.85719V15.4286L5.39516 12.8572H14.3952C14.7362 12.8572 15.0632 12.7217 15.3043 12.4806C15.5454 12.2395 15.6809 11.9125 15.6809 11.5715V3.85719C15.6809 3.14361 15.1023 2.57147 14.3952 2.57147Z" />,
text: "Comms",
description: "Case management"
},
{
image:
<path d="M0.251953 10.6011H3.8391L9.38052 -4.92572e-08L10.8977 11.5696L15.0377 6.28838L19.3191 10.6011H23.3948V13.1836H18.252L15.2562 10.175L9.1491 18L7.88909 8.41894L5.39481 13.1836H0.251953V10.6011Z" />,
text: "Network",
description: "Case management"
},
{
image:
<path d="M19.1722 8.9957L17.0737 6.60487L17.3661 3.44004L14.2615 2.73483L12.6361 -3.28068e-08L9.71206 1.25561L6.78803 -3.28068e-08L5.16261 2.73483L2.05797 3.43144L2.35038 6.59627L0.251953 8.9957L2.35038 11.3865L2.05797 14.56L5.16261 15.2652L6.78803 18L9.71206 16.7358L12.6361 17.9914L14.2615 15.2566L17.3661 14.5514L17.0737 11.3865L19.1722 8.9957ZM10.5721 13.2957H8.85205V11.5757H10.5721V13.2957ZM10.5721 9.85571H8.85205V4.69565H10.5721V9.85571Z" />,
text: "EDR & AV",
description: "Case management"
},
]
const LandingpageUsecases = (props) => {
const [selectedUsecase, setSelectedUsecase] = useState("Phishing")
const usecasekeys = usecases === undefined || usecases === null ? [] : Object.keys(usecases)
const buttonBackground = "linear-gradient(to right, #f86a3e, #f34079)"
const buttonStyle = {borderRadius: 25, height: 50, width: 260, margin: isMobile ? "15px auto 15px auto" : 20, fontSize: 18, backgroundImage: buttonBackground}
const HandleTitle = (props) => {
const { usecases, selectedUsecase, setSelecedUsecase } = props
const [progress, setProgress] = useState(0)
useEffect(() => {
const timer = setInterval(() => {
setProgress((oldProgress) => {
if (oldProgress >= 105) {
const foundIndex = usecasekeys.findIndex(key => key === selectedUsecase)
var newitem = usecasekeys[foundIndex+1]
if (newitem === undefined || newitem === 0) {
newitem = usecasekeys[1]
}
setSelectedUsecase(newitem)
return -18
}
if (oldProgress >= 65) {
return oldProgress + 3
}
if (oldProgress >= 80) {
return oldProgress + 1
}
return oldProgress + 6
})
}, 165)
return () => {
clearInterval(timer)
}
}, [])
if (usecases === null || usecases === undefined || usecases.length === 0) {
return null
}
const modifier = isMobile ? 17 : 22
return (
<span style={{margin: "auto", textAlign: isMobile ? "center" : "left", width: isMobile ? 280 : "100%",}}>
<b>Handle <br/>
<span style={{marginBottom: 10}}>
<i id="usecase-text">{selectedUsecase}</i>
<LinearProgress variant="determinate" value={progress} style={{marginTop: 0, marginBottom: 0, height: 3, width: isMobile ? "100%" : selectedUsecase.length*modifier, borderRadius: 10, }} />
</span>
with confidence</b>
</span>
)
}
const parsedWidth = isMobile ? "100%" : 1100
return (
<div style={{width: isMobile ? null : parsedWidth, margin: isMobile ? "0px 0px 0px 0px" : "auto", color: "white", textAlign: isMobile ? "center" : "left",}}>
<div style={{display: "flex", position: "relative",}}>
<div style={{maxWidth: isMobile ? "100%" : 420, paddingTop: isMobile ? 0 : 120, zIndex: 1000, margin: "auto",}}>
<Typography variant="h1" style={{margin: "auto", width: isMobile ? 280 : "100%", marginTop: isMobile ? 50 : 0}}>
<HandleTitle usecases={usecases} selectedUsecase={selectedUsecase} setSelectedUsecase={setSelectedUsecase} />
{/*<b>Security Automation <i>is Hard</i></b>*/}
</Typography>
<Typography variant="h6" style={{marginTop: isMobile ? 15 : 0,}}>
Connecting your everchanging environment is hard. We get it! That's why we built Shuffle, where you can use and share your security workflows to everyones benefit.
{/*Shuffle is an automation platform where you don't need to be an expert to automate. Get access to our large pool of security playbooks, apps and people.*/}
</Typography>
<div style={{display: "flex", textAlign: "center", itemAlign: "center",}}>
{isMobile ? null :
<Link rel="noopener noreferrer" to={"/pricing"} style={{textDecoration: "none"}}>
<Button
variant="contained"
onClick={() => {
ReactGA.event({
category: "landingpage",
action: "click_main_pricing",
label: "",
})
}}
style={{
borderRadius: 25, height: 40, width: 175, margin: "15px 0px 15px 0px", fontSize: 14, color: "white", backgroundImage: buttonBackground, marginRight: 10,
}}>
See Pricing
</Button>
</Link>
}
{isMobile ? null :
<Link rel="noopener noreferrer" to={"/register?message=You'll need to sign up first. No name, company or credit card required."} style={{textDecoration: "none"}}>
<Button
variant="contained"
onClick={() => {
ReactGA.event({
category: "landingpage",
action: "click_main_try_it_out",
label: "",
})
}}
style={{
borderRadius: 25, height: 40, width: 175, margin: "15px 0px 15px 0px", fontSize: 14, color: "white", backgroundImage: buttonBackground,
}}>
Start for free
</Button>
</Link>
}
</div>
</div>
{isMobile ? null :
<div style={{marginLeft: 200, marginTop: 125, zIndex: 1000}}>
<AppFramework showOptions={false} selectedOption={selectedUsecase} rolling={true} />
</div>
}
{isMobile ? null :
<div style={{position: "absolute", top: 50, right: -200, zIndex: 0, }}>
<svg width="351" height="433" viewBox="0 0 351 433" fill="none" xmlns="http://www.w3.org/2000/svg" style={{zIndex: 0, }}>
<path d="M167.781 184.839C167.781 235.244 208.625 276.104 259.03 276.104C309.421 276.104 350.28 235.244 350.28 184.839C350.28 134.448 309.421 93.5892 259.03 93.5892C208.625 93.5741 167.781 134.433 167.781 184.839ZM330.387 184.839C330.387 224.263 298.439 256.195 259.03 256.195C219.621 256.195 187.674 224.248 187.674 184.839C187.674 145.43 219.636 113.483 259.03 113.483C298.439 113.483 330.387 145.43 330.387 184.839Z" fill="white" fill-opacity="0.2"/>
<path d="M167.781 387.368C167.781 412.578 188.203 433 213.398 433C238.593 433 259.03 412.578 259.03 387.368C259.03 362.157 238.608 341.735 213.398 341.735C188.187 341.735 167.781 362.172 167.781 387.368ZM249.076 387.368C249.076 407.08 233.095 423.046 213.398 423.046C193.686 423.046 177.72 407.065 177.72 387.368C177.72 367.671 193.686 351.69 213.398 351.69C233.095 351.705 249.076 367.671 249.076 387.368Z" fill="white" fill-opacity="0.2"/>
<path d="M56.8637 0.738726C25.7052 0.738724 0.44632 25.9976 0.446317 57.1561C0.446314 88.3146 25.7052 113.573 56.8637 113.573C88.0221 113.573 113.281 88.3146 113.281 57.1561C113.281 25.9977 88.0222 0.738729 56.8637 0.738726Z" fill="white" fill-opacity="0.2"/>
</svg>
</div>
}
</div>
<div style={{display: "flex", width: isMobile ? "100%" : 300, itemAlign: "center", margin: "auto", marginTop: 20, flexDirection: isMobile ? "column" : "row", textAlign: "center",}}>
{isMobile ?
<Link rel="noopener noreferrer" to={"/pricing"} style={{textDecoration: "none"}}>
<Button
variant={isMobile ? "contained" : "outlined"}
color={isMobile ? "primary" : "secondary"}
style={buttonStyle}
onClick={() => {
ReactGA.event({
category: "landingpage",
action: "click_main_pricing",
label: "",
})
}}
>
See pricing
</Button>
</Link>
: null
}
{/*isMobile ?
<Link rel="noopener noreferrer" to={"/docs/features"} style={{textDecoration: "none"}}>
<Button
variant="outlined"
onClick={() => {
ReactGA.event({
category: "landingpage",
action: "click_main_features",
label: "",
})
}}
color="secondary"
style={buttonStyle}>
Features
</Button>
</Link>
: null*/}
</div>
{isMobile ? null :
<div style={{display: "flex", width: parsedWidth, margin: "auto", marginTop: 150}}>
{securityFramework.map((data, index) => {
return (
<div key={index} style={{flex: 1, textAlign: "center",}}>
<span style={{margin: "auto", width: 25,}}>
<svg width="25" height="25" fill="white" xmlns="http://www.w3.org/2000/svg" >
{data.image}
</svg>
</span>
<Typography variant="body2" style={{color: "white", marginRight: 5}}>
{data.text}
</Typography>
</div>
)
})}
</div>
}
</div>
)
}
export default LandingpageUsecases;

View File

@ -0,0 +1,106 @@
import React, {useState} from 'react';
import { useTheme } from '@mui/styles';
import {isMobile} from "react-device-detect";
import ReactGA from 'react-ga4';
import {
TextField,
Typography,
Button
} from '@mui/material';
const Newsletter = (props) => {
const { globalUrl, } = props;
const theme = useTheme();
const [email, setEmail] = useState("");
const [msg, setMsg] = useState("");
const [buttonActive, setButtonActive] = useState(true);
const buttonStyle = {minWidth: 300, borderRadius: 30, height: 60, width: 140, margin: isMobile ? "15px auto 15px auto" : "20px 20px 20px 10px", fontSize: 18,}
const newsletterSignup = (inemail) => {
if (inemail.length < 4) {
setMsg("Invalid email")
setButtonActive(true)
return
}
setButtonActive(false)
const data = {"email": inemail}
const url = globalUrl+'/api/v1/functions/newsletter_signup'
fetch(url, {
method: 'POST',
body: JSON.stringify(data),
headers: {
'Content-Type': 'application/json; charset=utf-8',
},
})
.then(response =>
response.json().then(responseJson => {
setButtonActive(true)
setMsg(responseJson["reason"])
if (responseJson["success"] === false) {
} else {
setEmail("")
}
}),
)
.catch(error => {
setMsg("Something went wrong: ", error.toString())
setButtonActive(true)
});
}
return (
<div style={{margin: "auto", color: "white", textAlign: "center",}}>
<Typography variant="h4" style={{marginTop: 35,}}>
Security Automation Newsletter
</Typography>
<Typography variant="h6" style={{color: "#7d7f82", marginTop: 20, }}>
Defensive security is 99% noise. Join us to sift through it.
</Typography>
<div style={{}}>
<TextField
style={{minWidth: isMobile ? "90%" : 450, backgroundColor: theme.palette.inputColor, marginTop: 20, borderRadius: 10, }}
InputProps={{
style:{
borderRadius: 10,
height: 60,
color: "white",
},
}}
color="primary"
value={email}
onChange={(e) => {
setEmail(e.target.value)
}}
placeholder="Your email"
id="standard-required"
margin="normal"
variant="outlined"
/>
</div>
<Button
variant="contained"
color="primary"
style={buttonStyle}
disabled={!buttonActive}
onClick={() => {
newsletterSignup(email)
ReactGA.event({
category: "newsletter",
action: `signup_click`,
label: "",
})
}}
>
Sign up
</Button>
<div/>
{msg}
</div>
)
}
export default Newsletter;

View File

@ -0,0 +1,937 @@
import React, { useRef, useState, useEffect, useLayoutEffect } from "react";
import { toast } from 'react-toastify';
import { useParams, useNavigate, Link } from "react-router-dom";
import theme from '../theme.jsx';
//import { useAlert
import { v4 as uuidv4 } from "uuid";
import {
ListItemText,
TextField,
Drawer,
Button,
Paper,
Grid,
Tabs,
InputAdornment,
Tab,
ButtonBase,
Tooltip,
Select,
MenuItem,
Divider,
Dialog,
Modal,
DialogActions,
DialogTitle,
InputLabel,
DialogContent,
FormControl,
IconButton,
Menu,
Input,
FormGroup,
FormControlLabel,
Typography,
Checkbox,
Breadcrumbs,
CircularProgress,
Switch,
Fade,
} from "@mui/material";
import {
LockOpen as LockOpenIcon,
SupervisorAccount as SupervisorAccountIcon,
} from "@mui/icons-material";
const ITEM_HEIGHT = 55;
const ITEM_PADDING_TOP = 8;
const MenuProps = {
PaperProps: {
style: {
maxHeight: ITEM_HEIGHT * 4.5 + ITEM_PADDING_TOP,
minWidth: 500,
maxWidth: 500,
scrollX: "auto",
},
},
variant: "menu",
getContentAnchorEl: null,
};
const registeredApps = [
"gmail",
"slack",
"webex",
"zoho_desk",
"outlook_graph",
"outlook_office365",
"microsoft_teams",
"microsoft_teams_user_access",
"todoist",
"microsoft_sentinel",
"microsoft_365_defender",
"google_chat",
"google_sheets",
"google_drive",
"google_disk",
"jira",
"jira_service_desk",
"jira_service_management",
"github",
]
const AuthenticationOauth2 = (props) => {
const {
saveWorkflow,
selectedApp,
workflow,
selectedAction,
authenticationType,
getAppAuthentication,
appAuthentication,
setSelectedAction,
setNewAppAuth,
isCloud,
autoAuth,
authButtonOnly,
isLoggedIn,
} = props;
let navigate = useNavigate();
//const alert = useAlert()
//const [update, setUpdate] = React.useState("|")
const [defaultConfigSet, setDefaultConfigSet] = React.useState(
authenticationType.client_id !== undefined &&
authenticationType.client_id !== null &&
authenticationType.client_id.length > 0 &&
authenticationType.client_secret !== undefined &&
authenticationType.client_secret !== null &&
authenticationType.client_secret.length > 0
);
const [clientId, setClientId] = React.useState(
defaultConfigSet ? authenticationType.client_id : ""
);
const [clientSecret, setClientSecret] = React.useState(
defaultConfigSet ? authenticationType.client_secret : ""
);
const [oauthUrl, setOauthUrl] = React.useState("");
const [buttonClicked, setButtonClicked] = React.useState(false);
const [offlineAccess, setOfflineAccess] = React.useState(true);
const allscopes = authenticationType.scope !== undefined ? authenticationType.scope : [];
const [selectedScopes, setSelectedScopes] = React.useState(allscopes.length === 1 ? [allscopes[0]] : [])
const [manuallyConfigure, setManuallyConfigure] = React.useState(
defaultConfigSet ? false : true
);
const [authenticationOption, setAuthenticationOptions] = React.useState({
app: JSON.parse(JSON.stringify(selectedApp)),
fields: {},
label: "",
usage: [
{
workflow_id: workflow !== undefined ? workflow.id : "",
},
],
id: uuidv4(),
active: true,
});
useEffect(() => {
if (isLoggedIn === false) {
navigate(`/login?view=${window.location.pathname}&message=Log in to authenticate this app`)
}
console.log("Should automatically click the auto-auth button?: ", autoAuth)
if (autoAuth === true && selectedApp !== undefined) {
startOauth2Request()
}
}, [])
if (selectedApp.authentication === undefined) {
return null;
}
const startOauth2Request = (admin_consent) => {
// Admin consent also means to add refresh tokens
console.log("Inside oauth2 request for app: ", selectedApp.name)
selectedApp.name = selectedApp.name.replace(" ", "_").toLowerCase()
//console.log("APP: ", selectedApp)
if (selectedApp.name.toLowerCase() == "outlook_graph" || selectedApp.name.toLowerCase() == "outlook_office365") {
handleOauth2Request(
"efe4c3fe-84a1-4821-a84f-23a6cfe8e72d",
"",
"https://graph.microsoft.com",
["Mail.ReadWrite", "Mail.Send", "offline_access"],
admin_consent,
);
} else if (selectedApp.name.toLowerCase() == "gmail") {
handleOauth2Request(
"253565968129-6ke8086pkp0at16m8t95rdcsas69ngt1.apps.googleusercontent.com",
"",
"https://gmail.googleapis.com",
["https://www.googleapis.com/auth/gmail.modify",
"https://www.googleapis.com/auth/gmail.send",
"https://www.googleapis.com/auth/gmail.insert",
"https://www.googleapis.com/auth/gmail.compose",
],
admin_consent,
"select_account%20consent",
)
} else if (selectedApp.name.toLowerCase() == "zoho_desk") {
handleOauth2Request(
"1000.ZR5MHUW6B0L6W1VUENFGIATFS0TOJT",
"",
"https://desk.zoho.com",
["Desk.tickets.READ",
"Desk.tickets.UPDATE",
"Desk.tickets.DELETE",
"Desk.tickets.CREATE",
"offline_access"],
admin_consent,
)
} else if (selectedApp.name.toLowerCase() == "slack") {
handleOauth2Request(
"5155508477298.5168162485601",
"",
"https://slack.com",
["chat:write:user", "im:read", "im:write", "search:read", "usergroups:read", "usergroups:write",],
admin_consent,
)
} else if (selectedApp.name.toLowerCase() == "webex") {
handleOauth2Request(
"Cab184f3d7271f540443c79b5b79845e3387abbbdb3db4233a87ea3a5432fb3d5",
"",
"https://webexapis.com",
["spark:all"],
admin_consent,
)
} else if (selectedApp.name.toLowerCase().includes("microsoft_teams")) {
handleOauth2Request(
"31cb4c84-658e-43d5-ae84-22c9142e967a",
"",
"https://graph.microsoft.com",
["ChannelMessage.Edit", "ChannelMessage.Read.All", "ChannelMessage.Send", "Chat.Create", "Chat.ReadWrite", "Chat.Read", "offline_access", "Team.ReadBasic.All"],
admin_consent,
)
} else if (selectedApp.name.toLowerCase().includes("todoist")) {
handleOauth2Request(
"35fa3a384040470db0c8527e90a3c2eb",
"",
"https://api.todoist.com",
["task:add",],
admin_consent,
)
} else if (selectedApp.name.toLowerCase().includes("microsoft_sentinel")) {
handleOauth2Request(
"4c16e8c4-3d34-4aa1-ac94-262ea170b7f7",
"",
"https://management.azure.com",
["https://management.azure.com/user_impersonation",],
admin_consent,
)
} else if (selectedApp.name.toLowerCase().includes("microsoft_365_defender")) {
handleOauth2Request(
"4c16e8c4-3d34-4aa1-ac94-262ea170b7f7",
"",
"https://graph.microsoft.com",
["SecurityEvents.ReadWrite.All",],
admin_consent,
)
} else if (selectedApp.name.toLowerCase().includes("google_sheets")) {
handleOauth2Request(
"253565968129-mppu17aciek8slr3kpgnb37hp86dmvmb.apps.googleusercontent.com",
"",
"https://sheets.googleapis.com",
["https://www.googleapis.com/auth/spreadsheets"],
admin_consent,
"consent",
)
} else if (selectedApp.name.toLowerCase().includes("google_drive") || selectedApp.name.toLowerCase().includes("google_disk")) {
handleOauth2Request(
"253565968129-6pij4g6ojim4gpum0h9m9u3bc357qsq7.apps.googleusercontent.com",
"",
"https://www.googleapis.com",
["https://www.googleapis.com/auth/drive",],
admin_consent,
"consent",
)
} else if (selectedApp.name.toLowerCase().includes("google_chat") || selectedApp.name.toLowerCase().includes("google_hangout")) {
handleOauth2Request(
"253565968129-6pij4g6ojim4gpum0h9m9u3bc357qsq7.apps.googleusercontent.com",
"",
"https://www.googleapis.com",
["https://www.googleapis.com/auth/chat.messages",],
admin_consent,
"consent",
)
} else if (selectedApp.name.toLowerCase().includes("jira_service_desk") || selectedApp.name.toLowerCase().includes("jira") || selectedApp.name.toLowerCase().includes("jira_service_management")) {
handleOauth2Request(
"AI02egeCQh1Zskm1QAJaaR6dzjR97V2F",
"",
"https://api.atlassian.com",
["read:jira-work", "write:jira-work", "read:servicedesk:jira-service-management", "write:servicedesk:jira-service-management", "read:request:jira-service-management", "write:request:jira-service-management",],
admin_consent,
)
} else if (selectedApp.name.toLowerCase().includes("github")) {
handleOauth2Request(
"3d272b1b782b100b1e61",
"",
"https://api.github.com",
["repo","user","project","notifications",],
admin_consent,
)
} else {
console.log("No match found for: ", selectedApp.name)
}
// write:request:jira-service-management
}
const handleOauth2Request = (client_id, client_secret, oauth_url, scopes, admin_consent, prompt) => {
setButtonClicked(true);
//console.log("SCOPES: ", scopes);
client_id = client_id.trim()
client_secret = client_secret.trim()
oauth_url = oauth_url.trim()
var resources = "";
if (scopes !== undefined && (scopes !== null) & (scopes.length > 0)) {
console.log("IN scope 1")
if (offlineAccess === true && !scopes.includes("offline_access")) {
console.log("IN scope 2")
if (!authenticationType.redirect_uri.includes("google")) {
console.log("Appending offline access")
scopes.push("offline_access")
}
}
resources = scopes.join(" ");
//resources = scopes.join(",");
}
const authentication_url = authenticationType.token_uri;
//console.log("AUTH: ", authenticationType)
//console.log("SCOPES2: ", resources)
const redirectUri = `${window.location.protocol}//${window.location.host}/set_authentication`;
const workflowId = workflow !== undefined ? workflow.id : "";
var state = `workflow_id%3D${workflowId}%26reference_action_id%3d${selectedAction.app_id}%26app_name%3d${selectedAction.app_name}%26app_id%3d${selectedAction.app_id}%26app_version%3d${selectedAction.app_version}%26authentication_url%3d${authentication_url}%26scope%3d${resources}%26client_id%3d${client_id}%26client_secret%3d${client_secret}`;
// This is to make sure authorization can be handled WITHOUT being logged in,
// kind of making it act like an api key
// https://shuffler.io/authorization -> 3rd party integration auth
const urlParams = new URLSearchParams(window.location.search);
const userAuth = urlParams.get("authorization");
if (userAuth !== undefined && userAuth !== null && userAuth.length > 0) {
console.log("Adding authorization from user side")
state += `%26authorization%3d${userAuth}`;
}
// Check for org_id
const orgId = urlParams.get("org_id");
if (orgId !== undefined && orgId !== null && orgId.length > 0) {
console.log("Adding org_id from user side")
state += `%26org_id%3d${orgId}`;
}
if (oauth_url !== undefined && oauth_url !== null && oauth_url.length > 0) {
state += `%26oauth_url%3d${oauth_url}`;
console.log("ADDING OAUTH2 URL: ", state);
}
if (
authenticationType.refresh_uri !== undefined &&
authenticationType.refresh_uri !== null &&
authenticationType.refresh_uri.length > 0
) {
state += `%26refresh_uri%3d${authenticationType.refresh_uri}`;
} else {
state += `%26refresh_uri%3d${authentication_url}`;
}
// No prompt forcing
//var url = `${authenticationType.redirect_uri}?client_id=${client_id}&redirect_uri=${redirectUri}&response_type=code&prompt=login&scope=${resources}&state=${state}&access_type=offline`;
var defaultPrompt = "login"
if (prompt !== undefined && prompt !== null && prompt.length > 0) {
defaultPrompt = prompt
}
var url = `${authenticationType.redirect_uri}?client_id=${client_id}&redirect_uri=${redirectUri}&response_type=code&prompt=${defaultPrompt}&scope=${resources}&state=${state}&access_type=offline`;
if (admin_consent === true) {
console.log("Running Oauth2 WITH admin consent")
//url = `${authenticationType.redirect_uri}?client_id=${client_id}&redirect_uri=${redirectUri}&response_type=code&prompt=consent&scope=${resources}&state=${state}&access_type=offline`;
url = `${authenticationType.redirect_uri}?client_id=${client_id}&redirect_uri=${redirectUri}&response_type=code&prompt=admin_consent&scope=${resources}&state=${state}&access_type=offline`;
}
console.log("URL: ", url)
// Force new consent
//const url = `${authenticationType.redirect_uri}?client_id=${client_id}&redirect_uri=${redirectUri}&response_type=code&scope=${resources}&prompt=consent&state=${state}&access_type=offline`;
// Admin consent
//const url = `https://accounts.zoho.com/oauth/v2/auth?response_type=code&client_id=${client_id}&scope=AaaServer.profile.Read&redirect_uri=${redirectUri}&prompt=consent`
// &resource=https%3A%2F%2Fgraph.microsoft.com&
// FIXME: Awful, but works for prototyping
// How can we get a callback properly realtime?
// How can we properly try-catch without breaks on error?
try {
var newwin = window.open(url, "", "width=582,height=700");
//console.log(newwin)
var open = true;
const timer = setInterval(() => {
if (newwin.closed) {
console.log("Closing?")
setButtonClicked(false);
clearInterval(timer);
//alert('"Secure Payment" window closed!');
//
if (getAppAuthentication !== undefined) {
getAppAuthentication(true, true, true);
}
} else {
console.log("Not closed")
}
}, 1000);
//do {
// setTimeout(() => {
// console.log(newwin)
// console.log("CLOSED", newwin.closed)
// if (newwin.closed) {
// open = false
// }
// }, 1000)
//}
//while(open === true)
} catch (e) {
toast(
"Failed authentication - probably bad credentials. Try again"
);
setButtonClicked(false);
}
return;
//do {
//} while (
};
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}`;
//toast("Label can't be empty")
//return
}
// 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 {
toast(
"Field " + selectedApp.authentication.parameters[key].name.replace("_basic", "", -1).replace("_", " ", -1) + " 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);
//appAuthentication.push(newAuthOption)
//setAppAuthentication(appAuthentication)
//
//if (configureWorkflowModalOpen) {
// setSelectedAction({})
//}
//setUpdate(authenticationOption.id)
/*
{selectedAction.authentication.map(data => (
<MenuItem key={data.id} style={{backgroundColor: inputColor, color: "white"}} value={data}>
*/
};
const handleScopeChange = (event) => {
const {
target: { value },
} = event;
console.log("VALUE: ", value);
// On autofill we get a the stringified value.
setSelectedScopes(typeof value === "string" ? value.split(",") : value);
};
if (
authenticationOption.label === null ||
authenticationOption.label === undefined
) {
authenticationOption.label = selectedApp.name + " authentication";
}
const autoAuthButton =
<Button
fullWidth
variant="contained"
style={{
marginBottom: 20,
marginTop: 20,
flex: 1,
textTransform: "none",
textAlign: "left",
justifyContent: "flex-start",
backgroundColor: "#ffffff",
color: "#2f2f2f",
borderRadius: theme.palette.borderRadius,
minWidth: 300,
maxWidth: 300,
maxHeight: 50,
overflow: "hidden",
border: `1px solid ${theme.palette.inputColor}`,
}}
color="primary"
disabled={
clientSecret.length > 0 || clientId.length > 0
}
fullWidth
onClick={() => {
// Hardcode some stuff?
// This could prolly be added to the app itself with a "default" client ID
startOauth2Request()
}}
color="primary"
>
{buttonClicked ? (
<CircularProgress style={{ color: "#f86a3e", width: 45, height: 45, margin: "auto", }} />
) : (
<span style={{display: "flex"}}>
<img
alt={selectedAction.app_name}
style={{ margin: 4, minHeight: 30, maxHeight: 30, borderRadius: theme.palette.borderRadius, }}
src={selectedAction.large_image}
/>
<Typography style={{ margin: 0, marginLeft: 10, marginTop: 5,}} variant="body1">
One-click Login
</Typography>
</span>
)}
</Button>
if (authButtonOnly === true) {
return autoAuthButton
}
return (
<div>
<DialogTitle>
<div style={{ color: "white" }}>
Authenticate {selectedApp.name.replaceAll("_", " ")}
</div>
</DialogTitle>
<DialogContent>
<span style={{}}>
Oauth2 requires a client ID and secret to authenticate, defined in the remote system. Your redirect URL is <b>{window.location.origin}/set_authentication</b>&nbsp;-&nbsp;
<a
target="_blank"
rel="norefferer"
href="/docs/apps#authentication"
style={{ textDecoration: "none", color: "#f85a3e" }}
>
{" "}
Learn more about Oauth2 with Shuffle
</a>
<div />
</span>
{isCloud && registeredApps.includes(selectedApp.name.toLowerCase()) ?
<span>
<span style={{display: "flex"}}>
{autoAuthButton}
{buttonClicked ?
null
:
<Tooltip
color="primary"
title={"Force Admin Consent"}
placement="top"
>
<Button
fullWidth
variant="outlined"
style={{
maxWidth: 50,
marginBottom: 20,
marginTop: 20,
maxHeight: 50,
}}
color="primary"
disabled={
clientSecret.length > 0 || clientId.length > 0
}
fullWidth
onClick={() => {
// Hardcode some stuff?
// This could prolly be added to the app itself with a "default" client ID
//startOauth2Request(true)
startOauth2Request()
}}
color="primary"
>
<SupervisorAccountIcon />
</Button>
</Tooltip>
}
</span>
<Typography style={{textAlign: "center", marginTop: 0, marginBottom: 10, }}>
OR
</Typography>
</span>
: null}
{/*<TextField
style={{backgroundColor: theme.palette.inputColor, borderRadius: theme.palette.borderRadius,}}
InputProps={{
style:{
},
}}
fullWidth
color="primary"
placeholder={"Auth july 2020"}
defaultValue={`Auth for ${selectedApp.name}`}
onChange={(event) => {
authenticationOption.label = event.target.value
}}
/>
<Divider style={{marginTop: 15, marginBottom: 15, backgroundColor: "rgb(91, 96, 100)"}}/>
*/}
{!manuallyConfigure ? null : (
<span>
{selectedApp.authentication.parameters.map((data, index) => {
//console.log(data, index)
if (data.name === "client_id" || data.name === "client_secret") {
return null;
}
if (data.name !== "url") {
return null;
}
if (oauthUrl.length === 0) {
setOauthUrl(data.value);
}
return (
<div key={index} style={{ marginTop: 10 }}>
<LockOpenIcon style={{ marginRight: 10 }} />
<b>{data.name}</b>
{data.schema !== undefined &&
data.schema !== null &&
data.schema.type === "bool" ? (
<Select
SelectDisplayProps={{
style: {
marginLeft: 10,
},
}}
defaultValue={"false"}
fullWidth
onChange={(e) => {
console.log("Value: ", e.target.value);
authenticationOption.fields[data.name] = e.target.value;
}}
style={{
backgroundColor: theme.palette.surfaceColor,
color: "white",
height: 50,
}}
>
<MenuItem
key={"false"}
style={{
backgroundColor: theme.palette.inputColor,
color: "white",
}}
value={"false"}
>
false
</MenuItem>
<MenuItem
key={"true"}
style={{
backgroundColor: theme.palette.inputColor,
color: "white",
}}
value={"true"}
>
true
</MenuItem>
</Select>
) : (
<TextField
style={{
backgroundColor: theme.palette.inputColor,
borderRadius: theme.palette.borderRadius,
}}
InputProps={{
style: {
},
}}
fullWidth
type={
data.example !== undefined &&
data.example.includes("***")
? "password"
: "text"
}
color="primary"
defaultValue={
data.value !== undefined && data.value !== null
? data.value
: ""
}
placeholder={data.example}
onChange={(event) => {
authenticationOption.fields[data.name] =
event.target.value;
console.log("Setting oauth url");
setOauthUrl(event.target.value);
//const [oauthUrl, setOauthUrl] = React.useState("")
}}
/>
)}
</div>
);
})}
<TextField
style={{
marginTop: 20,
backgroundColor: theme.palette.inputColor,
borderRadius: theme.palette.borderRadius,
}}
InputProps={{
style: {
},
}}
fullWidth
color="primary"
placeholder={"Client ID"}
onChange={(event) => {
setClientId(event.target.value);
//authenticationOption.label = event.target.value
}}
/>
<TextField
style={{
backgroundColor: theme.palette.inputColor,
borderRadius: theme.palette.borderRadius,
marginBottom: 10,
}}
InputProps={{
style: {
},
}}
fullWidth
color="primary"
placeholder={"Client Secret"}
onChange={(event) => {
setClientSecret(event.target.value);
//authenticationOption.label = event.target.value
}}
/>
{allscopes.length === 0 ? null : (
<div style={{width: "100%", marginTop: 10, display: "flex"}}>
<span>
Scopes
<Select
multiple
underline={false}
value={selectedScopes}
style={{
backgroundColor: theme.palette.inputColor,
color: "white",
padding: 5,
minWidth: 300,
maxWidth: 300,
}}
onChange={(e) => {
handleScopeChange(e)
}}
fullWidth
input={<Input id="select-multiple-native" />}
renderValue={(selected) => selected.join(", ")}
MenuProps={MenuProps}
>
{allscopes.map((data, index) => {
return (
<MenuItem key={index} value={data}>
<Checkbox checked={selectedScopes.indexOf(data) > -1} />
<ListItemText primary={data} />
</MenuItem>
);
})}
</Select>
</span>
<span>
<Tooltip
color="primary"
title={"Automatic Refresh (default: true)"}
placement="top"
>
<Checkbox style={{paddingTop: 20}} color="secondary" checked={offlineAccess} onClick={() => {
setOfflineAccess(!offlineAccess)
}}/>
</Tooltip>
</span>
</div>
)}
</span>
)}
<Button
style={{
marginBottom: 40,
marginTop: 20,
borderRadius: theme.palette.borderRadius,
}}
disabled={
clientSecret.length === 0 || clientId.length === 0 || buttonClicked || selectedScopes.length === 0
}
variant="contained"
fullWidth
onClick={() => {
handleOauth2Request(
clientId,
clientSecret,
oauthUrl,
selectedScopes
);
}}
color="primary"
>
{buttonClicked ? (
<CircularProgress style={{ color: "white" }} />
) : (
"Manually Authenticate"
)}
</Button>
{defaultConfigSet ? (
<span style={{}}>
... or
<Button
style={{
marginLeft: 10,
borderRadius: theme.palette.borderRadius,
}}
disabled={clientSecret.length === 0 || clientId.length === 0}
variant="text"
onClick={() => {
setManuallyConfigure(!manuallyConfigure);
if (manuallyConfigure) {
setClientId(authenticationType.client_id);
setClientSecret(authenticationType.client_secret);
} else {
setClientId("");
setClientSecret("");
}
}}
color="primary"
>
{manuallyConfigure
? "Use auto-config"
: "Manually configure Oauth2"}
</Button>
</span>
) : null}
</DialogContent>
</div>
);
};
export default AuthenticationOauth2;

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,873 @@
import React, { useEffect } from "react";
import { makeStyles } from "@mui/styles";
import theme from '../theme.jsx';
import { toast } from "react-toastify"
import {
FormControl,
InputLabel,
Paper,
OutlinedInput,
Checkbox,
Card,
Tooltip,
FormControlLabel,
Typography,
Switch,
Select,
MenuItem,
Divider,
TextField,
Button,
Tabs,
Tab,
Grid,
IconButton,
Autocomplete,
} from "@mui/material";
import {
ExpandLess as ExpandLessIcon,
ExpandMore as ExpandMoreIcon,
Save as SaveIcon,
} from "@mui/icons-material";
const useStyles = makeStyles({
notchedOutline: {
borderColor: "#f85a3e !important",
},
});
const OrgHeaderexpanded = (props) => {
const {
userdata,
selectedOrganization,
setSelectedOrganization,
globalUrl,
isCloud,
adminTab,
} = props;
const classes = useStyles();
const defaultBranch = "master";
const [orgName, setOrgName] = React.useState(selectedOrganization.name);
const [orgDescription, setOrgDescription] = React.useState(
selectedOrganization.description
);
const [appDownloadUrl, setAppDownloadUrl] = React.useState(
selectedOrganization.defaults === undefined
? "https://github.com/frikky/shuffle-apps"
: selectedOrganization.defaults.app_download_repo === undefined ||
selectedOrganization.defaults.app_download_repo.length === 0
? "https://github.com/frikky/shuffle-apps"
: selectedOrganization.defaults.app_download_repo
);
const [appDownloadBranch, setAppDownloadBranch] = React.useState(
selectedOrganization.defaults === undefined
? defaultBranch
: selectedOrganization.defaults.app_download_branch === undefined ||
selectedOrganization.defaults.app_download_branch.length === 0
? defaultBranch
: selectedOrganization.defaults.app_download_branch
);
const [workflowDownloadUrl, setWorkflowDownloadUrl] = React.useState(
selectedOrganization.defaults === undefined
? "https://github.com/frikky/shuffle-apps"
: selectedOrganization.defaults.workflow_download_repo === undefined ||
selectedOrganization.defaults.workflow_download_repo.length === 0
? "https://github.com/frikky/shuffle-workflows"
: selectedOrganization.defaults.workflow_download_repo
);
const [workflowDownloadBranch, setWorkflowDownloadBranch] = React.useState(
selectedOrganization.defaults === undefined
? defaultBranch
: selectedOrganization.defaults.workflow_download_branch === undefined ||
selectedOrganization.defaults.workflow_download_branch.length === 0
? defaultBranch
: selectedOrganization.defaults.workflow_download_branch
);
const [ssoEntrypoint, setSsoEntrypoint] = React.useState(
selectedOrganization.sso_config === undefined
? ""
: selectedOrganization.sso_config.sso_entrypoint === undefined ||
selectedOrganization.sso_config.sso_entrypoint.length === 0
? ""
: selectedOrganization.sso_config.sso_entrypoint
);
const [ssoCertificate, setSsoCertificate] = React.useState(
selectedOrganization.sso_config === undefined
? ""
: selectedOrganization.sso_config.sso_certificate === undefined ||
selectedOrganization.sso_config.sso_certificate.length === 0
? ""
: selectedOrganization.sso_config.sso_certificate
);
const [notificationWorkflow, setNotificationWorkflow] = React.useState(
selectedOrganization.defaults === undefined
? ""
: selectedOrganization.defaults.notification_workflow === undefined ||
selectedOrganization.defaults.notification_workflow.length === 0
? ""
: selectedOrganization.defaults.notification_workflow
);
const [documentationReference, setDocumentationReference] = React.useState(
selectedOrganization.defaults === undefined
? ""
: selectedOrganization.defaults.documentation_reference === undefined ||
selectedOrganization.defaults.documentation_reference.length === 0
? ""
: selectedOrganization.defaults.documentation_reference
);
const [openidClientId, setOpenidClientId] = React.useState(
selectedOrganization.sso_config === undefined
? ""
: selectedOrganization.sso_config.client_id === undefined ||
selectedOrganization.sso_config.client_id.length === 0
? ""
: selectedOrganization.sso_config.client_id
);
const [openidClientSecret, setOpenidClientSecret] = React.useState(
selectedOrganization.sso_config === undefined
? ""
: selectedOrganization.sso_config.client_secret === undefined ||
selectedOrganization.sso_config.client_secret.length === 0
? ""
: selectedOrganization.sso_config.client_secret
);
const [openidAuthorization, setOpenidAuthorization] = React.useState(
selectedOrganization.sso_config === undefined
? ""
: selectedOrganization.sso_config.openid_authorization === undefined ||
selectedOrganization.sso_config.openid_authorization.length === 0
? ""
: selectedOrganization.sso_config.openid_authorization
);
const [openidToken, setOpenidToken] = React.useState(
selectedOrganization.sso_config === undefined
? ""
: selectedOrganization.sso_config.openid_token === undefined ||
selectedOrganization.sso_config.openid_token.length === 0
? ""
: selectedOrganization.sso_config.openid_token
)
const [workflows, setWorkflows] = React.useState([])
const [workflow, setWorkflow] = React.useState({})
const getAvailableWorkflows = (trigger_index) => {
fetch(globalUrl + "/api/v1/workflows", {
method: "GET",
headers: {
"Content-Type": "application/json",
Accept: "application/json",
},
credentials: "include",
})
.then((response) => {
if (response.status !== 200) {
console.log("Status not 200 for workflows :O!");
return;
}
return response.json();
})
.then((responseJson) => {
if (responseJson !== undefined) {
setWorkflows(responseJson)
if (selectedOrganization.defaults !== undefined && selectedOrganization.defaults.notification_workflow !== undefined) {
const workflow = responseJson.find((workflow) => workflow.id === selectedOrganization.defaults.notification_workflow)
if (workflow !== undefined && workflow !== null) {
setWorkflow(workflow)
}
}
}
})
.catch((error) => {
console.log("Error getting workflows: " + error);
})
}
useEffect(() => {
getAvailableWorkflows()
}, [])
const handleEditOrg = (
name,
description,
orgId,
image,
defaults,
sso_config
) => {
const data = {
name: name,
description: description,
org_id: orgId,
image: image,
defaults: defaults,
sso_config: sso_config,
};
const url = globalUrl + `/api/v1/orgs/${selectedOrganization.id}`;
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) {
toast("Failed updating org: ", responseJson.reason);
} else {
toast("Successfully edited org!");
}
})
)
.catch((error) => {
toast("Err: " + error.toString());
});
};
const handleWorkflowSelectionUpdate = (e, isUserinput) => {
if (e.target.value === undefined || e.target.value === null || e.target.value.id === undefined) {
console.log("Returning as there's no id")
return null
}
setWorkflow(e.target.value)
setNotificationWorkflow(e.target.value.id)
toast("Updated notification workflow. Don't forget to save!")
}
const orgSaveButton = (
<Tooltip title="Save any unsaved data" placement="bottom">
<div>
<Button
style={{ width: 150, height: 55, flex: 1 }}
variant="contained"
color="primary"
disabled={
userdata === undefined ||
userdata === null ||
userdata.admin !== "true"
}
onClick={() =>
handleEditOrg(
orgName,
orgDescription,
selectedOrganization.id,
selectedOrganization.image,
{
app_download_repo: appDownloadUrl,
app_download_branch: appDownloadBranch,
workflow_download_repo: workflowDownloadUrl,
workflow_download_branch: workflowDownloadBranch,
notification_workflow: notificationWorkflow,
documentation_reference: documentationReference,
},
{
sso_entrypoint: ssoEntrypoint,
sso_certificate: ssoCertificate,
client_id: openidClientId,
client_secret: openidClientSecret,
openid_authorization: openidAuthorization,
openid_token: openidToken,
}
)
}
>
<SaveIcon />
</Button>
</div>
</Tooltip>
);
return (
<div style={{ textAlign: "center" }}>
<Grid container spacing={3} style={{ textAlign: "left" }}>
<Grid item xs={12} style={{}}>
<span>
<Typography>Notification Workflow</Typography>
{/*
<Typography variant="body2" color="textSecondary">
Add a Workflow that receives notifications from Shuffle when an error occurs in one of your workflows
</Typography>
*/}
<div style={{display: "flex", flexDirection: "row", alignItems: "center"}}>
{workflows !== undefined && workflows !== null && workflows.length > 0 ?
<Autocomplete
id="notification_workflow_search"
autoHighlight
freeSolo
//autoSelect
value={workflow}
classes={{ inputRoot: classes.inputRoot }}
ListboxProps={{
style: {
backgroundColor: theme.palette.inputColor,
color: "white",
},
}}
getOptionLabel={(option) => {
if (
option === undefined ||
option === null ||
option.name === undefined ||
option.name === null
) {
return "No Workflow Selected";
}
const newname = (
option.name.charAt(0).toUpperCase() + option.name.substring(1)
).replaceAll("_", " ");
return newname;
}}
options={workflows}
fullWidth
style={{
backgroundColor: theme.palette.inputColor,
height: 50,
borderRadius: theme.palette.borderRadius,
}}
onChange={(event, newValue) => {
console.log("Found value: ", newValue)
var parsedinput = { target: { value: newValue } }
// For variables
if (typeof newValue === 'string' && newValue.startsWith("$")) {
parsedinput = {
target: {
value: {
"name": newValue,
"id": newValue,
"actions": [],
"triggers": [],
}
}
}
}
handleWorkflowSelectionUpdate(parsedinput)
}}
renderOption={(props, data, state) => {
if (data.id === workflow.id) {
data = workflow;
}
return (
<Tooltip arrow placement="left" title={
<span style={{}}>
{data.image !== undefined && data.image !== null && data.image.length > 0 ?
<img src={data.image} alt={data.name} style={{ backgroundColor: theme.palette.surfaceColor, maxHeight: 200, minHeigth: 200, borderRadius: theme.palette.borderRadius, }} />
: null}
<Typography>
Choose {data.name}
</Typography>
</span>
} placement="bottom">
<MenuItem
style={{
backgroundColor: theme.palette.inputColor,
color: data.id === workflow.id ? "red" : "white",
}}
value={data}
onClick={(e) => {
var parsedinput = { target: { value: data } }
handleWorkflowSelectionUpdate(parsedinput)
}}
>
{data.name}
</MenuItem>
</Tooltip>
)
}}
renderInput={(params) => {
return (
<TextField
style={{
backgroundColor: theme.palette.inputColor,
borderRadius: theme.palette.borderRadius,
}}
{...params}
label="Find a notification workflow"
variant="outlined"
/>
);
}}
/>
:
<TextField
required
style={{
flex: "1",
marginTop: "5px",
marginRight: "15px",
backgroundColor: theme.palette.inputColor,
}}
fullWidth={true}
type="name"
id="outlined-with-placeholder"
margin="normal"
variant="outlined"
placeholder="ID of the workflow to receive notifications"
value={notificationWorkflow}
onChange={(e) => {
setNotificationWorkflow(e.target.value);
}}
InputProps={{
classes: {
notchedOutline: classes.notchedOutline,
},
style: {
color: "white",
},
}}
/>
}
<div style={{minWidth: 150, maxWidth: 150, marginTop: 5, marginLeft: 10, }}>
{orgSaveButton}
</div>
</div>
</span>
</Grid>
<Grid item xs={12} style={{}}>
<span>
<Typography>Org Documentation reference</Typography>
<TextField
required
style={{
flex: "1",
marginTop: "5px",
marginRight: "15px",
backgroundColor: theme.palette.inputColor,
}}
fullWidth={true}
type="name"
id="outlined-with-placeholder"
margin="normal"
variant="outlined"
placeholder="URL to an external reference for this implementation"
value={documentationReference}
onChange={(e) => {
setDocumentationReference(e.target.value);
}}
InputProps={{
classes: {
notchedOutline: classes.notchedOutline,
},
style: {
color: "white",
},
}}
/>
</span>
</Grid>
{isCloud ? null :
<Grid item xs={12} style={{marginTop: 50 }}>
<Typography variant="h4" style={{textAlign: "center",}}>OpenID connect</Typography>
<Grid container style={{marginTop: 10, }}>
<Grid item xs={6} style={{}}>
<span>
<Typography>Client ID</Typography>
<TextField
required
style={{
flex: "1",
marginTop: "5px",
marginRight: "15px",
backgroundColor: theme.palette.inputColor,
}}
fullWidth={true}
type="name"
multiline={true}
rows={2}
disabled={
selectedOrganization.manager_orgs !== undefined &&
selectedOrganization.manager_orgs !== null &&
selectedOrganization.manager_orgs.length > 0
}
id="outlined-with-placeholder"
margin="normal"
variant="outlined"
placeholder="The OpenID client ID from the identity provider"
value={openidClientId}
onChange={(e) => {
setOpenidClientId(e.target.value);
}}
InputProps={{
classes: {
notchedOutline: classes.notchedOutline,
},
style: {
color: "white",
},
}}
/>
</span>
</Grid>
<Grid item xs={6} style={{}}>
<span>
<Typography>Client Secret (optional)</Typography>
<TextField
required
style={{
flex: "1",
marginTop: "5px",
marginRight: "15px",
backgroundColor: theme.palette.inputColor,
}}
fullWidth={true}
type="name"
multiline={true}
rows={2}
disabled={
selectedOrganization.manager_orgs !== undefined &&
selectedOrganization.manager_orgs !== null &&
selectedOrganization.manager_orgs.length > 0
}
id="outlined-with-placeholder"
margin="normal"
variant="outlined"
placeholder="The OpenID client secret - DONT use this if dealing with implicit auth / PKCE"
value={openidClientSecret}
onChange={(e) => {
setOpenidClientSecret(e.target.value);
}}
InputProps={{
classes: {
notchedOutline: classes.notchedOutline,
},
style: {
color: "white",
},
}}
/>
</span>
</Grid>
</Grid>
<Grid container style={{marginTop: 10, }}>
<Grid item xs={6} style={{}}>
<span>
<Typography>Authorization URL</Typography>
<TextField
required
style={{
flex: "1",
marginTop: "5px",
marginRight: "15px",
backgroundColor: theme.palette.inputColor,
}}
fullWidth={true}
type="name"
id="outlined-with-placeholder"
margin="normal"
variant="outlined"
multiline={true}
rows={2}
placeholder="The OpenID authorization URL (usually ends with /authorize)"
value={openidAuthorization}
onChange={(e) => {
setOpenidAuthorization(e.target.value)
}}
InputProps={{
classes: {
notchedOutline: classes.notchedOutline,
},
style: {
color: "white",
},
}}
/>
</span>
</Grid>
<Grid item xs={6} style={{}}>
<span>
<Typography>Token URL</Typography>
<TextField
required
style={{
flex: "1",
marginTop: "5px",
marginRight: "15px",
backgroundColor: theme.palette.inputColor,
}}
fullWidth={true}
type="name"
id="outlined-with-placeholder"
margin="normal"
variant="outlined"
multiline={true}
rows={2}
placeholder="The OpenID token URL (usually ends with /token)"
value={openidToken}
onChange={(e) => {
setOpenidToken(e.target.value)
}}
InputProps={{
classes: {
notchedOutline: classes.notchedOutline,
},
style: {
color: "white",
},
}}
/>
</span>
</Grid>
</Grid>
</Grid>
}
{/*isCloud ? null : */}
<Grid item xs={12} style={{marginTop: 50,}}>
<Typography variant="h4" style={{textAlign: "center",}}>SAML SSO (v1.1)</Typography>
<Grid container style={{marginTop: 20, }}>
<Grid item xs={6} style={{}}>
<span>
<Typography>SSO Entrypoint (IdP)</Typography>
<TextField
required
style={{
flex: "1",
marginTop: "5px",
marginRight: "15px",
backgroundColor: theme.palette.inputColor,
}}
fullWidth={true}
type="name"
multiline={true}
rows={2}
disabled={
selectedOrganization.manager_orgs !== undefined &&
selectedOrganization.manager_orgs !== null &&
selectedOrganization.manager_orgs.length > 0
}
id="outlined-with-placeholder"
margin="normal"
variant="outlined"
placeholder="The entrypoint URL from your provider"
value={ssoEntrypoint}
onChange={(e) => {
setSsoEntrypoint(e.target.value);
}}
InputProps={{
classes: {
notchedOutline: classes.notchedOutline,
},
style: {
color: "white",
},
}}
/>
</span>
</Grid>
<Grid item xs={6} style={{}}>
<span>
<Typography>SSO Certificate (X509)</Typography>
<TextField
required
style={{
flex: "1",
marginTop: "5px",
marginRight: "15px",
backgroundColor: theme.palette.inputColor,
}}
fullWidth={true}
type="name"
id="outlined-with-placeholder"
margin="normal"
variant="outlined"
multiline={true}
rows={2}
placeholder="The X509 certificate to use"
value={ssoCertificate}
onChange={(e) => {
setSsoCertificate(e.target.value);
}}
InputProps={{
classes: {
notchedOutline: classes.notchedOutline,
},
style: {
color: "white",
},
}}
/>
</span>
</Grid>
</Grid>
{isCloud ?
<Typography variant="body2" style={{textAlign: "left",}} color="textSecondary">
IdP URL for Shuffle: https://shuffler.io/api/v1/login_sso
</Typography>
: null}
</Grid>
{isCloud ? null : (
<Grid item xs={6} style={{}}>
<span>
<Typography>App Download URL</Typography>
<TextField
required
style={{
flex: "1",
marginTop: "5px",
marginRight: "15px",
backgroundColor: theme.palette.inputColor,
}}
fullWidth={true}
type="name"
id="outlined-with-placeholder"
margin="normal"
variant="outlined"
placeholder="A description for the organization"
value={appDownloadUrl}
onChange={(e) => {
setAppDownloadUrl(e.target.value);
}}
InputProps={{
classes: {
notchedOutline: classes.notchedOutline,
},
style: {
color: "white",
},
}}
/>
</span>
</Grid>
)}
{isCloud ? null : (
<Grid item xs={6} style={{}}>
<span>
<Typography>App Download Branch</Typography>
<TextField
required
style={{
flex: "1",
marginTop: "5px",
marginRight: "15px",
backgroundColor: theme.palette.inputColor,
}}
fullWidth={true}
type="name"
id="outlined-with-placeholder"
margin="normal"
variant="outlined"
placeholder="A description for the organization"
value={appDownloadBranch}
onChange={(e) => {
setAppDownloadBranch(e.target.value);
}}
InputProps={{
classes: {
notchedOutline: classes.notchedOutline,
},
style: {
color: "white",
},
}}
/>
</span>
</Grid>
)}
{isCloud ? null : (
<Grid item xs={6} style={{}}>
<span>
<Typography>Workflow Download URL</Typography>
<TextField
required
style={{
flex: "1",
marginTop: "5px",
marginRight: "15px",
backgroundColor: theme.palette.inputColor,
}}
fullWidth={true}
type="name"
id="outlined-with-placeholder"
margin="normal"
variant="outlined"
placeholder="A description for the organization"
value={workflowDownloadUrl}
onChange={(e) => {
setWorkflowDownloadUrl(e.target.value);
}}
InputProps={{
classes: {
notchedOutline: classes.notchedOutline,
},
style: {
color: "white",
},
}}
/>
</span>
</Grid>
)}
{isCloud ? null : (
<Grid item xs={6} style={{}}>
<span>
<Typography>Workflow Download Branch</Typography>
<TextField
required
style={{
flex: "1",
marginTop: "5px",
marginRight: "15px",
backgroundColor: theme.palette.inputColor,
}}
fullWidth={true}
type="name"
id="outlined-with-placeholder"
margin="normal"
variant="outlined"
placeholder="A description for the organization"
value={workflowDownloadBranch}
onChange={(e) => {
setWorkflowDownloadBranch(e.target.value);
}}
InputProps={{
classes: {
notchedOutline: classes.notchedOutline,
},
style: {
color: "white",
},
}}
/>
</span>
</Grid>
)}
<div style={{ margin: "auto", textalign: "center", marginTop: 15, marginBottom: 15, }}>
{orgSaveButton}
</div>
{/*
<span style={{textAlign: "center"}}>
{expanded ?
<ExpandLessIcon />
:
<ExpandMoreIcon />
}
</span>
*/}
</Grid>
</div>
)
}
export default OrgHeaderexpanded;

View File

@ -0,0 +1,19 @@
import React, {useState, useEffect, useLayoutEffect} from 'react';
import Draggable from "react-draggable";
import {
Paper
} from "@mui/material";
const PaperComponent = (props) => {
return (
<Draggable
handle="#draggable-dialog-title"
cancel={'[class*="MuiDialogContent-root"]'}
>
<Paper {...props} />
</Draggable>
)
}
export default PaperComponent;

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,93 @@
import React, { useState, useEffect } from "react";
import theme from "../theme.jsx";
import {
Paper,
Typography,
Divider,
Button,
Grid,
Card,
Switch,
} from "@mui/material";
import Priority from "../components/Priority.jsx";
//import { useAlert
const Priorities = (props) => {
const { globalUrl, userdata, serverside, billingInfo, stripeKey, checkLogin, setAdminTab, setCurTab, } = props;
const [showDismissed, setShowDismissed] = React.useState(false);
const [showRead, setShowRead] = React.useState(false);
if (userdata === undefined || userdata === null) {
return
}
return (
<div style={{maxWidth: 1000, }}>
<h2 style={{ display: "inline" }}>Suggestions</h2>
<span style={{ marginLeft: 25 }}>
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.&nbsp;
<a
target="_blank"
rel="noopener noreferrer"
href="/docs/organizations#priorities"
style={{ textDecoration: "none", color: "#f85a3e" }}
>
Learn more
</a>
</span>
<div style={{marginTop: 10, }}/>
<Switch
checked={showDismissed}
onChange={() => {
setShowDismissed(!showDismissed);
}}
/>&nbsp; Show dismissed
{userdata.priorities === null || userdata.priorities === undefined || userdata.priorities.length === 0 ?
<Typography variant="h4">
No Suggestions found
</Typography>
:
userdata.priorities.map((priority, index) => {
if (showDismissed === false && priority.active === false) {
return null
}
return (
<Priority
key={index}
globalUrl={globalUrl}
priority={priority}
checkLogin={checkLogin}
setAdminTab={setAdminTab}
setCurTab={setCurTab}
/>
)
})
}
<Divider style={{marginTop: 50, marginBottom: 50, }} />
<h2 style={{ display: "inline" }}>Notifications</h2>
<span style={{ marginLeft: 25 }}>
Notifications help you find potential problems with your workflows and apps.&nbsp;
<a
target="_blank"
rel="noopener noreferrer"
href="/docs/organizations#notifications"
style={{ textDecoration: "none", color: "#f85a3e" }}
>
Learn more
</a>
</span>
<div/>
<Switch
checked={showRead}
onChange={() => {
setShowRead(!showRead);
}}
/>&nbsp; Show read
</div>
)
}
export default Priorities;

View File

@ -0,0 +1,176 @@
import React, { useState, useEffect } from "react";
import { toast } from 'react-toastify';
import ReactGA from 'react-ga4';
import theme from "../theme.jsx";
import { useNavigate, Link } from "react-router-dom";
import { findSpecificApp } from "../components/AppFramework.jsx"
import {
Paper,
Typography,
Divider,
Button,
Grid,
Card,
} from "@mui/material";
// import magic wand icon from material ui icons
import {
AutoFixHigh as AutoFixHighIcon,
ArrowForward as ArrowForwardIcon,
} from '@mui/icons-material';
//import { useAlert
const Priority = (props) => {
const { globalUrl, userdata, serverside, priority, checkLogin, setAdminTab, setCurTab, appFramework, } = props;
const isCloud = window.location.host === "localhost:3002" || window.location.host === "shuffler.io";
let navigate = useNavigate();
var realigned = false
let newdescription = priority.description
const descsplit = priority.description.split("&")
if (appFramework !== undefined && descsplit.length === 5 && priority.description.includes(":default")) {
console.log("descsplit: ", descsplit)
if (descsplit[1] === "") {
const item = findSpecificApp(appFramework, descsplit[0])
console.log("item: ", item)
if (item !== null) {
descsplit[1] = item.large_image
descsplit[0] = descsplit[0].split(":")[0]
}
realigned = true
}
if (descsplit[3] === "") {
const item = findSpecificApp(appFramework, descsplit[2])
console.log("item: ", item)
if (item !== null) {
descsplit[3] = item.large_image
descsplit[2] = descsplit[2].split(":")[0]
}
realigned = true
}
newdescription = descsplit.join("&")
}
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) {
toast("Failed change recommendation: ", responseJson.reason)
} else {
toast("Failed change recommendation");
}
}
})
.catch((error) => {
toast("Failed dismissing alert. Please contact support@shuffler.io if this persists.");
});
}
return (
<div style={{border: priority.active === false ? "1px solid #000000" : priority.severity === 1 ? "1px solid #f85a3e" : "1px solid rgba(255,255,255,0.3)", borderRadius: theme.palette.borderRadius, marginTop: 10, marginBottom: 10, padding: 15, textAlign: "center", minHeight: isCloud ? 70 : 100, maxHeight: isCloud ? 70 : 100, textAlign: "left", backgroundColor: theme.palette.surfaceColor, display: "flex", }}>
<div style={{flex: 2, overflow: "hidden",}}>
<span style={{display: "flex", }}>
{priority.type === "usecase" || priority.type == "apps" ? <AutoFixHighIcon style={{height: 19, width: 19, marginLeft: 3, marginRight: 10, }}/> : null}
<Typography variant="body1" >
{priority.name}
</Typography>
</span>
{priority.type === "usecase" && priority.description.includes("&") ?
<span style={{display: "flex", marginTop: 10, }}>
<img src={newdescription.split("&")[1]} alt={priority.name} style={{height: "auto", width: 30, marginRight: realigned ? -10 : 10, borderRadius: theme.palette.borderRadius, marginTop: realigned ? 5 : 0 }} />
<Typography variant="body2" color="textSecondary" style={{marginTop: 3, }}>
{newdescription.split("&")[0]}
</Typography>
{newdescription.split("&").length > 3 ?
<span style={{display: "flex", }}>
<ArrowForwardIcon style={{marginLeft: 15, marginRight: 15, }}/>
<img src={newdescription.split("&")[3]} alt={priority.name+"2"} style={{height: "auto", width: 30, marginRight: realigned ? -10 : 10, borderRadius: theme.palette.borderRadius, marginTop: realigned ? 5 : 0 }} />
<Typography variant="body2" color="textSecondary" style={{marginTop: 3}}>
{newdescription.split("&")[2]}
</Typography>
</span>
: null}
</span>
:
<Typography variant="body2" color="textSecondary">
{priority.description}
</Typography>
}
</div>
<div style={{flex: 1, display: "flex", marginLeft: 30, }}>
<Button style={{height: 50, borderRadius: 25, marginTop: 8, width: 175, marginRight: 10, color: priority.active === false ? "white" : "black", backgroundColor: priority.active === false ? theme.palette.inputColor : "rgba(255,255,255,0.8)", }} variant="contained" color="secondary" onClick={() => {
if (isCloud) {
ReactGA.event({
category: "recommendation",
action: `click_${priority.name}`,
label: "",
})
}
navigate(priority.url)
if (setAdminTab !== undefined && setCurTab !== undefined) {
if (priority.description.toLowerCase().includes("notification workflow")) {
setCurTab(0)
setAdminTab(0)
}
if (priority.description.toLowerCase().includes("hybrid shuffle")) {
setCurTab(6)
}
}
}}>
Explore
</Button>
{priority.active === true ?
<Button style={{borderRadius: 25, width: 100, height: 50, marginTop: 8, }} variant="text" color="secondary" onClick={() => {
// dismiss -> get envs
changeRecommendation(priority, "dismiss")
}}>
Dismiss
</Button>
: null }
</div>
</div>
)
}
export default Priority;

View File

@ -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";
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 (
<CytoscapeComponent
elements={elements}
minZoom={0.35}
maxZoom={2.0}
style={{
width: bodyWidth - 15,
height: bodyHeight - 5,
backgroundColor: surfaceColor,
}}
stylesheet={cystyle}
boxSelectionEnabled={true}
autounselectify={false}
showGrid={true}
cy={(incy) => {
// 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;

View File

@ -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 (
<CytoscapeComponent
elements={elements}
minZoom={0.35}
maxZoom={2.0}
style={{
width: bodyWidth - 15,
height: bodyHeight - 5,
backgroundColor: surfaceColor,
}}
stylesheet={cystyle}
boxSelectionEnabled={true}
autounselectify={false}
showGrid={true}
cy={(incy) => {
// 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;

View File

@ -0,0 +1,47 @@
import { useEffect } from "react";
//import { withRouter } from "react-router-dom";
import { useLocation } from "react-router-dom";
export const removeQuery = (query) => {
const urlSearchParams = new URLSearchParams(window.location.search)
const params = Object.fromEntries(urlSearchParams.entries())
if (params[query] !== undefined) {
delete params[query]
} else {
return
}
const queryString = Object.keys(params).map(key => key + '=' + params[key]).join('&')
const newurl = window.location.protocol + "//" + window.location.host + window.location.pathname + '?' + queryString
window.history.pushState({path:newurl},'',newurl)
}
// ensures scrolling happens in the right way on different pages and when changing
function ScrollToTop({ getUserNotifications, curpath, setCurpath, history }) {
let location = useLocation();
useEffect(() => {
// Custom handler for certain scroll mechanics
//
//console.log("OLD: ", curpath, "NeW: ", window.location.pathname)
if (curpath === window.location.pathname && curpath === "/usecases") {
} else {
window.scroll({
top: 0,
left: 0,
behavior: "smooth",
});
setCurpath(window.location.pathname);
getUserNotifications();
}
}, [location]);
return null;
}
// https://stackoverflow.com/questions/36904185/react-router-scroll-to-top-on-every-transition
//export default withRouter(ScrollToTop);
// https://v5.reactrouter.com/web/api/Hooks/uselocation
export default ScrollToTop;

View File

@ -0,0 +1,660 @@
import React, {useState, useEffect, useRef} from 'react';
import theme from '../theme.jsx';
import { useNavigate, Link, useParams } from "react-router-dom";
import {
Chip,
IconButton,
TextField,
InputAdornment,
List,
Card,
ListItem,
ListItemAvatar,
ListItemText,
Avatar,
Typography,
Tooltip,
} from '@mui/material';
import {
AvatarGroup,
} from "@mui/material"
import {Search as SearchIcon, Close as CloseIcon, Folder as FolderIcon, Code as CodeIcon, LibraryBooks as LibraryBooksIcon} from '@mui/icons-material'
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
let navigate = useNavigate();
const borderRadius = 3
const node = useRef()
const [searchOpen, setSearchOpen] = useState(false)
const [oldPath, setOldPath] = useState("")
const [value, setValue] = 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, } ) => {
const keyPressHandler = (e) => {
// e.preventDefault();
if (e.which === 13) {
// alert("You pressed enter!");
navigate("/search?q=" + currentRefinement, { state: value, replace: true });
}
};
/*
endAdornment: (
<InputAdornment position="end" style={{textAlign: "right", zIndex: 5001, cursor: "pointer", width: 100, }} onMouseOver={(event) => {
event.preventDefault()
}}>
<CloseIcon style={{marginRight: 5,}} onClick={() => {
setSearchOpen(false)
}} />
</InputAdornment>
),
*/
return (
<form id="search_form" noValidate type="searchbox" action="" role="search" style={{margin: "10px 0px 0px 0px", }} onClick={() => {
}}>
<TextField
fullWidth
style={{backgroundColor: theme.palette.surfaceColor, borderRadius: borderRadius, minWidth: 403, maxWidth: 403, }}
InputProps={{
style:{
color: "white",
fontSize: "1em",
height: 50,
margin: 0,
fontSize: "0.9em",
paddingLeft: 10,
},
disableUnderline: true,
endAdornment: (
<InputAdornment position="start">
<SearchIcon style={{marginLeft: 5, color: "#f86a3e",}}/>
</InputAdornment>
),
}}
autoComplete='off'
type="search"
color="primary"
placeholder="Find Public Apps, Workflows, Documentation..."
value={currentRefinement}
onKeyDown={keyPressHandler}
id="shuffle_search_field"
onClick={(event) => {
if (!searchOpen) {
setSearchOpen(true)
setTimeout(() => {
var tarfield = document.getElementById("shuffle_search_field")
//console.log("TARFIELD: ", tarfield)
tarfield.focus()
}, 100)
}
}}
onBlur={(event) => {
setTimeout(() => {
setSearchOpen(false)
}, 500)
}}
onChange={(event) => {
//if (event.currentTarget.value.length > 0 && !searchOpen) {
// setSearchOpen(true)
//}
refine(event.currentTarget.value)
}}
limit={5}
/>
{/*isSearchStalled ? 'My search is stalled' : ''*/}
</form>
)
}
const WorkflowHits = ({ hits }) => {
const [mouseHoverIndex, setMouseHoverIndex] = useState(0)
var tmp = searchOpen
if (!searchOpen) {
return null
}
const positionInfo = document.activeElement.getBoundingClientRect()
const outerlistitemStyle = {
width: "100%",
overflowX: "hidden",
overflowY: "hidden",
borderBottom: "1px solid rgba(255,255,255,0.4)",
}
if (hits.length > 4) {
hits = hits.slice(0, 4)
}
var type = "workflows"
const baseImage = <CodeIcon />
return (
<Card elevation={0} style={{position: "relative", marginLeft: 10, marginRight: 10, position: "absolute", color: "white", zIndex: 1002, backgroundColor: theme.palette.inputColor, width: 405, height: 408, left: 75, boxShadows: "none",}}>
<Typography variant="h6" style={{margin: "10px 10px 0px 20px", }}>
Workflows
</Typography>
<List style={{backgroundColor: theme.palette.inputColor, }}>
{hits.length === 0 ?
<ListItem style={outerlistitemStyle}>
<ListItemAvatar onClick={() => console.log(hits)}>
<Avatar>
<FolderIcon />
</Avatar>
</ListItemAvatar>
<ListItemText
primary={"No workflows found."}
secondary={"Try a broader search term"}
/>
</ListItem>
:
hits.map((hit, index) => {
const innerlistitemStyle = {
width: positionInfo.width+35,
overflowX: "hidden",
overflowY: "hidden",
borderBottom: "1px solid rgba(255,255,255,0.4)",
backgroundColor: mouseHoverIndex === index ? "#1f2023" : "inherit",
cursor: "pointer",
marginLeft: 5,
marginRight: 5,
maxHeight: 75,
minHeight: 75,
maxWidth: 420,
minWidth: "100%",
}
const name = hit.name === undefined ?
hit.filename.charAt(0).toUpperCase() + hit.filename.slice(1).replaceAll("_", " ") + " - " + hit.title :
(hit.name.charAt(0).toUpperCase()+hit.name.slice(1)).replaceAll("_", " ")
const secondaryText = hit.description !== undefined && hit.description !== null && hit.description.length > 3 ? hit.description.slice(0, 40)+"..." : ""
const appGroup = hit.action_references === undefined || hit.action_references === null ? [] : hit.action_references
const avatar = baseImage
var parsedUrl = isCloud ? `/workflows/${hit.objectID}` : `https://shuffler.io/workflows/${hit.objectID}`
parsedUrl += `?queryID=${hit.__queryID}`
// <a rel="noopener noreferrer" href="https://www.algolia.com/" target="_blank" style={{textDecoration: "none", color: "white"}}>
return (
<Link key={hit.objectID} to={parsedUrl} rel="noopener noreferrer" style={{textDecoration: "none", color: "white",}} onClick={(event) => {
//console.log("CLICK")
setSearchOpen(true)
aa('init', {
appId: searchClient.appId,
apiKey: searchClient.transporter.queryParameters["x-algolia-api-key"]
})
const timestamp = new Date().getTime()
aa('sendEvents', [
{
eventType: 'click',
eventName: 'Workflow Clicked',
index: 'workflows',
objectIDs: [hit.objectID],
timestamp: timestamp,
queryID: hit.__queryID,
positions: [hit.__position],
userToken: userdata === undefined || userdata === null || userdata.id === undefined ? "unauthenticated" : userdata.id,
}
])
if (!isCloud) {
event.preventDefault()
window.open(parsedUrl, '_blank');
}
}}>
<ListItem key={hit.objectID} style={innerlistitemStyle} onMouseOver={() => {
setMouseHoverIndex(index)
}}>
<ListItemAvatar>
{avatar}
</ListItemAvatar>
<div style={{}}>
<ListItemText
primary={name}
/>
<AvatarGroup max={10} style={{flexDirection: "row", padding: 0, margin: 0, itemAlign: "left", textAlign: "left",}}>
{appGroup.map((app, index) => {
// Putting all this in secondary of ListItemText looked weird.
return (
<div
key={index}
style={{
height: 24,
width: 24,
filter: "brightness(0.6)",
cursor: "pointer",
}}
onClick={() => {
navigate("/apps/"+app.id)
}}
>
<Tooltip color="primary" title={app.name} placement="bottom">
<Avatar alt={app.name} src={app.image_url} style={{width: 24, height: 24}}/>
</Tooltip>
</div>
)
})}
</AvatarGroup>
</div>
{/*
<ListItemSecondaryAction>
<IconButton edge="end" aria-label="delete">
<DeleteIcon />
</IconButton>
</ListItemSecondaryAction>
*/}
</ListItem>
</Link>
)})
}
</List>
{/*
<span style={{display: "flex", textAlign: "left", float: "left", position: "absolute", left: 15, bottom: 10, }}>
<Link to="/search" style={{textDecoration: "none", color: "#f85a3e"}}>
<Typography variant="body2" style={{}}>
See all workflows
</Typography>
</Link>
</span>
*/}
</Card>
)
}
const AppHits = ({ hits }) => {
const [mouseHoverIndex, setMouseHoverIndex] = useState(0)
var tmp = searchOpen
if (!searchOpen) {
return null
}
const positionInfo = document.activeElement.getBoundingClientRect()
const outerlistitemStyle = {
width: "100%",
overflowX: "hidden",
overflowY: "hidden",
borderBottom: "1px solid rgba(255,255,255,0.4)",
}
if (hits.length > 4) {
hits = hits.slice(0, 4)
}
var type = "app"
const baseImage = <LibraryBooksIcon />
return (
<Card elevation={0} style={{position: "relative", marginLeft: 10, marginRight: 10, position: "absolute", color: "white", zIndex: 1001, backgroundColor: theme.palette.inputColor, width: 1155, height: 408, left: -305, boxShadows: "none",}}>
<IconButton style={{zIndex: 5000, position: "absolute", right: 14, color: "grey"}} onClick={() => {
setSearchOpen(false)
}}>
<CloseIcon />
</IconButton>
<Typography variant="h6" style={{margin: "10px 10px 0px 20px", }}>
Apps
</Typography>
<List style={{backgroundColor: theme.palette.inputColor, }}>
{hits.length === 0 ?
<ListItem style={outerlistitemStyle}>
<ListItemAvatar onClick={() => console.log(hits)}>
<Avatar>
<FolderIcon />
</Avatar>
</ListItemAvatar>
<ListItemText
primary={"No apps found."}
secondary={"Try a broader search term"}
/>
</ListItem>
:
hits.map((hit, index) => {
const innerlistitemStyle = {
width: positionInfo.width+35,
overflowX: "hidden",
overflowY: "hidden",
borderBottom: "1px solid rgba(255,255,255,0.4)",
backgroundColor: mouseHoverIndex === index ? "#1f2023" : "inherit",
cursor: "pointer",
marginLeft: 5,
marginRight: 5,
maxHeight: 75,
minHeight: 75,
maxWidth: 420,
minWidth: "100%",
}
const name = hit.name === undefined ?
hit.filename.charAt(0).toUpperCase() + hit.filename.slice(1).replaceAll("_", " ") + " - " + hit.title :
(hit.name.charAt(0).toUpperCase()+hit.name.slice(1)).replaceAll("_", " ")
var secondaryText = hit.data !== undefined ? hit.data.slice(0, 40)+"..." : ""
const avatar = hit.image_url === undefined ?
baseImage
:
<Avatar
src={hit.image_url}
variant="rounded"
/>
//console.log(hit)
if (hit.categories !== undefined && hit.categories !== null && hit.categories.length > 0) {
secondaryText = hit.categories.slice(0,3).map((data, index) => {
if (index === 0) {
return data
}
return ", "+data
/*
<Chip
key={index}
style={chipStyle}
label={data}
onClick={() => {
//handleChipClick
}}
variant="outlined"
color="primary"
/>
*/
})
}
var parsedUrl = isCloud ? `/apps/${hit.objectID}` : `https://shuffler.io/apps/${hit.objectID}`
parsedUrl += `?queryID=${hit.__queryID}`
return (
<Link key={hit.objectID} to={parsedUrl} style={{textDecoration: "none", color: "white",}} onClick={(event) => {
console.log("CLICK")
setSearchOpen(true)
aa('init', {
appId: searchClient.appId,
apiKey: searchClient.transporter.queryParameters["x-algolia-api-key"]
})
const timestamp = new Date().getTime()
aa('sendEvents', [
{
eventType: 'click',
eventName: 'App Clicked',
index: 'appsearch',
objectIDs: [hit.objectID],
timestamp: timestamp,
queryID: hit.__queryID,
positions: [hit.__position],
userToken: userdata === undefined || userdata === null || userdata.id === undefined ? "unauthenticated" : userdata.id,
}
])
if (!isCloud) {
event.preventDefault()
window.open(parsedUrl, '_blank');
}
}}>
<ListItem key={hit.objectID} style={innerlistitemStyle} onMouseOver={() => {
setMouseHoverIndex(index)
}}>
<ListItemAvatar>
{avatar}
</ListItemAvatar>
<ListItemText
primary={name}
secondary={secondaryText}
/>
{/*
<ListItemSecondaryAction>
<IconButton edge="end" aria-label="delete">
<DeleteIcon />
</IconButton>
</ListItemSecondaryAction>
*/}
</ListItem>
</Link>
)})
}
</List>
<span style={{display: "flex", textAlign: "left", float: "left", position: "absolute", left: 15, bottom: 10, }}>
<Link to="/search" style={{textDecoration: "none", color: "#f85a3e"}}>
<Typography variant="body1" style={{}}>
See more
</Typography>
</Link>
</span>
</Card>
)
}
const DocHits = ({ hits }) => {
const [mouseHoverIndex, setMouseHoverIndex] = useState(0)
var tmp = searchOpen
if (!searchOpen) {
return null
}
const positionInfo = document.activeElement.getBoundingClientRect()
const outerlistitemStyle = {
width: "100%",
overflowX: "hidden",
overflowY: "hidden",
borderBottom: "1px solid rgba(255,255,255,0.4)",
}
if (hits.length > 4) {
hits = hits.slice(0, 4)
}
const type = "documentation"
const baseImage = <LibraryBooksIcon />
//console.log(type, hits.length, hits)
return (
<Card elevation={0} style={{position: "relative", marginLeft: 10, marginRight: 10, position: "absolute", color: "white", zIndex: 1002, backgroundColor: theme.palette.inputColor, width: 405, height: 408, left: 470, boxShadows: "none",}}>
<IconButton style={{zIndex: 5000, position: "absolute", right: 14, color: "grey"}} onClick={() => {
setSearchOpen(false)
}}>
<CloseIcon />
</IconButton>
<Typography variant="h6" style={{margin: "10px 10px 0px 20px", }}>
Documentation
</Typography>
{/*
<IconButton edge="end" aria-label="delete" style={{position: "absolute", top: 5, right: 15,}} onClick={() => {
setSearchOpen(false)
}}>
<DeleteIcon />
</IconButton>
*/}
<List style={{backgroundColor: theme.palette.inputColor, }}>
{hits.length === 0 ?
<ListItem style={outerlistitemStyle}>
<ListItemAvatar onClick={() => console.log(hits)}>
<Avatar>
<FolderIcon />
</Avatar>
</ListItemAvatar>
<ListItemText
primary={"No documentation."}
secondary={"Try a broader search term"}
/>
</ListItem>
:
hits.map((hit, index) => {
const innerlistitemStyle = {
width: positionInfo.width+35,
overflowX: "hidden",
overflowY: "hidden",
borderBottom: "1px solid rgba(255,255,255,0.4)",
backgroundColor: mouseHoverIndex === index ? "#1f2023" : "inherit",
cursor: "pointer",
marginLeft: 5,
marginRight: 5,
maxHeight: 75,
minHeight: 75,
maxWidth: 420,
minWidth: "100%",
}
var name = hit.name === undefined ?
hit.filename.charAt(0).toUpperCase() + hit.filename.slice(1).replaceAll("_", " ") + " - " + hit.title
:
(hit.name.charAt(0).toUpperCase()+hit.name.slice(1)).replaceAll("_", " ")
if (name.length > 30) {
name = name.slice(0, 30)+"..."
}
const secondaryText = hit.data !== undefined ? hit.data.slice(0, 40)+"..." : ""
const avatar = hit.image_url === undefined ?
baseImage
:
<Avatar
src={hit.image_url}
variant="rounded"
/>
var parsedUrl = hit.urlpath !== undefined ? hit.urlpath : ""
parsedUrl += `?queryID=${hit.__queryID}`
if (parsedUrl.includes("/apps/")) {
const extraHash = hit.url_hash === undefined ? "" : `#${hit.url_hash}`
parsedUrl = `/apps/${hit.filename}`
parsedUrl += `?tab=docs&queryID=${hit.__queryID}${extraHash}`
}
return (
<Link key={hit.objectID} to={parsedUrl} style={{textDecoration: "none", color: "white",}} onClick={(event) => {
aa('init', {
appId: searchClient.appId,
apiKey: searchClient.transporter.queryParameters["x-algolia-api-key"]
})
const timestamp = new Date().getTime()
aa('sendEvents', [
{
eventType: 'click',
eventName: 'Document Clicked',
index: 'documentation',
objectIDs: [hit.objectID],
timestamp: timestamp,
queryID: hit.__queryID,
positions: [hit.__position],
userToken: userdata === undefined || userdata === null || userdata.id === undefined ? "unauthenticated" : userdata.id,
}
])
console.log("CLICK")
setSearchOpen(true)
}}>
<ListItem key={hit.objectID} style={innerlistitemStyle} onMouseOver={() => {
setMouseHoverIndex(index)
}}>
<ListItemAvatar>
{avatar}
</ListItemAvatar>
<ListItemText
primary={name}
secondary={secondaryText}
/>
{/*
<ListItemSecondaryAction>
<IconButton edge="end" aria-label="delete">
<DeleteIcon />
</IconButton>
</ListItemSecondaryAction>
*/}
</ListItem>
</Link>
)})
}
</List>
{type === "documentation" ?
<span style={{display: "flex", textAlign: "right", position: "absolute", right: 15, bottom: 10,}}>
<Typography variant="body2" style={{}}>
Search by
</Typography>
<a rel="noopener noreferrer" href="https://www.algolia.com/" target="_blank" style={{textDecoration: "none", color: "white"}}>
<img src={"/images/logo-algolia-nebula-blue-full.svg"} alt="Algolia logo" style={{height: 17, marginLeft: 5, marginTop: 3,}} />
</a>
</span>
: null}
</Card>
)
}
const CustomSearchBox = connectSearchBox(SearchBox)
const CustomAppHits = connectHits(AppHits)
const CustomWorkflowHits = connectHits(WorkflowHits)
const CustomDocHits = connectHits(DocHits)
return (
<div ref={node} style={{width: "100%", maxWidth: 425, margin: "auto", position: "relative", }}>
<InstantSearch searchClient={searchClient} indexName="appsearch" onClick={() => {
console.log("CLICKED")
}}>
<Configure clickAnalytics />
<CustomSearchBox />
<Index indexName="appsearch">
<CustomAppHits />
</Index>
<Index indexName="documentation">
<CustomDocHits />
</Index>
<Index indexName="workflows">
<CustomWorkflowHits />
</Index>
</InstantSearch>
</div>
)
}
export default SearchField;

View File

@ -0,0 +1,252 @@
import React, {useState } from 'react';
import {isMobile} from "react-device-detect";
import AppFramework, { usecases } from "../components/AppFramework.jsx";
import {Link} from 'react-router-dom';
import ReactGA from 'react-ga4';
import { Button, LinearProgress, Typography } from '@mui/material';
export const securityFramework = [
{
image: <path d="M15.6408 8.39233H18.0922V10.0287H15.6408V8.39233ZM0.115234 8.39233H2.56663V10.0287H0.115234V8.39233ZM9.92083 0.21051V2.66506H8.28656V0.21051H9.92083ZM3.31839 2.25596L5.05889 4.00687L3.89856 5.16051L2.15807 3.42596L3.31839 2.25596ZM13.1485 3.99869L14.8808 2.25596L16.0493 3.42596L14.3088 5.16051L13.1485 3.99869ZM9.10369 4.30142C10.404 4.30142 11.651 4.81863 12.5705 5.73926C13.4899 6.65989 14.0065 7.90854 14.0065 9.21051C14.0065 11.0269 13.0178 12.6141 11.5551 13.4651V14.9378C11.5551 15.1548 11.469 15.3629 11.3158 15.5163C11.1625 15.6698 10.9547 15.756 10.738 15.756H7.46943C7.25271 15.756 7.04487 15.6698 6.89163 15.5163C6.73839 15.3629 6.6523 15.1548 6.6523 14.9378V13.4651C5.18963 12.6141 4.2009 11.0269 4.2009 9.21051C4.2009 7.90854 4.71744 6.65989 5.63689 5.73926C6.55635 4.81863 7.80339 4.30142 9.10369 4.30142ZM10.738 16.5741V17.3923C10.738 17.6093 10.6519 17.8174 10.4986 17.9709C10.3454 18.1243 10.1375 18.2105 9.92083 18.2105H8.28656C8.06984 18.2105 7.862 18.1243 7.70876 17.9709C7.55552 17.8174 7.46943 17.6093 7.46943 17.3923V16.5741H10.738ZM8.28656 14.1196H9.92083V12.3769C11.3345 12.0169 12.3722 10.7323 12.3722 9.21051C12.3722 8.34253 12.0279 7.5101 11.4149 6.89634C10.8019 6.28259 9.97056 5.93778 9.10369 5.93778C8.23683 5.93778 7.40546 6.28259 6.79249 6.89634C6.17953 7.5101 5.83516 8.34253 5.83516 9.21051C5.83516 10.7323 6.87292 12.0169 8.28656 12.3769V14.1196Z" />,
text: "Cases",
description: "Case management"
},
{
image:
<path d="M6.93767 0C8.71083 0 10.4114 0.704386 11.6652 1.9582C12.919 3.21202 13.6234 4.91255 13.6234 6.68571C13.6234 8.34171 13.0165 9.864 12.0188 11.0366L12.2965 11.3143H13.1091L18.252 16.4571L16.7091 18L11.5662 12.8571V12.0446L11.2885 11.7669C10.116 12.7646 8.59367 13.3714 6.93767 13.3714C5.16451 13.3714 3.46397 12.667 2.21015 11.4132C0.956339 10.1594 0.251953 8.45888 0.251953 6.68571C0.251953 4.91255 0.956339 3.21202 2.21015 1.9582C3.46397 0.704386 5.16451 0 6.93767 0ZM6.93767 2.05714C4.36624 2.05714 2.3091 4.11429 2.3091 6.68571C2.3091 9.25714 4.36624 11.3143 6.93767 11.3143C9.5091 11.3143 11.5662 9.25714 11.5662 6.68571C11.5662 4.11429 9.5091 2.05714 6.93767 2.05714Z" />,
text: "SIEM",
description: "Case management"
},
{
image:
<path d="M11.223 10.971L3.85195 14.4L7.28095 7.029L14.652 3.6L11.223 10.971ZM9.25195 0C8.07006 0 6.89973 0.232792 5.8078 0.685084C4.71587 1.13738 3.72372 1.80031 2.88799 2.63604C1.20016 4.32387 0.251953 6.61305 0.251953 9C0.251953 11.3869 1.20016 13.6761 2.88799 15.364C3.72372 16.1997 4.71587 16.8626 5.8078 17.3149C6.89973 17.7672 8.07006 18 9.25195 18C11.6389 18 13.9281 17.0518 15.6159 15.364C17.3037 13.6761 18.252 11.3869 18.252 9C18.252 7.8181 18.0192 6.64778 17.5669 5.55585C17.1146 4.46392 16.4516 3.47177 15.6159 2.63604C14.7802 1.80031 13.788 1.13738 12.6961 0.685084C11.6042 0.232792 10.4338 0 9.25195 0ZM9.25195 8.01C8.98939 8.01 8.73758 8.1143 8.55192 8.29996C8.36626 8.48563 8.26195 8.73744 8.26195 9C8.26195 9.26256 8.36626 9.51437 8.55192 9.70004C8.73758 9.8857 8.98939 9.99 9.25195 9.99C9.51452 9.99 9.76633 9.8857 9.95199 9.70004C10.1376 9.51437 10.242 9.26256 10.242 9C10.242 8.73744 10.1376 8.48563 9.95199 8.29996C9.76633 8.1143 9.51452 8.01 9.25195 8.01Z" />,
text: "Assets",
description: "Case management"
},
{
image:
<path d="M13.3318 2.223C13.2598 2.223 13.1878 2.205 13.1248 2.169C11.3968 1.278 9.90284 0.9 8.11184 0.9C6.32984 0.9 4.63784 1.323 3.09884 2.169C2.88284 2.286 2.61284 2.205 2.48684 1.989C2.36984 1.773 2.45084 1.494 2.66684 1.377C4.34084 0.468 6.17684 0 8.11184 0C10.0288 0 11.7028 0.423 13.5388 1.368C13.7638 1.485 13.8448 1.755 13.7278 1.971C13.6468 2.133 13.4938 2.223 13.3318 2.223ZM0.452843 6.948C0.362843 6.948 0.272843 6.921 0.191843 6.867C-0.015157 6.723 -0.0601571 6.444 0.0838429 6.237C0.974843 4.977 2.10884 3.987 3.45884 3.294C6.28484 1.836 9.90284 1.827 12.7378 3.285C14.0878 3.978 15.2218 4.959 16.1128 6.21C16.2568 6.408 16.2118 6.696 16.0048 6.84C15.7978 6.984 15.5188 6.939 15.3748 6.732C14.5648 5.598 13.5388 4.707 12.3238 4.086C9.74084 2.763 6.43784 2.763 3.86384 4.095C2.63984 4.725 1.61384 5.625 0.803843 6.759C0.731843 6.885 0.596843 6.948 0.452843 6.948ZM6.07784 17.811C5.96084 17.811 5.84384 17.766 5.76284 17.676C4.97984 16.893 4.55684 16.389 3.95384 15.3C3.33284 14.193 3.00884 12.843 3.00884 11.394C3.00884 8.721 5.29484 6.543 8.10284 6.543C10.9108 6.543 13.1968 8.721 13.1968 11.394C13.1968 11.646 12.9988 11.844 12.7468 11.844C12.4948 11.844 12.2968 11.646 12.2968 11.394C12.2968 9.216 10.4158 7.443 8.10284 7.443C5.78984 7.443 3.90884 9.216 3.90884 11.394C3.90884 12.69 4.19684 13.887 4.74584 14.859C5.32184 15.894 5.71784 16.335 6.41084 17.037C6.58184 17.217 6.58184 17.496 6.41084 17.676C6.31184 17.766 6.19484 17.811 6.07784 17.811ZM12.5308 16.146C11.4598 16.146 10.5148 15.876 9.74084 15.345C8.39984 14.436 7.59884 12.96 7.59884 11.394C7.59884 11.142 7.79684 10.944 8.04884 10.944C8.30084 10.944 8.49884 11.142 8.49884 11.394C8.49884 12.663 9.14684 13.86 10.2448 14.598C10.8838 15.03 11.6308 15.237 12.5308 15.237C12.7468 15.237 13.1068 15.21 13.4668 15.147C13.7098 15.102 13.9438 15.264 13.9888 15.516C14.0338 15.759 13.8718 15.993 13.6198 16.038C13.1068 16.137 12.6568 16.146 12.5308 16.146ZM10.7218 18C10.6858 18 10.6408 17.991 10.6048 17.982C9.17384 17.586 8.23784 17.055 7.25684 16.092C5.99684 14.841 5.30384 13.176 5.30384 11.394C5.30384 9.936 6.54584 8.748 8.07584 8.748C9.60584 8.748 10.8478 9.936 10.8478 11.394C10.8478 12.357 11.6848 13.14 12.7198 13.14C13.7548 13.14 14.5918 12.357 14.5918 11.394C14.5918 8.001 11.6668 5.247 8.06684 5.247C5.51084 5.247 3.17084 6.669 2.11784 8.874C1.76684 9.603 1.58684 10.458 1.58684 11.394C1.58684 12.096 1.64984 13.203 2.18984 14.643C2.27984 14.877 2.16284 15.138 1.92884 15.219C1.69484 15.309 1.43384 15.183 1.35284 14.958C0.911843 13.779 0.695843 12.609 0.695843 11.394C0.695843 10.314 0.902843 9.333 1.30784 8.478C2.50484 5.967 5.15984 4.338 8.06684 4.338C12.1618 4.338 15.4918 7.497 15.4918 11.385C15.4918 12.843 14.2498 14.031 12.7198 14.031C11.1898 14.031 9.94784 12.843 9.94784 11.385C9.94784 10.422 9.11084 9.639 8.07584 9.639C7.04084 9.639 6.20384 10.422 6.20384 11.385C6.20384 12.924 6.79784 14.364 7.88684 15.444C8.74184 16.29 9.56084 16.758 10.8298 17.109C11.0728 17.172 11.2078 17.424 11.1448 17.658C11.0998 17.865 10.9108 18 10.7218 18Z" />,
text: "IAM",
description: "Case management"
},
{
image: <path d="M16.1091 8.57143H14.8234V5.14286C14.8234 4.19143 14.052 3.42857 13.1091 3.42857H9.68052V2.14286C9.68052 1.57454 9.45476 1.02949 9.0529 0.627628C8.65103 0.225765 8.10599 0 7.53767 0C6.96935 0 6.4243 0.225765 6.02244 0.627628C5.62057 1.02949 5.39481 1.57454 5.39481 2.14286V3.42857H1.96624C1.51158 3.42857 1.07555 3.60918 0.754056 3.93067C0.432565 4.25216 0.251953 4.6882 0.251953 5.14286V8.4H1.53767C2.82338 8.4 3.85195 9.42857 3.85195 10.7143C3.85195 12 2.82338 13.0286 1.53767 13.0286H0.251953V16.2857C0.251953 16.7404 0.432565 17.1764 0.754056 17.4979C1.07555 17.8194 1.51158 18 1.96624 18H5.22338V16.7143C5.22338 15.4286 6.25195 14.4 7.53767 14.4C8.82338 14.4 9.85195 15.4286 9.85195 16.7143V18H13.1091C13.5638 18 13.9998 17.8194 14.3213 17.4979C14.6428 17.1764 14.8234 16.7404 14.8234 16.2857V12.8571H16.1091C16.6774 12.8571 17.2225 12.6314 17.6243 12.2295C18.0262 11.8277 18.252 11.2826 18.252 10.7143C18.252 10.146 18.0262 9.60092 17.6243 9.19906C17.2225 8.79719 16.6774 8.57143 16.1091 8.57143Z" />,
text: "Intel",
description: "Case management"
},
{
image:
<path d="M9.89516 7.71433H8.60945V5.1429H9.89516V7.71433ZM9.89516 10.2858H8.60945V9.00004H9.89516V10.2858ZM14.3952 2.57147H4.10944C3.76845 2.57147 3.44143 2.70693 3.20031 2.94805C2.95919 3.18917 2.82373 3.51619 2.82373 3.85719V15.4286L5.39516 12.8572H14.3952C14.7362 12.8572 15.0632 12.7217 15.3043 12.4806C15.5454 12.2395 15.6809 11.9125 15.6809 11.5715V3.85719C15.6809 3.14361 15.1023 2.57147 14.3952 2.57147Z" />,
text: "Comms",
description: "Case management"
},
{
image:
<path d="M0.251953 10.6011H3.8391L9.38052 -4.92572e-08L10.8977 11.5696L15.0377 6.28838L19.3191 10.6011H23.3948V13.1836H18.252L15.2562 10.175L9.1491 18L7.88909 8.41894L5.39481 13.1836H0.251953V10.6011Z" />,
text: "Network",
description: "Case management"
},
{
image:
<path d="M19.1722 8.9957L17.0737 6.60487L17.3661 3.44004L14.2615 2.73483L12.6361 -3.28068e-08L9.71206 1.25561L6.78803 -3.28068e-08L5.16261 2.73483L2.05797 3.43144L2.35038 6.59627L0.251953 8.9957L2.35038 11.3865L2.05797 14.56L5.16261 15.2652L6.78803 18L9.71206 16.7358L12.6361 17.9914L14.2615 15.2566L17.3661 14.5514L17.0737 11.3865L19.1722 8.9957ZM10.5721 13.2957H8.85205V11.5757H10.5721V13.2957ZM10.5721 9.85571H8.85205V4.69565H10.5721V9.85571Z" />,
text: "EDR & AV",
description: "Case management"
},
]
const LandingpageUsecases = (props) => {
const { userdata } = props
const [selectedUsecase, setSelectedUsecase] = useState("Phishing")
const usecasekeys = usecases === undefined || usecases === null ? [] : Object.keys(usecases)
const buttonBackground = "linear-gradient(to right, #f86a3e, #f34079)"
const buttonStyle = {borderRadius: 25, height: 50, width: 260, margin: isMobile ? "15px auto 15px auto" : 20, fontSize: 18, backgroundImage: buttonBackground}
const HandleTitle = (props) => {
const { usecases, selectedUsecase, setSelecedUsecase } = props
const [progress, setProgress] = useState(0)
React.useEffect(() => {
const timer = setInterval(() => {
setProgress((oldProgress) => {
if (oldProgress >= 105) {
const foundIndex = usecasekeys.findIndex(key => key === selectedUsecase)
var newitem = usecasekeys[foundIndex+1]
if (newitem === undefined || newitem === 0) {
newitem = usecasekeys[1]
}
setSelectedUsecase(newitem)
return -18
}
if (oldProgress >= 65) {
return oldProgress + 3
}
if (oldProgress >= 80) {
return oldProgress + 1
}
return oldProgress + 6
})
}, 165)
return () => {
clearInterval(timer)
}
}, [])
if (usecases === null || usecases === undefined || usecases.length === 0) {
return null
}
const modifier = isMobile ? 17 : 22
return (
<span style={{margin: "auto", textAlign: isMobile ? "center" : "left", width: isMobile ? 280 : "100%",}}>
<b>Handle <br/>
<span style={{marginBottom: 10}}>
<i id="usecase-text">{selectedUsecase}</i>
<LinearProgress variant="determinate" value={progress} style={{marginTop: 0, marginBottom: 0, height: 3, width: isMobile ? "100%" : selectedUsecase.length*modifier, borderRadius: 10, }} />
</span>
with confidence</b>
</span>
)
}
const parsedWidth = isMobile ? "100%" : 1100
return (
<div style={{width: isMobile ? null : parsedWidth, margin: isMobile ? "0px 0px 0px 0px" : "auto", color: "white", textAlign: isMobile ? "center" : "left",}}>
<div style={{display: "flex", position: "relative",}}>
<div style={{maxWidth: isMobile ? "100%" : 420, paddingTop: isMobile ? 0 : 120, zIndex: 1000, margin: "auto",}}>
<Typography variant="h1" style={{margin: "auto", width: isMobile ? 280 : "100%", marginTop: isMobile ? 50 : 0}}>
<HandleTitle usecases={usecases} selectedUsecase={selectedUsecase} setSelectedUsecase={setSelectedUsecase} />
{/*<b>Security Automation <i>is Hard</i></b>*/}
</Typography>
<Typography variant="h6" style={{marginTop: isMobile ? 15 : 0,}}>
Connecting your everchanging environment is hard. We get it! That's why we built Shuffle, where you can use and share your security workflows to everyones benefit.
{/*Shuffle is an automation platform where you don't need to be an expert to automate. Get access to our large pool of security playbooks, apps and people.*/}
</Typography>
<div style={{display: "flex", textAlign: "center", itemAlign: "center",}}>
{isMobile ? null :
<Link rel="noopener noreferrer" to={"/pricing"} style={{textDecoration: "none"}}>
<Button
variant="contained"
onClick={() => {
ReactGA.event({
category: "landingpage",
action: "click_main_pricing",
label: "",
})
}}
style={{
borderRadius: 25, height: 40, width: 175, margin: "15px 0px 15px 0px", fontSize: 14, color: "white", backgroundImage: buttonBackground, marginRight: 10,
}}>
See Pricing
</Button>
</Link>
}
{isMobile ? null :
<Link rel="noopener noreferrer" to={"/register?message=You'll need to sign up first. No name, company or credit card required."} style={{textDecoration: "none"}}>
<Button
variant="contained"
onClick={() => {
ReactGA.event({
category: "landingpage",
action: "click_main_try_it_out",
label: "",
})
}}
style={{
borderRadius: 25, height: 40, width: 175, margin: "15px 0px 15px 0px", fontSize: 14, color: "white", backgroundImage: buttonBackground,
}}>
Start for free
</Button>
</Link>
}
</div>
</div>
{isMobile ? null :
<div style={{marginLeft: 200, marginTop: 125, zIndex: 1000}}>
<AppFramework
userdata={userdata}
showOptions={false}
selectedOption={selectedUsecase}
rolling={true}
/>
</div>
}
{isMobile ? null :
<div style={{position: "absolute", top: 50, right: -200, zIndex: 0, }}>
<svg width="351" height="433" viewBox="0 0 351 433" fill="none" xmlns="http://www.w3.org/2000/svg" style={{zIndex: 0, }}>
<path d="M167.781 184.839C167.781 235.244 208.625 276.104 259.03 276.104C309.421 276.104 350.28 235.244 350.28 184.839C350.28 134.448 309.421 93.5892 259.03 93.5892C208.625 93.5741 167.781 134.433 167.781 184.839ZM330.387 184.839C330.387 224.263 298.439 256.195 259.03 256.195C219.621 256.195 187.674 224.248 187.674 184.839C187.674 145.43 219.636 113.483 259.03 113.483C298.439 113.483 330.387 145.43 330.387 184.839Z" fill="white" fill-opacity="0.2"/>
<path d="M167.781 387.368C167.781 412.578 188.203 433 213.398 433C238.593 433 259.03 412.578 259.03 387.368C259.03 362.157 238.608 341.735 213.398 341.735C188.187 341.735 167.781 362.172 167.781 387.368ZM249.076 387.368C249.076 407.08 233.095 423.046 213.398 423.046C193.686 423.046 177.72 407.065 177.72 387.368C177.72 367.671 193.686 351.69 213.398 351.69C233.095 351.705 249.076 367.671 249.076 387.368Z" fill="white" fill-opacity="0.2"/>
<path d="M56.8637 0.738726C25.7052 0.738724 0.44632 25.9976 0.446317 57.1561C0.446314 88.3146 25.7052 113.573 56.8637 113.573C88.0221 113.573 113.281 88.3146 113.281 57.1561C113.281 25.9977 88.0222 0.738729 56.8637 0.738726Z" fill="white" fill-opacity="0.2"/>
</svg>
</div>
}
</div>
<div style={{display: "flex", width: isMobile ? "100%" : 300, itemAlign: "center", margin: "auto", marginTop: 20, flexDirection: isMobile ? "column" : "row", textAlign: "center",}}>
{isMobile ?
<Link rel="noopener noreferrer" to={"/pricing"} style={{textDecoration: "none"}}>
<Button
variant={isMobile ? "contained" : "outlined"}
color={isMobile ? "primary" : "secondary"}
style={buttonStyle}
onClick={() => {
ReactGA.event({
category: "landingpage",
action: "click_main_pricing",
label: "",
})
}}
>
See pricing
</Button>
</Link>
: null
}
{/*isMobile ?
<Link rel="noopener noreferrer" to={"/docs/features"} style={{textDecoration: "none"}}>
<Button
variant="outlined"
onClick={() => {
ReactGA.event({
category: "landingpage",
action: "click_main_features",
label: "",
})
}}
color="secondary"
style={buttonStyle}>
Features
</Button>
</Link>
: null*/}
</div>
{isMobile ? null :
<div style={{display: "flex", width: parsedWidth, margin: "auto", marginTop: 150}}>
{securityFramework.map((data, index) => {
return (
<div key={index} style={{flex: 1, textAlign: "center",}}>
<span style={{margin: "auto", width: 25,}}>
<svg width="25" height="25" fill="white" xmlns="http://www.w3.org/2000/svg" >
{data.image}
</svg>
</span>
<Typography variant="body2" style={{color: "white", marginRight: 5}}>
{data.text}
</Typography>
</div>
)
})}
</div>
}
</div>
)
}
export default LandingpageUsecases;

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,233 @@
import React, { useState, useEffect } from 'react';
import ReactGA from 'react-ga4';
import theme from '../theme.jsx';
import PaperComponent from "../components/PaperComponent.jsx"
import UsecaseSearch, { usecaseTypes } from "../components/UsecaseSearch.jsx"
import {
Paper,
Typography,
Divider,
IconButton,
Badge,
CircularProgress,
Tooltip,
Dialog,
} from "@mui/material";
import {
Close as CloseIcon,
Delete as DeleteIcon,
AutoFixHigh as AutoFixHighIcon,
Done as DoneIcon,
} from "@mui/icons-material";
const SuggestedWorkflows = (props) => {
const { globalUrl, userdata, usecaseSuggestions, frameworkData, setUsecaseSuggestions, inputSearch, apps, } = props
const [usecaseSearch, setUsecaseSearch] = React.useState("")
const [usecaseSearchType, setUsecaseSearchType] = React.useState("")
const [finishedUsecases, setFinishedUsecases] = React.useState([])
const [previousUsecase, setPreviousUsecase] = React.useState("")
const [closeWindow, setCloseWindow] = React.useState(false)
const isCloud =
window.location.host === "localhost:3002" ||
window.location.host === "shuffler.io";
useEffect(() => {
if (closeWindow === true) {
console.log("WINDOW CLOSED")
finishedUsecases.push(usecaseSearch)
setFinishedUsecases(finishedUsecases)
setCloseWindow(false)
}
}, [closeWindow])
if (usecaseSuggestions === undefined || usecaseSuggestions.length === 0) {
return null
}
if (inputSearch !== previousUsecase) {
setPreviousUsecase(inputSearch)
setFinishedUsecases([])
}
if (finishedUsecases.length === usecaseSuggestions.length) {
console.log("Closing finished usecases 2")
return null
}
//useEffect(() => {
// //if (defaultSearch ===
// //setFinishedUsecases(finishedUsecases)
// console.log("Finished default usecase?", usecaseSearch)
//}, [usecaseSearch])
const foundZindex = usecaseSearch.length > 0 && usecaseSearchType.length > 0 ? -1 : 12500
const IndividualUsecase = (props) => {
const { usecase, index } = props
const [hovering, setHovering] = React.useState(false)
const usecasename = usecase.name
const bordercolor = usecase.color !== undefined ? usecase.color : "rgba(255,255,255,0.3)"
const srcimage = usecase.items[0].app
var dstimage = usecase.items[1].app
if (usecase.items.length > 2) {
dstimage = usecase.items[2].app
}
if (srcimage === undefined || dstimage === undefined) {
console.log("Error in src or dst: returning!")
return null
}
const finished = finishedUsecases.includes(usecasename)
const selectedIcon = finished ? <DoneIcon /> : <AutoFixHighIcon />
if (finished) {
return null
}
// Simple visual of the usecase
return (
<Tooltip
title={`Try usecase "${usecasename}"`}
placement="top"
style={{ zIndex: 10011 }}
>
<div key={index} style={{cursor: finished ? "auto" : "pointer", marginTop: 10, padding: 10, borderRadius: theme.palette.borderRadius, border: `1px solid ${bordercolor}`, display: "flex", backgroundColor: hovering === true ? theme.palette.inputColor : theme.palette.surfaceColor, }} onMouseOver={() => {
setHovering(true)
}} onMouseOut={() => {
setHovering(false)
}} onClick={() => {
if (isCloud) {
ReactGA.event({
category: "welcome",
action: "click_suggested_workflow",
label: usecasename,
})
}
console.log("Try usecase ", usecasename)
setUsecaseSearchType(usecase.type)
setUsecaseSearch(usecasename)
}}>
<div style={{flex: 10}}>
<Typography variant="body2">
{usecasename}
</Typography>
<div style={{display: "flex", marginTop: 5, }}>
<img alt={srcimage.large_image} src={srcimage.large_image} style={{borderRadius: 20, height: 30, width: 30, marginRight: 15, }}/>
<img alt={dstimage.large_image} src={dstimage.large_image} style={{borderRadius: 20, height: 30, width: 30, }}/>
</div>
</div>
<div style={{flex: 1}}>
{selectedIcon}
</div>
</div>
</Tooltip>
)
}
//<Paper style={{width: 275, maxHeight: 400, overflow: "hidden", zIndex: 12500, padding: 25, paddingRight: 35, backgroundColor: theme.palette.surfaceColor, border: "1px solid rgba(255,255,255,0.2)", position: "absolute", top: -50, left: 50, }}>
return (
<Paper style={{margin: "auto", position: "relative", backgroundColor: theme.palette.surfaceColor, borderRadius: theme.palette.borderRadius, zIndex: foundZindex, border: "1px solid rgba(255,255,255,0.2)", top: 100, left: 85,}}>
<Dialog
open={usecaseSearch.length > 0 && usecaseSearchType.length > 0}
onClose={() => {
finishedUsecases.push(usecaseSearch)
setFinishedUsecases(finishedUsecases)
setUsecaseSearch("")
setUsecaseSearchType("")
}}
PaperProps={{
style: {
pointerEvents: "auto",
backgroundColor: theme.palette.surfaceColor,
color: "white",
minWidth: 450,
padding: 50,
overflow: "hidden",
zIndex: 10050,
border: theme.palette.defaultBorder,
},
}}
>
<IconButton
style={{
zIndex: 5000,
position: "absolute",
top: 14,
right: 18,
color: "grey",
}}
onClick={() => {
finishedUsecases.push(usecaseSearch)
setFinishedUsecases(finishedUsecases)
setUsecaseSearch("")
setUsecaseSearchType("")
}}
>
<CloseIcon />
</IconButton>
<UsecaseSearch
globalUrl={globalUrl}
defaultSearch={usecaseSearchType}
usecaseSearch={usecaseSearch}
appFramework={frameworkData}
userdata={userdata}
autotry={true}
setCloseWindow={setCloseWindow}
setUsecaseSearch={setUsecaseSearch}
apps={apps}
/>
</Dialog>
<div style={{minWidth: 250, maxWidth: 250, padding: 15, borderRadius: theme.palette.borderRadius, position: "relative", }}>
<Typography variant="body1" style={{textAlign: "center"}}>
Suggested Workflows ({finishedUsecases.length}/{usecaseSuggestions.length})
</Typography>
<IconButton
style={{
zIndex: 5000,
position: "absolute",
top: 8,
right: 8,
color: "grey",
padding: 2,
}}
onClick={() => {
if (setUsecaseSuggestions !== undefined) {
setUsecaseSuggestions([])
}
}}
>
<CloseIcon style={{height: 18, width: 18, }} />
</IconButton>
{usecaseSuggestions.map((usecase, index) => {
return (
<IndividualUsecase
key={index}
usecase={usecase}
index={index}
/>
)
})}
</div>
</Paper>
)
}
export default SuggestedWorkflows;

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,730 @@
import React, { useState, useEffect, useRef } from "react";
import ReactGA from "react-ga4";
import Checkbox from "@mui/material/Checkbox";
import AliceCarousel from "react-alice-carousel";
import "react-alice-carousel/lib/alice-carousel.css";
import SearchIcon from "@mui/icons-material/Search";
import EmailIcon from "@mui/icons-material/Email";
import NewReleasesIcon from "@mui/icons-material/NewReleases";
import ExtensionIcon from "@mui/icons-material/Extension";
import LightbulbIcon from "@mui/icons-material/Lightbulb";
import TrendingFlatIcon from "@mui/icons-material/TrendingFlat";
import theme from "../theme.jsx";
import CheckBoxSharpIcon from "@mui/icons-material/CheckBoxSharp";
import {
Button,
Collapse,
IconButton,
FormGroup,
FormControl,
InputLabel,
FormLabel,
FormControlLabel,
Select,
MenuItem,
Grid,
Paper,
Typography,
TextField,
Zoom,
List,
ListItem,
ListItemText,
Divider,
Tooltip,
Chip,
ButtonGroup,
} from "@mui/material";
//import { useAlert
import { useNavigate, Link } from "react-router-dom";
import WorkflowSearch from "../components/Workflowsearch.jsx";
import AuthenticationItem from "../components/AuthenticationItem.jsx";
import WorkflowPaper from "../components/WorkflowPaper.jsx";
import UsecaseSearch from "../components/UsecaseSearch.jsx";
import ExploreWorkflow from "../components/ExploreWorkflow.jsx";
import AppSelection from "../components/AppSelection.jsx";
const responsive = {
0: { items: 1 },
};
const imagestyle = {
height: 40,
borderRadius: 40,
//border: "2px solid rgba(255,255,255,0.3)",
};
const WelcomeForm = (props) => {
const {
userdata,
globalUrl,
discoveryWrapper,
setDiscoveryWrapper,
appFramework,
getFramework,
activeStep,
setActiveStep,
steps,
skipped,
setSkipped,
getApps,
apps,
handleSetSearch,
usecaseButtons,
defaultSearch,
setDefaultSearch,
selectionOpen,
setSelectionOpen,
checkLogin,
} = props;
const [moreButton, setMoreButton] = useState(false);
const ref = useRef()
const [usecaseItems, setUsecaseItems] = useState([
{
search: "Phishing",
usecase_search: undefined,
},
{
search: "Enrichment",
usecase_search: undefined,
},
{
search: "Enrichment",
usecase_search: "SIEM alert enrichment",
},
{
search: "Build your own",
usecase_search: undefined,
},
]);
/*
<div style={{minWidth: "95%", maxWidth: "95%", marginLeft: 5, marginRight: 5, }}>
<UsecaseSearch
globalUrl={globalUrl}
defaultSearch={"Phishing"}
appFramework={appFramework}
apps={apps}
getFramework={getFramework}
userdata={userdata}
/>
</div>
,
<div style={{minWidth: "95%", maxWidth: "95%", marginLeft: 5, marginRight: 5, }}>
<UsecaseSearch
globalUrl={globalUrl}
defaultSearch={"Enrichment"}
appFramework={appFramework}
apps={apps}
getFramework={getFramework}
userdata={userdata}
/>
</div>
,
<div style={{minWidth: "95%", maxWidth: "95%", marginLeft: 5, marginRight: 5, }}>
<UsecaseSearch
globalUrl={globalUrl}
defaultSearch={"Enrichment"}
usecaseSearch={"SIEM alert enrichment"}
appFramework={appFramework}
apps={apps}
getFramework={getFramework}
userdata={userdata}
/>
</div>
,
<div style={{minWidth: "95%", maxWidth: "95%", marginLeft: 5, marginRight: 5, }}>
<UsecaseSearch
globalUrl={globalUrl}
defaultSearch={"Build your own"}
appFramework={appFramework}
apps={apps}
getFramework={getFramework}
userdata={userdata}
/>
</div>
])
*/
const [name, setName] = React.useState("")
const [orgName, setOrgName] = React.useState("")
const [role, setRole] = React.useState("")
const [orgType, setOrgType] = React.useState("")
const [finishedApps, setFinishedApps] = React.useState([])
const [authentication, setAuthentication] = React.useState([]);
const [thumbIndex, setThumbIndex] = useState(0);
const [thumbAnimation, setThumbAnimation] = useState(false);
const [clickdiff, setclickdiff] = useState(0);
const [mouseHoverIndex, setMouseHoverIndex] = useState(-1)
const isCloud = window.location.host === "localhost:3002" || window.location.host === "shuffler.io";
//const alert = useAlert();
let navigate = useNavigate();
const iconStyles = {
color: "rgba(255, 255, 255, 1)",
};
useEffect(() => {
if (userdata.id === undefined) {
return;
}
if (
userdata.name !== undefined &&
userdata.name !== null &&
userdata.name.length > 0
) {
setName(userdata.name);
}
if (
userdata.active_org !== undefined &&
userdata.active_org.name !== undefined &&
userdata.active_org.name !== null &&
userdata.active_org.name.length > 0
) {
setOrgName(userdata.active_org.name);
}
}, [userdata]);
useEffect(() => {
if (discoveryWrapper === undefined || discoveryWrapper.id === undefined) {
setDefaultSearch("");
var newfinishedApps = finishedApps;
newfinishedApps.push(defaultSearch);
setFinishedApps(finishedApps);
}
}, [discoveryWrapper]);
useEffect(() => {
if (window.location.search !== undefined && window.location.search !== null) {
const urlSearchParams = new URLSearchParams(window.location.search);
const params = Object.fromEntries(urlSearchParams.entries());
const foundTemplate = params["workflow_template"];
if (foundTemplate !== null && foundTemplate !== undefined) {
console.log("Found workflow template: ", foundTemplate);
var sourceapp = undefined;
var destinationapp = undefined;
var action = undefined;
const srcapp = params["source_app"];
if (srcapp !== null && srcapp !== undefined) {
sourceapp = srcapp;
}
const dstapp = params["dest_app"];
if (dstapp !== null && dstapp !== undefined) {
destinationapp = dstapp;
}
const act = params["action"];
if (act !== null && act !== undefined) {
action = act;
}
//defaultSearch={foundTemplate}
//
usecaseItems[0] = {
search: "enrichment",
usecase_search: foundTemplate,
sourceapp: sourceapp,
destinationapp: destinationapp,
autotry: action === "try",
};
console.log("Adding: ", usecaseItems[0]);
setUsecaseItems(usecaseItems);
}
}
}, []);
const isStepOptional = (step) => {
return step === 1;
};
const sendUserUpdate = (name, role, userId) => {
const data = {
tutorial: "welcome",
firstname: name,
company_role: role,
user_id: userId,
};
const url = `${globalUrl}/api/v1/users/updateuser`;
fetch(url, {
mode: "cors",
method: "PUT",
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) {
console.log("Update user success");
//toast("Failed updating org: ", responseJson.reason);
} else {
console.log("Update success!");
//toast("Successfully edited org!");
}
})
)
.catch((error) => {
console.log("Update err: ", error.toString());
//toast("Err: " + error.toString());
});
};
const sendOrgUpdate = (orgname, company_type, orgId, priority) => {
var data = {
org_id: orgId,
};
if (orgname.length > 0) {
data.name = orgname;
}
if (company_type.length > 0) {
data.company_type = company_type;
}
if (priority.length > 0) {
data.priority = priority;
}
const url = globalUrl + `/api/v1/orgs/${orgId}`;
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) {
console.log("Update of org failed");
//toast("Failed updating org: ", responseJson.reason);
} else {
//toast("Successfully edited org!");
}
})
)
.catch((error) => {
console.log("Update err: ", error.toString());
//toast("Err: " + error.toString());
});
};
var workflowDelay = -50;
const NewHits = ({ hits }) => {
const [mouseHoverIndex, setMouseHoverIndex] = useState(-1);
var counted = 0;
const paperAppContainer = {
display: "flex",
flexWrap: "wrap",
alignContent: "space-between",
marginTop: 5,
};
return (
<Grid container spacing={4} style={paperAppContainer}>
{hits.map((data, index) => {
workflowDelay += 50;
if (index > 3) {
return null;
}
return (
<Zoom
key={index}
in={true}
style={{ transitionDelay: `${workflowDelay}ms` }}
>
<Grid item xs={6} style={{ padding: "12px 10px 12px 10px" }}>
<WorkflowPaper key={index} data={data} />
</Grid>
</Zoom>
);
})}
</Grid>
);
};
const isStepSkipped = (step) => {
return skipped.has(step);
};
const handleNext = () => {
setDefaultSearch("");
if (activeStep === 0) {
console.log("Should send basic information about org (fetch)");
setclickdiff(240);
navigate(`/welcome?tab=2`);
setActiveStep(1);
if (isCloud) {
ReactGA.event({
category: "welcome",
action: "click_page_one_next",
label: "",
});
}
if (
userdata.active_org !== undefined &&
userdata.active_org.id !== undefined &&
userdata.active_org.id !== null &&
userdata.active_org.id.length > 0
) {
sendOrgUpdate(orgName, orgType, userdata.active_org.id, "");
}
if (
userdata.id !== undefined &&
userdata.id !== null &&
userdata.id.length > 0
) {
sendUserUpdate(name, role, userdata.id);
}
} else if (activeStep === 1) {
console.log("Should send secondary info about apps and other things");
setDiscoveryWrapper({});
navigate(`/welcome?tab=3`);
//handleSetSearch("Enrichment", "2. Enrich")
handleSetSearch(usecaseButtons[0].name, usecaseButtons[0].usecase);
getApps();
setActiveStep(2);
// Make sure it's up to date
if (getFramework !== undefined) {
getFramework();
}
} else if (activeStep === 2) {
console.log(
"Should send third page with workflows activated and the like"
);
}
let newSkipped = skipped;
if (isStepSkipped(activeStep)) {
newSkipped = new Set(newSkipped.values());
newSkipped.delete(activeStep);
}
setActiveStep((prevActiveStep) => prevActiveStep + 1);
setSkipped(newSkipped);
};
const handleBack = () => {
setActiveStep((prevActiveStep) => prevActiveStep - 1);
if (activeStep === 2) {
setDiscoveryWrapper({});
if (getFramework !== undefined) {
getFramework();
}
navigate("/welcome?tab=2");
} else if (activeStep === 1) {
navigate("/welcome?tab=1");
}
};
const handleSkip = () => {
setclickdiff(240);
if (!isStepOptional(activeStep)) {
throw new Error("You can't skip a step that isn't optional.");
}
setActiveStep((prevActiveStep) => prevActiveStep + 1);
setSkipped((prevSkipped) => {
const newSkipped = new Set(prevSkipped.values());
newSkipped.add(activeStep);
return newSkipped;
});
};
const handleReset = () => {
setActiveStep(0);
};
//const buttonWidth = 145
const buttonWidth = 450;
const buttonMargin = 10;
const sizing = 510;
const bottomButtonStyle = {
borderRadius: 200,
marginTop: moreButton ? 44 : "",
height: 51,
width: 464,
fontSize: 16,
// background: "linear-gradient(89.83deg, #FF8444 0.13%, #F2643B 99.84%)",
background: "linear-gradient(90deg, #F86744 0%, #F34475 100%)",
padding: "16px 24px",
// top: 20,
// margin: "auto",
itemAlign: "center",
// marginLeft: "65px",
};
const slideNext = () => {
if (!thumbAnimation && thumbIndex < usecaseItems.length - 1) {
//handleSetSearch(usecaseButtons[0].name, usecaseButtons[0].usecase)
setThumbIndex(thumbIndex + 1);
} else if (!thumbAnimation && thumbIndex === usecaseItems.length - 1) {
setThumbIndex(0);
}
};
const slidePrev = () => {
if (!thumbAnimation && thumbIndex > 0) {
setThumbIndex(thumbIndex - 1);
} else if (!thumbAnimation && thumbIndex === 0) {
setThumbIndex(usecaseItems.length - 1);
}
};
const newButtonStyle = {
padding: 22,
flex: 1,
margin: buttonMargin,
minWidth: buttonWidth,
maxWidth: buttonWidth,
};
const formattedCarousel =
appFramework === undefined || appFramework === null
? []
: usecaseItems.map((item, index) => {
return (
<div
style={{
minWidth: "95%",
maxWidth: "95%",
marginLeft: 5,
marginRight: 5,
}}
>
<UsecaseSearch
globalUrl={globalUrl}
defaultSearch={item.search}
usecaseSearch={item.usecase_search}
appFramework={appFramework}
apps={apps}
getFramework={getFramework}
userdata={userdata}
autotry={item.autotry}
sourceapp={item.sourceapp}
destinationapp={item.destinationapp}
/>
</div>
);
});
const getStepContent = (step) => {
switch (step) {
case 0:
return (
<Collapse in={true}>
<Grid
container
spacing={1}
style={{
margin: "auto",
maxWidth: 500,
minWidth: 500,
minHeight: sizing,
maxHeight: sizing,
}}
>
{/*isCloud ? null :
<Typography variant="body1" style={{marginLeft: 8, marginTop: 10, marginRight: 30, }} color="textSecondary">
This data will be used within the product and NOT be shared unless <a href="https://shuffler.io/docs/organizations#cloud_synchronization" target="_blank" rel="norefferer" style={{color: "#f86a3e", textDecoration: "none"}}>cloud synchronization</a> is configured.
</Typography>
*/}
<Typography
variant="body1"
style={{ marginLeft: 8, marginTop: 10, marginRight: 30 }}
color="textSecondary"
>
In order to understand how we best can help you find relevant
Usecases, please provide the information below. This is
optional, but highly encouraged.
</Typography>
<Grid item xs={11} style={{ marginTop: 16, padding: 0 }}>
<TextField
required
style={{ width: "100%", marginTop: 0 }}
placeholder="Name"
autoFocus
label="Name"
type="name"
id="standard-required"
autoComplete="name"
margin="normal"
variant="outlined"
value={name}
onChange={(e) => {
setName(e.target.value);
}}
/>
</Grid>
<Grid item xs={11} style={{ marginTop: 10, padding: 0 }}>
<TextField
required
style={{ width: "100%", marginTop: 0 }}
placeholder="Company / Institution"
label="Company Name"
type="companyname"
id="standard-required"
autoComplete="CompanyName"
margin="normal"
variant="outlined"
value={orgName}
onChange={(e) => {
setOrgName(e.target.value);
}}
/>
</Grid>
<Grid item xs={11} style={{ marginTop: 10 }}>
<FormControl fullWidth={true}>
<InputLabel style={{ marginLeft: 10, color: "#B9B9BA" }}>
Your Role
</InputLabel>
<Select
variant="outlined"
required
onChange={(e) => {
setRole(e.target.value);
}}
>
<MenuItem value={"Student"}>Student</MenuItem>
<MenuItem value={"Security Analyst/Engineer"}>
Security Analyst/Engineer
</MenuItem>
<MenuItem value={"SOC Manager"}>SOC Manager</MenuItem>
<MenuItem value={"C-Level"}>C-Level</MenuItem>
<MenuItem value={"Other"}>Other</MenuItem>
</Select>
</FormControl>
</Grid>
<Grid item xs={11} style={{ marginTop: 16 }}>
<FormControl fullWidth={true}>
<InputLabel style={{ marginLeft: 10, color: "#B9B9BA" }}>
Company Type
</InputLabel>
<Select
required
variant="outlined"
onChange={(e) => {
setOrgType(e.target.value);
}}
>
<MenuItem value={"Education"}>Education</MenuItem>
<MenuItem value={"MSSP"}>MSSP</MenuItem>
<MenuItem value={"Security Product Company"}>
Security Product Company
</MenuItem>
<MenuItem value={"Other"}>Other</MenuItem>
</Select>
</FormControl>
</Grid>
</Grid>
</Collapse>
);
case 1:
return (
<AppSelection
globalUrl={globalUrl}
userdata={userdata}
appFramework={appFramework}
setActiveStep={setActiveStep}
defaultSearch={defaultSearch}
setDefaultSearch={setDefaultSearch}
selectionOpen={selectionOpen}
setSelectionOpen={setSelectionOpen}
checkLogin={checkLogin}
/>
)
case 2:
return (
<Collapse in={true}>
<div style={{ marginTop: 0, maxWidth: 700, minWidth: 700, margin: "auto", minHeight: sizing, maxHeight: sizing, }}>
<div style={{ marginTop: 0, }}>
<div className="thumbs" style={{ display: "flex" }}>
<div style={{ minWidth: 554, maxWidth: 554, borderRadius: theme.palette.borderRadius, }}>
<ExploreWorkflow
globalUrl={globalUrl}
userdata={userdata}
appFramework={appFramework}
/>
</div>
</div>
</div>
</div>
</Collapse>
)
default:
return "unknown step"
}
}
const extraHeight = isCloud ? -7 : 0;
return (
<div style={{}}>
{/*selectionOpen ?
<WorkflowSearch
defaultSearch={defaultSearch}
newSelectedApp={newSelectedApp}
setNewSelectedApp={setNewSelectedApp}
/>
: null*/}
<div>
{activeStep === steps.length ? (
<div paddingTop="20px">
You Will be Redirected to getting Start Page Wait for 5-sec.
<Button onClick={handleReset}>Reset</Button>
<script>
setTimeout(function() {navigate("/workflows")}, 5000);
</script>
<Button>
<Link
style={{ color: "#f86a3e" }}
to="/workflows"
className="btn btn-primary"
>
Getting Started
</Link>
</Button>
</div>
) : (
<div>
{getStepContent(activeStep)}
</div>
)}
</div>
</div>
);
};
export default WelcomeForm;

View File

@ -0,0 +1,387 @@
import React, { useEffect, useState } from 'react';
import {Link} from 'react-router-dom';
import theme from '../theme.jsx';
import { removeQuery } from '../components/ScrollToTop.jsx';
import { Search as SearchIcon, CloudQueue as CloudQueueIcon, Code as CodeIcon } from '@mui/icons-material';
import algoliasearch from 'algoliasearch/lite';
import { InstantSearch, Configure, connectSearchBox, connectHits } from 'react-instantsearch-dom';
import {
Grid,
Paper,
TextField,
ButtonBase,
InputAdornment,
Typography,
Button,
Tooltip,
Zoom,
Chip,
} from '@mui/material';
import WorkflowPaper from "../components/WorkflowPaper.jsx"
import WorkflowPaperNew from "../components/WorkflowPaperNew.jsx"
const searchClient = algoliasearch("JNSS5CFDZZ", "db08e40265e2941b9a7d8f644b6e5240")
const AppGrid = props => {
const { maxRows, showName, showSuggestion, isMobile, globalUrl, parsedXs, alternativeView, onlyResults, inputsearch } = 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 : 4 : parsedXs
//const [apps, setApps] = React.useState([]);
//const [filteredApps, setFilteredApps] = React.useState([]);
const [formMail, setFormMail] = React.useState("");
const [message, setMessage] = React.useState("");
const [formMessage, setFormMessage] = React.useState("");
const [usecases, setUsecases] = React.useState([]);
const [localMessage, setLocalMessage] = React.useState("");
const buttonStyle = {borderRadius: 30, height: 50, width: 220, margin: isMobile ? "15px auto 15px auto" : 20, fontSize: 18,}
const innerColor = "rgba(255,255,255,0.65)"
const borderRadius = 3
window.title = "Shuffle | Workflows | Discover your use-case"
const submitContact = (email, message) => {
const data = {
"firstname": "",
"lastname": "",
"title": "",
"companyname": "",
"email": email,
"phone": "",
"message": message,
}
const errorMessage = "Something went wrong. Please contact frikky@shuffler.io directly."
fetch(globalUrl+"/api/v1/contact", {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(data),
})
.then(response => response.json())
.then(response => {
if (response.success === true) {
setFormMessage(response.reason)
//toast("Thanks for submitting!")
} else {
setFormMessage(errorMessage)
}
setFormMail("")
setMessage("")
})
.catch(error => {
setFormMessage(errorMessage)
console.log(error)
});
}
const handleKeysetting = (categorydata, workflows) => {
console.log("Workflows: ", workflows)
//workflows[0].category = ["detect"]
//workflows[0].usecase_ids = ["Correlate tickets"]
if (workflows !== undefined && workflows !== null) {
const newcategories = []
for (var key in categorydata) {
var category = categorydata[key]
category.matches = []
for (var subcategorykey in category.list) {
var subcategory = category.list[subcategorykey]
subcategory.matches = []
for (var workflowkey in workflows) {
const workflow = workflows[workflowkey]
if (workflow.usecase_ids !== undefined && workflow.usecase_ids !== null) {
for (var usecasekey in workflow.usecase_ids) {
if (workflow.usecase_ids[usecasekey].toLowerCase() === subcategory.name.toLowerCase()) {
console.log("Got match: ", workflow.usecase_ids[usecasekey])
category.matches.push({
"workflow": workflow.id,
"category": subcategory.name,
})
subcategory.matches.push(workflow.id)
break
}
}
}
if (subcategory.matches.length > 0) {
break
}
}
}
newcategories.push(category)
}
console.log("Categories: ", newcategories)
setUsecases(newcategories)
} else {
for (var key in categorydata) {
categorydata[key].matches = []
}
setUsecases(categorydata)
}
}
const fetchUsecases = (workflows) => {
fetch(globalUrl + "/api/v1/workflows/usecases", {
method: "GET",
headers: {
"Content-Type": "application/json",
Accept: "application/json",
},
credentials: "include",
})
.then((response) => {
if (response.status !== 200) {
console.log("Status not 200 for usecases");
}
return response.json();
})
.then((responseJson) => {
if (responseJson.success !== false) {
//handleKeysetting(responseJson, workflows)
}
})
.catch((error) => {
//toast("ERROR: " + error.toString());
console.log("ERROR: " + error.toString());
});
};
useEffect(() => {
fetchUsecases()
}, [])
// value={currentRefinement}
const SearchBox = ({currentRefinement, refine, isSearchStalled} ) => {
var defaultSearch = ""
useEffect(() => {
if (window !== undefined && window.location !== undefined && window.location.search !== undefined && window.location.search !== null) {
const urlSearchParams = new URLSearchParams(window.location.search)
const params = Object.fromEntries(urlSearchParams.entries())
const foundQuery = params["q"]
if (foundQuery !== null && foundQuery !== undefined) {
console.log("Got query: ", foundQuery)
refine(foundQuery)
defaultSearch = foundQuery
}
}
}, [])
if (localMessage !== inputsearch && inputsearch !== undefined && inputsearch !== null && inputsearch.length > 0) {
//setLocalMessage(inputsearch)
refine(inputsearch)
defaultSearch = inputsearch
return null
} else if (onlyResults === true) {
// Don't return anything unless refinement works
return null
}
return (
<form noValidate action="" role="search">
{onlyResults !== true ?
<TextField
defaultValue={defaultSearch}
fullWidth
style={{backgroundColor: theme.palette.inputColor, borderRadius: borderRadius, margin: 10, width: "100%",}}
InputProps={{
style:{
},
startAdornment: (
<InputAdornment position="start">
<SearchIcon style={{marginLeft: 5}}/>
</InputAdornment>
),
}}
autoComplete='off'
type="search"
color="primary"
value={currentRefinement}
placeholder="Find Workflows..."
id="shuffle_search_field"
onChange={(event) => {
removeQuery("q")
refine(event.currentTarget.value)
}}
limit={5}
/>
: null}
</form>
)
}
const paperAppContainer = {
display: "flex",
flexWrap: "wrap",
alignContent: "space-between",
marginTop: 5,
}
var workflowDelay = -50
const Hits = ({ hits }) => {
const [mouseHoverIndex, setMouseHoverIndex] = useState(-1)
var counted = 0
return (
<div>
{onlyResults === true && hits.length > 0 ?
null
: null}
<Grid container spacing={4} style={paperAppContainer}>
{hits.map((data, index) => {
workflowDelay += 50
if (counted === 12/xs*rowHandler) {
return null
}
counted += 1
return (
<Grid item xs={xs} style={{ padding: "12px 10px 12px 10px",}}>
{/*<Zoom key={index} in={true} style={{ transitionDelay: `${workflowDelay}ms` }}>*/}
{alternativeView === true ?
<WorkflowPaperNew key={index} data={data} />
:
<WorkflowPaper key={index} data={data} />
}
</Grid>
)
})}
</Grid>
</div>
)
}
const CustomSearchBox = connectSearchBox(SearchBox)
const CustomHits = connectHits(Hits)
return (
<div style={{width: "100%", position: "relative", height: "100%",}}>
<InstantSearch searchClient={searchClient} indexName="workflows">
<Configure clickAnalytics />
<div style={{maxWidth: 450, margin: "auto", marginTop: 15, marginBottom: 15, }}>
<CustomSearchBox />
</div>
{usecases !== null && usecases !== undefined && usecases.length > 0 ?
<div style={{ display: "flex", margin: "auto", width: 875,}}>
{usecases.map((usecase, index) => {
console.log(usecase)
return (
<Chip
key={usecase.name}
style={{
backgroundColor: theme.palette.surfaceColor,
marginRight: 10,
paddingLeft: 5,
paddingRight: 5,
height: 28,
cursor: "pointer",
border: `1px solid ${usecase.color}`,
color: "white",
}}
label={`${usecase.name} (${usecase.matches.length}/${usecase.list.length})`}
onClick={() => {
console.log("Clicked!")
//addFilter(usecase.name.slice(3,usecase.name.length))
}}
variant="outlined"
color="primary"
/>
)
})}
</div>
: null}
<CustomHits hitsPerPage={5}/>
</InstantSearch>
{showSuggestion === true ?
<div style={{maxWidth: isMobile ? "100%" : "60%", margin: "auto", paddingTop: 0, textAlign: "center",}}>
<Typography variant="h6" style={{color: "white", marginTop: 50,}}>
Can't find what you're looking for?
</Typography>
<div style={{flex: "1", display: "flex", flexDirection: "row"}}>
<TextField
required
style={{flex: "1", marginRight: "15px", backgroundColor: theme.palette.inputColor}}
InputProps={{
style:{
color: "#ffffff",
},
}}
color="primary"
fullWidth={true}
placeholder="Email (optional)"
type="email"
id="email-handler"
autoComplete="email"
margin="normal"
variant="outlined"
onChange={e => setFormMail(e.target.value)}
/>
<TextField
required
style={{flex: "1", backgroundColor: theme.palette.inputColor}}
InputProps={{
style:{
color: "#ffffff",
},
}}
color="primary"
fullWidth={true}
placeholder="What apps do you want to see?"
type=""
id="standard-required"
margin="normal"
variant="outlined"
autoComplete="off"
onChange={e => setMessage(e.target.value)}
/>
</div>
<Button
variant="contained"
color="primary"
style={buttonStyle}
disabled={message.length === 0}
onClick={() => {
submitContact(formMail, message)
}}
>
Submit
</Button>
<Typography style={{color: "white"}} variant="body2">{formMessage}</Typography>
</div>
: null
}
{onlyResults === true ? null :
<span style={{position: "absolute", display: "flex", textAlign: "right", float: "right", right: 0, bottom: 120, }}>
<Typography variant="body2" color="textSecondary" style={{}}>
Search by
</Typography>
<a rel="noopener noreferrer" href="https://www.algolia.com/" target="_blank" style={{textDecoration: "none", color: "white"}}>
<img src={"/images/logo-algolia-nebula-blue-full.svg"} alt="Algolia logo" style={{height: 17, marginLeft: 5, marginTop: 3,}} />
</a>
</span>
}
</div>
)
}
export default AppGrid;

View File

@ -0,0 +1,302 @@
import React, { useState, useEffect, useLayoutEffect } from "react";
import theme from '../theme.jsx';
import {
Chip,
Typography,
Paper,
Avatar,
Grid,
Tooltip,
} from "@mui/material";
import {
AvatarGroup,
} from "@mui/material"
import {
Restore as RestoreIcon,
Edit as EditIcon,
BubbleChart as BubbleChartIcon,
MoreVert as MoreVertIcon,
} from '@mui/icons-material';
import { useNavigate, Link, useParams } from "react-router-dom";
const workflowActionStyle = {
display: "flex",
width: 160,
height: 44,
justifyContent: "space-between",
}
const chipStyle = {
backgroundColor: "#3d3f43",
marginRight: 5,
paddingLeft: 5,
paddingRight: 5,
height: 28,
cursor: "pointer",
borderColor: "#3d3f43",
color: "white",
}
const WorkflowPaper = (props) => {
const { data } = props;
let navigate = useNavigate();
const [open, setOpen] = React.useState(false);
const [anchorEl, setAnchorEl] = React.useState(null);
const appGroup = data.action_references === undefined || data.action_references === null ? [] : data.action_references
const isCloud =
window.location.host === "localhost:3002" ||
window.location.host === "shuffler.io";
//console.log("Workflow: ", data)
var boxColor = "#86c142";
const paperAppStyle = {
minHeight: 130,
maxHeight: 130,
overflow: "hidden",
width: "100%",
color: "white",
backgroundColor: theme.palette.surfaceColor,
padding: "12px 12px 0px 15px",
borderRadius: 5,
display: "flex",
boxSizing: "border-box",
position: "relative",
}
var parsedName = data.name;
if (
parsedName !== undefined &&
parsedName !== null &&
parsedName.length > 20
) {
parsedName = parsedName.slice(0, 21) + "..";
}
const imageStyle = {
width: 24,
height: 24,
marginRight: 10,
border: "1px solid rgba(255,255,255,0.3)",
}
var image = data.creator_info !== undefined && data.creator_info !== null && data.creator_info.image !== undefined && data.creator_info.image !== null && data.creator_info.image.length > 0 ? <Avatar alt={data.creator} src={data.creator_info.image} style={imageStyle}/> : <Avatar alt={"shuffle_image"} src={theme.palette.defaultImage} style={imageStyle}/>
const creatorname = data.creator_info !== undefined && data.creator_info !== null && data.creator_info.username !== undefined && data.creator_info.username !== null && data.creator_info.username.length > 0 ? data.creator_info.username : ""
var orgName = "";
var orgId = "";
if ((data.objectID === undefined || data.objectID === null) && data.id !== undefined && data.id !== null) {
data.objectID = data.id
}
//console.log("IMG: ", data)
var parsedUrl = `/workflows/${data.objectID}`
if (data.__queryID !== undefined && data.__queryID !== null) {
parsedUrl += `?queryID=${data.__queryID}`
}
if (!isCloud) {
parsedUrl = `https://shuffler.io${parsedUrl}`
}
return (
<div style={{width: "100%", position: "relative",}}>
<Paper square style={paperAppStyle}>
<div
style={{
position: "absolute",
bottom: 1,
left: 1,
height: 12,
width: 12,
backgroundColor: boxColor,
borderRadius: "0 100px 0 0",
}}
/>
<Grid
item
style={{ display: "flex", flexDirection: "column", width: "100%" }}
>
<Grid item style={{ display: "flex", maxHeight: 34 }}>
<Tooltip title={`${creatorname}`} placement="bottom">
<div
style={{ cursor: data.creator_info !== undefined ? "pointer" : "inherit" }}
onClick={() => {
if (data.creator_info !== undefined) {
navigate("/creators/"+data.creator_info.username)
}
}}
>
{image}
</div>
</Tooltip>
<Tooltip title={`Edit ${data.name}`} placement="bottom">
<Typography
variant="body1"
style={{
marginBottom: 0,
paddingBottom: 0,
maxHeight: 30,
flex: 10,
}}
>
<a
href={parsedUrl}
rel="norefferer"
target="_blank"
style={{ textDecoration: "none", color: "inherit" }}
>
{parsedName}
</a>
</Typography>
</Tooltip>
</Grid>
<Grid item style={workflowActionStyle}>
{appGroup.length > 0 ?
<div style={{display: "flex", marginTop: 8, }}>
<AvatarGroup max={4} style={{marginLeft: 5, maxHeight: 24,}}>
{appGroup.map((app, index) => {
return (
<div
key={index}
style={{
height: 24,
width: 24,
filter: "brightness(0.6)",
cursor: "pointer",
}}
onClick={() => {
navigate("/apps/"+app.id)
}}
>
<Tooltip color="primary" title={app.name} placement="bottom">
<Avatar alt={app.name} src={app.image_url} style={{width: 24, height: 24}}/>
</Tooltip>
</div>
)
})}
</AvatarGroup>
</div>
:
<Tooltip color="primary" title="Action amount" placement="bottom">
<span style={{ color: "#979797", display: "flex" }}>
<BubbleChartIcon
style={{ marginTop: "auto", marginBottom: "auto" }}
/>
<Typography
style={{
marginLeft: 5,
marginTop: "auto",
marginBottom: "auto",
}}
>
{data.actions === undefined || data.actions === null ? 1 : data.actions.length}
</Typography>
</span>
</Tooltip>
}
<Tooltip
color="primary"
title="Trigger amount"
placement="bottom"
>
<span
style={{ marginLeft: 15, color: "#979797", display: "flex" }}
>
<RestoreIcon
style={{
color: "#979797",
marginTop: "auto",
marginBottom: "auto",
}}
/>
<Typography
style={{
marginLeft: 5,
marginTop: "auto",
marginBottom: "auto",
}}
>
{data.triggers === undefined || data.triggers === null ? 1 : data.triggers.length}
</Typography>
</span>
</Tooltip>
<Tooltip color="primary" title="Subflows used" placement="bottom">
<span
style={{
marginLeft: 15,
display: "flex",
color: "#979797",
cursor: "pointer",
}}
onClick={() => {
}}
>
<svg
width="18"
height="18"
viewBox="0 0 18 18"
fill="none"
xmlns="http://www.w3.org/2000/svg"
style={{
color: "#979797",
marginTop: "auto",
marginBottom: "auto",
}}
>
<path
d="M0 0H15V15H0V0ZM16 16H18V18H16V16ZM16 13H18V15H16V13ZM16 10H18V12H16V10ZM16 7H18V9H16V7ZM16 4H18V6H16V4ZM13 16H15V18H13V16ZM10 16H12V18H10V16ZM7 16H9V18H7V16ZM4 16H6V18H4V16Z"
fill="#979797"
/>
</svg>
<Typography
style={{
marginLeft: 5,
marginTop: "auto",
marginBottom: "auto",
}}
>
{0}
</Typography>
</span>
</Tooltip>
</Grid>
<Grid
item
style={{
justifyContent: "left",
overflow: "hidden",
marginTop: 5,
}}
>
{data.tags !== undefined && data.tags !== null
? data.tags.map((tag, index) => {
if (index >= 2) {
return null;
}
return (
<Chip
key={index}
style={chipStyle}
label={tag}
variant="outlined"
color="primary"
/>
);
})
: null}
</Grid>
</Grid>
</Paper>
</div>
)
}
export default WorkflowPaper

View File

@ -0,0 +1,332 @@
import React, { useState, useEffect, useLayoutEffect } from "react";
import theme from '../theme.jsx';
import {
Chip,
Typography,
Paper,
Avatar,
Grid,
Tooltip,
Button,
} from "@mui/material";
import {
AvatarGroup,
} from "@mui/material"
import {
Restore as RestoreIcon,
Edit as EditIcon,
BubbleChart as BubbleChartIcon,
MoreVert as MoreVertIcon,
} from '@mui/icons-material';
import { useNavigate, Link, useParams } from "react-router-dom";
const workflowActionStyle = {
display: "flex",
width: 160,
height: 44,
justifyContent: "space-between",
}
const paperAppStyle = {
minHeight: 130,
maxHeight: 130,
overflow: "hidden",
width: "100%",
color: "white",
backgroundColor: theme.palette.surfaceColor,
padding: "12px 12px 0px 15px",
borderRadius: 5,
display: "flex",
boxSizing: "border-box",
position: "relative",
}
const chipStyle = {
backgroundColor: "#3d3f43",
marginRight: 5,
paddingLeft: 5,
paddingRight: 5,
height: 28,
cursor: "pointer",
borderColor: "#3d3f43",
color: "white",
}
const WorkflowPaper = (props) => {
const { data } = props;
let navigate = useNavigate();
const [open, setOpen] = React.useState(false);
const [anchorEl, setAnchorEl] = React.useState(null);
const appGroup = data.action_references === undefined || data.action_references === null ? [] : data.action_references
const activateWorkflow = (workflow) => {
console.log("Should activate: ", workflow)
}
//console.log("Workflow: ", data)
var boxColor = "#86c142";
var parsedName = data.name;
if (
parsedName !== undefined &&
parsedName !== null &&
parsedName.length > 35
) {
parsedName = parsedName.slice(0, 36) + "..";
}
const imageStyle = {
width: 28,
height: 28,
marginRight: 10,
border: "1px solid rgba(255,255,255,0.3)",
}
var image = data.creator_info !== undefined && data.creator_info !== null && data.creator_info.image !== undefined && data.creator_info.image !== null && data.creator_info.image.length > 0 ? <Avatar alt={data.creator} src={data.creator_info.image} style={imageStyle}/> : <Avatar alt={"shuffle_image"} src={theme.palette.defaultImage} style={imageStyle}/>
const creatorname = data.creator_info !== undefined && data.creator_info !== null && data.creator_info.username !== undefined && data.creator_info.username !== null && data.creator_info.username.length > 0 ? data.creator_info.username : "Shuffle"
var orgName = "";
var orgId = "";
if ((data.objectID === undefined || data.objectID === null) && data.id !== undefined && data.id !== null) {
data.objectID = data.id
}
//console.log("IMG: ", data)
var parsedUrl = `/workflows/${data.objectID}`
if (data.__queryID !== undefined && data.__queryID !== null) {
parsedUrl += `?queryID=${data.__queryID}`
}
const paperImgStyle = {
height: 150,
width: "100%",
backgroundImage: "linear-gradient(to right, #f86a3e, #f34079)",
color: "white",
position: "relative",
borderRadius: "10px 10px 0% 0%",
}
const bgImage1 = "https://avatars.githubusercontent.com/u/5719530?v=4"
const bgImage2 = "https://avatars.githubusercontent.com/u/5719530?v=4"
const itemSize = 70
return (
<div style={{width: "100%", position: "relative",}}>
<div style={paperImgStyle}>
<div style={{position: "absolute", left: 55, top: 42, height: itemSize, width: itemSize, }}>
<img src={bgImage1} alt="Image alt" style={{overflow: "hidden", width: itemSize, height: itemSize, borderRadius: 50, border: "1px solid rgba(255,255,255,0.3)"}} />
</div>
<div style={{position: "absolute", left: 160, top: 42, height: itemSize, width: itemSize, }}>
<img src={bgImage2} alt="Image alt" style={{overflow: "hidden", width: itemSize, height: itemSize, borderRadius: 50, border: "1px solid rgba(255,255,255,0.3)"}} />
</div>
</div>
<Paper square style={paperAppStyle}>
<div
style={{
position: "absolute",
bottom: 1,
left: 1,
height: 12,
width: 12,
backgroundColor: boxColor,
borderRadius: "0 100px 0 0",
}}
/>
<Grid
item
style={{ display: "flex", flexDirection: "column", width: "100%" }}
>
<Grid item style={{ display: "flex", maxHeight: 34 }}>
<Tooltip title={`Released by ${creatorname}`} placement="bottom">
<div
style={{
cursor: data.creator_info !== undefined ? "pointer" : "inherit",
}}
onClick={() => {
if (data.creator_info !== undefined) {
navigate("/creators/"+data.creator_info.username)
}
}}
>
{image}
</div>
</Tooltip>
<Tooltip title={`See ${data.name}`} placement="bottom">
<Typography
variant="h6"
style={{
marginBottom: 0,
paddingBottom: 0,
maxHeight: 30,
flex: 10,
}}
>
<Link
to={parsedUrl}
style={{ textDecoration: "none", color: "inherit" }}
>
{parsedName}
</Link>
</Typography>
</Tooltip>
</Grid>
<Grid item style={workflowActionStyle}>
{/*
{appGroup.length > 0 ?
<div style={{display: "flex", marginTop: 8, }}>
<AvatarGroup max={4} style={{marginLeft: 5, maxHeight: 24,}}>
{appGroup.map((app, index) => {
return (
<div
key={index}
style={{
height: 24,
width: 24,
filter: "brightness(0.6)",
cursor: "pointer",
}}
onClick={() => {
navigate("/apps/"+app.id)
}}
>
<Tooltip color="primary" title={app.name} placement="bottom">
<Avatar alt={app.name} src={app.image_url} style={{width: 24, height: 24}}/>
</Tooltip>
</div>
)
})}
</AvatarGroup>
</div>
:
<Tooltip color="primary" title="Action amount" placement="bottom">
<span style={{ color: "#979797", display: "flex" }}>
<BubbleChartIcon
style={{ marginTop: "auto", marginBottom: "auto" }}
/>
<Typography
style={{
marginLeft: 5,
marginTop: "auto",
marginBottom: "auto",
}}
>
{data.actions === undefined || data.actions === null ? 1 : data.actions.length}
</Typography>
</span>
</Tooltip>
}
*/}
{/*
<Tooltip
color="primary"
title="Trigger amount"
placement="bottom"
>
<span
style={{ marginLeft: 15, color: "#979797", display: "flex" }}
>
<RestoreIcon
style={{
color: "#979797",
marginTop: "auto",
marginBottom: "auto",
}}
/>
<Typography
style={{
marginLeft: 5,
marginTop: "auto",
marginBottom: "auto",
}}
>
{data.triggers === undefined || data.triggers === null ? 1 : data.triggers.length}
</Typography>
</span>
</Tooltip>
<Tooltip color="primary" title="Subflows used" placement="bottom">
<span
style={{
marginLeft: 15,
display: "flex",
color: "#979797",
cursor: "pointer",
}}
onClick={() => {
}}
>
<svg
width="18"
height="18"
viewBox="0 0 18 18"
fill="none"
xmlns="http://www.w3.org/2000/svg"
style={{
color: "#979797",
marginTop: "auto",
marginBottom: "auto",
}}
>
<path
d="M0 0H15V15H0V0ZM16 16H18V18H16V16ZM16 13H18V15H16V13ZM16 10H18V12H16V10ZM16 7H18V9H16V7ZM16 4H18V6H16V4ZM13 16H15V18H13V16ZM10 16H12V18H10V16ZM7 16H9V18H7V16ZM4 16H6V18H4V16Z"
fill="#979797"
/>
</svg>
<Typography
style={{
marginLeft: 5,
marginTop: "auto",
marginBottom: "auto",
}}
>
{0}
</Typography>
</span>
</Tooltip>
*/}
</Grid>
{/*
<Grid
item
style={{
justifyContent: "left",
overflow: "hidden",
marginTop: 5,
}}
>
{data.tags !== undefined && data.tags !== null
? data.tags.map((tag, index) => {
if (index >= 3) {
return null;
}
return (
<Chip
key={index}
style={chipStyle}
label={tag}
variant="outlined"
color="primary"
/>
);
})
: null}
</Grid>
*/}
<Button variant="outlined" style={{textDecoration: "none", borderRadius: 25,}} onClick={() => {
activateWorkflow(data)
}}>
Try this workflow
</Button>
</Grid>
</Paper>
</div>
)
}
export default WorkflowPaper

View File

@ -0,0 +1,455 @@
import React, { useState, useEffect } from "react";
import { toast } from "react-toastify"
import theme from '../theme.jsx';
import { useNavigate, Link, useParams } from "react-router-dom";
import {
Button,
Typography,
Dialog,
DialogTitle,
DialogContent,
DialogActions,
Drawer,
CircularProgress,
IconButton,
Tooltip,
} from "@mui/material";
import {
Check as CheckIcon,
TrendingFlat as TrendingFlatIcon,
Close as CloseIcon,
} from '@mui/icons-material';
import WorkflowTemplatePopup2 from "./WorkflowTemplatePopup.jsx";
import ConfigureWorkflow from "../components/ConfigureWorkflow.jsx";
const WorkflowTemplatePopup = (props) => {
const { userdata, globalUrl, img1, srcapp, img2, dstapp, title, description, visualOnly, apps } = props;
const [isActive, setIsActive] = useState(false);
const [isHovered, setIsHovered] = useState(false);
const [modalOpen, setModalOpen] = useState(false);
const [errorMessage, setErrorMessage] = useState("");
const [workflowLoading, setWorkflowLoading] = useState(false);
const [workflow, setWorkflow] = useState({});
const [appAuthentication, setAppAuthentication] = React.useState(undefined);
const isCloud = window.location.host === "localhost:3002" || window.location.host === "shuffler.io";
let navigate = useNavigate();
const imagestyleWrapper = {
height: 40,
width: 40,
borderRadius: 40,
border: "1px solid rgba(255,255,255,0.3)",
overflow: "hidden",
display: "flex",
}
const imagestyleWrapperDefault = {
height: 40,
width: 40,
borderRadius: 40,
border: "1px solid rgba(255,255,255,0.3)",
overflow: "hidden",
display: "flex",
}
const imagestyle = {
height: 40,
width: 40,
borderRadius: 40,
border: "1px solid rgba(255,255,255,0.3)",
overflow: "hidden",
}
const imagestyleDefault = {
display: "block",
marginLeft: 11,
marginTop: 11,
height: 35,
width: "auto",
}
if (title === undefined || title === null || title === "") {
console.log("No title for workflow template popup!");
return null
}
const getWorkflow = (workflowId) => {
fetch(`${globalUrl}/api/v1/workflows/${workflowId}`, {
method: "GET",
headers: {
"Content-Type": "application/json",
Accept: "application/json",
},
credentials: "include",
})
.then((response) => {
if (response.status !== 200) {
console.log("Status not 200 for framework!");
}
return response.json();
})
.then((responseJson) => {
if (responseJson.success === false) {
console.log("Error in workflow loading for ID ", workflowId)
} else {
setWorkflow(responseJson)
}
})
.catch((error) => {
console.log("err in framework: ", error.toString());
setWorkflowLoading(false)
})
}
const loadAppAuth = () => {
if (userdata === undefined || userdata === null) {
setErrorMessage("You need to be logged in to try usecases. Redirecting in 5 seconds...")
// Send the user to the login screen after 3 seconds
setTimeout(() => {
// Make it cancel if the state modalOpen changes
if (modalOpen === false) {
return
}
navigate("/login?view=" + window.location.pathname + window.location.search)
}, 4500)
return
}
fetch(`${globalUrl}/api/v1/apps/authentication`, {
method: "GET",
headers: {
"Content-Type": "application/json",
Accept: "application/json",
},
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) {
toast("Failed to get app auth: " + responseJson.reason);
return
}
var newauth = [];
for (let authkey in responseJson.data) {
if (responseJson.data[authkey].defined === false) {
continue;
}
newauth.push(responseJson.data[authkey]);
}
setAppAuthentication(newauth);
})
.catch((error) => {
//toast(error.toString());
console.log("New auth error: ", error.toString());
});
}
const getGeneratedWorkflow = () => {
// POST
// https://shuffler.io/api/v1/workflows/merge
// destination: {app_id: "b9c2feaf99b6309dabaeaa8518c61d3d", app_name: "Servicenow_API", app_version: "",…}
// id: ""
// middle:[]
// name: "Email analysis"
// source:{app_id: "accdaaf2eeba6a6ed43b2efc0112032d", app_name
if (srcapp.includes(":default") || dstapp.includes(":default")) {
toast("You need to select both a source and destination app before generating this workflow.")
return
}
setWorkflowLoading(true)
// FIXME: Remove hardcoding here after testing, and user srcapp/dstapp
const newsrcapp = srcapp
const newdstapp = dstapp
const mergedata = {
name: title,
id: "",
source: {
app_name: newsrcapp,
},
middle: [],
destination: {
app_name: newdstapp,
},
}
//fetch(globalUrl + "/api/v1/workflows/merge", {
fetch("https://shuffler.io/api/v1/workflows/merge", {
method: "POST",
headers: {
"Content-Type": "application/json",
Accept: "application/json",
},
credentials: "include",
body: JSON.stringify(mergedata),
})
.then((response) => {
if (response.status !== 200) {
console.log("Status not 200 for framework!");
}
setWorkflowLoading(false)
return response.json();
})
.then((responseJson) => {
if (responseJson.success === false) {
console.log("Error in workflow template: ", responseJson.error);
setErrorMessage("Failed to generate workflow for these tools - the Shuffle team has been notified. Click out of this window to continue. Contact support@shuffler.io for further assistance.")
setIsActive(true)
//setTimeout(() => {
// setModalOpen(false)
//}, 5000)
} else {
console.log("Success in workflow template: ", responseJson);
setIsActive(true)
if (responseJson.workflow_id === "") {
console.log("Failed to build workflow for these tools. Closing in 3 seconds.")
return
}
getWorkflow(responseJson.workflow_id)
}
})
.catch((error) => {
console.log("err in framework: ", error.toString());
setWorkflowLoading(false)
})
}
const isFinished = () => {
// Look for configuration fields being done in the current modal
// 1. Start by finding the modal
const template = document.getElementById("workflow-template")
if (template === null || template == undefined) {
return true
}
// Find item in template with id app-config
const appconfig = template.getElementsByClassName("app-config")
if (appconfig === null || appconfig == undefined) {
return true
}
return false
}
const ModalView = () => {
return (
<Drawer
anchor={"left"}
open={modalOpen}
onClose={() => {
setModalOpen(false);
}}
PaperProps={{
style: {
backgroundColor: "black",
color: "white",
minWidth: 700,
maxWidth: 700,
paddingTop: 75,
itemAlign: "center",
},
}}
>
<IconButton
style={{
zIndex: 5000,
position: "absolute",
top: 14,
right: 14,
color: "white",
}}
onClick={() => {
setModalOpen(false);
}}
>
<CloseIcon />
</IconButton>
<DialogContent style={{marginTop: 0, marginLeft: 75, maxWidth: 470, }}>
<Typography variant="h4">
<b>Configure Workflow</b>
</Typography>
<Typography variant="body2" color="textSecondary" style={{marginTop: 25, }}>
Selected Workflow:
</Typography>
<div style={{marginBottom: 0, }} id="workflow-template">
<WorkflowTemplatePopup2
globalUrl={globalUrl}
img1={img1}
srcapp={srcapp}
img2={img2}
dstapp={dstapp}
title={title}
description={description}
visualOnly={true}
/>
</div>
{workflowLoading ?
<div style={{marginTop: 75, textAlign: "center", }}>
<Typography variant="h4"> Generating the Workflow...
</Typography>
<CircularProgress style={{marginLeft: 125, marginTop: 10, }}/>
</div>
:
<div>
<Typography variant="h6" style={{marginTop: 75, }}>
{errorMessage !== "" ? errorMessage : ""}
</Typography>
</div>
}
<ConfigureWorkflow
userdata={userdata}
theme={theme}
globalUrl={globalUrl}
workflow={workflow}
appAuthentication={appAuthentication}
setAppAuthentication={setAppAuthentication}
apps={apps}
/>
{errorMessage === "" ?
<Button
style={{marginTop: 50, }}
variant={isFinished() ? "contained" : "outlined"}
onClick={() => {
setModalOpen(false);
}}
>
Done
</Button>
: null}
</DialogContent>
</Drawer>
)
}
var parsedTitle = title
const maxlength = 30
if (title.length > maxlength) {
parsedTitle = title.substring(0, maxlength) + "..."
}
parsedTitle = parsedTitle.replaceAll("_", " ")
const parsedDescription = description !== undefined && description !== null ? description.replaceAll("_", " ") : ""
return (
<div style={{ display: "flex", maxWidth: isCloud ? 470 : 450, minWidth: isCloud ? 470 : 450, height: 78, borderRadius: 8 }}>
<ModalView />
<div
// variant={isActive === 1 ? "contained" : "outlined"}
color="secondary"
disabled={visualOnly === true}
style={{
margin: 4,
width: "100%",
borderRadius: 8,
textTransform: "none",
backgroundColor: theme.palette.inputColor,
border: isActive ? errorMessage !== "" ? "1px solid red" : `2px solid ${theme.palette.green}` : isHovered ? "1px solid #f85a3e" : "1px solid rgba(33, 33, 33, 1)",
cursor: isActive ? errorMessage !== "" ? "not-allowed" : "pointer" : "pointer",
padding: "10px 20px 10px 20px",
position: "relative",
}}
onMouseEnter={() => {
setIsHovered(true)
}}
onMouseLeave={() => {
setIsHovered(false)
}}
onClick={() => {
if (visualOnly === true) {
console.log("Not showing more than visuals.")
return
}
//setIsActive(!isActive)
if (errorMessage !== "") {
toast("Already failed to generate a workflow for this usecase. Please try again later or contact support@shuffler.io.")
setModalOpen(true)
} else if (isActive) {
toast("Workflow already generated. Please try another workflow template!")
// FIXME: Remove these?
loadAppAuth()
setModalOpen(true)
//getGeneratedWorkflow()
} else {
loadAppAuth()
setModalOpen(true)
getGeneratedWorkflow()
}
}}
>
<div style={{ display: "flex", itemAlign: "left", textAlign: "left", }}>
<div style={{display: "flex", flex: 1, marginTop: 3, }}>
{img1 !== undefined && img1 !== "" && srcapp !== undefined && srcapp !== "" ?
<Tooltip title={srcapp.replaceAll(":default", "").replaceAll("_", " ").replaceAll(" API", "")} placement="top">
<span style={srcapp !== undefined && srcapp.includes(":default") ? imagestyleWrapperDefault : imagestyleWrapper}>
<img src={img1} style={srcapp !== undefined && srcapp.includes(":default") ? imagestyleDefault : imagestyle} />
</span>
</Tooltip>
:
<span style={{width: 50, }} />
}
{img2 !== undefined && img2 !== "" && dstapp !== undefined && dstapp !== "" ?
<Tooltip title={dstapp.replaceAll(":default", "").replaceAll("_", " ").replaceAll(" API", "")} placement="top">
<span style={{display: "flex", }}>
<TrendingFlatIcon style={{ marginTop: 7, }} />
<span style={dstapp !== undefined && dstapp.includes(":default") ? imagestyleWrapperDefault : imagestyleWrapper}>
<img src={img2} style={dstapp !== undefined && dstapp.includes(":default") ? imagestyleDefault : imagestyle} />
</span>
</span>
</Tooltip>
:
<span style={{width: 50, }} />
}
</div>
<div style={{ flex: 3, marginLeft: 20, }}>
<Typography variant="body1" style={{ marginTop: parsedDescription.length === 0 ? 10 : 0, }} color="rgba(241, 241, 241, 1)">
{parsedTitle}
</Typography>
<Typography variant="body2" color="textSecondary" style={{ marginTop: 0, marginRight: 0, maxHeight: 16, overflow: "hidden",}} color="rgba(158, 158, 158, 1)">
{parsedDescription}
</Typography>
</div>
</div>
<div>
{isActive === true && errorMessage === "" ?
<CheckIcon color="primary" sx={{ borderRadius: 4 }} style={{ position: "absolute", color: theme.palette.green, top: 10, right: 10, }} />
: ""}
</div>
</div>
</div>
)
}
export default WorkflowTemplatePopup

View File

@ -0,0 +1,199 @@
import React, { useState, useEffect } from 'react';
import {Link} from 'react-router-dom';
import theme from '../theme.jsx';
import { Search as SearchIcon, CloudQueue as CloudQueueIcon, Code as CodeIcon } from '@mui/icons-material';
//import algoliasearch from 'algoliasearch/lite';
import algoliasearch from 'algoliasearch';
import { InstantSearch, connectSearchBox, connectHits } from 'react-instantsearch-dom';
import { Grid, Paper, TextField, ButtonBase, InputAdornment, Typography, Button, Tooltip} from '@mui/material';
const searchClient = algoliasearch("JNSS5CFDZZ", "db08e40265e2941b9a7d8f644b6e5240")
const WorkflowSearch = props => {
const { maxRows, showName, showSuggestion, isMobile, globalUrl, parsedXs, newSelectedApp, setNewSelectedApp, defaultSearch, showSearch, ConfiguredHits, selectAble, } = props
const rowHandler = maxRows === undefined || maxRows === null ? 50 : maxRows
const xs = parsedXs === undefined || parsedXs === null ? 12 : parsedXs
//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 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)
//toast("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 (
<form noValidate action="" role="search">
<TextField
fullWidth
style={{backgroundColor: theme.palette.inputColor, borderRadius: borderRadius, width: "100%",}}
InputProps={{
style:{
color: "white",
fontSize: "1em",
height: 50,
},
startAdornment: (
<InputAdornment position="start">
<SearchIcon style={{marginLeft: 5}}/>
</InputAdornment>
),
}}
autoComplete='on'
type="search"
color="primary"
defaultValue={defaultSearch}
placeholder={`Find ${defaultSearch} Workflows...`}
id="shuffle_workflow_search_field"
onChange={(event) => {
refine(event.currentTarget.value)
}}
limit={5}
/>
{/*isSearchStalled ? 'My search is stalled' : ''*/}
</form>
)
//value={currentRefinement}
}
if (selectAble === true) {
console.log("Make it possible to select a Workflow!!")
}
const Hits = ({ hits }) => {
const [mouseHoverIndex, setMouseHoverIndex] = useState(-1)
var counted = 0
return (
<Grid container spacing={0} style={{border: "1px solid rgba(255,255,255,0.2)", maxHeight: 250, minHeight: 250, overflowY: "auto", overflowX: "hidden",}}>
{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%",
}
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("_", " ")
return (
<Paper key={index} elevation={0} style={paperStyle} onMouseOver={() => {
setMouseHoverIndex(index)
}} onMouseOut={() => {
setMouseHoverIndex(-1)
}} onClick={() => {
setNewSelectedApp(data)
}}>
<div style={{display: "flex"}}>
{/*<img alt={data.name} src={data.image_url} style={{width: "100%", maxWidth: 30, minWidth: 30, minHeight: 30, maxHeight: 30, display: "block", }} />*/}
<Typography variant="body1" style={{marginTop: 2, marginLeft: 10, }}>
{parsedname}
</Typography>
</div>
</Paper>
)
})}
</Grid>
)
}
const InputHits = ConfiguredHits === undefined ? Hits : ConfiguredHits
const CustomSearchBox = connectSearchBox(SearchBox)
const CustomHits = connectHits(InputHits)
return (
<div style={{width: "100%", textAlign: "center", position: "relative", height: "100%",}}>
<InstantSearch searchClient={searchClient} indexName="workflows">
{/* showSearch === false ? null :
<div style={{maxWidth: 450, margin: "auto", }}>
<CustomSearchBox />
</div>
*/}
<div style={{maxWidth: 450, margin: "auto", }}>
<CustomSearchBox />
</div>
<CustomHits hitsPerPage={5}/>
</InstantSearch>
</div>
)
}
export default WorkflowSearch;

View File

@ -0,0 +1,8 @@
import { createBrowserHistory } from "history";
var localExport;
if (typeof window !== "undefined") {
localExport = createBrowserHistory({ forceRefresh: true });
}
export default localExport;

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,45 @@
/* cyrillic-ext */
@font-face {
font-family: "Nunito Sans";
font-style: normal;
font-weight: 400;
src: url("./font1.woff2") format("woff2");
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F,
U+FE2E-FE2F;
}
/* cyrillic */
@font-face {
font-family: "Nunito Sans";
font-style: normal;
font-weight: 400;
src: url("./font2.woff2") format("woff2");
unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
/* vietnamese */
@font-face {
font-family: "Nunito Sans";
font-style: normal;
font-weight: 400;
src: url("./font3.woff2") format("woff2");
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1,
U+01AF-01B0, U+1EA0-1EF9, U+20AB;
}
/* latin-ext */
@font-face {
font-family: "Nunito Sans";
font-style: normal;
font-weight: 400;
src: url("./font4.woff2") format("woff2");
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB,
U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
font-family: "Nunito Sans";
font-style: normal;
font-weight: 400;
src: url("./font5.woff2") format("woff2");
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215,
U+FEFF, U+FFFD;
}

View File

@ -0,0 +1,434 @@
const data = [
{
selector: "node",
css: {
label: "data(label)",
"text-valign": "center",
"font-family":
"Segoe UI, Tahoma, Geneva, Verdana, sans-serif, sans-serif",
"font-weight": "lighter",
"margin-right": "10px",
"font-size": "18px",
width: "80px",
height: "80px",
color: "white",
padding: "10px",
margin: "5px",
"border-width": "1px",
"text-margin-x": "10px",
"z-index": 5001,
},
},
{
selector: "edge",
css: {
"target-arrow-shape": "triangle",
"target-arrow-color": "grey",
"curve-style": "unbundled-bezier",
label: "data(label)",
"text-margin-y": "-15px",
width: "5px",
color: "white",
"line-fill": "linear-gradient",
"line-gradient-stop-positions": ["0.0", "100"],
"line-gradient-stop-colors": ["grey", "grey"],
"z-index": 5001,
},
},
{
selector: `node[type="ACTION"]`,
css: {
shape: "roundrectangle",
"background-color": "#213243",
"border-color": "#81c784",
"background-width": "100%",
"background-height": "100%",
"border-radius": "5px",
"z-index": 5001,
},
},
{
selector: `node[type="COMMENT"]`,
css: {
shape: "roundrectangle",
color: "data(color)",
width: "data(width)",
height: "data(height)",
padding: "0px",
margin: "0px",
"background-color": "data(backgroundcolor)",
"background-image": "data(backgroundimage)",
"border-color": "#ffffff",
"text-margin-x": "0px",
"z-index": 4999,
"border-radius": "5px",
"background-opacity": "0.5",
"text-wrap": "wrap",
},
},
{
selector: `node[app_name="Shuffle Tools"]`,
css: {
width: "30px",
height: "30px",
"z-index": 5000,
"font-size": "0px",
"background-width": "75%",
"background-height": "75%",
"background-color": "data(iconBackground)",
"background-fill": "data(fillstyle)",
"background-gradient-direction": "to-right",
"background-gradient-stop-colors": "data(fillGradient)",
},
},
{
selector: `node[app_name="Testing"]`,
css: {
width: "30px",
height: "30px",
"z-index": 5000,
"font-size": "0px",
},
},
{
selector: `node[?small_image]`,
css: {
"background-image": "data(small_image)",
"text-halign": "right",
},
},
{
selector: `node[?large_image]`,
css: {
"background-image": "data(large_image)",
"text-halign": "right",
},
},
{
selector: `node[type="CONDITION"]`,
css: {
shape: "diamond",
"border-color": "##FFEB3B",
padding: "30px",
},
},
{
selector: `node[type="eventAction"]`,
css: {
"background-color": "#edbd21",
},
},
{
selector: `node[type="TRIGGER"]`,
css: {
shape: "octagon",
"border-radius": "5px",
"border-color": "orange",
"background-color": "#213243",
"background-width": "100px",
"background-height": "100px",
},
},
{
selector: `node[status="running"]`,
css: {
"border-color": "#81c784",
},
},
{
selector: `node[status="stopped"]`,
css: {
"border-color": "orange",
},
},
{
selector: 'node[type="mq"]',
css: {
"background-color": "#edbd21",
},
},
{
selector: "node[?isButton]",
css: {
shape: "ellipse",
width: "15px",
height: "15px",
"z-index": "5002",
"font-size": "0px",
border: "1px solid rgba(255,255,255,0.9)",
"background-image": "data(icon)",
"background-color": "data(iconBackground)",
},
},
{
selector: "node[?isSuggestion]",
css: {
shape: "roundrectangle",
width: "30px",
height: "30px",
"z-index": "5002",
filter: "grayscale(100%)",
border: "1px solid rgba(255,255,255,0.9)",
"background-image": "data(large_image)",
"background-fit": "cover",
"font-size": "20px",
label: "data(label_replaced)",
},
},
{
selector: "node[?canConnect]",
css: {
"border-color": "#f86a3e",
"border-width": "10px",
"z-index": "5002",
"background-color": "#f86a3e",
},
},
{
selector: "node[?isDescriptor]",
css: {
shape: "ellipse",
"border-color": "#80deea",
width: "5px",
height: "5px",
"z-index": "5002",
"font-size": "10px",
"text-valign": "center",
"text-halign": "center",
border: "1px solid black",
"margin-right": "0px",
"text-margin-x": "0px",
"background-color": "data(imageColor)",
"background-image": "data(image)",
label: "data(label)",
},
},
{
selector: "node[?isStartNode]",
css: {
shape: "ellipse",
"border-color": "#80deea",
width: "80px",
height: "80px",
"font-size": "18px",
"background-width": "100%",
"background-height": "100%",
},
},
{
selector: "node[!is_valid]",
css: {
"border-color": "red",
"border-width": "10px",
},
},
{
selector: ":selected",
css: {
"background-color": "#77b0d0",
"border-color": "#77b0d0",
"border-width": "20px",
},
},
{
selector: ".skipped-highlight",
css: {
"background-color": "grey",
"border-color": "grey",
"border-width": "8px",
"transition-property": "background-color",
"transition-duration": "0.5s",
},
},
{
selector: ".success-highlight",
css: {
"background-color": "#41dcab",
"border-color": "#41dcab",
"border-width": "5px",
"transition-property": "background-color",
"transition-duration": "0.5s",
},
},
{
selector: ".hover-highlight",
css: {
"background-color": "#5f9265",
"border-color": "#5f9265",
"border-width": "5px",
"transition-property": "background-color",
"transition-duration": "0.5s",
},
},
{
selector: ".failure-highlight",
css: {
"background-color": "#8e3530",
"border-color": "#8e3530",
"border-width": "5px",
"transition-property": "background-color",
"transition-duration": "0.5s",
},
},
{
selector: ".not-executing-highlight",
css: {
"background-color": "grey",
"border-color": "grey",
"border-width": "5px",
"transition-property": "#ffef47",
"transition-duration": "0.25s",
},
},
{
selector: ".executing-highlight",
css: {
"background-color": "#ffef47",
"border-color": "#ffef47",
"border-width": "8px",
"transition-property": "border-width",
"transition-duration": "0.25s",
},
},
{
selector: ".awaiting-data-highlight",
css: {
"background-color": "#f4ad42",
"border-color": "#f4ad42",
"border-width": "5px",
"transition-property": "border-color",
"transition-duration": "0.5s",
},
},
{
selector: ".shuffle-hover-highlight",
css: {
"background-color": "#f85a3e",
"border-color": "#f85a3e",
"border-width": "12px",
"transition-property": "border-width",
"transition-duration": "0.25s",
label: "data(label)",
"font-size": "18px",
color: "white",
},
},
{
selector: "$node > node",
css: {
"padding-top": "10px",
"padding-left": "10px",
"padding-bottom": "10px",
"padding-right": "10px",
},
},
{
selector: "edge.executing-highlight",
css: {
width: "5px",
"target-arrow-color": "#ffef47",
"line-color": "#ffef47",
"transition-property": "line-color, width",
"transition-duration": "0.25s",
},
},
{
selector: `edge[?decorator]`,
css: {
width: "1px",
"line-style": "dashed",
"line-fill": "linear-gradient",
"target-arrow-color": "#555555",
"line-gradient-stop-positions": ["0.0", "100"],
"line-gradient-stop-colors": ["#555555", "#555555"],
},
},
{
selector: "edge.success-highlight",
css: {
width: "5px",
"target-arrow-color": "#41dcab",
"line-color": "#41dcab",
"transition-property": "line-color, width",
"transition-duration": "0.5s",
"line-fill": "linear-gradient",
"line-gradient-stop-positions": ["0.0", "100"],
"line-gradient-stop-colors": ["#41dcab", "#41dcab"],
},
},
{
selector: ".eh-handle",
style: {
"background-color": "#337ab7",
width: "1px",
height: "1px",
shape: "circle",
"border-width": "1px",
"border-color": "black",
},
},
{
selector: ".eh-source",
style: {
"border-width": "3",
"border-color": "#337ab7",
},
},
{
selector: ".eh-target",
style: {
"border-width": "3",
"border-color": "#337ab7",
},
},
{
selector: ".eh-preview, .eh-ghost-edge",
style: {
"background-color": "#337ab7",
"line-color": "#337ab7",
"target-arrow-color": "#337ab7",
"source-arrow-color": "#337ab7",
},
},
{
selector: "edge:selected",
css: {
"target-arrow-color": "#f85a3e",
},
},
{
selector: `edge[?source_workflow]`,
css: {
"background-opacity": "1",
"font-size": "0px",
},
},
{
selector: `node[?source_workflow]`,
css: {
"background-opacity": "0",
"font-size": "0px",
},
},
{
selector: "node:selected",
css: {
"border-color": "#f86a3e",
"border-width": "7px",
},
},
];
//{
// selector: 'edge[?hasErrors]',
// css: {
// 'target-arrow-color': '#991818',
// 'line-color': '#991818',
// 'line-style': 'dashed',
// "line-fill": "linear-gradient",
// "line-gradient-stop-positions": ["0.0", "100"],
// "line-gradient-stop-colors": ["#991818", "#991818"],
// },
//},
export default data;

View File

@ -0,0 +1,112 @@
const data = [{
selector: 'node',
css: {
'label': 'data(label)',
'text-valign': 'center',
'font-family': 'Segoe UI, Tahoma, Geneva, Verdana, sans-serif, sans-serif',
'font-weight': 'lighter',
'font-size': 'data(font_size)',
'text-algin': 'center',
'border-width': '3px',
'border-color': '#8a8a8a',
'color': '#f85a3e',
'text-margin-x': '0px',
'text-margin-y': 'data(text_margin_y)',
'background-color': '#27292d',
'background-image': 'data(large_image)',
'background-position-x': 'data(margin_x)',
'background-position-y': 'data(margin_y)',
'background-clip': "node",
'background-width': 'data(width)',
'background-height': 'data(width)',
'width': 'data(boxwidth)',
'height': 'data(boxheight)',
}
},
{
selector: 'edge',
css: {
'target-arrow-shape': 'triangle',
'target-arrow-color': '#8a8a8a',
'curve-style': 'bezier',
'label': 'data(label)',
'text-wrap': 'wrap',
'text-max-width': '120px',
"color": "rgba(255,255,255,0.7)",
'line-style': 'dashed',
"line-fill": "linear-gradient",
"line-gradient-stop-positions": ["0.0", "100"],
"line-gradient-stop-colors": ["#8a8a8a", "#8a8a8a"],
'width': '1px',
'z-compound-depth': 'top',
'font-size': '13px',
},
},
{
selector: `node[!is_valid]`,
css: {
'height': '20px',
'width': '20px',
'background-color': '#6d9eeb',
'border-color': '#4c6ea4',
'border-width': '1px',
},
},
{
selector: `edge[?human]`,
css: {
'target-arrow-color': '#6d9eeb',
'line-style': 'solid',
"line-gradient-stop-positions": ["0.0", "100"],
"line-gradient-stop-colors": ["#6d9eeb", "#6d9eeb"],
}
},
{
selector: `node[?middle_node]`,
css: {
'background-image': 'data(large_image)',
'height': 'data(width)',
'width': 'data(height)',
'background-width': 'data(width)',
'background-height': 'data(height)',
'background-position-x': '0px',
'background-position-y': '0px',
'border-width': '2px',
},
},
{
selector: `node[?invisible]`,
css: {
'height': '10x',
'width': '10px',
'background-position-x': '0px',
'background-position-y': '0px',
'border-width': '0px',
'font-size': '0px',
},
},
{
selector: `node[?font_size]`,
css: {
'font-size': 'data(font_size)',
},
},
{
selector: ".eh-preview, .eh-ghost-edge",
style: {
"background-color": "#337ab7",
"line-color": "#337ab7",
"target-arrow-color": "#337ab7",
"source-arrow-color": "#337ab7",
},
},
{
selector: "node:selected",
css: {
"border-color": "#f86a3e",
"border-width": "3px",
},
},
]
export default data

View File

@ -0,0 +1,33 @@
@import url("./css/nunito.css");
body {
margin: 0;
padding: 0;
font-family: "Nunito Sans", sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New",
monospace;
}
.cm-tab::before{
content: "———";
margin-right: -13px;
}
/* .cm-string{
z-index: -1;
}
.CodeMirror-selected{
background-color: #007500 !important;
z-index: 100 !important;
} */
.CodeMirror-selectedtext{
background-color: rgba(28, 47, 69, 0.6) !important;
color: rgb(255, 255, 255) !important;
}

View File

@ -0,0 +1,16 @@
import React from "react";
import { createRoot } from "react-dom/client";
import App from "./App";
//import "./index.css";
//import reportWebVitals from "./reportWebVitals";
const rootElement = document.getElementById("root");
const root = createRoot(rootElement);
root.render(
<React.Fragment>
<App />
</React.Fragment>
);
//reportWebVitals();

View File

@ -0,0 +1,127 @@
// In production, we register a service worker to serve assets from local cache.
// This lets the app load faster on subsequent visits in production, and gives
// it offline capabilities. However, it also means that developers (and users)
// will only see deployed updates on the "N+1" visit to a page, since previously
// cached resources are updated in the background.
// To learn more about the benefits of this model, read https://goo.gl/KwvDNy.
// This link also includes instructions on opting out of this behavior.
const isLocalhost = Boolean(
window.location.hostname === "localhost" ||
// [::1] is the IPv6 localhost address.
window.location.hostname === "[::1]" ||
// 127.0.0.1/8 is considered localhost for IPv4.
window.location.hostname.match(
/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
)
);
export function register(config) {
if (process.env.NODE_ENV === "production" && "serviceWorker" in navigator) {
// The URL constructor is available in all browsers that support SW.
const publicUrl = new URL(process.env.PUBLIC_URL, window.location);
if (publicUrl.origin !== window.location.origin) {
// Our service worker won't work if PUBLIC_URL is on a different origin
// from what our page is served on. This might happen if a CDN is used to
// serve assets; see https://github.com/facebook/create-react-app/issues/2374
return;
}
window.addEventListener("load", () => {
const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
if (isLocalhost) {
// This is running on localhost. Let's check if a service worker still exists or not.
checkValidServiceWorker(swUrl, config);
// Add some additional logging to localhost, pointing developers to the
// service worker/PWA documentation.
navigator.serviceWorker.ready.then(() => {
console.log(
"This web app is being served cache-first by a service " +
"worker. To learn more, visit https://goo.gl/SC7cgQ"
);
});
} else {
// Is not local host. Just register service worker
registerValidSW(swUrl, config);
}
});
}
}
function registerValidSW(swUrl, config) {
navigator.serviceWorker
.register(swUrl)
.then((registration) => {
registration.onupdatefound = () => {
const installingWorker = registration.installing;
installingWorker.onstatechange = () => {
if (installingWorker.state === "installed") {
if (navigator.serviceWorker.controller) {
// At this point, the old content will have been purged and
// the fresh content will have been added to the cache.
// It's the perfect time to display a "New content is
// available; please refresh." message in your web app.
console.log("New content is available; please refresh.");
// Execute callback
if (config.onUpdate) {
config.onUpdate(registration);
}
} else {
// At this point, everything has been precached.
// It's the perfect time to display a
// "Content is cached for offline use." message.
console.log("Content is cached for offline use.");
// Execute callback
if (config.onSuccess) {
config.onSuccess(registration);
}
}
}
};
};
})
.catch((error) => {
console.error("Error during service worker registration:", error);
});
}
function checkValidServiceWorker(swUrl, config) {
// Check if the service worker can be found. If it can't reload the page.
fetch(swUrl)
.then((response) => {
// Ensure service worker exists, and that we really are getting a JS file.
if (
response.status === 404 ||
response.headers.get("content-type").indexOf("javascript") === -1
) {
// No service worker found. Probably a different app. Reload the page.
navigator.serviceWorker.ready.then((registration) => {
registration.unregister().then(() => {
window.location.reload();
});
});
} else {
// Service worker found. Proceed as normal.
registerValidSW(swUrl, config);
}
})
.catch(() => {
console.log(
"No internet connection found. App is running in offline mode."
);
});
}
export function unregister() {
if ("serviceWorker" in navigator) {
navigator.serviceWorker.ready.then((registration) => {
registration.unregister();
});
}
}

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,74 @@
import React from "react";
const hrefStyle = {
color: "#f85a3e",
textDecoration: "none",
};
const About = () => {
return (
<div>
<h1>About</h1>
<p>
Endao was started as a project in late 2018 as a free service to analyze
APK (and soon IPA) files for vulnerabilities. The project was started
after I,
<a href="https://twitter.com/frikkylikeme" style={hrefStyle}>
@frikkylikeme
</a>
, found multiple vulnerabilities in IoT devices based purely on their
apps. As I wanted to learn more about these kind of vulnerabilities, I
looked for solutions that work for my purpose, but didn't find any good,
free and easy to use service - hence this site was born.
</p>
<p>
My personal goal has and will always be to make the internet safer. As
the IoT sphere grows, I want to be able to add ways of finding possible
vulnerabilities fast to this website. This will hopefully include
blogposts when I get around to it, as well as actual implementations.
The vulnerability discovery field is in no way new, but I'll try my best
to add whatever I can to it. As a disclaimer, I'm an "Ops" person, and I
had never done frontend before creating this site. This is as much of a
learning project within web development as it is in vulnerability
discovery.
</p>
<p>This site currently uses the following projects</p>
<ul>
<li>
<a style={hrefStyle} href="https://superanalyzer.rocks">
SUPER Android Analyzer
</a>
</li>
<li>
<a style={hrefStyle} href="https://github.com/linkedin/qark">
Qark
</a>
</li>
<li>
<a style={hrefStyle} href="https://virustotal.com">
Virustotal
</a>{" "}
for malware checks in known APKs
</li>
<li>Some selfmade gibberish</li>
</ul>
<p>Hopefully it is of use to some people :)</p>
<h3>Thanks</h3>
<p>Thanks to Andy for the initial frontend help :)</p>
<h3>Regards</h3>
<p>
<a href="https://twitter.com/frikkylikeme" style={hrefStyle}>
@frikkylikeme
</a>
</p>
</div>
);
};
export default About;

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,233 @@
/* eslint-disable react/no-multi-comp */
import React, { useState } from "react";
import { makeStyles } from "@mui/styles";
import {
CircularProgress,
TextField,
Button,
Paper,
Typography,
} from "@mui/material";
const bodyDivStyle = {
margin: "auto",
marginTop: "100px",
width: "500px",
};
const surfaceColor = "#27292D";
const inputColor = "#383B40";
const boxStyle = {
paddingLeft: "30px",
paddingRight: "30px",
paddingBottom: "30px",
paddingTop: "30px",
backgroundColor: surfaceColor,
};
const useStyles = makeStyles({
notchedOutline: {
borderColor: "#f85a3e !important",
},
});
const AdminAccount = (props) => {
const { globalUrl, isLoaded, isLoggedIn } = props;
const [username, setUsername] = useState("");
const [password, setPassword] = useState("");
const [firstRequest, setFirstRequest] = useState(true);
const [loginLoading, setLoginLoading] = useState(false);
// Used to swap from login to register. True = login, false = register
const register = true;
const classes = useStyles();
// Error messages etc
const [loginInfo, setLoginInfo] = useState("");
const handleValidateForm = () => {
return username.length > 1 && password.length > 1;
};
if (isLoggedIn === true) {
window.location.pathname = "/workflows";
}
const checkAdmin = () => {
const url = globalUrl + "/api/v1/checkusers";
fetch(url, {
method: "GET",
headers: {
"Content-Type": "application/json",
},
})
.then((response) =>
response.json().then((responseJson) => {
if (responseJson["success"] === false) {
setLoginInfo(responseJson["reason"]);
} else {
if (responseJson.reason === "redirect") {
window.location.pathname = "/login";
}
}
})
)
.catch((error) => {
setLoginInfo("Error in userdata: ", error);
});
};
if (firstRequest) {
setFirstRequest(false);
checkAdmin();
}
const onSubmit = (e) => {
setLoginLoading(true);
e.preventDefault();
// FIXME - add some check here ROFL
// Just use this one?
var data = { username: username, password: password };
var baseurl = globalUrl;
const url = baseurl + "/api/v1/register";
fetch(url, {
method: "POST",
body: JSON.stringify(data),
headers: {
"Content-Type": "application/json",
},
})
.then((response) =>
response.json().then((responseJson) => {
setLoginLoading(false);
if (responseJson["success"] === false) {
setLoginInfo(responseJson["reason"]);
} else {
setLoginInfo("Successful register :)");
window.location.pathname = "/login";
}
})
)
.catch((error) => {
setLoginLoading(false);
setLoginInfo("Error in userdata: ", error);
});
};
const onChangeUser = (e) => {
setUsername(e.target.value);
};
const onChangePass = (e) => {
setPassword(e.target.value);
};
//const onClickRegister = () => {
// if (props.location.pathname === "/login") {
// window.location.pathname = "/register"
// } else {
// window.location.pathname = "/login"
// }
// setLoginCheck(!register)
//}
//var loginChange = register ? (<div><p onClick={setLoginCheck(false)}>Want to register? Click here.</p></div>) : (<div><p onClick={setLoginCheck(true)}>Go back to login? Click here.</p></div>);
var formtitle = register ? <div>Login</div> : <div>Register</div>;
formtitle = "Create administrator account";
const basedata = (
<div style={bodyDivStyle}>
<Paper style={boxStyle}>
<form
onSubmit={onSubmit}
style={{ color: "white", margin: "15px 15px 15px 15px" }}
>
<h2>{formtitle}</h2>
Username
<div>
<TextField
color="primary"
style={{ backgroundColor: inputColor }}
autoFocus
InputProps={{
classes: {
notchedOutline: classes.notchedOutline,
},
style: {
height: "50px",
color: "white",
fontSize: "1em",
},
}}
required
fullWidth={true}
autoComplete="username"
placeholder="username@example.com"
id="emailfield"
margin="normal"
variant="outlined"
onChange={onChangeUser}
/>
</div>
Password
<div>
<TextField
color="primary"
style={{ backgroundColor: inputColor }}
InputProps={{
classes: {
notchedOutline: classes.notchedOutline,
},
style: {
height: "50px",
color: "white",
fontSize: "1em",
},
}}
required
id="outlined-password-input"
fullWidth={true}
type="password"
autoComplete="current-password"
placeholder="**********"
margin="normal"
variant="outlined"
onChange={onChangePass}
/>
</div>
<div style={{ display: "flex", marginTop: "15px" }}>
<Button
color="primary"
variant="contained"
type="submit"
style={{ flex: "1", marginRight: "5px" }}
disabled={!handleValidateForm() || loginLoading}
>
{loginLoading ? (
<CircularProgress
color="secondary"
style={{ color: "white" }}
/>
) : (
"SUBMIT"
)}
</Button>
</div>
<div style={{ marginTop: "10px" }}>{loginInfo}</div>
</form>
</Paper>
</div>
);
const loadedCheck = isLoaded ? <div>{basedata}</div> : <div></div>;
return <div>{loadedCheck}</div>;
};
export default AdminAccount;

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,759 @@
import React, { useState, useEffect } from "react";
import { useInterval } from "react-powerhooks";
import { makeStyles, } from "@mui/styles";
// nodejs library that concatenates classes
import classNames from "classnames";
import theme from '../theme.jsx';
import { useNavigate, Link, useParams } from "react-router-dom";
// react plugin used to create charts
//import { Line, Bar } from "react-chartjs-2";
//import { useAlert
import { ToastContainer, toast } from "react-toastify"
import Draggable from "react-draggable";
import {
Autocomplete,
Tooltip,
TextField,
IconButton,
Button,
Typography,
Grid,
Paper,
Chip,
Checkbox,
} from "@mui/material";
import {
Close as CloseIcon,
DoneAll as DoneAllIcon,
Description as DescriptionIcon,
PlayArrow as PlayArrowIcon,
Edit as EditIcon,
CheckBox as CheckBoxIcon,
CheckBoxOutlineBlank as CheckBoxOutlineBlankIcon,
OpenInNew as OpenInNewIcon,
} from "@mui/icons-material";
import WorkflowPaper from "../components/WorkflowPaper.jsx"
import { removeParam } from "../views/AngularWorkflow.jsx"
// core components
//import {
// chartExample1,
// chartExample2,
// chartExample3,
// chartExample4,
//} from "../charts.js";
import {
RadialBarChart,
RadialAreaChart,
RadialAxis,
StackedBarSeries,
TooltipArea,
ChartTooltip,
TooltipTemplate,
RadialAreaSeries,
RadialPointSeries,
RadialArea,
RadialLine,
TreeMap,
TreeMapSeries,
TreeMapLabel,
TreeMapRect,
Line,
LineChart,
LineSeries,
LinearYAxis,
LinearXAxis,
LinearYAxisTickSeries,
LinearXAxisTickSeries,
AreaChart,
AreaSeries,
PointSeries,
} from 'reaviz';
const useStyles = makeStyles({
notchedOutline: {
borderColor: "#f85a3e !important",
},
root: {
"& .MuiAutocomplete-listbox": {
border: "2px solid #f85a3e",
color: "white",
fontSize: 18,
"& li:nth-child(even)": {
backgroundColor: "#CCC",
},
"& li:nth-child(odd)": {
backgroundColor: "#FFF",
},
},
},
inputRoot: {
color: "white",
"&:hover .MuiOutlinedInput-notchedOutline": {
borderColor: "#f86a3e",
},
},
});
const inputdata = [
{
"key": "Threat Intel",
"value": 18,
"x": "2020-02-17T08:00:00.000Z",
"x0": "2020-02-17T08:00:00.000Z",
"x1": "2020-02-17T08:00:00.000Z",
"y": 18,
"y0": 0,
"y1": 18
},
{
"key": "Threat Intel",
"value": 3,
"x": "2020-02-21T08:00:00.000Z",
"x0": "2020-02-21T08:00:00.000Z",
"x1": "2020-02-21T08:00:00.000Z",
"y": 3,
"y0": 0,
"y1": 3
},
{
"key": "Threat Intel",
"value": 14,
"x": "2020-02-26T08:00:00.000Z",
"x0": "2020-02-26T08:00:00.000Z",
"x1": "2020-02-26T08:00:00.000Z",
"y": 14,
"y0": 0,
"y1": 14
},
{
"key": "Threat Intel",
"value": 18,
"x": "2020-02-29T08:00:00.000Z",
"x0": "2020-02-29T08:00:00.000Z",
"x1": "",
"y": 18,
"y0": 0,
"y1": 18
}
]
const LineChartWrapper = ({keys, height, width}) => {
const [hovered, setHovered] = useState("");
//console.log("Date: ", new Date("2019-11-14T08:00:00.000Z"))
console.log("Keys: ", keys)
var inputdata = keys.data
/*
const inputdata = [{
"key": "Intel",
"data": [
{ key: new Date('11/22/2019'), data: 3, metadata: {color: "orange", "name": "Intel"}},
{ key: new Date('11/24/2019'), data: 8, metadata: {color: "orange", "name": "Intel"}},
{ key: new Date('11/29/2019'), data: 2, metadata: {color: "orange", "name": "Intel"}},
]},
{
"key": "Popper",
"data": [
{ key: new Date('11/24/2019'), data: 9, },
{ key: new Date('11/29/2019'), data: 3, },
]
}
]
*/
return (
<div style={{}}>
<Typography variant="h6" style={{marginBotton: 15}}>
{keys.title}
</Typography>
<AreaChart
style={{marginTop: 15}}
height={height}
width={width}
data={inputdata}
series={
<AreaSeries
type="grouped"
symbols={
<PointSeries show={true} />
}
colorScheme={(colorInput) => {
var color = "cybertron"
if (colorInput !== undefined && colorInput.length > 0) {
color = colorInput[0].metadata !== undefined && colorInput[0].metadata.color !== undefined ? colorInput[0].metadata.color : color
}
return color
}}
tooltip={
<TooltipArea
color={"#000000"}
style={{
backgroundColor: "red",
}}
isRadial={true}
onValueEnter={(event) => {
if (hovered !== event.value.x) {
//setHovered(event.value.x)
}
}}
tooltip={
<ChartTooltip
followCursor={true}
modifiers={{
offset: '5px, 5px'
}}
content={(data, color) => {
console.log("DATA: ", data)
const name = data.metadata !== undefined && data.metadata.name !== undefined ? data.metadata.name : "No"
return (
<div style={{borderRadius: theme.palette.borderRadius, backgroundColor: theme.palette.inputColor, border: "1px solid rgba(255,255,255,0.3)", color: "white", padding: 5, cursor: "pointer",}}>
<Typography variant="body1">
{name}
</Typography>
</div>
)
/*
<TooltipTemplate
color={"#ffffff"}
value={{
x: data.x,
}}
/>
)
*/
}
}
/>
}
/>
}
/>
}
/>
</div>
)
}
const RadialChart = ({keys, setSelectedCategory}) => {
const [hovered, setHovered] = useState("");
return (
<div style={{cursor: "pointer",}} onClick={() => {
console.log("Click: ", hovered)
if (setSelectedCategory !== undefined) {
setSelectedCategory(hovered)
}
}}>
<RadialAreaChart
id="workflow_categories"
height={500}
width={500}
data={keys}
axis={<RadialAxis type="category" />}
series={
<RadialAreaSeries
interpolation="smooth"
colorScheme={(colorInput) => {
return '#f86a3e'
}}
animated={false}
id="workflow_series_id"
style={{cursor: "pointer",}}
line={
<RadialLine
color={"#000000"}
data={(data, color) => {
console.log("INFO: ", data, color)
return (
null
)
}}
/>
}
tooltip={
<TooltipArea
color={"#000000"}
style={{
backgroundColor: "red",
}}
isRadial={true}
onValueEnter={(event) => {
if (hovered !== event.value.x) {
setHovered(event.value.x)
}
}}
tooltip={
<ChartTooltip
followCursor={true}
modifiers={{
offset: '5px, 5px'
}}
content={(data, color) => {
return (
<div style={{borderRadius: theme.palette.borderRadius, backgroundColor: theme.palette.inputColor, border: "1px solid rgba(255,255,255,0.3)", color: "white", padding: 5, cursor: "pointer",}}>
<Typography variant="body1">
{data.x}
</Typography>
</div>
)
/*
<TooltipTemplate
color={"#ffffff"}
value={{
x: data.x,
}}
/>
)
*/
}
}
/>
}
/>
}
/>
}
/>
</div>
)
//axis={<RadialAxis type="category" />}
}
// This is the start of a dashboard that can be used.
// What data do we fill in here? Idk
const Dashboard = (props) => {
const { globalUrl, isLoggedIn } = props;
//const alert = useAlert();
const [bigChartData, setBgChartData] = useState("data1");
const [dayAmount, setDayAmount] = useState(7);
const [firstRequest, setFirstRequest] = useState(true);
const [stats, setStats] = useState({});
const [changeme, setChangeme] = useState("");
const [statsRan, setStatsRan] = useState(false);
const [keys, setKeys] = useState([])
const [treeKeys, setTreeKeys] = useState([])
const [selectedUsecaseCategory, setSelectedUsecaseCategory] = useState("");
const [selectedUsecases, setSelectedUsecases] = useState([]);
const [usecases, setUsecases] = useState([]);
const [workflows, setWorkflows] = useState([]);
const [frameworkData, setFrameworkData] = useState(undefined);
const [widgetData, setWidgetData] = useState([]);
let navigate = useNavigate();
const isCloud =
window.location.host === "localhost:3002" ||
window.location.host === "shuffler.io";
useEffect(() => {
if (selectedUsecaseCategory.length === 0) {
setSelectedUsecases(usecases)
} else {
const foundUsecase = usecases.find(data => data.name === selectedUsecaseCategory)
if (foundUsecase !== undefined && foundUsecase !== null) {
setSelectedUsecases([foundUsecase])
}
}
}, [selectedUsecaseCategory])
const checkSelectedParams = () => {
const urlSearchParams = new URLSearchParams(window.location.search)
const params = Object.fromEntries(urlSearchParams.entries())
const curpath = typeof window === "undefined" || window.location === undefined ? "" : window.location.pathname;
const cursearch = typeof window === "undefined" || window.location === undefined ? "" : window.location.search;
const foundQuery = params["selected"]
if (foundQuery !== null && foundQuery !== undefined) {
setSelectedUsecaseCategory(foundQuery)
const newitem = removeParam("selected", cursearch);
navigate(curpath + newitem)
}
const foundQuery2 = params["selected_object"]
if (foundQuery2 !== null && foundQuery2 !== undefined) {
console.log("Got selected_object: ", foundQuery2)
const queryName = foundQuery2.toLowerCase().replaceAll("_", " ")
// Waiting a bit for it to render
setTimeout(() => {
const foundItem = document.getElementById(queryName)
if (foundItem !== undefined && foundItem !== null) {
foundItem.click()
} else {
//console.log("Couldn't find item with name ", queryName)
}
}, 100);
}
}
useEffect(() => {
if (usecases.length > 0) {
console.log(usecases)
checkSelectedParams()
}
}, [usecases])
const getWidget = (dashboard, widget) => {
fetch(`${globalUrl}/api/v1/dashboards/${dashboard}/widgets/${widget}`, {
method: "GET",
headers: {
"Content-Type": "application/json",
Accept: "application/json",
},
credentials: "include",
})
.then((response) => {
if (response.status !== 200) {
console.log("Status not 200 for framework!");
}
return response.json();
})
.then((responseJson) => {
console.log("Resp: ", responseJson)
if (responseJson.success === false) {
if (responseJson.reason !== undefined) {
//toast("Failed loading: " + responseJson.reason)
} else {
//toast("Failed to load framework for your org.")
}
} else {
var tmpdata = responseJson
for (var key in tmpdata.data) {
for (var subkey in tmpdata.data[key].data) {
tmpdata.data[key].data[subkey].key = new Date(tmpdata.data[key].data[subkey].key)
}
}
const foundWidget = widgetData.findIndex(data => data.title === widget)
console.log("Found: ", foundWidget)
if (foundWidget !== undefined && foundWidget !== null && foundWidget >= 0) {
widgetData[foundWidget] = tmpdata
} else {
widgetData.push(tmpdata)
}
console.log("Data: ", widgetData)
setWidgetData(widgetData)
}
})
.catch((error) => {
//toast(error.toString());
})
}
document.title = "Shuffle - Dashboard";
var dayGraphLabels = [60, 80, 65, 130, 80, 105, 90, 130, 70, 115, 60, 130];
var dayGraphData = [60, 80, 65, 130, 80, 105, 90, 130, 70, 115, 60, 130];
const handleKeysetting = (categorydata) => {
var allCategories = []
var treeCategories = []
for (key in categorydata) {
const category = categorydata[key]
allCategories.push({"key": category.name, "data": category.list.length, "color": category.color})
treeCategories.push({"key": category.name, "data": 100, "color": category.color,})
for (var subkey in category.list) {
treeCategories.push({"key": category.list[subkey].name, "data": 20, "color": category.color})
}
}
setKeys(allCategories)
setTreeKeys(treeCategories)
}
useEffect(() => {
getWidget("main", "Overall")
getWidget("main", "Overall2")
}, []);
const fetchdata = (stats_id) => {
fetch(globalUrl + "/api/v1/stats/" + stats_id, {
method: "GET",
headers: {
"Content-Type": "application/json",
Accept: "application/json",
},
credentials: "include",
})
.then((response) => {
if (response.status !== 200) {
console.log("Status not 200 for " + stats_id);
}
return response.json();
})
.then((responseJson) => {
stats[stats_id] = responseJson;
setStats(stats);
// Used to force updates
setChangeme(stats_id);
})
.catch((error) => {
//toast("ERROR: " + error.toString());
console.log("ERROR: " + error.toString());
});
};
let chart1_2_options = {
maintainAspectRatio: false,
legend: {
display: false,
},
tooltips: {
backgroundColor: "#f5f5f5",
titleFontColor: "#333",
bodyFontColor: "#666",
bodySpacing: 4,
xPadding: 12,
mode: "nearest",
intersect: 0,
position: "nearest",
},
responsive: true,
scales: {
yAxes: [
{
barPercentage: 1.6,
gridLines: {
drawBorder: false,
color: "rgba(29,140,248,0.0)",
zeroLineColor: "transparent",
},
ticks: {
suggestedMin: 60,
suggestedMax: 125,
padding: 20,
fontColor: "#9a9a9a",
},
},
],
xAxes: [
{
barPercentage: 1.6,
gridLines: {
drawBorder: false,
color: "rgba(29,140,248,0.1)",
zeroLineColor: "transparent",
},
ticks: {
padding: 20,
fontColor: "#9a9a9a",
},
},
],
},
};
const dayGraph = {
data: (canvas) => {
let ctx = canvas.getContext("2d");
let gradientStroke = ctx.createLinearGradient(0, 230, 0, 50);
gradientStroke.addColorStop(1, "rgba(29,140,248,0.2)");
gradientStroke.addColorStop(0.4, "rgba(29,140,248,0.0)");
gradientStroke.addColorStop(0, "rgba(29,140,248,0)"); //blue colors
return {
labels: dayGraphLabels,
datasets: [
{
label: "My First dataset",
fill: true,
backgroundColor: gradientStroke,
borderColor: "#1f8ef1",
borderWidth: 2,
borderDash: [],
borderDashOffset: 0.0,
pointBackgroundColor: "#1f8ef1",
pointBorderColor: "rgba(255,255,255,0)",
pointHoverBackgroundColor: "#1f8ef1",
pointBorderWidth: 20,
pointHoverRadius: 4,
pointHoverBorderWidth: 15,
pointRadius: 4,
data: dayGraphData,
},
],
};
},
options: chart1_2_options,
};
// All these are currently tracked.
const variables = [
"backend_executions",
"workflow_executions",
"workflow_executions_aborted",
"workflow_executions_success",
"total_apps_created",
"total_apps_loaded",
"openapi_apps_created",
"total_apps_deleted",
"total_webhooks_ran",
"total_workflows",
"total_workflow_actions",
"total_workflow_triggers",
];
const runUpdate = () => {
for (var key in variables) {
fetchdata(variables[key]);
}
};
// Refresh every 60 seconds
const autoUpdate = 60000;
const { start, stop } = useInterval({
duration: autoUpdate,
startImmediate: false,
callback: () => {
runUpdate();
},
});
if (firstRequest) {
setFirstRequest(false);
//start();
//runUpdate();
} else if (!statsRan) {
// FIXME: Run this under runUpdate schedule?
// 1. Fix labels in dayGraphy.data
// 2. Add data to the daygraph
// Every time there's an update :)
// This should probably be done in the backend.. bleh
if (
stats["workflow_executions"] !== undefined &&
stats["workflow_executions"] !== null &&
stats["workflow_executions"].data !== undefined
) {
setStatsRan(true);
//console.log("NEW DATA?: ", stats)
console.log("SET WORKFLOW: ", stats["workflow_executions"]);
//var curday = startDate.getDate()
// Index = what day are we on
// 0 = today
var newDayGraphLabels = [];
var newDayGraphData = [];
for (var i = dayAmount; i > 0; i--) {
var enddate = new Date();
enddate.setDate(-i);
enddate.setHours(23, 59, 59, 999);
var startdate = new Date();
startdate.setDate(-i);
startdate.setHours(0, 0, 0, 0);
var endtime = enddate.getTime() / 1000;
var starttime = startdate.getTime() / 1000;
console.log(
"START: ",
starttime,
"END: ",
endtime,
"Data: ",
stats["workflow_executions"]
);
for (var key in stats["workflow_executions"].data) {
const item = stats["workflow_executions"]["data"][key];
console.log("ITEM: ", item.timestamp, endtime);
console.log(endtime - starttime);
if (
endtime - starttime > endtime - item.timestamp &&
endtime.timestamp >= 0
) {
console.log("HIT? ");
}
console.log(item.timestamp - endtime);
//console.log(item.timestamp-endtime)
break;
if (item.timestamp > endtime && item.timestamp < starttime) {
if (newDayGraphData[i - 1] === undefined) {
newDayGraphData[i - 1] = 1;
} else {
newDayGraphData[i - 1] += 1;
}
//break
}
}
newDayGraphLabels.push(i);
}
console.log(newDayGraphLabels);
console.log(newDayGraphData);
}
}
const newdata =
Object.getOwnPropertyNames(stats).length > 0 ? (
<div>
Autoupdate every {autoUpdate / 1000} seconds
{variables.map((data) => {
if (stats[data] === undefined || stats[data] === null) {
return null;
}
if (stats[data].total === undefined) {
return null;
}
return (
<div>
{data}: {stats[data].total}
</div>
);
})}
</div>
) : null;
const data = (
<div className="content" style={{width: 1000, margin: "auto", paddingBottom: 200, textAlign: "center",}}>
<div style={{width: 500, margin: "auto"}}>
{keys.length > 0 ?
<span>
<RadialChart keys={keys} setSelectedCategory={setSelectedUsecaseCategory} />
</span>
: null}
</div>
{widgetData === undefined || widgetData === null || widgetData === [] || widgetData.length === 0 ? null :
<Draggable>
<Paper style={{height: 350, width: 500, padding: "15px 15px 15px 15px", }}>
<LineChartWrapper keys={widgetData[0]} height={280} width={470} />
</Paper>
</Draggable>
}
</div>
);
const dataWrapper = (
<div style={{ maxWidth: 1366, margin: "auto" }}>{data}</div>
);
return dataWrapper;
};
export default Dashboard;

View File

@ -0,0 +1,961 @@
import React, { useEffect, useState } from "react";
import ReactMarkdown from "react-markdown";
import { BrowserView, MobileView } from "react-device-detect";
import { useParams, useNavigate, Link } from "react-router-dom";
import { isMobile } from "react-device-detect";
import theme from '../theme.jsx';
import remarkGfm from 'remark-gfm'
import KeyboardArrowRightIcon from '@mui/icons-material/KeyboardArrowRight';
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
import {
Grid,
TextField,
IconButton,
Tooltip,
Divider,
Button,
Menu,
MenuItem,
Typography,
Paper,
List,
Collapse,
ListItemButton,
ListItemText
} from "@mui/material";
import {
Link as LinkIcon,
Edit as EditIcon,
} from "@mui/icons-material";
const Body = {
//maxWidth: 1000,
//minWidth: 768,
maxWidth: "100%",
minWidth: "100%",
display: "flex",
height: "100%",
color: "white",
position: "relative",
//textAlign: "center",
};
const dividerColor = "rgb(225, 228, 232)";
const hrefStyle = {
color: "rgba(255, 255, 255, 0.40)",
textDecoration: "none",
};
const hrefStyle2 = {
color: "#f86a3e",
textDecoration: "none",
};
const innerHrefStyle = {
color: "rgba(255, 255, 255, 0.75)",
textDecoration: "none",
};
const Docs = (defaultprops) => {
const { globalUrl, selectedDoc, serverside, serverMobile } = defaultprops;
let navigate = useNavigate();
// Quickfix for react router 5 -> 6
const params = useParams();
//var props = JSON.parse(JSON.stringify(defaultprops))
var props = Object.assign({ selected: false }, defaultprops);
props.match = {}
props.match.params = params
useEffect(() => {
//if (params["key"] === undefined) {
// navigate("/docs/about")
// return
//}
}, [])
//console.log("PARAMS: ", params)
const [mobile, setMobile] = useState(serverMobile === true || isMobile === true ? true : false);
const [data, setData] = useState("");
const [firstrequest, setFirstrequest] = useState(true);
const [list, setList] = useState([]);
const [isopen, setOpen] = useState(-1);
const [, setListLoaded] = useState(false);
const [anchorEl, setAnchorEl] = React.useState(null);
const [headingSet, setHeadingSet] = React.useState(false);
const [selectedMeta, setSelectedMeta] = React.useState({
link: "hello",
read_time: 2,
});
const [tocLines, setTocLines] = React.useState([]);
const [baseUrl, setBaseUrl] = React.useState(
serverside === true ? "" : window.location.href
);
function handleClick(event) {
setAnchorEl(event.currentTarget);
}
function handleClose() {
setAnchorEl(null);
}
const handleCollapse = (index) => {
setOpen(isopen === index ? -1 : index)
};
const SidebarPaperStyle = {
backgroundColor: theme.palette.surfaceColor,
overflowX: "hidden",
position: "relative",
paddingLeft: 15,
paddingRight: 15,
paddingTop: 15,
marginTop: 15,
minHeight: "80vh",
//height: "50vh",
};
const SideBar = {
minWidth: 300,
width: "20%",
left: 0,
position: "sticky",
top: 50,
minHeight: "90vh",
maxHeight: "90vh",
overflowX: "hidden",
overflowY: "auto",
zIndex: 1000,
//borderRight: "1px solid rgba(255,255,255,0.3)",
};
const fetchDocList = () => {
fetch(globalUrl + "/api/v1/docs", {
method: "GET",
headers: {
"Content-Type": "application/json",
Accept: "application/json",
},
})
.then((response) => response.json())
.then((responseJson) => {
if (responseJson.success) {
setList(responseJson.list);
} else {
setList([
"# Error loading documentation. Please contact us if this persists.",
]);
}
setListLoaded(true);
})
.catch((error) => { });
};
const fetchDocs = (docId) => {
fetch(globalUrl + "/api/v1/docs/" + docId, {
method: "GET",
headers: {
"Content-Type": "application/json",
Accept: "application/json",
},
})
.then((response) => response.json())
.then((responseJson) => {
if (responseJson.success) {
setData(responseJson.reason);
if (docId === undefined) {
document.title = "Shuffle documentation introduction";
} else {
document.title = "Shuffle " + docId + " documentation";
}
if (responseJson.reason !== undefined && responseJson.reason !== null && responseJson.reason.includes("404: Not Found")) {
navigate("/docs")
return
}
if (responseJson.meta !== undefined) {
setSelectedMeta(responseJson.meta);
}
//console.log("TOC list: ", responseJson.reason)
if (
responseJson.reason !== undefined &&
responseJson.reason !== null
) {
const splitkey = responseJson.reason.split("\n");
var innerTocLines = [];
var record = false;
for (var key in splitkey) {
const line = splitkey[key];
//console.log("Line: ", line)
if (line.toLowerCase().includes("table of contents")) {
record = true;
continue;
}
if (record && line.length < 3) {
record = false;
}
if (record) {
const parsedline = line.split("](");
if (parsedline.length > 1) {
parsedline[0] = parsedline[0].replaceAll("*", "");
parsedline[0] = parsedline[0].replaceAll("[", "");
parsedline[0] = parsedline[0].replaceAll("]", "");
parsedline[0] = parsedline[0].replaceAll("(", "");
parsedline[0] = parsedline[0].replaceAll(")", "");
parsedline[0] = parsedline[0].trim();
parsedline[1] = parsedline[1].replaceAll("*", "");
parsedline[1] = parsedline[1].replaceAll("[", "");
parsedline[1] = parsedline[1].replaceAll("]", "");
parsedline[1] = parsedline[1].replaceAll(")", "");
parsedline[1] = parsedline[1].replaceAll("(", "");
parsedline[1] = parsedline[1].trim();
//console.log(parsedline[0], parsedline[1])
innerTocLines.push({
text: parsedline[0],
link: parsedline[1],
});
} else {
console.log("Bad line for parsing: ", line);
}
}
}
setTocLines(innerTocLines);
}
} else {
setData("# Error\nThis page doesn't exist.");
}
})
.catch((error) => { });
};
if (firstrequest) {
setFirstrequest(false);
if (!serverside) {
if (window.innerWidth < 768) {
setMobile(true);
}
}
if (selectedDoc !== undefined) {
setData(selectedDoc.reason);
setList(selectedDoc.list);
setListLoaded(true);
} else {
if (!serverside) {
fetchDocList();
//const propkey = props.match.params.key
//if (propkey === undefined) {
// navigate("/docs/about")
// return null
//}
//
if (props.match.params.key === undefined) {
} else {
console.log("DOCID: ", props.match.params.key)
fetchDocs(props.match.params.key)
}
}
}
}
// Handles search-based changes that origin from outside this file
if (serverside !== true && window.location.href !== baseUrl) {
setBaseUrl(window.location.href);
fetchDocs(props.match.params.key);
}
const parseElementScroll = () => {
const offset = 45;
var parent = document.getElementById("markdown_wrapper_outer");
if (parent !== null) {
//console.log("IN PARENT")
var elements = parent.getElementsByTagName("h2");
const name = window.location.hash
.slice(1, window.location.hash.lenth)
.toLowerCase()
.split("%20")
.join(" ")
.split("_")
.join(" ")
.split("-")
.join(" ")
.split("?")[0]
//console.log(name)
var found = false;
for (var key in elements) {
const element = elements[key];
if (element.innerHTML === undefined) {
continue;
}
// Fix location..
if (element.innerHTML.toLowerCase() === name) {
//console.log(element.offsetTop)
element.scrollIntoView({ behavior: "smooth" });
//element.scrollTo({
// top: element.offsetTop+offset,
// behavior: "smooth"
//})
found = true;
//element.scrollTo({
// top: element.offsetTop-100,
// behavior: "smooth"
//})
}
}
// H#
if (!found) {
elements = parent.getElementsByTagName("h3");
//console.log("NAMe: ", name)
found = false;
for (key in elements) {
const element = elements[key];
if (element.innerHTML === undefined) {
continue;
}
// Fix location..
if (element.innerHTML.toLowerCase() === name) {
element.scrollIntoView({ behavior: "smooth" });
//element.scrollTo({
// top: element.offsetTop-offset,
// behavior: "smooth"
//})
found = true;
//element.scrollTo({
// top: element.offsetTop-100,
// behavior: "smooth"
//})
}
}
}
}
//console.log(element)
//console.log("NAME: ", name)
//console.log(document.body.innerHTML)
// parent = document.getElementById(parent);
//var descendants = parent.getElementsByTagName(tagname);
// this.scrollDiv.current.scrollIntoView({ behavior: 'smooth' });
//$(".parent").find("h2:contains('Statistics')").parent();
};
if (serverside !== true && window.location.hash.length > 0) {
parseElementScroll();
}
const markdownStyle = {
color: "rgba(255, 255, 255, 0.65)",
overflow: "hidden",
paddingBottom: 100,
margin: "auto",
maxWidth: "100%",
minWidth: "100%",
overflow: "hidden",
fontSize: isMobile ? "1.3rem" : "1.0rem",
};
function OuterLink(props) {
console.log("Link: ", props.href)
if (props.href.includes("http") || props.href.includes("mailto")) {
return (
<a
href={props.href}
style={{ color: "#f85a3e", textDecoration: "none" }}
>
{props.children}
</a>
);
}
return (
<Link
to={props.href}
style={{ color: "#f85a3e", textDecoration: "none" }}
>
{props.children}
</Link>
);
}
function Img(props) {
return <img style={{ borderRadius: theme.palette.borderRadius, width: 750, maxWidth: "100%", marginTop: 15, marginBottom: 15, }} alt={props.alt} src={props.src} />;
}
function CodeHandler(props) {
console.log("PROPS: ", props)
const propvalue = props.value !== undefined && props.value !== null ? props.value : props.children !== undefined && props.children !== null && props.children.length > 0 ? props.children[0] : ""
return (
<div
style={{
padding: 15,
minWidth: "50%",
maxWidth: "100%",
backgroundColor: theme.palette.inputColor,
overflowY: "auto",
}}
>
<code
style={{
// Wrap if larger than X
whiteSpace: "pre-wrap",
overflow: "auto",
}}
>{propvalue}</code>
</div>
);
}
const Heading = (props) => {
const element = React.createElement(
`h${props.level}`,
{ style: { marginTop: props.level === 1 ? 20 : 50 } },
props.children
);
const [hover, setHover] = useState(false);
var extraInfo = "";
if (props.level === 1) {
extraInfo = (
<div
style={{
backgroundColor: theme.palette.inputColor,
padding: 15,
borderRadius: theme.palette.borderRadius,
marginBottom: 30,
display: "flex",
}}
>
<div style={{ flex: 3, display: "flex", vAlign: "center", position: "sticky", top: 50, }}>
{mobile ? null : (
<Typography style={{ display: "inline", marginTop: 6 }}>
<a
rel="noopener noreferrer"
target="_blank"
href={selectedMeta.link}
style={{ textDecoration: "none", color: "#f85a3e" }}
>
<Button style={{ color: "white", }} variant="outlined" color="secondary">
<EditIcon /> &nbsp;&nbsp;Edit
</Button>
</a>
</Typography>
)}
{mobile ? null : (
<div
style={{
height: "100%",
width: 1,
backgroundColor: "white",
marginLeft: 50,
marginRight: 50,
}}
/>
)}
<Typography style={{ display: "inline", marginTop: 11 }}>
{selectedMeta.read_time} minute
{selectedMeta.read_time === 1 ? "" : "s"} to read
</Typography>
</div>
<div style={{ flex: 2 }}>
{mobile ||
selectedMeta.contributors === undefined ||
selectedMeta.contributors === null ? (
""
) : (
<div style={{ margin: 10, height: "100%", display: "inline" }}>
{selectedMeta.contributors.slice(0, 7).map((data, index) => {
return (
<a
key={index}
rel="noopener noreferrer"
target="_blank"
href={data.url}
target="_blank"
style={{ textDecoration: "none", color: "#f85a3e" }}
>
<Tooltip title={data.url} placement="bottom">
<img
alt={data.url}
src={data.image}
style={{
marginTop: 5,
marginRight: 10,
height: 40,
borderRadius: 40,
}}
/>
</Tooltip>
</a>
);
})}
</div>
)}
</div>
</div>
);
}
return (
<Typography
onMouseOver={() => {
setHover(true);
}}
>
{props.level !== 1 ? (
<Divider
style={{
width: "90%",
marginTop: 40,
backgroundColor: theme.palette.inputColor,
}}
/>
) : null}
{element}
{/*hover ? <LinkIcon onMouseOver={() => {setHover(true)}} style={{cursor: "pointer", display: "inline", }} onClick={() => {
window.location.href += "#hello"
console.log(window.location)
//window.history.pushState('page2', 'Title', '/page2.php');
//window.history.replaceState('page2', 'Title', '/page2.php');
}} />
: ""
*/}
{extraInfo}
</Typography>
);
};
//React.createElement("p", {style: {color: "red", backgroundColor: "blue"}}, this.props.paragraph)
//function unicodeToChar(text) {
// return text.replace(/\\u[\dA-F]{4}/gi,
// function (match) {
// return String.fromCharCode(parseInt(match.replace(/\\u/g, ''), 16));
// }
// );
//}
const CustomButton = (props) => {
const { title, icon, link } = props
const [hover, setHover] = useState(false)
return (
<a
href={link}
rel="noopener noreferrer"
target="_blank"
style={{ textDecoration: "none", color: "inherit", flex: 1, margin: 10, }}
>
<div style={{ cursor: hover ? "pointer" : "default", borderRadius: theme.palette.borderRadius, flex: 1, border: "1px solid rgba(255,255,255,0.3)", backgroundColor: hover ? theme.palette.surfaceColor : theme.palette.inputColor, padding: 25, }}
onClick={(event) => {
if (link === "" || link === undefined) {
event.preventDefault()
console.log("IN CLICK!")
if (window.drift !== undefined) {
window.drift.api.startInteraction({ interactionId: 340043 })
} else {
console.log("Couldn't find drift in window.drift and not .drift-open-chat with querySelector: ", window.drift)
}
} else {
console.log("Link defined: ", link)
}
}} onMouseOver={() => {
setHover(true)
}}
onMouseOut={() => {
setHover(false);
}}
>
{icon}
<Typography variant="body1" style={{}} >
{title}
</Typography>
</div>
</a>
)
}
const DocumentationButton = (props) => {
const { item, link } = props
const [hover, setHover] = useState(false);
console.log("Link: ", link)
if (link === undefined || link === null) {
return null
}
return (
<Link to={link} style={hrefStyle}>
<div style={{ width: "100%", height: 80, cursor: hover ? "pointer" : "default", borderRadius: theme.palette.borderRadius, border: "1px solid rgba(255,255,255,0.3)", backgroundColor: hover ? theme.palette.surfaceColor : theme.palette.inputColor, }}
onMouseOver={() => {
setHover(true)
}}
onMouseOut={() => {
setHover(false);
}}
>
<Typography variant="body1" style={{}} >
{item}
</Typography>
</div>
</Link>
)
}
const headerStyle = {
marginTop: 25,
}
const mainpageInfo =
<div style={{
color: "rgba(255, 255, 255, 0.65)",
flex: "1",
overflow: "hidden",
paddingBottom: 100,
marginLeft: mobile ? 0 : 50,
marginTop: 50,
textAlign: "center",
margin: "auto",
marginTop: 50,
}}>
<Typography variant="h4" style={{ textAlign: "center", }}>
Documentation
</Typography>
<div style={{ display: "flex", marginTop: 25, }}>
<CustomButton title="Talk to Support" icon=<img src="/images/Shuffle_logo_new.png" style={{ height: 35, width: 35, border: "", borderRadius: theme.palette.borderRadius, }} /> />
<CustomButton title="Ask the community" icon=<img src="/images/social/discord.png" style={{ height: 35, width: 35, border: "", borderRadius: theme.palette.borderRadius, }} /> link="https://discord.gg/B2CBzUm" />
</div>
<div style={{ textAlign: "left" }}>
<Typography variant="h6" style={headerStyle} >Tutorial</Typography>
<Typography variant="body1">
<b>Dive in.</b> Hands-on is the best approach to see how Shuffle can transform your security operations. Our set of tutorials and videos teach you how to build your skills. Check out the <Link to="/docs/getting-started" style={hrefStyle2}>getting started</Link> section to give it a go!
</Typography>
<Typography variant="h6" style={headerStyle}>Why Shuffle?</Typography>
<Typography variant="body1">
<b>Security first.</b> We incentivize trying before buying, and give you the full set of tools you need to automate your operations. What's more is we also help you <a href="https://shuffler.io/pricing?tag=docs" target="_blank" style={hrefStyle2}>find usecases</a> that fit your unique needs. Accessibility is key, and we intend to help every SOC globally use and share their usecases.
</Typography>
<Typography variant="h6" style={headerStyle}>Get help</Typography>
<Typography variant="body1">
<b>Our promise</b> is to make it easier and easier to automate your operations. In some cases however, it may be good with a helping hand. That's where <a href="https://shuffler.io/pricing?tag=docs" target="_blank" style={hrefStyle2}>Shuffle's consultancy and support</a> services come in handy. We help you build and automate your operational processes to a level you haven't seen before with the help of our <a href="https://shuffler.io/usecases?tag=docs" target="_blank" style={hrefStyle2}>usecases</a>.
</Typography>
<Typography variant="h6" style={headerStyle}>APIs</Typography>
<Typography variant="body1">
<b>Learn.</b> We're all about learning, and are continuously creating documentation and video tutorials to better understand how to get started. APIs are an extremely important part of how the internet works today, and our goal is helping every security professional learn about them.
</Typography>
<Typography variant="h6" style={headerStyle}>Workflow building</Typography>
<Typography variant="body1">
<b>Build.</b> Creating workflows has never been easier. Jump into things with our <Link to="/getting-started" style={hrefStyle2}>getting Started</Link> section and build to your hearts content. Workflows make it all come together, with an easy to use area.
</Typography>
<Typography variant="h6" style={headerStyle}>Managing Shuffle</Typography>
<Typography variant="body1">
<b>Organize.</b> Whether an organization of 1000 or 1, management tools are necessary. In Shuffle we offer full user management, MFA and single-signon options, multi-tenancy and a lot more - for free!
</Typography>
</div>
{/*
<Grid container spacing={2} style={{marginTop: 50, }}>
{list.map((data, index) => {
const item = data.name;
if (item === undefined) {
return null;
}
const path = "/docs/" + item;
const newname =
item.charAt(0).toUpperCase() +
item.substring(1).split("_").join(" ").split("-").join(" ");
const itemMatching = props.match.params.key === undefined ? false :
props.match.params.key.toLowerCase() === item.toLowerCase();
return (
<Grid key={index} item xs={4}>
<DocumentationButton key={index} item={newname} link={"/docs/"+data.name} />
</Grid>
)
})}
</Grid>
*/}
{/*
<TextField
required
style={{
flex: "1",
backgroundColor: theme.palette.inputColor,
height: 50,
}}
InputProps={{
style:{
color: "white",
height: 50,
},
}}
placeholder={"Search Knowledgebase"}
color="primary"
fullWidth={true}
type="firstname"
id={"Searchfield"}
margin="normal"
variant="outlined"
onChange={(event) => {
console.log("Change: ", event.target.value)
}}
/>
*/}
</div>
// PostDataBrowser Section
const postDataBrowser =
list === undefined || list === null ? null : (
<div style={Body}>
<div style={SideBar}>
<Paper style={SidebarPaperStyle}>
<List style={{ listStyle: "none", paddingLeft: "0" }}>
{list.map((data, index) => {
const item = data.name;
if (item === undefined) {
return null;
}
const path = "/docs/" + item;
const newname =
item.charAt(0).toUpperCase() +
item.substring(1).split("_").join(" ").split("-").join(" ");
const itemMatching = props.match.params.key === undefined ? false :
props.match.params.key.toLowerCase() === item.toLowerCase();
return (
<li key={index}>
<ListItemButton
component={Link}
key={index}
style={hrefStyle}
to={path}
onClick={() => {
setTocLines([]);
fetchDocs(item);
handleCollapse(index);
}}
>
<ListItemText
style={{ color: itemMatching ? "#f86a3e" : "inherit" }}
variant="body1"
>
{newname}
</ListItemText>
{isopen === index ? <ExpandMoreIcon /> : <KeyboardArrowRightIcon />}
</ListItemButton>
{itemMatching &&
tocLines !== null &&
tocLines !== undefined &&
tocLines.length > 0 ? (
<Collapse in={isopen === index} timeout="auto" unmountOnExit>
{tocLines.map((data, index) => {
return (
<ListItemButton
component={Link}
key={index}
style={innerHrefStyle}
to={data.link}
>
<ListItemText
variant="body2"
style={{ cursor: "pointer" }}
>
{data.text}
</ListItemText>
</ListItemButton>
);
})}
</Collapse>
) : null}
</li>
);
})}
</List>
</Paper>
</div>
<div style={{ width: "70%", margin: "auto", overflow: "hidden", marginTop: 50, paddingRight: 50 }}>
{props.match.params.key === undefined ?
mainpageInfo
:
<div id="markdown_wrapper_outer" style={markdownStyle}>
<ReactMarkdown
components={{
img: Img,
code: CodeHandler,
h1: Heading,
h2: Heading,
h3: Heading,
h4: Heading,
h5: Heading,
h6: Heading,
a: OuterLink,
}}
id="markdown_wrapper"
escapeHtml={false}
style={{
maxWidth: "100%", minWidth: "100%",
}}
>
{data}
</ReactMarkdown>
</div>
}
</div>
</div>
);
// remarkPlugins={[remarkGfm]}
const mobileStyle = {
color: "white",
marginLeft: 25,
marginRight: 25,
paddingBottom: 50,
backgroundColor: "inherit",
display: "flex",
flexDirection: "column",
};
const postDataMobile =
list === undefined || list === null ? null : (
<div style={mobileStyle}>
<div>
<Button
fullWidth
aria-controls="simple-menu"
aria-haspopup="true"
variant="outlined"
color="primary"
onClick={handleClick}
>
<div style={{ color: "white" }}>More docs</div>
</Button>
<Menu
id="simple-menu"
anchorEl={anchorEl}
style={{}}
keepMounted
open={Boolean(anchorEl)}
onClose={handleClose}
>
{list.map((data, index) => {
const item = data.name;
if (item === undefined) {
return null;
}
const path = "/docs/" + item;
const newname =
item.charAt(0).toUpperCase() +
item.substring(1).split("_").join(" ").split("-").join(" ");
return (
<MenuItem
key={index}
style={{ color: "white" }}
onClick={() => {
window.location.pathname = path;
}}
>
{newname}
</MenuItem>
);
})}
</Menu>
</div>
{props.match.params.key === undefined ?
mainpageInfo
:
<div id="markdown_wrapper_outer" style={markdownStyle}>
<ReactMarkdown
components={{
img: Img,
code: CodeHandler,
h1: Heading,
h2: Heading,
h3: Heading,
h4: Heading,
h5: Heading,
h6: Heading,
a: OuterLink,
}}
id="markdown_wrapper"
escapeHtml={false}
style={{
maxWidth: "100%", minWidth: "100%",
}}
>
{data}
</ReactMarkdown>
</div>
}
<Divider
style={{
marginTop: "10px",
marginBottom: "10px",
backgroundColor: dividerColor,
}}
/>
<Button
fullWidth
aria-controls="simple-menu"
aria-haspopup="true"
variant="outlined"
color="primary"
onClick={handleClick}
>
<div style={{ color: "white" }}>More docs</div>
</Button>
</div>
);
//const imageModal =
// <Dialog modal
// open={imageModalOpen}
// </Dialog>
// {imageModal}
// Padding and zIndex etc set because of footer in cloud.
const loadedCheck = (
<div style={{ minHeight: 1000, paddingBottom: 100, zIndex: 50000, }}>
<BrowserView>{postDataBrowser}</BrowserView>
<MobileView>{postDataMobile}</MobileView>
</div>
);
return <div style={{}}>{loadedCheck}</div>;
};
export default Docs;

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,258 @@
import React, {useState} from 'react';
import {isMobile} from "react-device-detect";
import {Link} from 'react-router-dom';
import {Divider, List, ListItem, ListItemText, Card, CardContent, Grid, Typography, Button, ButtonGroup, FormControl, Dialog, DialogTitle, DialogActions, DialogContent, Tooltip} from '@mui/material';
import {ExpandMore as ExpandMoreIcon, ExpandLess as ExpandLessIcon} from '@mui/icons-material';
const hrefStyle = {
textDecoration: "none",
color: "#f85a3e"
}
/*
* More questions:
* What happens with IPv6 vs IPv6?
* How long can contracts be?
* Any discount? 20% with 1 year+
* How can we pay? Manual or not
* How is support handled?
* How big is the team EXACTLY?
* What are requirements for everything?
* What level of support does Fredrik/Shuffle provide to paying customers with enterprise license agreements?
* Whats their guaranteed response time? 2 hours, 4 hours, next business day? Support 365/24/7, or just weekdays?
* How can customers submit support requests? Email, phone, and/or web?
* Is there a support team, or is Fredrik the only support person right now?
* Whats the annual Shuffle release schedule / frequency? One major release once a year with minor releases quarterly?
* The ability to run our own, private instance of Shuffle in a public or private cloud, as well as on virtualized or bare metal, standalone/isolated servers is very important.
* We were wondering how the shuffle environment handles a playbook in production(workflow editing and testing phase) vs. in operations (playbook/workflow is operational in a SOC).
* Is Shuffle capable of pushing notifications/messages to REDPro if a playbook is Active, Inactive or in Error so its general status can be understood via the Playbook Library.
* Will cloud webhooks behave any differently from on premise webhooks if we are hosting our own cloud.
* If we are hosting on our own cloud and the cloud is not connected to the open internet, will there a be a work around for delivering app updates.
* What other maintenance and troubleshooting considerations should we be aware of in an isolated cloud environment
* Do you have any documentation for putting workflows into a github.
*/
export const pricingFaq = [
{
"question": "What currency are your prices in?",
"answers": [
"They are in US Dollars.",
],
},
{
"question": "Do you offer discounts or free trials?",
"answers": [
"We offer free trials, and may offer discounts and features for testing in certain scenarios.",
],
},
{
"question": "What payment methods do you offer?",
"answers": [
"We accept credit cards, Apple Pay, Google Pay and any other payment Stripe supports.",
],
},
{
"question": "How can I switch to annual billing?",
"answers": [
"Contact us at <a href='/contact' style={hrefStyle}>Contact</a> page!",
],
},
{
"question": "When does my membership get activated?",
"answers": [
"As soon as the payment is finished, you should see more features available in the Admin view.",
],
},
{
"question": "How can I switch my plan?",
"answers": [
"Contact us at <a href='/contact' style={hrefStyle}>Contact</a> page!",
],
},
{
"question": "What happens after payment is finished?",
"answers": [
"We will automatically and immediately apply all the featuers to your organization.",
],
},
{
"question": "How can I cancel my plan?",
"answers": [
"As an Admin of your organization, you can manage it from the Admin page.",
],
},
{
"question": "What is your refund policy?",
"answers": [
"For monthly and yearly subscriptions, you have 48 hours after the transaction to request a refund. Note that we reserve the right to decline requests if we detect high activity on your account within this time." ,
],
},
{
"question": "Do you offer support?",
"answers": [
"Yes! We offer priority support with an SLA to our enterprise customers, and will answer any questions directed our way on the Contact page otherwise." ,
],
},
{
"question": "Can you help me automate my operations?",
"answers": [
"Yes! We offer support with setup, configuration, automation and app creation. This can be bought as an addition withour needing a subscription.",
],
}
]
export const faqData = [
{
"question": "What is Niceable? Whats your mission?",
"answers": [
"Check out our cool <a href='/about' style={hrefStyle}>About</a> page!",
],
},
{
"question": "How does it work?",
"answers": [
"<a href='/' style={hrefStyle}>We've got you covered!</a>",
],
},
{
"question": "When will winners be announced?",
"answers": [
"When the prizedraw's 'ticket threshold' is reached, all contributors will receive an email notification about when the live announcement of the winners—the prize winner and the winning charity—will take place. In general, the live announcement happens within 48 hours of the email notification being sent."
],
},
{
"question": "How much goes to charity?",
"answers": [
"All prizedraws are guaranteed to give the majority of user contributions—more than 50%—to the winning charity. Individual prizedraw hosts (ie, prize vendors) may choose to take a smaller amount for themselves and give a larger percentage to the winning charity. In any case, we are the only prizedraw hosting platform that guarantees that the majority goes to charity. Its the right thing to do."
],
},
{
"question": "How are winning charities selected?",
"answers": [
"Charities are selected through a voting process that happens separately for each prizedraw. The community of contributors for a given prizedraw use our voting system to determine the best destination for their crowdsourced contribution. The current vote distribution can be seen on each prizedraw pages charity leaderboard.",
],
},
{
"question": "How are the charitable options chosen?",
"answers": [
"All of the charities that users can vote for have been selected based on them receiving top ratings from the most respected “charity evaluator” organizations. These assessments focus on transparency and financial optimization as well as the nature of their mission and demonstrated impact of their activities. Ultimately, however, YOUR assessment matters most. So, discuss with our community and then decide for yourself!",
"If youd like to recommend a charity or you are part of a charity thats interested in being featured on our site, please let us know <a href='mailto:adam@niceable.co' style={hrefStyle}>here (adam@niceable.co).</a>",
"In the future, additional charities will be added as options with the least voted for charities being replaced. That way, all of our charitable options will be ones that have been top rated by charity evaluator organizations and top vote getters from our wise and beloved Niceable users.",
"We are also working on adding lots of information and statistics about each charity to our site, something that our charitable partners are helping us with.",
],
},
{
"question": "Can I “write-off” my contribution on my taxes?",
"answers": [
"That depends on where you live. We do not claim to be tax experts and do not offer any advice on such matters. Basically, in some places, you can. In others, you can't. Check with a licensed tax expert in your area.",
],
},
{
"question": "Can I buy prizedraw prizes directly?",
"answers": [
"We encourage users to check out prizedraw hosts, many of whom promote our prizedraws and charitable partnerships through social media. They offer prizes because they want to support great charities and offer products and experiences to people who may not always have the money to buy their products directly. Making super nice(able) things accessible to you and everyone else is a major part of our mission and they help us do that.",
"The current constraints of capitalism are BS and we're out to change that. Thanks for being a hero! Our prizedraw hosts are reaching out to you and--unlike almost all other organizations--trust YOU to choose the most-worthy charity to support. So, we certainly encourage you to check out their other offerings. They are helping all of you make the impact that YOU want to make and may offer something super nice(able) that's also a perfect fit for you.",
],
},
{
"question": "How do I enter a promocode?",
"answers": [
"If its your first time visiting us, you can do it in a Raffle on the right hand side. If you are already logged in, click the 'My account' button in the upper right corner of the screen. Then click the 'enter a promotional code, before submitting the code you have.",
"You should now have received more entries!",
],
},
{
"question": "How do you select your vendors?",
"answers": [
"Currently, our #1 priority is learning more about YOU. What do our users want? What prizes, charities, site features and technology, support, etc.? Therefore, we are currently trying to maximize the diversity of our prizes to see what YOU value most. Its about you, not us or our vendors.",
"Do you most value products and experiences that are ethically-produced? Crazy expensive? Mid-priced? Rare or one-of-a-kind? Created by independent vendors like artists and craftspeople? By everyday people offering services customized for you and you alone? Luxury brands? Houses? Vacations? Cutting-edge technology? Whatever you want, well work hard to offer it. We believe that EVERYONE should be able to have super nice(able) things!",
"We are, however, limiting the number of active prizedraws that we have to ensure that these prizedraws fill up quickly, allowing prize winners and winning charities to enjoy their winnings sooner. In the future, we plan to offer many more prizedraws at one time.",
],
},
{
"question": "Can I host a prizedraw so that I can make some money, support great charities, and reach new audiences?",
"answers": [
"<a href='mailto:adam@niceable.co' style={hrefStyle}>Contact us here</Link> (adam@niceable.co)",
],
},
{
"question": "Can I host a prizedraw and donate the prize (because Im a super nice person)?",
"answers": [
"<a href='mailto:adam@niceable.co' style={hrefStyle}>Contact us here</Link> (adam@niceable.co)",
],
}
]
const Faq = (props) => {
const { theme } = props;
// Hahah, this is a hack fml
const HandleAnswer = (props) => {
const [answers, setAnswers] = useState("");
const current = props.current
const loadAnswers = () => {
if (answers === "") {
const data = current.answers.map(answer => {
return answer
})
setAnswers(data.join("<div/>"))
} else {
setAnswers("")
}
}
const icon = answers === "" ? <ExpandMoreIcon /> : <ExpandLessIcon />
return (
<ListItem button onClick={() => loadAnswers()} style={{textAlign: "center"}}>
<div style={{marginRight: 5, }}>{icon}</div>
<ListItemText primary=<Typography variant="body1">{current.question}</Typography> secondary=<td style={{color: "rgba(255,255,255,0.8)"}} dangerouslySetInnerHTML={{__html: answers}} />/>
</ListItem>
)
}
const width = isMobile ? "100%" : 1000
const FAQ =
<div elevation={1} style={{padding: isMobile ? "20px 0px 10px 0px" : 100, textAlign: "center", color: "rgba(255,255,255,0.9)", minWidth: width, maxWidth: width, margin: "auto"}}>
<Typography variant="h4" style={{textAlign: "center", marginBottom: 50, }}>
Frequently asked questions
</Typography>
<Grid container spacing={4} style={{textAlign: "center", width: isMobile?"100%":"100%"}}>
{pricingFaq.map((current) => {
return (
<Grid item xs={isMobile ? 12 : 6} key={current.question} style={{margin: "auto"}}>
<HandleAnswer current={current} />
<Divider style={{backgroundColor: "rgba(255,255,255,0.2)"}}/>
</Grid>
)
})}
</Grid>
{/*
<Divider />
<Typography variant="body1" color="textPrimary" style={{textAlign: "left", marginTop: 15, marginBottom: 5}}>
Thanks for reading! Have a super nice(able) time entering prizedraws for amazing prizes, enjoying our awesome community, and making the impact that YOU want to make in the world!
</Typography>
*/}
</div>
const landingpageData =
<div style={{margin: "auto", maxWidth: isMobile ? "100%" : 2560, backgroundColor: theme.palette.inputColor}}>
{FAQ}
</div>
return (
<div>
{landingpageData}
</div>
)
}
export default Faq;

View File

@ -0,0 +1,87 @@
import React, { useEffect, useState } from 'react';
import ReactDOM from "react-dom"
import AppFramework from "../components/AppFramework.jsx";
//import { useAlert
import { ToastContainer, toast } from "react-toastify"
import { Link, useParams } from "react-router-dom";
import theme from '../theme.jsx';
import {
Button,
} from "@mui/material";
const Framework = (props) => {
const {globalUrl, isLoaded, isLoggedIn, showOptions, selectedOption, rolling, } = props;
//const alert = useAlert()
const [frameworkLoaded, setFrameworkLoaded] = useState(false)
const [frameworkData, setFrameworkData] = useState()
const getFramework = () => {
fetch(globalUrl + "/api/v1/apps/frameworkConfiguration", {
method: "GET",
headers: {
"Content-Type": "application/json",
Accept: "application/json",
},
credentials: "include",
})
.then((response) => {
if (response.status !== 200) {
console.log("Status not 200 for framework!");
}
return response.json();
})
.then((responseJson) => {
if (responseJson.success === false) {
if (responseJson.reason !== undefined) {
toast("Failed loading: " + responseJson.reason)
} else {
toast("Failed to load framework for your org.")
}
setFrameworkLoaded(true)
} else {
ReactDOM.unstable_batchedUpdates(() => {
setFrameworkData(responseJson)
setFrameworkLoaded(true)
})
}
})
.catch((error) => {
setFrameworkLoaded(true)
toast(error.toString());
})
}
useEffect(() => {
getFramework()
}, [])
return (
<div>
<div style={{marginBottom: 25, marginTop: 25, width: 300, margin: "auto", textAlign: "center",}}>
<Link style={{textDecoration: "none", }} to="/getting-started">
<Button variant="outlined" color="secondary">
Back to getting started
</Button>
</Link>
</div>
{frameworkLoaded === true && isLoaded ?
<AppFramework
frameworkData={frameworkData}
selectedOption={"Draw"}
showOptions={false}
isLoaded={isLoaded}
isLoggedIn={isLoggedIn}
globalUrl={globalUrl}
/>
: null}
</div>
)
}
export default Framework;

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,7 @@
import React from "react";
const HandlePayment = () => {
return null;
};
export default HandlePayment;

Some files were not shown because too many files have changed in this diff Show More