586
shuffle/frontend/src/App.jsx
Normal 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;
|
||||
1350
shuffle/frontend/src/__test__/appdata.js
Normal file
6
shuffle/frontend/src/__test__/environmentdata.js
Normal file
@@ -0,0 +1,6 @@
|
||||
const data = [
|
||||
{ name: "cloud", type: "cloud" },
|
||||
{ name: "onprem", type: "onprem" },
|
||||
];
|
||||
|
||||
export default data;
|
||||
29
shuffle/frontend/src/__test__/scheduledata.js
Normal 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;
|
||||
15
shuffle/frontend/src/__test__/webhookdata.js
Normal 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;
|
||||
169
shuffle/frontend/src/__test__/workflowdata.js
Normal 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;
|
||||
9
shuffle/frontend/src/assets/img/bag.svg
Normal file
|
After Width: | Height: | Size: 14 KiB |
9
shuffle/frontend/src/assets/img/book.svg
Normal file
|
After Width: | Height: | Size: 20 KiB |
1
shuffle/frontend/src/assets/img/default-monochrome.svg
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
shuffle/frontend/src/assets/img/github_phishing_email.png
Normal file
|
After Width: | Height: | Size: 449 KiB |
BIN
shuffle/frontend/src/assets/img/github_shuffle_img.png
Normal file
|
After Width: | Height: | Size: 431 KiB |
BIN
shuffle/frontend/src/assets/img/icpl_logo.png
Normal file
|
After Width: | Height: | Size: 6.2 KiB |
BIN
shuffle/frontend/src/assets/img/kafka.png
Normal file
|
After Width: | Height: | Size: 9.6 KiB |
BIN
shuffle/frontend/src/assets/img/logo.png
Normal file
|
After Width: | Height: | Size: 58 KiB |
26
shuffle/frontend/src/assets/img/logo.svg
Normal 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 |
BIN
shuffle/frontend/src/assets/img/logo.webp
Normal file
|
After Width: | Height: | Size: 5.7 KiB |
9
shuffle/frontend/src/assets/img/mobile.svg
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
shuffle/frontend/src/assets/img/shuffle_adminaccount.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
shuffle/frontend/src/assets/img/shuffle_architecture.png
Normal file
|
After Width: | Height: | Size: 143 KiB |
BIN
shuffle/frontend/src/assets/img/shuffle_webhook.png
Normal file
|
After Width: | Height: | Size: 440 KiB |
3
shuffle/frontend/src/assets/img/transform.sh
Normal 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>
|
||||
BIN
shuffle/frontend/src/assets/img/webhook.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
427
shuffle/frontend/src/charts.js
Normal 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
|
||||
};
|
||||
19
shuffle/frontend/src/components/AlertPopup.js
Normal 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;
|
||||
53
shuffle/frontend/src/components/AlertTemplate.js
Normal 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;
|
||||
2319
shuffle/frontend/src/components/AppFramework.jsx
Normal file
385
shuffle/frontend/src/components/AppGrid.jsx
Normal 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;
|
||||
403
shuffle/frontend/src/components/AppSearchButtons.jsx
Normal 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
|
||||
433
shuffle/frontend/src/components/AppSelection.jsx
Normal 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;
|
||||
220
shuffle/frontend/src/components/Appsearch.jsx
Normal 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;
|
||||
190
shuffle/frontend/src/components/AppsearchPopout.jsx
Normal 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;
|
||||
278
shuffle/frontend/src/components/AuthenticationItem.jsx
Normal 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
|
||||
366
shuffle/frontend/src/components/AuthenticationNormal.jsx
Normal 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
|
||||
469
shuffle/frontend/src/components/AuthenticationWindow.jsx
Normal 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;
|
||||
1011
shuffle/frontend/src/components/Billing.jsx
Normal file
280
shuffle/frontend/src/components/BillingStats.jsx
Normal 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;
|
||||
166
shuffle/frontend/src/components/Branding.jsx
Normal 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", }}>
|
||||
|
||||
</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;
|
||||
504
shuffle/frontend/src/components/CacheView.jsx
Normal 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.
|
||||
<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;
|
||||
1357
shuffle/frontend/src/components/ConfigureWorkflow.jsx
Normal file
434
shuffle/frontend/src/components/Countries.jsx
Normal 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
|
||||
304
shuffle/frontend/src/components/CreatorGrid.jsx
Normal 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;
|
||||
357
shuffle/frontend/src/components/DocsGrid.jsx
Normal 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;
|
||||
94
shuffle/frontend/src/components/Dropzone.jsx
Normal 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;
|
||||
601
shuffle/frontend/src/components/EditWorkflow.jsx
Normal 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;
|
||||
382
shuffle/frontend/src/components/ExploreWorkflow.jsx
Normal 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 }}>
|
||||
Here’s 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
|
||||
27
shuffle/frontend/src/components/ExtraApps.jsx
Normal 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
|
||||
23
shuffle/frontend/src/components/FAQ.jsx
Normal 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;
|
||||
810
shuffle/frontend/src/components/Files.jsx
Normal 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;
|
||||
28
shuffle/frontend/src/components/FrameworkData.jsx
Normal file
1050
shuffle/frontend/src/components/Header.jsx
Normal file
245
shuffle/frontend/src/components/LandingpageUsecases.jsx
Normal 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;
|
||||
106
shuffle/frontend/src/components/Newsletter.jsx
Normal 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;
|
||||
937
shuffle/frontend/src/components/Oauth2Auth.jsx
Normal 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> -
|
||||
<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;
|
||||
275
shuffle/frontend/src/components/OrgHeader.jsx
Normal file
873
shuffle/frontend/src/components/OrgHeaderexpanded.jsx
Normal 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;
|
||||
19
shuffle/frontend/src/components/PaperComponent.jsx
Normal 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;
|
||||
3771
shuffle/frontend/src/components/ParsedAction.jsx
Normal file
93
shuffle/frontend/src/components/Priorities.jsx
Normal 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.
|
||||
<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);
|
||||
}}
|
||||
/> 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.
|
||||
<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);
|
||||
}}
|
||||
/> Show read
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Priorities;
|
||||
176
shuffle/frontend/src/components/Priority.jsx
Normal 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;
|
||||
157
shuffle/frontend/src/components/RenderCytoscape.js
Normal 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;
|
||||
157
shuffle/frontend/src/components/RenderCytoscape.jsx
Normal 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;
|
||||
47
shuffle/frontend/src/components/ScrollToTop.jsx
Normal 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;
|
||||
660
shuffle/frontend/src/components/Searchfield.jsx
Normal 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;
|
||||
252
shuffle/frontend/src/components/SecurityFramework.jsx
Normal 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;
|
||||
1745
shuffle/frontend/src/components/ShuffleCodeEditor.jsx
Normal file
233
shuffle/frontend/src/components/SuggestedWorkflows.jsx
Normal 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;
|
||||
1654
shuffle/frontend/src/components/UsecaseSearch.jsx
Normal file
730
shuffle/frontend/src/components/WelcomeForm2.jsx
Normal 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;
|
||||
387
shuffle/frontend/src/components/WorkflowGrid.jsx
Normal 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;
|
||||
302
shuffle/frontend/src/components/WorkflowPaper.jsx
Normal 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
|
||||
332
shuffle/frontend/src/components/WorkflowPaperNew.jsx
Normal 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
|
||||
455
shuffle/frontend/src/components/WorkflowTemplatePopup.jsx
Normal 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
|
||||
199
shuffle/frontend/src/components/Workflowsearch.jsx
Normal 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;
|
||||
8
shuffle/frontend/src/components/history.jsx
Normal file
@@ -0,0 +1,8 @@
|
||||
import { createBrowserHistory } from "history";
|
||||
|
||||
var localExport;
|
||||
if (typeof window !== "undefined") {
|
||||
localExport = createBrowserHistory({ forceRefresh: true });
|
||||
}
|
||||
|
||||
export default localExport;
|
||||
BIN
shuffle/frontend/src/css/font1.woff2
Normal file
BIN
shuffle/frontend/src/css/font2.woff2
Normal file
BIN
shuffle/frontend/src/css/font3.woff2
Normal file
BIN
shuffle/frontend/src/css/font4.woff2
Normal file
BIN
shuffle/frontend/src/css/font5.woff2
Normal file
45
shuffle/frontend/src/css/nunito.css
Normal 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;
|
||||
}
|
||||
434
shuffle/frontend/src/defaultCytoscapeStyle.jsx
Normal 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;
|
||||
112
shuffle/frontend/src/frameworkStyle.jsx
Normal 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
|
||||
33
shuffle/frontend/src/index.css
Normal 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;
|
||||
}
|
||||
16
shuffle/frontend/src/index.js
Normal 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();
|
||||
127
shuffle/frontend/src/serviceWorker.js
Normal 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();
|
||||
});
|
||||
}
|
||||
}
|
||||
97
shuffle/frontend/src/theme.jsx
Normal file
74
shuffle/frontend/src/views/About.jsx
Normal 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;
|
||||
4419
shuffle/frontend/src/views/Admin.jsx
Normal file
233
shuffle/frontend/src/views/AdminSetup.jsx
Normal 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;
|
||||
17841
shuffle/frontend/src/views/AngularWorkflow.jsx
Normal file
5987
shuffle/frontend/src/views/AppCreator.jsx
Normal file
3019
shuffle/frontend/src/views/Apps.jsx
Normal file
1686
shuffle/frontend/src/views/Dashboard.jsx
Normal file
759
shuffle/frontend/src/views/DashboardViews.jsx
Normal 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;
|
||||
961
shuffle/frontend/src/views/Docs.jsx
Normal 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 /> 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;
|
||||
377
shuffle/frontend/src/views/EditWorkflow.jsx
Normal file
258
shuffle/frontend/src/views/Faq.jsx
Normal 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?
|
||||
* What’s 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?
|
||||
* What’s 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 it’s 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? What’s 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. It’s 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 page’s 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 you’d like to recommend a charity or you are part of a charity that’s 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. It’s 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, we’ll 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 I’m 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;
|
||||
87
shuffle/frontend/src/views/FrameworkWrapper.jsx
Normal 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;
|
||||
2813
shuffle/frontend/src/views/GettingStarted.jsx
Normal file
7
shuffle/frontend/src/views/HandlePayment.js
Normal file
@@ -0,0 +1,7 @@
|
||||
import React from "react";
|
||||
|
||||
const HandlePayment = () => {
|
||||
return null;
|
||||
};
|
||||
|
||||
export default HandlePayment;
|
||||