Merge branch '2.0.0'
This commit is contained in:
+132
-40
@@ -14,7 +14,9 @@ import HealthPage from "./components/HealthPage.jsx";
|
||||
//import Header from "./components/Header.jsx";
|
||||
import theme from "./theme";
|
||||
import Apps from "./views/Apps";
|
||||
import Apps2 from "./views/Apps2.jsx";
|
||||
import AppCreator from "./views/AppCreator";
|
||||
import DetectionDashBoard from "./views/DetectionDashboard.jsx";
|
||||
|
||||
import Welcome from "./views/Welcome.jsx";
|
||||
import Dashboard from "./views/Dashboard.jsx";
|
||||
@@ -22,6 +24,7 @@ import DashboardView from "./views/DashboardViews.jsx";
|
||||
import AdminSetup from "./views/AdminSetup";
|
||||
import Admin from "./views/Admin";
|
||||
import Docs from "./views/Docs.jsx";
|
||||
import Usecases2 from "./views/Usecases2.jsx";
|
||||
//import Introduction from "./views/Introduction";
|
||||
import SetAuthentication from "./views/SetAuthentication";
|
||||
import SetAuthenticationSSO from "./views/SetAuthenticationSSO";
|
||||
@@ -42,11 +45,20 @@ import AlertTemplate from "./components/AlertTemplate";
|
||||
import { isMobile } from "react-device-detect";
|
||||
import RuntimeDebugger from "./components/RuntimeDebugger.jsx"
|
||||
|
||||
import MFASetUp from './components/MFASetUP.jsx';
|
||||
import ApiExplorerWrapper from './views/ApiExplorerWrapper.jsx';
|
||||
import LeftSideBar from './components/LeftSideBar.jsx';
|
||||
import CodeWorkflow from './views/CodeWorkflow.jsx';
|
||||
import NotFound from './views/404.jsx';
|
||||
|
||||
import { ToastContainer, toast } from 'react-toastify';
|
||||
import 'react-toastify/dist/ReactToastify.css';
|
||||
|
||||
import Drift from "react-driftjs";
|
||||
|
||||
import { AppContext } from './context/ContextApi.jsx';
|
||||
import Workflows2 from "./views/Workflows2.jsx";
|
||||
|
||||
// Production - backend proxy forwarding in nginx
|
||||
var globalUrl = window.location.origin;
|
||||
|
||||
@@ -194,28 +206,38 @@ const App = (message, props) => {
|
||||
/>
|
||||
}
|
||||
|
||||
<div style={{ minHeight: 68, maxHeight: 68, }}>
|
||||
<Header
|
||||
billingInfo={{}}
|
||||
|
||||
{curpath.includes("/workflows") && curpath.includes("/run") ?
|
||||
<div style={{ height: 60, }} />
|
||||
:
|
||||
isLoggedIn ?
|
||||
<div style={{ position: 'fixed', top: 16, left: 10, zIndex: 100000 }}>
|
||||
<LeftSideBar userdata={userdata} globalUrl={globalUrl} serverside={false} notifications={notifications} />
|
||||
</div>
|
||||
:
|
||||
<div style={{ minHeight: 68, maxHeight: 68, }}>
|
||||
<Header
|
||||
billingInfo={{}}
|
||||
|
||||
notifications={notifications}
|
||||
setNotifications={setNotifications}
|
||||
checkLogin={checkLogin}
|
||||
cookies={cookies}
|
||||
removeCookie={removeCookie}
|
||||
isLoaded={isLoaded}
|
||||
globalUrl={globalUrl}
|
||||
setIsLoggedIn={setIsLoggedIn}
|
||||
isLoggedIn={isLoggedIn}
|
||||
userdata={userdata}
|
||||
notifications={notifications}
|
||||
setNotifications={setNotifications}
|
||||
checkLogin={checkLogin}
|
||||
cookies={cookies}
|
||||
removeCookie={removeCookie}
|
||||
isLoaded={isLoaded}
|
||||
globalUrl={globalUrl}
|
||||
setIsLoggedIn={setIsLoggedIn}
|
||||
isLoggedIn={isLoggedIn}
|
||||
userdata={userdata}
|
||||
|
||||
curpath={curpath}
|
||||
serverside={false}
|
||||
isMobile={false}
|
||||
curpath={curpath}
|
||||
serverside={false}
|
||||
isMobile={false}
|
||||
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
|
||||
{/*
|
||||
<div style={{ height: 60 }} />
|
||||
@@ -375,6 +397,19 @@ const App = (message, props) => {
|
||||
{...props}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path="/usecases2"
|
||||
element={
|
||||
<Usecases2
|
||||
userdata={userdata}
|
||||
isLoaded={isLoaded}
|
||||
isLoggedIn={isLoggedIn}
|
||||
globalUrl={globalUrl}
|
||||
{...props}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
@@ -401,6 +436,23 @@ const App = (message, props) => {
|
||||
{...props}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path="/apps2"
|
||||
element={
|
||||
<Apps2
|
||||
serverside={false}
|
||||
isLoaded={isLoaded}
|
||||
isLoggedIn={isLoggedIn}
|
||||
checkLogin={checkLogin}
|
||||
userdata={userdata}
|
||||
globalUrl={globalUrl}
|
||||
surfaceColor={theme.palette.surfaceColor}
|
||||
inputColor={theme.palette.inputColor}
|
||||
{...props}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
@@ -414,6 +466,12 @@ const App = (message, props) => {
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Route exact path="/apis/:appid" element={<ApiExplorerWrapper serverside={false} userdata={userdata} isLoggedIn={isLoggedIn} isMobile={false} selectedApp={undefined} isLoaded={isLoaded} isLoggedIn={isLoggedIn} globalUrl={globalUrl} surfaceColor={theme.palette.surfaceColor} inputColor={theme.palette.inputColor} checkLogin={checkLogin} {...props} />} />
|
||||
<Route
|
||||
exact
|
||||
path="/detections/sigma"
|
||||
element={<DetectionDashBoard globalUrl={globalUrl} />}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path="/workflows"
|
||||
@@ -430,6 +488,23 @@ const App = (message, props) => {
|
||||
{...props}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path="/workflows2"
|
||||
element={
|
||||
<Workflows2
|
||||
checkLogin={checkLogin}
|
||||
cookies={cookies}
|
||||
removeCookie={removeCookie}
|
||||
isLoaded={isLoaded}
|
||||
isLoggedIn={isLoggedIn}
|
||||
globalUrl={globalUrl}
|
||||
cookies={cookies}
|
||||
userdata={userdata}
|
||||
{...props}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
@@ -463,8 +538,14 @@ const App = (message, props) => {
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Route exact path="/workflows/:key/code" element={<CodeWorkflow serverside={false} userdata={userdata} globalUrl={globalUrl} isLoaded={isLoaded} isLoggedIn={isLoggedIn} surfaceColor={theme.palette.surfaceColor} inputColor={theme.palette.inputColor}{...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="/forms" element={<RunWorkflow serverside={false} userdata={userdata} globalUrl={globalUrl} isLoaded={isLoaded} isLoggedIn={isLoggedIn} surfaceColor={theme.palette.surfaceColor} inputColor={theme.palette.inputColor}{...props} />} />
|
||||
<Route exact path="/forms/:key/run" element={<RunWorkflow serverside={false} userdata={userdata} globalUrl={globalUrl} isLoaded={isLoaded} isLoggedIn={isLoggedIn} surfaceColor={theme.palette.surfaceColor} inputColor={theme.palette.inputColor}{...props} />} />
|
||||
<Route exact path="/forms/:key" element={<RunWorkflow serverside={false} userdata={userdata} globalUrl={globalUrl} isLoaded={isLoaded} isLoggedIn={isLoggedIn} surfaceColor={theme.palette.surfaceColor} inputColor={theme.palette.inputColor}{...props} />} />
|
||||
|
||||
<Route
|
||||
exact
|
||||
path="/docs/:key"
|
||||
@@ -520,6 +601,7 @@ const App = (message, props) => {
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Route exact path="/login/:key/mfa-setup" element={<MFASetUp setCookie={setCookie} serverside={false} mainColor={theme.palette.backgroundColor} userdata={userdata} stripeKey={undefined} globalUrl={globalUrl} inputColor={theme.palette.inputColor} isLoaded={isLoaded} {...props} />} />
|
||||
<Route
|
||||
exact
|
||||
path="/login_sso"
|
||||
@@ -598,30 +680,40 @@ const App = (message, props) => {
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</Routes>
|
||||
|
||||
<Route
|
||||
exact
|
||||
path="/*"
|
||||
element={
|
||||
<NotFound />
|
||||
}
|
||||
/>
|
||||
</Routes>
|
||||
</div>
|
||||
|
||||
return (
|
||||
<ThemeProvider theme={theme}>
|
||||
<CssBaseline />
|
||||
<CookiesProvider>
|
||||
<BrowserRouter>
|
||||
{includedData}
|
||||
</BrowserRouter>
|
||||
<ToastContainer
|
||||
position="bottom-center"
|
||||
autoClose={5000}
|
||||
hideProgressBar={false}
|
||||
newestOnTop={false}
|
||||
closeOnClick
|
||||
rtl={false}
|
||||
pauseOnFocusLoss
|
||||
draggable
|
||||
pauseOnHover
|
||||
theme="dark"
|
||||
/>
|
||||
</CookiesProvider>
|
||||
</ThemeProvider>
|
||||
<AppContext>
|
||||
<ThemeProvider theme={theme}>
|
||||
<CssBaseline />
|
||||
<CookiesProvider>
|
||||
<BrowserRouter>
|
||||
{includedData}
|
||||
</BrowserRouter>
|
||||
<ToastContainer
|
||||
position="bottom-center"
|
||||
autoClose={5000}
|
||||
hideProgressBar={false}
|
||||
newestOnTop={false}
|
||||
closeOnClick
|
||||
rtl={false}
|
||||
pauseOnFocusLoss
|
||||
draggable
|
||||
pauseOnHover
|
||||
theme="dark"
|
||||
/>
|
||||
</CookiesProvider>
|
||||
</ThemeProvider>
|
||||
</AppContext>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -36,6 +36,7 @@ import edgehandles from "cytoscape-edgehandles";
|
||||
|
||||
import cytoscape from "cytoscape";
|
||||
import { toast } from 'react-toastify';
|
||||
import { isMobile } from 'react-device-detect';
|
||||
|
||||
cytoscape.use(edgehandles)
|
||||
|
||||
@@ -52,13 +53,15 @@ export const findSpecificApp = (framework, inputcategory) => {
|
||||
}
|
||||
|
||||
const category = inputcategory.toLowerCase().split(":")[0].trim()
|
||||
|
||||
//console.log("findSpecificApp: ", category, framework)
|
||||
if (category === "edr" || category === "eradication" || category === "edr & av") {
|
||||
if (framework["EDR & AV"] !== undefined && framework["EDR & AV"].name !== undefined) {
|
||||
if (framework["EDR & AV"] !== undefined && framework["EDR & AV"].name !== undefined && framework["EDR & AV"].name !== "") {
|
||||
return framework["EDR & AV"]
|
||||
}
|
||||
|
||||
if (framework["edr"] !== undefined && framework["edr"].name !== undefined && framework["edr"].name !== "") {
|
||||
return framework["edr"]
|
||||
}
|
||||
|
||||
return {
|
||||
name: "EDR :default",
|
||||
large_image: parsedDatatypeImages()["EDR & AV"],
|
||||
@@ -67,10 +70,14 @@ export const findSpecificApp = (framework, inputcategory) => {
|
||||
id: "",
|
||||
}
|
||||
} else if (category === "communication" || category === "comms") {
|
||||
if (framework["Comms"] !== undefined && framework["Comms"].name !== undefined) {
|
||||
if (framework["Comms"] !== undefined && framework["Comms"].name !== undefined && framework["Comms"].name !== "") {
|
||||
return framework["Comms"]
|
||||
}
|
||||
|
||||
if (framework["communication"] !== undefined && framework["communication"].name !== undefined && framework["communication"].name !== "") {
|
||||
return framework["communication"]
|
||||
}
|
||||
|
||||
return {
|
||||
name: "COMMS :default",
|
||||
large_image: parsedDatatypeImages()["COMMS"],
|
||||
@@ -79,10 +86,14 @@ export const findSpecificApp = (framework, inputcategory) => {
|
||||
id: "",
|
||||
}
|
||||
} else if (category === "email") {
|
||||
if (framework["Email"] !== undefined && framework["Email"].name !== undefined) {
|
||||
if (framework["Email"] !== undefined && framework["Email"].name !== undefined && framework["Email"].name !== "") {
|
||||
return framework["Email"]
|
||||
}
|
||||
|
||||
if (framework["email"] !== undefined && framework["email"].name !== undefined && framework["email"].name !== "") {
|
||||
return framework["email"]
|
||||
}
|
||||
|
||||
return {
|
||||
name: "COMMS :default",
|
||||
large_image: parsedDatatypeImages()["COMMS"],
|
||||
@@ -91,10 +102,14 @@ export const findSpecificApp = (framework, inputcategory) => {
|
||||
id: "",
|
||||
}
|
||||
} else if (category === "assets") {
|
||||
if (framework["Assets"] !== undefined && framework["Assets"].name !== undefined) {
|
||||
if (framework["Assets"] !== undefined && framework["Assets"].name !== undefined && framework["Assets"].name !== "") {
|
||||
return framework["Assets"]
|
||||
}
|
||||
|
||||
if (framework["assets"] !== undefined && framework["assets"].name !== undefined && framework["assets"].name !== "") {
|
||||
return framework["assets"]
|
||||
}
|
||||
|
||||
return {
|
||||
name: "ASSETS :default",
|
||||
large_image: parsedDatatypeImages()["ASSETS"],
|
||||
@@ -103,10 +118,14 @@ export const findSpecificApp = (framework, inputcategory) => {
|
||||
id: "",
|
||||
}
|
||||
} else if (category === "cases") {
|
||||
if (framework["Cases"] !== undefined && framework["Cases"].name !== undefined) {
|
||||
if (framework["Cases"] !== undefined && framework["Cases"].name !== undefined && framework["Cases"].name !== "") {
|
||||
return framework["Cases"]
|
||||
}
|
||||
|
||||
if (framework["cases"] !== undefined && framework["cases"].name !== undefined && framework["cases"].name !== "") {
|
||||
return framework["cases"]
|
||||
}
|
||||
|
||||
return {
|
||||
name: "CASES :default",
|
||||
large_image: parsedDatatypeImages()["CASES"],
|
||||
@@ -115,10 +134,14 @@ export const findSpecificApp = (framework, inputcategory) => {
|
||||
id: "",
|
||||
}
|
||||
} else if (category === "iam") {
|
||||
if (framework["IAM"] !== undefined && framework["IAM"].name !== undefined) {
|
||||
if (framework["IAM"] !== undefined && framework["IAM"].name !== undefined && framework["IAM"].name !== "") {
|
||||
return framework["IAM"]
|
||||
}
|
||||
|
||||
if (framework["iam"] !== undefined && framework["iam"].name !== undefined && framework["iam"].name !== "") {
|
||||
return framework["iam"]
|
||||
}
|
||||
|
||||
return {
|
||||
name: "IAM :default",
|
||||
large_image: parsedDatatypeImages()["IAM"],
|
||||
@@ -127,10 +150,14 @@ export const findSpecificApp = (framework, inputcategory) => {
|
||||
id: "",
|
||||
}
|
||||
} else if (category === "network") {
|
||||
if (framework["Network"] !== undefined && framework["Network"].name !== undefined) {
|
||||
if (framework["Network"] !== undefined && framework["Network"].name !== undefined && framework["Network"].name !== "") {
|
||||
return framework["Network"]
|
||||
}
|
||||
|
||||
if (framework["network"] !== undefined && framework["network"].name !== undefined && framework["network"].name !== "") {
|
||||
return framework["network"]
|
||||
}
|
||||
|
||||
return {
|
||||
name: "Network :default",
|
||||
large_image: parsedDatatypeImages()["NETWORK"],
|
||||
@@ -139,10 +166,14 @@ export const findSpecificApp = (framework, inputcategory) => {
|
||||
id: "",
|
||||
}
|
||||
} else if (category === "intel") {
|
||||
if (framework["Intel"] !== undefined && framework["Intel"].name !== undefined) {
|
||||
if (framework["Intel"] !== undefined && framework["Intel"].name !== undefined && framework["Intel"].name !== "") {
|
||||
return framework["Intel"]
|
||||
}
|
||||
|
||||
if (framework["intel"] !== undefined && framework["intel"].name !== undefined && framework["intel"].name !== "") {
|
||||
return framework["intel"]
|
||||
}
|
||||
|
||||
return {
|
||||
name: "INTEL :default",
|
||||
large_image: parsedDatatypeImages()["INTEL"],
|
||||
@@ -151,9 +182,13 @@ export const findSpecificApp = (framework, inputcategory) => {
|
||||
id: "",
|
||||
}
|
||||
} else if (category === "siem") {
|
||||
if (framework["SIEM"] !== undefined && framework["SIEM"].name !== undefined) {
|
||||
if (framework["SIEM"] !== undefined && framework["SIEM"].name !== undefined && framework["SIEM"].name !== "") {
|
||||
return framework["SIEM"]
|
||||
}
|
||||
|
||||
if (framework["siem"] !== undefined && framework["siem"].name !== undefined && framework["siem"].name !== "") {
|
||||
return framework["siem"]
|
||||
}
|
||||
|
||||
return {
|
||||
name: "SIEM :default",
|
||||
@@ -1094,7 +1129,7 @@ const AppFramework = (props) => {
|
||||
}, [newSelectedApp])
|
||||
|
||||
|
||||
const isCloud = window.location.host === "localhost:3002" || window.location.host === "shuffler.io";
|
||||
const isCloud = (window.location.host === "localhost:3002" || window.location.host === "shuffler.io") ? true : (process.env.IS_SSR === "true");
|
||||
const imgSize = 50;
|
||||
var parsedFrameworkData = frameworkData === undefined ? {} : frameworkData
|
||||
|
||||
@@ -1937,14 +1972,14 @@ const AppFramework = (props) => {
|
||||
{data.name}
|
||||
</Typography>
|
||||
<div style={{display: "flex", width: 200, margin: "auto", marginTop: 15, }}>
|
||||
<div style={{backgroundColor: theme.palette.inputColor, height: 75, width: 75, borderRadius: theme.palette.borderRadius, border: "1px solid rgba(255,255,255,0.7)", marginRight: 15, position: "relative",}}>
|
||||
<div style={{backgroundColor: theme.palette.inputColor, height: 75, width: 75, borderRadius: theme.palette?.borderRadius, border: "1px solid rgba(255,255,255,0.7)", marginRight: 15, position: "relative",}}>
|
||||
{parsedLeftImage}
|
||||
{parsedLeftText}
|
||||
</div>
|
||||
<div style={{backgroundColor: theme.palette.inputColor, maxHeight: 30, maxWidth: 30, height: 30, width: 30, borderRadius: theme.palette.borderRadius, border: "1px solid rgba(255,255,255,0.7)", marginTop: 22, padding: "10px 0px 0px 9px",}}>
|
||||
<div style={{backgroundColor: theme.palette.inputColor, maxHeight: 30, maxWidth: 30, height: 30, width: 30, borderRadius: theme.palette?.borderRadius, border: "1px solid rgba(255,255,255,0.7)", marginTop: 22, padding: "10px 0px 0px 9px",}}>
|
||||
{svgIcon}
|
||||
</div>
|
||||
<div style={{backgroundColor: theme.palette.inputColor, height: 75, width: 75, borderRadius: theme.palette.borderRadius, border: "1px solid rgba(255,255,255,0.7)", marginLeft: 15, position: "relative",}}>
|
||||
<div style={{backgroundColor: theme.palette.inputColor, height: 75, width: 75, borderRadius: theme.palette?.borderRadius, border: "1px solid rgba(255,255,255,0.7)", marginLeft: 15, position: "relative",}}>
|
||||
{parsedRightImage}
|
||||
{parsedRightText}
|
||||
</div>
|
||||
@@ -2152,7 +2187,7 @@ const AppFramework = (props) => {
|
||||
|
||||
{
|
||||
Object.getOwnPropertyNames(discoveryData).length > 0 ?
|
||||
<Paper style={{width: 300, maxHeight: 400, overflow: "hidden", zIndex: 12500, padding: 25, paddingRight: 25, backgroundColor: theme.palette.surfaceColor, border: "1px solid rgba(255,255,255,0.2)", position: "absolute", top: -50, left: 50, }}>
|
||||
<Paper style={{width: 300, maxHeight: 400, overflow: "hidden", zIndex: 12500, padding: 25, paddingRight: 25, backgroundColor: theme.palette.surfaceColor, border: "1px solid rgba(255,255,255,0.2)", position: "absolute", top: -50, left: isMobile?-50: 50, }}>
|
||||
{paperTitle.length > 0 ?
|
||||
<span>
|
||||
<Typography variant="h6" style={{textAlign: "center"}}>
|
||||
@@ -2321,7 +2356,7 @@ const AppFramework = (props) => {
|
||||
elements={elements}
|
||||
minZoom={0.35}
|
||||
maxZoom={2.00}
|
||||
style={{width: 560*scale, height: 560*scale, backgroundColor: theme.palette.backgroundColor, margin: "auto",}}
|
||||
style={{width: isMobile?null:560*scale, height: 560*scale, backgroundColor: theme.palette.backgroundColor, margin: isMobile?null:"auto",}}
|
||||
stylesheet={frameworkStyle}
|
||||
boxSelectionEnabled={false}
|
||||
panningEnabled={false}
|
||||
|
||||
@@ -79,6 +79,7 @@ const AppGrid = (props) => {
|
||||
const [formMail, setFormMail] = React.useState("");
|
||||
const [message, setMessage] = React.useState("");
|
||||
const [formMessage, setFormMessage] = React.useState("");
|
||||
const [deactivatedIndexes, setDeactivatedIndexes] = React.useState([]);
|
||||
|
||||
const buttonStyle = {
|
||||
borderRadius: 30,
|
||||
@@ -232,6 +233,11 @@ const AppGrid = (props) => {
|
||||
removeQuery("q");
|
||||
refine(event.currentTarget.value);
|
||||
}}
|
||||
onKeyDown={(event) => {
|
||||
if(event.key === "Enter") {
|
||||
event.preventDefault();
|
||||
}
|
||||
}}
|
||||
limit={5}
|
||||
/>
|
||||
{/*isSearchStalled ? 'My search is stalled' : ''*/}
|
||||
@@ -293,7 +299,7 @@ const AppGrid = (props) => {
|
||||
|
||||
useEffect(() => {
|
||||
var baseurl = globalUrl;
|
||||
fetch(baseurl + "/api/v1/getinfo", {
|
||||
fetch(baseurl + "/api/v1/me", {
|
||||
credentials: "include",
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
@@ -347,7 +353,7 @@ const AppGrid = (props) => {
|
||||
if (responseJson.success === false) {
|
||||
toast.error(responseJson.reason);
|
||||
} else {
|
||||
toast.success(`App ${type}d Successfully!`);
|
||||
//toast.success(`App ${type}d Successfully!`);
|
||||
if (type === 'activate') {
|
||||
setAllActivatedAppIds(prev => [...prev, data.objectID]);
|
||||
setIsAnyAppActivated(true);
|
||||
@@ -390,7 +396,7 @@ const AppGrid = (props) => {
|
||||
{!isLoading ? (
|
||||
<div>
|
||||
{hits.length === 0 && searchQuery.length >= 0 && showNoAppFound ? (
|
||||
<Typography variant="body1" style={{ marginTop: '30%' }}>No App Found</Typography>
|
||||
<Typography variant="body1" style={{ marginTop: '30%' }}>No Apps Found</Typography>
|
||||
) : (
|
||||
<Grid item spacing={2} justifyContent="flex-start">
|
||||
<div
|
||||
@@ -458,8 +464,17 @@ const AppGrid = (props) => {
|
||||
}}
|
||||
>
|
||||
<img
|
||||
id={`image_${index}`}
|
||||
alt={data.name}
|
||||
src={data.image_url ? data.image_url : "/images/no_image.png"}
|
||||
onError={(e) => {
|
||||
// Replace the image with the default image
|
||||
const foundImage = document.getElementById(`image_${index}`)
|
||||
if (foundImage !== undefined && foundImage !== null) {
|
||||
foundImage.src = theme.palette.defaultImage
|
||||
data.image_url = theme.palette.defaultImage
|
||||
}
|
||||
}}
|
||||
style={{
|
||||
width: 80,
|
||||
height: 80,
|
||||
@@ -989,11 +1004,16 @@ const AppGrid = (props) => {
|
||||
}}
|
||||
autoComplete="off"
|
||||
color="primary"
|
||||
placeholder="Search your Activated or Self-built apps"
|
||||
placeholder="Search your Activated or self-built apps"
|
||||
id="shuffle_search_field"
|
||||
onChange={(event) => {
|
||||
setSearchQuery(event.currentTarget.value);
|
||||
}}
|
||||
onKeyDown={(event) => {
|
||||
if(event.key === "Enter") {
|
||||
event.preventDefault();
|
||||
}
|
||||
}}
|
||||
limit={5}
|
||||
/>
|
||||
{/*isSearchStalled ? 'My search is stalled' : ''*/}
|
||||
@@ -1541,9 +1561,13 @@ const AppGrid = (props) => {
|
||||
<Typography variant="h5" style={{ marginBottom: 30, marginTop: 30, fontWeight: "400", fontSize: 24 }}>
|
||||
Filter By
|
||||
</Typography>
|
||||
|
||||
<FilterUsersAndOrgsAppByCategory selectedCategoryForUsersAndOgsApps={selectedCategoryForUsersAndOgsApps} setselectedCategoryForUsersAndOgsApps={setselectedCategoryForUsersAndOgsApps} />
|
||||
|
||||
<FilterUsersAndOrgsAppByActionLabel selectedTagsForUserAndOrgApps={selectedTagsForUserAndOrgApps} setSelectedTagsForUserAndOrgApps={setSelectedTagsForUserAndOrgApps} />
|
||||
|
||||
<FilterUsersAndOrgsAppByCreatedWith selectedOptionOfCreatedWith={selectedOptionOfCreatedWith} setSelectedOptionOfCreatedWith={setSelectedOptionOfCreatedWith} />
|
||||
|
||||
<FilterUsersAndOrgsAppCreatedBy />
|
||||
</div>
|
||||
)}
|
||||
@@ -1712,6 +1736,10 @@ const AppGrid = (props) => {
|
||||
? `/apps/${data.id}`
|
||||
: `https://shuffler.io/apps/${data.id}`;
|
||||
|
||||
if (data.name === "" && data.id === "") {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<Zoom
|
||||
key={index}
|
||||
@@ -1808,17 +1836,71 @@ const AppGrid = (props) => {
|
||||
width: 230,
|
||||
textAlign: 'start',
|
||||
marginLeft: 8,
|
||||
color: "rgba(158, 158, 158, 1)"
|
||||
color: "rgba(158, 158, 158, 1)",
|
||||
display: "flex",
|
||||
}}
|
||||
>
|
||||
{data.tags &&
|
||||
data.tags.map((tag, tagIndex) => (
|
||||
<span key={tagIndex}>
|
||||
{normalizedString(tag)}
|
||||
{tagIndex < data.tags.length - 1 ? ", " : ""}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
<div style={{minWidth: 120, overflow: "hidden", }}>
|
||||
{data.generated !== true ?
|
||||
<div>
|
||||
{data.tags &&
|
||||
data.tags.slice(0,2).map((tag, tagIndex) => (
|
||||
<span key={tagIndex}>
|
||||
{normalizedString(tag)}
|
||||
{tagIndex < data.tags.length - 1 ? ", " : ""}
|
||||
</span>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
: null}
|
||||
</div>
|
||||
{currTab === 1 && !deactivatedIndexes.includes(index) && mouseHoverIndex === index && data.generated === true ?
|
||||
<Button style={{
|
||||
marginLeft: 15,
|
||||
width: 102,
|
||||
height: 35,
|
||||
borderRadius: 200,
|
||||
backgroundColor: "rgba(73, 73, 73, 1)",
|
||||
color: "rgba(241, 241, 241, 1)",
|
||||
textTransform: "none",
|
||||
}}
|
||||
onClick={(event) => {
|
||||
//deactivatedIndexes.push(index)
|
||||
//setDeactivatedIndexes(deactivatedIndexes)
|
||||
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
//handleActivateButton(event, data, "deactivate");
|
||||
// FIXME: Put this in a function lol
|
||||
const url = `${globalUrl}/api/v1/apps/${data.id}/deactivate`;
|
||||
|
||||
fetch(url, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
},
|
||||
credentials: "include",
|
||||
})
|
||||
.then((response) => response.json())
|
||||
.then((responseJson) => {
|
||||
if (responseJson.success === false) {
|
||||
toast.error(responseJson.reason);
|
||||
} else {
|
||||
toast.success("App Deactivated Successfully. Reload UI to see updated changes.")
|
||||
//const updatedIds = allActivatedAppIds.filter(id => id !== data.objectID);
|
||||
//setAllActivatedAppIds(updatedIds);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.log("app error: ", error.toString());
|
||||
})
|
||||
}}>
|
||||
Deactivate
|
||||
</Button>
|
||||
: null}
|
||||
</div>
|
||||
|
||||
{/* )} */}
|
||||
</div>
|
||||
</ButtonBase>
|
||||
@@ -1936,6 +2018,7 @@ const AppGrid = (props) => {
|
||||
selectedOptionOfCreatedWith={selectedOptionOfCreatedWith}
|
||||
/>
|
||||
)}
|
||||
|
||||
<AppTab
|
||||
selectedCategoryForUsersAndOgsApps={selectedCategoryForUsersAndOgsApps}
|
||||
selectedTagsForUserAndOrgApps={selectedTagsForUserAndOrgApps}
|
||||
@@ -1944,6 +2027,7 @@ const AppGrid = (props) => {
|
||||
setSelectedTagsForUserAndOrgApps={setSelectedTagsForUserAndOrgApps}
|
||||
setSelectedOptionOfCreatedWith={setSelectedOptionOfCreatedWith}
|
||||
/>
|
||||
|
||||
</div>
|
||||
<Configure clickAnalytics />
|
||||
</InstantSearch>
|
||||
|
||||
@@ -0,0 +1,759 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useNavigate } from 'react-router';
|
||||
|
||||
import {
|
||||
Dialog,
|
||||
DialogTitle,
|
||||
DialogContent,
|
||||
IconButton,
|
||||
Typography,
|
||||
Box,
|
||||
Button,
|
||||
Stack,
|
||||
Avatar,
|
||||
} from '@mui/material';
|
||||
|
||||
import CloseIcon from '@mui/icons-material/Close';
|
||||
import EditIcon from '@mui/icons-material/Edit';
|
||||
import Search from '@mui/icons-material/Search';
|
||||
import AddIcon from '@mui/icons-material/Add';
|
||||
import ForkRightIcon from '@mui/icons-material/ForkRight';
|
||||
import OpenInNewIcon from '@mui/icons-material/OpenInNew';
|
||||
import LaunchIcon from '@mui/icons-material/Launch';
|
||||
import CheckCircleIcon from '@mui/icons-material/CheckCircle';
|
||||
import { CloudDownloadOutlined } from '@mui/icons-material';
|
||||
import { findSpecificApp } from './AppFramework';
|
||||
import theme from "../theme";
|
||||
import YAML from 'yaml';
|
||||
import { toast } from 'react-toastify';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
const AppModal = ({ open, onClose, app, globalUrl }) => {
|
||||
|
||||
const [frameworkData, setFrameworkData] = useState({})
|
||||
const [userdata, setUserdata] = useState({})
|
||||
const [usecases, setUsecases] = useState([])
|
||||
const [workflows, setWorkflows] = useState([])
|
||||
const [prevSubcase, setPrevSubcase] = useState({})
|
||||
const [inputUsecase, setInputUsecase] = useState({})
|
||||
const [latestUsecase, setLatestUsecase] = useState([])
|
||||
const [foundAppUsecase, setFoundAppUsecase] = useState({})
|
||||
const navigate = useNavigate();
|
||||
const parseUsecase = (subcase) => {
|
||||
const srcdata = findSpecificApp(frameworkData, subcase.type)
|
||||
const dstdata = findSpecificApp(frameworkData, subcase.last)
|
||||
|
||||
if (srcdata !== undefined && srcdata !== null) {
|
||||
subcase.srcimg = srcdata.large_image
|
||||
subcase.srcapp = srcdata.name
|
||||
}
|
||||
|
||||
if (dstdata !== undefined && dstdata !== null) {
|
||||
subcase.dstimg = dstdata.large_image
|
||||
subcase.dstapp = dstdata.name
|
||||
}
|
||||
return subcase
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
var baseurl = globalUrl;
|
||||
fetch(baseurl + "/api/v1/me", {
|
||||
credentials: "include",
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(responseJson => {
|
||||
if (responseJson.success) {
|
||||
setUserdata(responseJson)
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.log("Failed login check: ", error);
|
||||
});
|
||||
}, [app]);
|
||||
|
||||
|
||||
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) {
|
||||
const preparedData = {
|
||||
"siem": findSpecificApp({}, "SIEM"),
|
||||
"communication": findSpecificApp({}, "COMMUNICATION"),
|
||||
"assets": findSpecificApp({}, "ASSETS"),
|
||||
"cases": findSpecificApp({}, "CASES"),
|
||||
"network": findSpecificApp({}, "NETWORK"),
|
||||
"intel": findSpecificApp({}, "INTEL"),
|
||||
"edr": findSpecificApp({}, "EDR"),
|
||||
"iam": findSpecificApp({}, "IAM"),
|
||||
"email": findSpecificApp({}, "EMAIL"),
|
||||
}
|
||||
|
||||
setFrameworkData(preparedData)
|
||||
} else {
|
||||
setFrameworkData(responseJson)
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log("Error getting framework: ", error)
|
||||
})
|
||||
}
|
||||
|
||||
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) => {
|
||||
|
||||
|
||||
const newUsecases = [...usecases]
|
||||
newUsecases.forEach((category, index) => {
|
||||
category.list.forEach((subcase, subindex) => {
|
||||
getUsecase(subcase, index, subindex)
|
||||
})
|
||||
})
|
||||
|
||||
setLatestUsecase(newUsecases)
|
||||
// Matching workflows with usecases
|
||||
if (responseJson.success !== false) {
|
||||
if (workflows !== undefined && workflows !== null && workflows.length > 0) {
|
||||
var categorydata = responseJson
|
||||
|
||||
var 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()) {
|
||||
|
||||
category.matches.push({
|
||||
"workflow": workflow.id,
|
||||
"category": subcategory?.name,
|
||||
})
|
||||
|
||||
subcategory.matches.push(workflow)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (subcategory.matches.length > 0) {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
newcategories.push(category)
|
||||
}
|
||||
|
||||
if (newcategories !== undefined && newcategories !== null && newcategories.length > 0) {
|
||||
setUsecases(newcategories)
|
||||
} else {
|
||||
setUsecases(responseJson)
|
||||
}
|
||||
} else {
|
||||
setUsecases(responseJson)
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
//toast("ERROR: " + error.toString());
|
||||
console.log("ERROR: " + error.toString());
|
||||
});
|
||||
};
|
||||
|
||||
const getAvailableWorkflows = () => {
|
||||
fetch(globalUrl + "/api/v1/workflows", {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Accept: "application/json",
|
||||
},
|
||||
credentials: "include",
|
||||
})
|
||||
.then((response) => {
|
||||
if (response.status !== 200) {
|
||||
fetchUsecases()
|
||||
console.log("Status not 200 for workflows :O!: ", response.status);
|
||||
return;
|
||||
}
|
||||
|
||||
return response.json();
|
||||
})
|
||||
.then((responseJson) => {
|
||||
fetchUsecases(responseJson)
|
||||
|
||||
if (responseJson !== undefined) {
|
||||
setWorkflows(responseJson);
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
fetchUsecases()
|
||||
//toast(error.toString());
|
||||
});
|
||||
}
|
||||
|
||||
const getUsecase = (subcase, index, subindex) => {
|
||||
subcase = parseUsecase(subcase)
|
||||
setPrevSubcase(subcase)
|
||||
|
||||
fetch(`${globalUrl}/api/v1/workflows/usecases/${escape(subcase?.name?.replaceAll(" ", "_"))}`, {
|
||||
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) => {
|
||||
var parsedUsecase = responseJson
|
||||
|
||||
if (responseJson.success === false) {
|
||||
parsedUsecase = subcase
|
||||
} else {
|
||||
parsedUsecase = responseJson
|
||||
|
||||
parsedUsecase.srcimg = subcase.srcimg
|
||||
parsedUsecase.srcapp = subcase.srcapp
|
||||
parsedUsecase.dstimg = subcase.dstimg
|
||||
parsedUsecase.dstapp = subcase.dstapp
|
||||
}
|
||||
// Look for the type of app and fill in img1, srcapp...
|
||||
setInputUsecase(parsedUsecase)
|
||||
})
|
||||
.catch((error) => {
|
||||
//toast(error.toString());
|
||||
setInputUsecase(subcase)
|
||||
console.log("Error getting usecase: ", error)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
getAvailableWorkflows()
|
||||
getFramework()
|
||||
}, [app])
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
const foundCategory = latestUsecase?.find((category) =>
|
||||
category?.list?.some((subcase) => subcase?.srcapp === app?.name || subcase?.dstapp === app?.name)
|
||||
);
|
||||
|
||||
const foundSubcase = foundCategory?.list?.find(
|
||||
(subcase) => subcase?.srcapp === app?.name || subcase?.dstapp === app?.name
|
||||
);
|
||||
|
||||
setFoundAppUsecase(foundSubcase);
|
||||
}, [latestUsecase])
|
||||
|
||||
const downloadApp = (inputdata) => {
|
||||
const id = inputdata.id;
|
||||
|
||||
toast("Downloading..");
|
||||
fetch(globalUrl + "/api/v1/apps/" + id + "/config", {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Accept: "application/json",
|
||||
},
|
||||
credentials: "include",
|
||||
})
|
||||
.then((response) => {
|
||||
if (response.status !== 200) {
|
||||
window.location.pathname = "/apps";
|
||||
}
|
||||
|
||||
return response.json();
|
||||
})
|
||||
.then((responseJson) => {
|
||||
if (!responseJson.success) {
|
||||
toast("Failed to download file");
|
||||
} else {
|
||||
console.log(responseJson);
|
||||
const basedata = atob(responseJson.openapi);
|
||||
console.log("BASE: ", basedata);
|
||||
var inputdata = JSON.parse(basedata);
|
||||
console.log("POST INPUT: ", inputdata);
|
||||
inputdata = JSON.parse(inputdata.body);
|
||||
|
||||
const newpaths = {};
|
||||
if (inputdata["paths"] !== undefined) {
|
||||
Object.keys(inputdata["paths"]).forEach(function (key) {
|
||||
newpaths[key.split("?")[0]] = inputdata.paths[key];
|
||||
});
|
||||
}
|
||||
|
||||
inputdata.paths = newpaths;
|
||||
console.log("INPUT: ", inputdata);
|
||||
var name = inputdata.info.title;
|
||||
name = name.replace(/ /g, "_", -1);
|
||||
name = name.toLowerCase();
|
||||
|
||||
delete inputdata.id;
|
||||
delete inputdata.editing;
|
||||
|
||||
const data = YAML.stringify(inputdata);
|
||||
var blob = new Blob([data], {
|
||||
type: "application/octet-stream",
|
||||
});
|
||||
|
||||
var url = URL.createObjectURL(blob);
|
||||
var link = document.createElement("a");
|
||||
link.setAttribute("href", url);
|
||||
link.setAttribute("download", `${name}.yaml`);
|
||||
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);
|
||||
//link.parentNode.removeChild(link)
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log(error);
|
||||
toast(error.toString());
|
||||
});
|
||||
};
|
||||
|
||||
const isCloud =
|
||||
window.location.host === "localhost:3002" ||
|
||||
window.location.host === "shuffler.io" || window.location.host === "localhost:3000"
|
||||
? true
|
||||
: false;
|
||||
|
||||
var newAppname = app?.name;
|
||||
if (newAppname === undefined) {
|
||||
newAppname = "Undefined";
|
||||
} else {
|
||||
newAppname = newAppname.charAt(0).toUpperCase() + newAppname.substring(1);
|
||||
newAppname = newAppname?.replaceAll("_", " ");
|
||||
}
|
||||
|
||||
var canEditApp = userdata.admin === "true" || userdata?.id === app?.owner || app?.owner === "" || (userdata.admin === "true" && userdata.active_org.id === app?.reference_org) || !app?.generated
|
||||
|
||||
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open={open}
|
||||
onClose={onClose}
|
||||
maxWidth="sm"
|
||||
fullWidth
|
||||
PaperProps={{
|
||||
sx: {
|
||||
borderRadius: 2,
|
||||
border: "1px solid #494949",
|
||||
minWidth: '440px',
|
||||
fontFamily: theme?.typography?.fontFamily,
|
||||
backgroundColor: "#212121",
|
||||
'& .MuiDialogContent-root': {
|
||||
backgroundColor: "#212121",
|
||||
},
|
||||
'& .MuiDialogTitle-root': {
|
||||
backgroundColor: "#212121",
|
||||
},
|
||||
'& .MuiTypography-root': {
|
||||
fontFamily: theme?.typography?.fontFamily,
|
||||
},
|
||||
'& .MuiButton-root': {
|
||||
fontFamily: theme?.typography?.fontFamily,
|
||||
},
|
||||
}
|
||||
}}
|
||||
>
|
||||
<DialogTitle
|
||||
sx={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
pb: 2,
|
||||
pt: 2,
|
||||
pl: 3,
|
||||
pr: 2,
|
||||
fontFamily: theme?.typography?.fontFamily
|
||||
}}
|
||||
>
|
||||
<Typography component="div" sx={{ fontWeight: 500, color: "#F1F1F1", fontSize: "22px" }}>
|
||||
About {app?.name.replace(/_/g, ' ').replace(/\b\w/g, char => char.toUpperCase())}
|
||||
</Typography>
|
||||
<IconButton
|
||||
onClick={onClose}
|
||||
sx={{
|
||||
color: 'rgba(255, 255, 255, 0.7)',
|
||||
'&:hover': { bgcolor: 'rgba(255, 255, 255, 0.1)' }
|
||||
}}
|
||||
>
|
||||
<CloseIcon />
|
||||
</IconButton>
|
||||
</DialogTitle>
|
||||
|
||||
<DialogContent sx={{ py: 3, px: 3 }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'space-between', justifyContent: 'space-between' }}>
|
||||
|
||||
<div style={{ display: "flex", flexDirection: "row", gap: 10, fontFamily: theme?.typography?.fontFamily }}>
|
||||
<img
|
||||
alt={app?.name}
|
||||
src={app?.large_image || app?.image_url}
|
||||
style={{
|
||||
borderRadius: 4,
|
||||
maxWidth: 100,
|
||||
minWidth: 100,
|
||||
maxHeight: "100%",
|
||||
display: "block",
|
||||
margin: "0 auto",
|
||||
boxShadow: "0px 0px 10px 0px rgba(0, 0, 0, 0.2)"
|
||||
}}
|
||||
/>
|
||||
<div style={{ display: "flex", flexDirection: "column", justifyContent: "center", paddingLeft: 8 }}>
|
||||
<div style={{
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
}}>
|
||||
<Typography variant="h5" component="div" sx={{ fontWeight: 600 }}>
|
||||
{newAppname}
|
||||
</Typography>
|
||||
{
|
||||
isCloud && (
|
||||
<Link
|
||||
to={"/apps/" + (app?.id || app?.objectID)}
|
||||
style={{ textDecoration: "none", color: "#f85a3e", marginTop: "-2px" }}
|
||||
>
|
||||
<IconButton
|
||||
style={{
|
||||
color: "#f85a3e",
|
||||
fontSize: 20,
|
||||
}}
|
||||
>
|
||||
<OpenInNewIcon />
|
||||
</IconButton>
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
<Typography
|
||||
variant="body2"
|
||||
color="textSecondary"
|
||||
>
|
||||
{app?.categories ? app.categories.join(", ") : "Communication"}
|
||||
</Typography>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ display: "flex", flexDirection: "row", justifyContent: "center", alignItems: "center", gap: 10 }}>
|
||||
|
||||
{app?.activated &&
|
||||
app?.private_id !== undefined &&
|
||||
app?.private_id?.length > 0 &&
|
||||
app?.generated ? (
|
||||
<Button
|
||||
variant="contained"
|
||||
sx={{
|
||||
bgcolor: '#494949',
|
||||
'&:hover': { bgcolor: '#494949' },
|
||||
textTransform: 'none',
|
||||
borderRadius: 1,
|
||||
minWidth: '45px',
|
||||
width: '45px',
|
||||
height: '40px',
|
||||
padding: 2,
|
||||
color: "#fff",
|
||||
fontFamily: theme?.typography?.fontFamily
|
||||
}}
|
||||
onClick={(event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
downloadApp(app);
|
||||
}}
|
||||
>
|
||||
<CloudDownloadOutlined />
|
||||
</Button>) : null}
|
||||
<Button
|
||||
variant="contained"
|
||||
sx={{
|
||||
bgcolor: "#494949",
|
||||
'&:hover': { bgcolor: '#494949' },
|
||||
textTransform: 'none',
|
||||
borderRadius: 1,
|
||||
py: 1,
|
||||
px: 3,
|
||||
height: '40px',
|
||||
color: "#fff",
|
||||
fontFamily: theme?.typography?.fontFamily
|
||||
}}
|
||||
startIcon={canEditApp ? <EditIcon /> :
|
||||
(app?.generated && app?.activated && userdata?.id !== app?.owner && isCloud ?
|
||||
<ForkRightIcon /> : null
|
||||
)}
|
||||
onClick={() => {
|
||||
if (canEditApp) {
|
||||
const editUrl = "/apps/edit/" + (app?.id || app?.objectID);
|
||||
navigate(editUrl)
|
||||
}else{
|
||||
const forkUrl = "/apps/new?id=" + (app?.id || app?.objectID);
|
||||
navigate(forkUrl)
|
||||
}
|
||||
}}
|
||||
>
|
||||
{canEditApp ? "Edit" : "Fork"}
|
||||
</Button>
|
||||
</div>
|
||||
</Box>
|
||||
|
||||
<div style={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
fontFamily: theme?.typography?.fontFamily,
|
||||
padding: "26px 0px"
|
||||
}}>
|
||||
<div style={{
|
||||
textAlign: "start",
|
||||
flex: 1,
|
||||
}}>
|
||||
<Typography
|
||||
variant="h6"
|
||||
sx={{
|
||||
fontFamily: theme?.typography?.fontFamily,
|
||||
fontSize: '24px',
|
||||
fontWeight: 600,
|
||||
mb: 0.3,
|
||||
color: '#fff'
|
||||
}}
|
||||
>
|
||||
20
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="body2"
|
||||
sx={{
|
||||
color: 'rgba(255, 255, 255, 0.7)',
|
||||
fontFamily: theme?.typography?.fontFamily,
|
||||
fontSize: '14px'
|
||||
}}
|
||||
>
|
||||
Public Workflow
|
||||
</Typography>
|
||||
</div>
|
||||
<div style={{
|
||||
flex: 1,
|
||||
textAlign: "start",
|
||||
borderLeft: "1px solid rgba(255, 255, 255, 0.12)",
|
||||
paddingLeft: "10px",
|
||||
height: "100%",
|
||||
}}>
|
||||
<Typography variant="h6"
|
||||
sx={{
|
||||
fontWeight: 600,
|
||||
mb: 0.3,
|
||||
color: '#fff'
|
||||
}}>
|
||||
{Array.isArray(app?.actions) ? app.actions.length : app?.actions}
|
||||
</Typography>
|
||||
<Typography variant="body2" sx={{ color: 'rgba(255, 255, 255, 0.7)' }}>
|
||||
Actions
|
||||
</Typography>
|
||||
</div>
|
||||
<div style={{
|
||||
borderLeft: "1px solid rgba(255, 255, 255, 0.12)",
|
||||
flex: 1,
|
||||
paddingLeft: "10px",
|
||||
paddingTop: "5px"
|
||||
}}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '4px', marginBottom: "5px", fontFamily: theme?.typography?.fontFamily, fontSize: "14px", fontWeight: 600, color: 'white' }}>
|
||||
{
|
||||
app?.collection ? (
|
||||
<>
|
||||
<CheckCircleIcon sx={{ color: '#4CAF50' }} />
|
||||
<Typography variant="body1" sx={{
|
||||
fontWeight: 500,
|
||||
color: '#fff',
|
||||
marginTop: "1px",
|
||||
fontFamily: theme?.typography?.fontFamily,
|
||||
fontSize: "16px"
|
||||
}}>
|
||||
app.collection
|
||||
|
||||
</Typography>
|
||||
</>
|
||||
) : (
|
||||
<Typography sx={{
|
||||
fontSize: "16px",
|
||||
fontWeight: 500,
|
||||
marginTop: "1px",
|
||||
color: 'rgba(255, 255, 255, 0.7)',
|
||||
fontFamily: theme?.typography?.fontFamily
|
||||
}}>
|
||||
No collection yet
|
||||
</Typography>
|
||||
)
|
||||
}
|
||||
|
||||
</div>
|
||||
<Typography variant="body2" sx={{ color: 'rgba(158, 158, 158, 1)' }}>
|
||||
Part of a collection
|
||||
</Typography>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
justifyContent: "start",
|
||||
width: "100%"
|
||||
}}>
|
||||
<div style={{
|
||||
fontFamily: theme?.typography?.fontFamily,
|
||||
fontSize: "16px",
|
||||
color: "#fff",
|
||||
marginBottom: "16px",
|
||||
fontWeight: 600
|
||||
}}>
|
||||
{
|
||||
(foundAppUsecase?.srcapp !== undefined && foundAppUsecase?.dstapp !== undefined) ? (
|
||||
"Connect " + foundAppUsecase?.srcapp?.replaceAll("_", " ") + " to " + foundAppUsecase?.dstapp?.replaceAll("_", " ")
|
||||
) : (
|
||||
"Connect " + app?.name + " to any tool"
|
||||
)
|
||||
}
|
||||
</div>
|
||||
|
||||
<Box sx={{
|
||||
bgcolor: '#2F2F2F',
|
||||
p: 2,
|
||||
borderRadius: 2,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
mb: 3
|
||||
}}>
|
||||
<Stack direction="row" spacing={-1}>
|
||||
{
|
||||
foundAppUsecase === undefined ? (
|
||||
<Avatar sx={{ width: 32, height: 32, bgcolor: 'background.paper', border: 1, borderColor: 'divider' }}>
|
||||
<Search sx={{ color: 'text.primary', zIndex: 10, fontSize: 18 }} />
|
||||
</Avatar>
|
||||
) : (
|
||||
<Avatar
|
||||
src={foundAppUsecase?.srcimg}
|
||||
sx={{
|
||||
width: 32,
|
||||
height: 32,
|
||||
bgcolor: 'background.paper',
|
||||
border: 1,
|
||||
borderColor: 'divider',
|
||||
zIndex: 10
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
{
|
||||
foundAppUsecase === undefined ? (
|
||||
<Avatar sx={{ width: 32, height: 32, bgcolor: 'background.paper', border: 1, borderColor: 'divider' }}>
|
||||
<AddIcon sx={{ color: 'text.primary', zIndex: 10, fontSize: 18 }} />
|
||||
</Avatar>
|
||||
) : (
|
||||
<Avatar
|
||||
src={foundAppUsecase?.dstimg}
|
||||
sx={{
|
||||
width: 32,
|
||||
height: 32,
|
||||
bgcolor: 'background.paper',
|
||||
border: 1,
|
||||
borderColor: 'divider',
|
||||
zIndex: 10
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
</Stack>
|
||||
<Typography sx={{ ml: 2, fontSize: "16px", letterSpacing: "0.5px" }}>
|
||||
{foundAppUsecase?.name || "Search for a Usecase"}
|
||||
</Typography>
|
||||
</Box>
|
||||
</div>
|
||||
|
||||
<div style={{ display: "flex", justifyContent: "center", fontFamily: theme?.typography?.fontFamily }}>
|
||||
<Button
|
||||
variant="contained"
|
||||
sx={{
|
||||
bgcolor: '#FF8544',
|
||||
'&:hover': { bgcolor: '#FF8544' },
|
||||
textTransform: 'none',
|
||||
borderRadius: 1,
|
||||
py: 1,
|
||||
px: 7,
|
||||
fontSize: "14px",
|
||||
letterSpacing: "0.5px",
|
||||
color: "black",
|
||||
fontFamily: theme?.typography?.fontFamily,
|
||||
minWidth: '200px'
|
||||
}}
|
||||
onClick={() => {
|
||||
navigate("/usecases2")
|
||||
}}
|
||||
>
|
||||
Find a Usecase
|
||||
</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default AppModal;
|
||||
@@ -2,7 +2,7 @@ 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 { isMobile } from 'react-device-detect';
|
||||
import { Search as Searchicon, CloudQueue as CloudQueueicon, Code as Codeicon, Close as Closeicon, Folder as Foldericon, LibraryBooks as LibraryBooksicon, Delete as DeleteIcon, Close as CloseIcon, } from '@mui/icons-material';
|
||||
import aa from 'search-insights'
|
||||
import Deleteicon from '@mui/icons-material/Delete';
|
||||
@@ -47,6 +47,8 @@ const AppSearchButtons = (props) => {
|
||||
const [newSelectedApp, setNewSelectedApp] = useState(undefined)
|
||||
|
||||
useEffect(() => {
|
||||
console.log("UPDATED APP: ", newSelectedApp)
|
||||
|
||||
if (newSelectedApp !== undefined && setMissing != undefined) {
|
||||
const submitAppFramework = {
|
||||
"description": newSelectedApp.description,
|
||||
@@ -136,8 +138,14 @@ const AppSearchButtons = (props) => {
|
||||
|
||||
const icon = foundApp.large_image
|
||||
var foundAppImage = AppImage
|
||||
if (foundApp.name !== undefined && foundApp.name !== null && !foundApp.name.includes(":default")) {
|
||||
foundAppImage = foundApp.large_image
|
||||
if (foundApp.name !== undefined && foundApp.name !== null && foundApp.name.length > 0 && !foundApp.name.includes(":default")) {
|
||||
|
||||
if (AppImage === undefined || AppImage === null || AppImage.length < 10) {
|
||||
foundAppImage = foundApp.large_image
|
||||
}
|
||||
} else {
|
||||
const newapp = findSpecificApp(appFramework, appType)
|
||||
// const { userdata, globalUrl, appFramework, moreButton, finishedApps, appType, totalApps, index, onNodeSelect, setDiscoveryData, appName, AppImage, setDefaultSearch, discoveryData, checkLogin, setMissing, getAppFramework, } = props
|
||||
}
|
||||
|
||||
let xsValue = 12;
|
||||
@@ -181,7 +189,7 @@ const AppSearchButtons = (props) => {
|
||||
width: 319,
|
||||
height: 395,
|
||||
flexShrink: 0,
|
||||
marginLeft: 70,
|
||||
marginLeft: isMobile? null:70,
|
||||
marginTop: 68,
|
||||
position: "absolute",
|
||||
zIndex: 100,
|
||||
@@ -204,9 +212,14 @@ const AppSearchButtons = (props) => {
|
||||
<IconButton
|
||||
style={{
|
||||
flex: 1,
|
||||
position: "absolute",
|
||||
right: 0,
|
||||
top: 10,
|
||||
height: 10,
|
||||
|
||||
// width: 224,
|
||||
marginLeft: discoveryData === ("communication") ? 112 : 200,
|
||||
width: "100%",
|
||||
//marginLeft: discoveryData === ("communication") ? 112 : 200,
|
||||
//width: "100%",
|
||||
marginBottom: 23,
|
||||
fontSize: 16,
|
||||
background: "rgba(33, 33, 33, 1)",
|
||||
@@ -220,6 +233,7 @@ const AppSearchButtons = (props) => {
|
||||
<Closeicon style={{ width: 16 }} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
{/*
|
||||
<Tooltip
|
||||
title="Delete app"
|
||||
placement="bottom"
|
||||
@@ -259,6 +273,7 @@ const AppSearchButtons = (props) => {
|
||||
<DeleteIcon style={{ height: 15, width: 15, }} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
*/}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
Paper,
|
||||
TextField,
|
||||
Collapse,
|
||||
Fade,
|
||||
IconButton,
|
||||
Avatar,
|
||||
ButtonBase,
|
||||
@@ -41,10 +42,13 @@ const AppSelection = props => {
|
||||
userdata,
|
||||
globalUrl,
|
||||
appFramework,
|
||||
setAppFramework,
|
||||
setActiveStep,
|
||||
defaultSearch,
|
||||
setDefaultSearch,
|
||||
checkLogin,
|
||||
isAppPage=false
|
||||
|
||||
} = props;
|
||||
const [discoveryData, setDiscoveryData] = React.useState({})
|
||||
const [selectionOpen, setSelectionOpen] = React.useState(false)
|
||||
@@ -57,9 +61,10 @@ const AppSelection = props => {
|
||||
const [moreButton, setMoreButton] = useState(false);
|
||||
|
||||
// const [mouseHoverIndex, setMouseHoverIndex] = useState(-1)
|
||||
document.title = "Choose your apps"
|
||||
const ref = useRef()
|
||||
let navigate = useNavigate();
|
||||
const isCloud = window.location.host === "localhost:3002" || window.location.host === "shuffler.io";
|
||||
const isCloud = (window.location.host === "localhost:3002" || window.location.host === "shuffler.io") ? true : (process.env.IS_SSR === "true");
|
||||
|
||||
useEffect(() => {
|
||||
if (newSelectedApp === undefined || newSelectedApp.objectID === undefined || newSelectedApp.objectID === undefined || newSelectedApp.objectID.length === 0) {
|
||||
@@ -98,74 +103,75 @@ const AppSelection = props => {
|
||||
else if (discoveryData === "IAM") {
|
||||
appFramework.iam = submitNewApp
|
||||
}
|
||||
setFrameworkItem(submitNewApp);
|
||||
setSelectionOpen(false);
|
||||
console.log("Selected app changed (effect)");
|
||||
|
||||
setFrameworkItem(submitNewApp)
|
||||
setSelectionOpen(false)
|
||||
|
||||
if (setAppFramework !== undefined) {
|
||||
setAppFramework(appFramework)
|
||||
}
|
||||
GetApps()
|
||||
}, [newSelectedApp]);
|
||||
|
||||
const reloadAppButtons = (framework) => {
|
||||
var tempApps = []
|
||||
const lastApps = {}
|
||||
let endTypes = ["network", "assets", "iam"]
|
||||
|
||||
if (framework === undefined || framework === null || Object.keys(framework).length === 0) {
|
||||
//window.location.href = "/welcome"
|
||||
return
|
||||
}
|
||||
|
||||
Object.entries(framework).forEach(([key, value]) => {
|
||||
// Overwrrite email properly
|
||||
if (key.toLowerCase() === "communication") {
|
||||
value["type"] = "email"
|
||||
framework["email"] = value
|
||||
return
|
||||
}
|
||||
|
||||
if (key.toLowerCase() === "other") {
|
||||
return
|
||||
}
|
||||
|
||||
value.type = key;
|
||||
if (endTypes.includes(value.type.toLowerCase())) {
|
||||
lastApps[value.type] = value
|
||||
return
|
||||
}
|
||||
|
||||
if (lastPosted.type === value.type) {
|
||||
value = lastPosted
|
||||
}
|
||||
|
||||
tempApps.push(JSON.parse(JSON.stringify(value)));
|
||||
});
|
||||
|
||||
tempApps.sort((a, b) => {
|
||||
if (a.type.length > b.type.length) {
|
||||
return -1;
|
||||
} else if (a.type.length < b.type.length) {
|
||||
return 1;
|
||||
}
|
||||
});
|
||||
|
||||
let lastType = lastPosted.type === undefined ? "" : lastPosted.type.toLowerCase()
|
||||
if (endTypes.includes(lastType)) {
|
||||
lastApps[lastPosted.type] = lastPosted
|
||||
}
|
||||
|
||||
if (moreButton) {
|
||||
tempApps.push(JSON.parse(JSON.stringify(lastApps["network"])))
|
||||
tempApps.push(JSON.parse(JSON.stringify(lastApps["assets"])))
|
||||
tempApps.push(JSON.parse(JSON.stringify(lastApps["iam"])))
|
||||
}
|
||||
|
||||
setAppButtons(tempApps)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
var tempApps = []
|
||||
if (tempApps.length === 0) {
|
||||
// Object.entries(appFramework).forEach(([key, value]) => {
|
||||
// value.type = key;
|
||||
// tempApps.push(value);
|
||||
// });
|
||||
|
||||
// // Define the custom sorting order
|
||||
// const customSortingOrder = ["CASES", "SIEM", "ENDPOINT", "INTEL", "EMAIL"];
|
||||
|
||||
const lastApps = {}
|
||||
let endTypes = ["network", "assets", "iam"]
|
||||
|
||||
if (appFramework === undefined || appFramework === null || Object.keys(appFramework).length === 0) {
|
||||
//window.location.href = "/welcome"
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
Object.entries(appFramework).forEach(([key, value]) => {
|
||||
if (key.toLowerCase() === "other" || key.toLowerCase() === "communication") {
|
||||
return
|
||||
}
|
||||
|
||||
value.type = key;
|
||||
|
||||
if (endTypes.includes(value.type.toLowerCase())) {
|
||||
lastApps[value.type] = value
|
||||
return
|
||||
}
|
||||
|
||||
if (lastPosted.type === value.type) {
|
||||
value = lastPosted
|
||||
}
|
||||
|
||||
tempApps.push(JSON.parse(JSON.stringify(value)));
|
||||
});
|
||||
|
||||
tempApps.sort((a, b) => {
|
||||
if (a.type.length > b.type.length) {
|
||||
return -1;
|
||||
} else if (a.type.length < b.type.length) {
|
||||
return 1;
|
||||
}
|
||||
});
|
||||
|
||||
let lastType = lastPosted.type === undefined ? "" : lastPosted.type.toLowerCase()
|
||||
|
||||
if (endTypes.includes(lastType)) {
|
||||
lastApps[lastPosted.type] = lastPosted
|
||||
}
|
||||
|
||||
if (moreButton) {
|
||||
tempApps.push(JSON.parse(JSON.stringify(lastApps["network"])))
|
||||
tempApps.push(JSON.parse(JSON.stringify(lastApps["assets"])))
|
||||
tempApps.push(JSON.parse(JSON.stringify(lastApps["iam"])))
|
||||
}
|
||||
|
||||
setAppButtons(tempApps)
|
||||
console.log("Updated appButtons: ", appButtons)
|
||||
GetApps()
|
||||
}
|
||||
reloadAppButtons(appFramework)
|
||||
}, [lastPosted, moreButton])
|
||||
|
||||
if (appFramework === undefined || appFramework === null || Object.keys(appFramework).length === 0) {
|
||||
@@ -174,11 +180,6 @@ const AppSelection = props => {
|
||||
}
|
||||
|
||||
const setFrameworkItem = (data) => {
|
||||
console.log("Setting framework item: ", data, isCloud)
|
||||
// if (!isCloud) {
|
||||
// activateApp(data.id)
|
||||
// }
|
||||
|
||||
fetch(globalUrl + "/api/v1/apps/frameworkConfiguration", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
@@ -241,31 +242,39 @@ const AppSelection = props => {
|
||||
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"
|
||||
}]
|
||||
.then((response) => {
|
||||
return response.json()
|
||||
})
|
||||
.then((responseJson) => {
|
||||
if (responseJson === null || responseJson === undefined) {
|
||||
console.log("null-response from server")
|
||||
const pretend_apps = [{
|
||||
"description": "TBD",
|
||||
"id": "TBD",
|
||||
"large_image": "",
|
||||
"name": "TBD",
|
||||
"type": "TBD"
|
||||
}]
|
||||
|
||||
setApps(pretend_apps)
|
||||
return
|
||||
}
|
||||
setApps(pretend_apps)
|
||||
return
|
||||
}
|
||||
|
||||
if (responseJson.success === false) {
|
||||
console.log("error loading apps: ", responseJson)
|
||||
return
|
||||
}
|
||||
if (responseJson.success === false) {
|
||||
console.log("error loading apps: ", responseJson)
|
||||
return
|
||||
}
|
||||
|
||||
setApps(responseJson);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log("App loading error: " + error.toString());
|
||||
})
|
||||
if (setAppFramework !== undefined) {
|
||||
setAppFramework(responseJson)
|
||||
}
|
||||
|
||||
setApps(responseJson)
|
||||
reloadAppButtons(responseJson)
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log("App loading error: " + error.toString());
|
||||
})
|
||||
}
|
||||
|
||||
const onNodeSelect = (label) => {
|
||||
@@ -277,8 +286,6 @@ const AppSelection = props => {
|
||||
});
|
||||
}
|
||||
|
||||
console.log("NODESELECT: ", label)
|
||||
|
||||
setDiscoveryData(label)
|
||||
setSelectionOpen(true)
|
||||
setDefaultSearch(label.charAt(0).toUpperCase() + (label.substring(1)).toLowerCase())
|
||||
@@ -304,209 +311,256 @@ const AppSelection = props => {
|
||||
};
|
||||
|
||||
return (
|
||||
<Collapse in={true}>
|
||||
<Tooltip
|
||||
title="Back"
|
||||
placement="top"
|
||||
style={{ zIndex: 10011 }}
|
||||
>
|
||||
<IconButton
|
||||
style={{
|
||||
}}
|
||||
onClick={() => {
|
||||
navigate('/welcome');
|
||||
window.location.reload();
|
||||
}}
|
||||
>
|
||||
<ArrowBackIcon style={{ width: 20 }} />
|
||||
<Typography style={{fontSize : 16, marginLeft : 2}}>Back</Typography>
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<div
|
||||
style={{
|
||||
// minHeight: sizing,
|
||||
// maxHeight: sizing,
|
||||
marginTop: 10,
|
||||
width: isMobile ? 350 : 500,
|
||||
marginBottom: 25,
|
||||
textAlign: isMobile ? "center" : null,
|
||||
}}
|
||||
>
|
||||
{selectionOpen ? (
|
||||
<div
|
||||
style={{
|
||||
width: isMobile ? 225 : 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 === ("communication") ? 112 : 200,
|
||||
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: isMobile ? null : 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>
|
||||
<Grid rowSpacing={1} columnSpacing={2} container >
|
||||
{appButtons.map((appData, index) => {
|
||||
// This is here due to a memory issue with setting apps properly
|
||||
if (appData.id === "remove") {
|
||||
console.log("Removed as appdata is overridden: ", appData)
|
||||
<Fade in={true} timeout={1250}>
|
||||
<div>
|
||||
{/*
|
||||
<Tooltip
|
||||
title="Back"
|
||||
placement="top"
|
||||
style={{ zIndex: 10011 }}
|
||||
>
|
||||
<IconButton
|
||||
style={{
|
||||
}}
|
||||
onClick={() => {
|
||||
navigate('/welcome');
|
||||
window.location.reload();
|
||||
}}
|
||||
>
|
||||
<ArrowBackIcon style={{ width: 20 }} />
|
||||
<Typography style={{fontSize : 16, marginLeft : 2}}>Back</Typography>
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
*/}
|
||||
<div
|
||||
style={{
|
||||
// minHeight: sizing,
|
||||
// maxHeight: sizing,
|
||||
marginTop: 10,
|
||||
width: isMobile ? 350 : 500,
|
||||
marginBottom: 25,
|
||||
textAlign: isMobile ? "center" : null,
|
||||
}}
|
||||
>
|
||||
{selectionOpen ? (
|
||||
<div
|
||||
style={{
|
||||
width: isMobile ? 225 : 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,
|
||||
|
||||
appData = {
|
||||
"count": 0,
|
||||
"description": "",
|
||||
"id": "",
|
||||
"large_image": "",
|
||||
"name": "",
|
||||
"type": appData.type,
|
||||
}
|
||||
}
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
right: 6,
|
||||
}}
|
||||
onClick={() => {
|
||||
setSelectionOpen(false)
|
||||
}}
|
||||
>
|
||||
<CloseIcon style={{ width: 16 }} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Tooltip
|
||||
title="Remove selection"
|
||||
placement="bottom"
|
||||
style={{ zIndex: 10011 }}
|
||||
>
|
||||
<IconButton
|
||||
style={{ zIndex: 12501, position: "absolute", top: 26, right: 6, }}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
setSelectionOpen(false)
|
||||
setDefaultSearch("")
|
||||
const submitDeletedApp = {
|
||||
"description": "",
|
||||
"id": "remove",
|
||||
"name": "",
|
||||
"type": discoveryData
|
||||
}
|
||||
|
||||
const appName = appData.name
|
||||
const AppImage = appData.large_image
|
||||
const appType = appData.type
|
||||
setFrameworkItem(submitDeletedApp)
|
||||
setNewSelectedApp({})
|
||||
setTimeout(() => {
|
||||
setDiscoveryData({})
|
||||
setFrameworkItem(submitDeletedApp)
|
||||
setNewSelectedApp({})
|
||||
}, 200)
|
||||
//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}
|
||||
{
|
||||
!isAppPage && (
|
||||
<>
|
||||
<Typography
|
||||
variant="h4"
|
||||
style={{
|
||||
marginLeft: 8,
|
||||
marginTop: isMobile ? null : 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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
{
|
||||
isAppPage && (
|
||||
<div style={{marginBottom: 20}}>
|
||||
<span
|
||||
style={{
|
||||
fontSize: 16,
|
||||
color: "rgba(158, 158, 158, 1)",
|
||||
fontFamily: theme?.typography?.fontFamily,
|
||||
}}
|
||||
>
|
||||
Your organization has no apps yet, select your starting apps here
|
||||
or discover more apps using the <span
|
||||
onClick={() => {
|
||||
navigate("/apps2?tab=all_apps")
|
||||
}}
|
||||
style={{ color: "#FF8444", fontWeight: "medium", fontSize: 16, cursor:"pointer" }}>App Library</span>
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
<Grid rowSpacing={1} columnSpacing={2} container >
|
||||
{appButtons.map((appData, index) => {
|
||||
// This is here due to a memory issue with setting apps properly
|
||||
if (appData.id === "remove") {
|
||||
appData = {
|
||||
"count": 0,
|
||||
"description": "",
|
||||
"id": "",
|
||||
"large_image": "",
|
||||
"name": "",
|
||||
"type": appData.type,
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<AppSearchButtons
|
||||
appFramework={appFramework}
|
||||
index={index}
|
||||
totalApps={appButtons.length}
|
||||
appName={appName}
|
||||
appType={appType}
|
||||
AppImage={AppImage}
|
||||
defaultSearch={defaultSearch}
|
||||
finishedApps={finishedApps}
|
||||
onNodeSelect={onNodeSelect}
|
||||
discoveryData={discoveryData}
|
||||
setDiscoveryData={setDiscoveryData}
|
||||
setDefaultSearch={setDefaultSearch}
|
||||
apps={apps}
|
||||
setMoreButton={setMoreButton}
|
||||
moreButton={moreButton}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</Grid>
|
||||
</div>
|
||||
{!moreButton ? (
|
||||
<div style={{ width: "100%", marginLeft: isMobile ? 80 : 200, marginBottom: 20, textAlign: isMobile ? "center" : null }}>
|
||||
<Link style={{ color: "#FF8444" }} onClick={() => {
|
||||
setMoreButton(true)
|
||||
|
||||
setTimeout(() => {
|
||||
navigate("/welcome?tab=2")
|
||||
}, 250)
|
||||
}}
|
||||
>See More Apps</Link>
|
||||
</div>) : ""}
|
||||
<div style={{ flexDirection: "row", width: isMobile ? 340 : null, textAlign: isMobile ? "center" : null }}>
|
||||
<Button variant="contained" type="submit" fullWidth style={bottomButtonStyle} onClick={() => {
|
||||
navigate("/welcome?tab=3")
|
||||
setActiveStep(2)
|
||||
}}>
|
||||
Continue
|
||||
</Button>
|
||||
</div>
|
||||
</Collapse>
|
||||
if (appData === undefined || appData === null || appData.name === undefined || appData.name === "") {
|
||||
appData = {
|
||||
"count": 0,
|
||||
"description": "",
|
||||
"id": "",
|
||||
"large_image": "",
|
||||
"name": "",
|
||||
"type": appData.type,
|
||||
}
|
||||
}
|
||||
|
||||
//console.log("APP: ", appData)
|
||||
|
||||
const appName = appData.name
|
||||
const AppImage = appData.large_image
|
||||
const appType = appData.type
|
||||
|
||||
return (
|
||||
<AppSearchButtons
|
||||
appFramework={appFramework}
|
||||
index={index}
|
||||
totalApps={appButtons.length}
|
||||
|
||||
appName={appName}
|
||||
appType={appType}
|
||||
AppImage={AppImage}
|
||||
|
||||
defaultSearch={defaultSearch}
|
||||
finishedApps={finishedApps}
|
||||
onNodeSelect={onNodeSelect}
|
||||
discoveryData={discoveryData}
|
||||
setDiscoveryData={setDiscoveryData}
|
||||
setDefaultSearch={setDefaultSearch}
|
||||
apps={apps}
|
||||
setMoreButton={setMoreButton}
|
||||
moreButton={moreButton}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</Grid>
|
||||
</div>
|
||||
{
|
||||
!isAppPage && (
|
||||
<>
|
||||
{!moreButton ? (
|
||||
<div style={{ width: "100%", marginLeft: isMobile ? 80 : 200, marginBottom: 20, textAlign: isMobile ? "center" : null }}>
|
||||
<Link style={{ color: "#FF8444" }} onClick={() => {
|
||||
setMoreButton(true)
|
||||
setTimeout(() => {
|
||||
navigate("/welcome?tab=2")
|
||||
}, 250)
|
||||
}}
|
||||
>See More Apps</Link>
|
||||
</div>) : ""}
|
||||
|
||||
<div style={{ flexDirection: "row", width: isMobile ? 340 : null, textAlign: isMobile ? "center" : null }}>
|
||||
<Button variant="contained" type="submit" fullWidth style={bottomButtonStyle} onClick={() => {
|
||||
navigate("/usecases2")
|
||||
setActiveStep(2)
|
||||
}}>
|
||||
See usecases
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</Fade>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -24,12 +24,9 @@ const searchClient = algoliasearch("JNSS5CFDZZ", "db08e40265e2941b9a7d8f644b6e52
|
||||
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 isCloud = (window.location.host === "localhost:3002" || window.location.host === "shuffler.io") ? true : (process.env.IS_SSR === "true");
|
||||
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("");
|
||||
|
||||
@@ -45,21 +45,21 @@ const AuthenticationItem = (props) => {
|
||||
data.fields = [
|
||||
{
|
||||
key: "url",
|
||||
value: "Secret. Replaced during app execution!",
|
||||
value: "URL Secret. Replaced during runtime",
|
||||
},
|
||||
{
|
||||
key: "client_id",
|
||||
value: "Secret. Replaced during app execution!",
|
||||
value: "ClientID Secret. Replaced during runtime.",
|
||||
},
|
||||
{
|
||||
key: "client_secret",
|
||||
value: "Secret. Replaced during app execution!",
|
||||
value: "Client Secret. Replaced during runtime.",
|
||||
},
|
||||
{
|
||||
key: "scope",
|
||||
value: "Secret. Replaced during app execution!",
|
||||
value: "Scope Secret. Replaced during runtime.",
|
||||
},
|
||||
];
|
||||
]
|
||||
}
|
||||
|
||||
const deleteAuthentication = (data) => {
|
||||
@@ -152,7 +152,7 @@ const AuthenticationItem = (props) => {
|
||||
src={data.app.large_image}
|
||||
style={{
|
||||
maxWidth: 50,
|
||||
borderRadius: theme.palette.borderRadius,
|
||||
borderRadius: theme.palette?.borderRadius,
|
||||
}}
|
||||
/>
|
||||
style={{ minWidth: 75, maxWidth: 75 }}
|
||||
|
||||
@@ -222,7 +222,7 @@ const AuthenticationData = (props) => {
|
||||
<TextField
|
||||
style={{
|
||||
backgroundColor: theme.palette.inputColor,
|
||||
borderRadius: theme.palette.borderRadius,
|
||||
borderRadius: theme.palette?.borderRadius,
|
||||
}}
|
||||
InputProps={{
|
||||
style: {
|
||||
@@ -304,7 +304,7 @@ const AuthenticationData = (props) => {
|
||||
<TextField
|
||||
style={{
|
||||
backgroundColor: theme.palette.inputColor,
|
||||
borderRadius: theme.palette.borderRadius,
|
||||
borderRadius: theme.palette?.borderRadius,
|
||||
}}
|
||||
InputProps={{
|
||||
style: {
|
||||
@@ -344,12 +344,13 @@ const AuthenticationData = (props) => {
|
||||
onClick={() => {
|
||||
setAuthenticationModalOpen(false);
|
||||
}}
|
||||
color="primary"
|
||||
color="secondary"
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
style={{ borderRadius: "0px" }}
|
||||
variant="outlined"
|
||||
onClick={() => {
|
||||
setAuthenticationOptions(authenticationOption);
|
||||
handleSubmitCheck();
|
||||
|
||||
@@ -104,14 +104,19 @@ const AuthenticationData = (props) => {
|
||||
toast("Failed to set app auth: " + responseJson.reason);
|
||||
}
|
||||
} else {
|
||||
setSubmitSuccessful(true)
|
||||
if (getAppAuthentication !== undefined) {
|
||||
getAppAuthentication(true, false);
|
||||
}
|
||||
setSubmitSuccessful(true)
|
||||
if (getAppAuthentication !== undefined) {
|
||||
|
||||
if (setAuthenticationModalOpen !== undefined) {
|
||||
setAuthenticationModalOpen(false)
|
||||
}
|
||||
if (workflow !== undefined && workflow !== null && workflow.org_id !== undefined && workflow.org_id !== null && workflow.org_id.length > 0) {
|
||||
getAppAuthentication(true, false, undefined, workflow.org_id)
|
||||
} else {
|
||||
getAppAuthentication(true, false)
|
||||
}
|
||||
}
|
||||
|
||||
if (setAuthenticationModalOpen !== undefined) {
|
||||
setAuthenticationModalOpen(false)
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
@@ -289,7 +294,7 @@ const AuthenticationData = (props) => {
|
||||
<TextField
|
||||
style={{
|
||||
backgroundColor: theme.palette.inputColor,
|
||||
borderRadius: theme.palette.borderRadius,
|
||||
borderRadius: theme.palette?.borderRadius,
|
||||
}}
|
||||
InputProps={{
|
||||
style: {
|
||||
@@ -323,10 +328,10 @@ const AuthenticationData = (props) => {
|
||||
|
||||
const authenticationButtons = <span>
|
||||
<Button
|
||||
style={{ borderRadius: theme.palette.borderRadius, marginTop: authFieldsOnly ? 20 : 0 }}
|
||||
style={{ borderRadius: theme.palette?.borderRadius, marginTop: authFieldsOnly ? 20 : 0 }}
|
||||
onClick={() => {
|
||||
setAuthenticationOptions(authenticationOption);
|
||||
handleSubmitCheck();
|
||||
setAuthenticationOptions(authenticationOption)
|
||||
handleSubmitCheck()
|
||||
}}
|
||||
variant={"contained"}
|
||||
disabled={submitSuccessful}
|
||||
@@ -432,7 +437,7 @@ const AuthenticationData = (props) => {
|
||||
<TextField
|
||||
style={{
|
||||
backgroundColor: theme.palette.inputColor,
|
||||
borderRadius: theme.palette.borderRadius,
|
||||
borderRadius: theme.palette?.borderRadius,
|
||||
}}
|
||||
InputProps={{
|
||||
style: {
|
||||
|
||||
+1871
-753
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,4 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import React, { useState, useEffect, useContext, memo, useMemo } from 'react';
|
||||
|
||||
import theme from '../theme.jsx';
|
||||
import classNames from "classnames";
|
||||
@@ -37,13 +37,14 @@ import {
|
||||
} from 'reaviz';
|
||||
|
||||
import { typecost, typecost_single, } from "../views/HandlePaymentNew.jsx";
|
||||
import { Context } from '../context/ContextApi.jsx';
|
||||
|
||||
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, backgroundColor: theme.palette.platformColor, overflow: "hidden", }}>
|
||||
<div style={{color: "white", border: "1px solid rgba(255,255,255,0.3)", borderRadius: theme.palette?.borderRadius, padding: 30, marginTop: 15, backgroundColor: theme.palette.platformColor, overflow: "hidden", }}>
|
||||
<Typography variant="h6" style={{marginBotton: 15, }}>
|
||||
{inputname}
|
||||
</Typography>
|
||||
@@ -82,8 +83,8 @@ const AppStats = (defaultprops) => {
|
||||
const [workflows, setWorkflows] = useState(inputWorkflows === undefined ? [] : inputWorkflows)
|
||||
const [resultRows, setResultRows] = useState([])
|
||||
const [resultLoading, setResultLoading] = useState(true)
|
||||
|
||||
const includedExecutions = selectedOrganization.sync_features.app_executions !== undefined ? selectedOrganization.sync_features.app_executions.limit : 0
|
||||
|
||||
const includedExecutions = selectedOrganization?.sync_features?.app_executions !== undefined ? selectedOrganization?.sync_features?.app_executions?.limit : 0
|
||||
|
||||
useEffect(() => {
|
||||
if (workflows === undefined || workflows === null || workflows.length === 0) {
|
||||
@@ -92,7 +93,6 @@ const AppStats = (defaultprops) => {
|
||||
}, [])
|
||||
|
||||
|
||||
|
||||
const getWorkflowStats = async (workflow, startTime, endTime) => {
|
||||
if (!userdata.support) {
|
||||
return workflow
|
||||
@@ -134,6 +134,9 @@ const AppStats = (defaultprops) => {
|
||||
Accept: "application/json",
|
||||
},
|
||||
credentials: "include",
|
||||
}).catch((error) => {
|
||||
console.log("Error getting workflow stats: " + error);
|
||||
return workflow
|
||||
})
|
||||
|
||||
if (response.status !== 200) {
|
||||
@@ -159,8 +162,6 @@ const AppStats = (defaultprops) => {
|
||||
|
||||
const loadWorkflowStats = (foundWorkflows, startTime, endTime) => {
|
||||
if (!userdata.support) {
|
||||
console.log("Not support")
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
@@ -485,8 +486,13 @@ const AppStats = (defaultprops) => {
|
||||
setApprunCosts(appcostRuns)
|
||||
}
|
||||
|
||||
const getStats = () => {
|
||||
fetch(`${globalUrl}/api/v1/orgs/${selectedOrganization.id}/stats`, {
|
||||
const getStats = (orgid) => {
|
||||
|
||||
if (orgid === undefined || orgid === null) {
|
||||
return
|
||||
}
|
||||
|
||||
fetch(`${globalUrl}/api/v1/orgs/${orgid}/stats`, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
@@ -516,8 +522,10 @@ const AppStats = (defaultprops) => {
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
getStats()
|
||||
}, [])
|
||||
if(selectedOrganization?.id?.length > 0) {
|
||||
getStats(selectedOrganization.id)
|
||||
}
|
||||
}, [selectedOrganization])
|
||||
|
||||
const paperStyle = {
|
||||
textAlign: "center",
|
||||
@@ -636,7 +644,7 @@ const AppStats = (defaultprops) => {
|
||||
|
||||
const data = (
|
||||
<div className="content" style={{width: "100%", margin: "auto", }}>
|
||||
<Typography variant="body1" style={{margin: "auto", marginLeft: 10, marginBottom: 20, }} color="textSecondary">
|
||||
<Typography style={{margin: "auto", marginLeft: 10, marginBottom: 20, fontSize: 16}} color="textSecondary">
|
||||
All shown statistics are gathered from <a
|
||||
href={`${globalUrl}/api/v1/orgs/${selectedOrganization.id}/stats`}
|
||||
target="_blank"
|
||||
@@ -719,6 +727,40 @@ const AppStats = (defaultprops) => {
|
||||
</div>
|
||||
: null}
|
||||
|
||||
{clickedFromOrgTab? (
|
||||
<LocalizationProvider dateAdapter={AdapterDayjs} style={{ flex: 1 }}>
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: "10px" }}>
|
||||
<div style={{ flex: 1, maxWidth: "200px", }}>
|
||||
<DateTimePicker
|
||||
sx={{
|
||||
marginTop: 1,
|
||||
marginLeft: 1,
|
||||
}}
|
||||
ampm={false}
|
||||
label="Search from"
|
||||
format="YYYY-MM-DD HH:mm:ss"
|
||||
value={startTime}
|
||||
onChange={handleStartTimeChange}
|
||||
renderInput={(params) => <TextField {...params} />}
|
||||
/>
|
||||
</div>
|
||||
<div style={{ flex: 1, maxWidth: "200px",}}>
|
||||
<DateTimePicker
|
||||
sx={{
|
||||
marginTop: 1,
|
||||
marginLeft: 1,
|
||||
}}
|
||||
ampm={false}
|
||||
label="Search until"
|
||||
format="YYYY-MM-DD HH:mm:ss"
|
||||
value={endTime}
|
||||
onChange={handleEndTimeChange}
|
||||
renderInput={(params) => <TextField {...params} />}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</LocalizationProvider>
|
||||
):(
|
||||
<LocalizationProvider dateAdapter={AdapterDayjs} style={{flex: 1, }}>
|
||||
<div style={{display: "flex", flexDirection: "column", }}>
|
||||
<DateTimePicker
|
||||
@@ -751,6 +793,8 @@ const AppStats = (defaultprops) => {
|
||||
/>
|
||||
</div>
|
||||
</LocalizationProvider>
|
||||
)}
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,17 +1,28 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import React, { useState, useEffect, useContext } from "react";
|
||||
import ReactGA from 'react-ga4';
|
||||
import theme from "../theme.jsx";
|
||||
import { ToastContainer, toast } from "react-toastify"
|
||||
|
||||
import {
|
||||
CheckCircle as CheckCircleIcon,
|
||||
} from "@mui/icons-material";
|
||||
|
||||
import {
|
||||
Paper,
|
||||
Typography,
|
||||
Divider,
|
||||
Button,
|
||||
Tooltip,
|
||||
Grid,
|
||||
Card,
|
||||
} from "@mui/material";
|
||||
|
||||
import {
|
||||
red,
|
||||
green,
|
||||
} from "../views/AngularWorkflow.jsx"
|
||||
import { Context } from "../context/ContextApi.jsx";
|
||||
|
||||
//import { useAlert
|
||||
|
||||
const Branding = (props) => {
|
||||
@@ -20,7 +31,8 @@ const Branding = (props) => {
|
||||
const [publishingInfo, setPublishingInfo] = useState("");
|
||||
const [publishRequirements, setPublishRequirements] = useState([])
|
||||
|
||||
|
||||
const { leftSideBarOpenByClick } = useContext(Context)
|
||||
|
||||
const handleEditOrg = (joinStatus) => {
|
||||
const data = {
|
||||
"org_id": selectedOrganization.id,
|
||||
@@ -45,7 +57,7 @@ const Branding = (props) => {
|
||||
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.")
|
||||
setPublishingInfo("Your organization is now part of the Partner Program. You can now create, publish and manage content for your organization's public page.")
|
||||
} 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.")
|
||||
}
|
||||
@@ -70,7 +82,15 @@ const Branding = (props) => {
|
||||
}
|
||||
|
||||
const isOrganizationReady = () => {
|
||||
console.log("Is organization ready?")
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
// A simple checklist to ensure the button shows up properly
|
||||
if (selectedOrganization.name === selectedOrganization.org) {
|
||||
@@ -82,15 +102,6 @@ const Branding = (props) => {
|
||||
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)) {
|
||||
@@ -102,38 +113,89 @@ const Branding = (props) => {
|
||||
return true
|
||||
}
|
||||
|
||||
const isPublished = selectedOrganization.creator_id === ""
|
||||
const leadinfo = selectedOrganization.lead_info === undefined || selectedOrganization.lead_info === null || selectedOrganization.lead_info === "" ? "" : JSON.stringify(selectedOrganization.lead_info)
|
||||
const isPartner = leadinfo.includes("partner")
|
||||
|
||||
|
||||
return (
|
||||
<div style={{ width: clickedFromOrgTab? 1030: "auto", padding: 27, height: "auto", backgroundColor: '#212121', borderRadius: '16px', }}>
|
||||
<h2 style={{marginTop: clickedFromOrgTab ?0:null,}}>
|
||||
Branding
|
||||
</h2>
|
||||
<Typography variant="body1" color="textSecondary" style={{ marginTop: 20, marginBottom: 10 }}>
|
||||
<div style={{ width: clickedFromOrgTab? "100%": "auto", height: "100%", minHeight: 1100, boxSizing: 'border-box', transition: "width 0.3s ease", padding: "27px 10px 19px 27px", height: "auto", backgroundColor: '#212121', borderRadius: '16px', }}>
|
||||
<div style={{height: 843, overflowY: "auto",}}>
|
||||
<div style={{width: "100%", overflowX: 'hidden', }}>
|
||||
<Typography style={{fontSize: 24, fontWeight: "bold", marginTop: clickedFromOrgTab ?0:null,}}>
|
||||
Partner Status & Branding
|
||||
</Typography>
|
||||
<Typography variant="body1" color="textSecondary" style={{ marginTop: 10, marginBottom: 10, fontSize: 16 }}>
|
||||
You can customize your organization's branding by uploading a logo, changing the color scheme and a lot more.
|
||||
</Typography>
|
||||
|
||||
<Typography variant="body1" color="textSecondary" style={{display: 'flex', marginTop: 20, marginBottom: 10 }}>
|
||||
{isPublished ? <CheckCircleIcon style={{color: red, }} /> : <CheckCircleIcon style={{color: green, }} />}
|
||||
<span style={{marginLeft: 10, color: isPublished ? red : green, fontSize: 16 }}>{isPublished ? "Not Published" : "Published"}</span>
|
||||
</Typography>
|
||||
|
||||
<a href="https://shuffler.io/partners" target="_blank" style={{ textDecoration: "none", }}>
|
||||
<Typography variant="body1" color="textSecondary" style={{display: 'flex', marginTop: 20, marginBottom: 10 }}>
|
||||
{!isPartner ? <CheckCircleIcon style={{color: red, }} /> : <CheckCircleIcon style={{color: green, }} />}
|
||||
<Tooltip title="Official Partner Program (manual verification)" placement="top" arrow>
|
||||
<span style={{marginLeft: 10, color: !isPartner ? red : green, fontSize: 16}}>{!isPartner? "Not Officially Partnered" : "Officially Partnered"}</span>
|
||||
</Tooltip>
|
||||
</Typography>
|
||||
</a>
|
||||
|
||||
{!isPublished ? (
|
||||
<a
|
||||
href={`/partners/${selectedOrganization.creator_id}/edit`}
|
||||
target="_blank"
|
||||
style={{ textDecoration: "none" }} // Optional: remove underline
|
||||
>
|
||||
<Button
|
||||
variant="contained"
|
||||
style={{
|
||||
marginTop: 20,
|
||||
marginBottom: 10,
|
||||
textTransform: 'none',
|
||||
fontSize: 16
|
||||
}}
|
||||
>
|
||||
Modify Public Partner Details
|
||||
</Button>
|
||||
</a>
|
||||
) : (
|
||||
<Button
|
||||
disabled
|
||||
variant="contained"
|
||||
style={{
|
||||
marginTop: 20,
|
||||
marginBottom: 10,
|
||||
textTransform: 'none',
|
||||
fontSize: 16
|
||||
}}
|
||||
>
|
||||
Modify Public Partner Details
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<Divider style={{marginTop: 50, marginBottom: 50, }} />
|
||||
<h2>
|
||||
Creator Incentive Program
|
||||
</h2>
|
||||
<div style={{ display: "flex", width: 900, }}>
|
||||
<Typography style={{fontSize: 24, fontWeight: "bold"}}>
|
||||
Partner Program
|
||||
</Typography>
|
||||
<div style={{ display: "flex", width: 900, marginTop: 10}}>
|
||||
<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 variant="body1" color="textSecondary" style={{fontSize: 16}}>
|
||||
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>
|
||||
null
|
||||
}
|
||||
|
||||
<Button
|
||||
style={{ height: 40, marginTop: 10, width: 300, }}
|
||||
style={{ height: 40, marginTop: 10, width: 300, textTransform: 'none', fontSize: 18, backgroundColor: "#ff8544", color: "#1a1a1a" }}
|
||||
variant={selectedOrganization.creator_id == "" ? "contained" : "outlined"}
|
||||
color={selectedOrganization.creator_id == "" ? "primary" : "secondary"}
|
||||
disabled={!isOrganizationReady()}
|
||||
@@ -141,7 +203,7 @@ const Branding = (props) => {
|
||||
handleChangePublishing();
|
||||
}}
|
||||
>
|
||||
{selectedOrganization.creator_id == "" ? "Join" : "Leave"} Creators
|
||||
{selectedOrganization.creator_id == "" ? "Join" : "Leave"} Partner Program
|
||||
|
||||
</Button>
|
||||
<Typography variant="body1" color="textSecondary" style={{ marginTop: 20, marginBottom: 10, color: "white", }}>
|
||||
@@ -159,6 +221,8 @@ const Branding = (props) => {
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import React, { useState, useEffect, useContext, memo } from "react";
|
||||
import theme from "../theme.jsx";
|
||||
import { toast } from 'react-toastify';
|
||||
import ReactJson from "react-json-view";
|
||||
import ReactJson from "react-json-view-ssr";
|
||||
|
||||
import {
|
||||
Typography,
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
Dialog,
|
||||
DialogTitle,
|
||||
DialogActions,
|
||||
Skeleton,
|
||||
} from "@mui/material";
|
||||
|
||||
import {
|
||||
@@ -47,6 +48,7 @@ import {
|
||||
VisibilityOff as VisibilityOffIcon,
|
||||
} from "@mui/icons-material";
|
||||
import { validateJson, } from "../views/Workflows.jsx";
|
||||
import { Context } from "../context/ContextApi.jsx";
|
||||
|
||||
const scrollStyle1 = {
|
||||
height: 100,
|
||||
@@ -65,7 +67,7 @@ const scrollStyle2 = {
|
||||
}
|
||||
|
||||
|
||||
const CacheView = (props) => {
|
||||
const CacheView = memo((props) => {
|
||||
const { globalUrl, userdata, serverside, orgId, isSelectedDataStore } = props;
|
||||
const [orgCache, setOrgCache] = React.useState("");
|
||||
const [listCache, setListCache] = React.useState([]);
|
||||
@@ -78,11 +80,13 @@ const CacheView = (props) => {
|
||||
const [cacheCursor, setCacheCursor] = React.useState("");
|
||||
const [dataValue, setDataValue] = React.useState({});
|
||||
const [editCache, setEditCache] = React.useState(false);
|
||||
const [cachedLoaded, setCachedLoaded] = React.useState(false);
|
||||
const [show, setShow] = useState({});
|
||||
|
||||
useEffect(() => {
|
||||
listOrgCache(orgId);
|
||||
}, []);
|
||||
if(orgId?.length >0){
|
||||
listOrgCache(orgId);
|
||||
}
|
||||
}, [orgId]);
|
||||
|
||||
const listOrgCache = (orgId) => {
|
||||
fetch(globalUrl + `/api/v1/orgs/${orgId}/list_cache`, {
|
||||
@@ -104,6 +108,7 @@ const CacheView = (props) => {
|
||||
.then((responseJson) => {
|
||||
if (responseJson.success === true) {
|
||||
setListCache(responseJson.keys);
|
||||
setCachedLoaded(true);
|
||||
}
|
||||
|
||||
if (responseJson.cursor !== undefined && responseJson.cursor !== null && responseJson.cursor !== "") {
|
||||
@@ -232,6 +237,34 @@ const CacheView = (props) => {
|
||||
}
|
||||
}
|
||||
|
||||
const handleReactJsonClipboard = (copy) => {
|
||||
const elementName = "copy_element_shuffle";
|
||||
let copyText = document.getElementById(elementName);
|
||||
|
||||
if (copyText) {
|
||||
if (copy.namespace && copy.name && copy.src) {
|
||||
copy = copy.src;
|
||||
}
|
||||
|
||||
const clipboard = navigator.clipboard;
|
||||
if (!clipboard) {
|
||||
toast("Can only copy over HTTPS (port 3443)");
|
||||
return;
|
||||
}
|
||||
|
||||
let stringified = JSON.stringify(copy);
|
||||
if (stringified.startsWith('"') && stringified.endsWith('"')) {
|
||||
stringified = stringified.slice(1, -1);
|
||||
}
|
||||
|
||||
navigator.clipboard.writeText(stringified);
|
||||
toast("Copied value to clipboard, NOT json path.");
|
||||
} else {
|
||||
console.log("Failed to copy from " + elementName + ": ", copyText);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const modalView = (
|
||||
// console.log("key:", dataValue.key),
|
||||
//console.log("value:",dataValue.value),
|
||||
@@ -241,11 +274,23 @@ const CacheView = (props) => {
|
||||
setModalOpen(false);
|
||||
}}
|
||||
PaperProps={{
|
||||
style: {
|
||||
backgroundColor: theme.palette.surfaceColor,
|
||||
color: "white",
|
||||
sx: {
|
||||
borderRadius: theme?.palette?.DialogStyle?.borderRadius,
|
||||
border: theme?.palette?.DialogStyle?.border,
|
||||
minWidth: "800px",
|
||||
minHeight: "320px",
|
||||
fontFamily: theme?.typography?.fontFamily,
|
||||
backgroundColor: theme?.palette?.DialogStyle?.backgroundColor,
|
||||
zIndex: 1000,
|
||||
'& .MuiDialogContent-root': {
|
||||
backgroundColor: theme?.palette?.DialogStyle?.backgroundColor,
|
||||
},
|
||||
'& .MuiDialogTitle-root': {
|
||||
backgroundColor: theme?.palette?.DialogStyle?.backgroundColor,
|
||||
},
|
||||
'& .MuiDialogActions-root': {
|
||||
backgroundColor: theme?.palette?.DialogStyle?.backgroundColor,
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
@@ -254,7 +299,7 @@ const CacheView = (props) => {
|
||||
{ editCache ? "Edit Cache" : "Add Cache" }
|
||||
</span>
|
||||
</DialogTitle>
|
||||
<div style={{ paddingLeft: "30px", paddingRight: '30px' }}>
|
||||
<div style={{ paddingLeft: "30px", paddingRight: '30px', backgroundColor: "#212121", }}>
|
||||
Key
|
||||
<TextField
|
||||
color="primary"
|
||||
@@ -278,7 +323,7 @@ const CacheView = (props) => {
|
||||
onChange={(e) => setKey(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div style={{ paddingLeft: 30, paddingRight: 30 }}>
|
||||
<div style={{ paddingLeft: 30, paddingRight: 30, backgroundColor: "#212121" }}>
|
||||
<div style={{display: "flex", }}>
|
||||
<Typography style={{marginTop: 25, marginBottom: 0, flex: 20, }}>
|
||||
Value - ({isValidJson.valid === true ? "Valid" : "Invalid"} JSON)
|
||||
@@ -320,7 +365,7 @@ const CacheView = (props) => {
|
||||
</div>
|
||||
<DialogActions style={{ paddingLeft: "30px", paddingRight: '30px' }}>
|
||||
<Button
|
||||
style={{ borderRadius: "0px" }}
|
||||
style={{ borderRadius: "2px", fontSize: 16, color: "#ff8544", textTransform:"none" }}
|
||||
onClick={() => {
|
||||
setModalOpen(false)
|
||||
setValue("")
|
||||
@@ -332,7 +377,7 @@ const CacheView = (props) => {
|
||||
</Button>
|
||||
<Button
|
||||
variant="contained"
|
||||
style={{ borderRadius: "0px" }}
|
||||
style={{ borderRadius: "2px", backgroundColor: "#ff8544",color: "#1a1a1a", textTransform:"none" }}
|
||||
onClick={() => {
|
||||
{editCache ? editOrgCache(orgId) : addOrgCache(orgId)}
|
||||
|
||||
@@ -348,10 +393,11 @@ const CacheView = (props) => {
|
||||
);
|
||||
|
||||
return (
|
||||
|
||||
<div style={{paddingBottom: isSelectedDataStore?null:250, width: isSelectedDataStore?1030:null, padding:isSelectedDataStore?27:null, height: isSelectedDataStore?"auto":null, color: isSelectedDataStore?'#ffffff':null, backgroundColor: isSelectedDataStore?'#212121':null, borderRadius: isSelectedDataStore?'16px':null, }}>
|
||||
<div style={{paddingBottom: isSelectedDataStore?null:250, minHeight: 1000, boxSizing: "border-box", width: isSelectedDataStore? "100%" :null, transition: "width 0.3s ease", padding:isSelectedDataStore?"27px 10px 27px 27px":null, height: isSelectedDataStore?"100%":null, color: isSelectedDataStore?'#ffffff':null, backgroundColor: isSelectedDataStore?'#212121':null, borderTopRightRadius: isSelectedDataStore?'8px':null, borderBottomRightRadius: isSelectedDataStore?'8px':null, borderLeft: "1px solid #494949" }}>
|
||||
{modalView}
|
||||
<div style={{ marginTop: isSelectedDataStore?null:20, marginBottom: 20 }}>
|
||||
<div style={{height: "100%", maxHeight: 1700, overflowY: "auto", scrollbarColor: '#494949 transparent', scrollbarWidth: 'thin'}}>
|
||||
<div style={{ height: "100%", width: "calc(100% - 20px)", scrollbarColor: '#494949 transparent', scrollbarWidth: 'thin' }}>
|
||||
<div style={{ marginTop: isSelectedDataStore?null:20, marginBottom: 20 }}>
|
||||
<h2 style={{ display: isSelectedDataStore?null: "inline" }}>Shuffle Datastore</h2>
|
||||
<span style={{ marginLeft: isSelectedDataStore?null:25, color:isSelectedDataStore?"#9E9E9E":null}}>
|
||||
Datastore is a permanent key-value database for storing data that can be used cross-workflow. <br/>You can store anything from lists of IPs to complex configurations.
|
||||
@@ -366,7 +412,7 @@ const CacheView = (props) => {
|
||||
</span>
|
||||
</div>
|
||||
<Button
|
||||
style={{backgroundColor: isSelectedDataStore?'rgba(255, 132, 68, 0.2)':null, boxShadow: isSelectedDataStore ? "none":null,textTransform: isSelectedDataStore ? 'capitalize':null, color:isSelectedDataStore?"#FF8444":null, borderRadius:isSelectedDataStore?200:null, width:isSelectedDataStore?162:null, height:isSelectedDataStore?40:null}}
|
||||
style={{backgroundColor: isSelectedDataStore? "#ff8544":null, fontSize: 16, boxShadow: isSelectedDataStore ? "none":null,textTransform: isSelectedDataStore ? 'capitalize':null, color:isSelectedDataStore?"#1a1a1a":null, borderRadius:isSelectedDataStore?8:null, width:isSelectedDataStore?162:null, height:isSelectedDataStore?40:null}}
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={() =>{
|
||||
@@ -379,7 +425,7 @@ const CacheView = (props) => {
|
||||
Add Cache
|
||||
</Button>
|
||||
<Button
|
||||
style={{ marginLeft: 5, marginRight: 15, backgroundColor: isSelectedDataStore?"#2F2F2F":null, boxShadow: isSelectedDataStore ? "none":null,textTransform: isSelectedDataStore ? 'capitalize':null,borderRadius:isSelectedDataStore?200:null, width:isSelectedDataStore?81:null, height:isSelectedDataStore?40:null, }}
|
||||
style={{ marginLeft: 5, marginRight: 15, backgroundColor: isSelectedDataStore?"#2F2F2F":null, boxShadow: isSelectedDataStore ? "none":null,textTransform: isSelectedDataStore ? 'capitalize':null,borderRadius:isSelectedDataStore?8:null, width:isSelectedDataStore?81:null, height:isSelectedDataStore?40:null, }}
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={() => listOrgCache(orgId)}
|
||||
@@ -392,28 +438,79 @@ const CacheView = (props) => {
|
||||
marginBottom: 20,
|
||||
}}
|
||||
/>}
|
||||
<List style={{borderRadius: isSelectedDataStore?8:null, border:isSelectedDataStore?"1px solid #494949":null, marginTop:isSelectedDataStore?24:null}}>
|
||||
<ListItem style={{width: isSelectedDataStore?"100%":null, borderBottom:isSelectedDataStore?"1px solid #494949":null}}>
|
||||
<ListItemText
|
||||
primary="Key"
|
||||
style={{ minWidth: isSelectedDataStore?200:250, maxWidth: isSelectedDataStore?200:250, }}
|
||||
/>
|
||||
<ListItemText
|
||||
primary="Value"
|
||||
style={{ minWidth: isSelectedDataStore?300:400, maxWidth: isSelectedDataStore?300:400, overflowX: "auto", overflowY: "hidden", }}
|
||||
/>
|
||||
<ListItemText
|
||||
primary="Actions"
|
||||
style={{ minWidth: 150, maxWidth: 150, marginLeft: isSelectedDataStore?80:null,}}
|
||||
/>
|
||||
<ListItemText
|
||||
style={{textAlign:isSelectedDataStore?"center":null}}
|
||||
primary="Updated"
|
||||
/>
|
||||
<div
|
||||
style={{
|
||||
borderRadius: 8,
|
||||
marginTop: 24,
|
||||
border: "1px solid #494949",
|
||||
width: "100%",
|
||||
overflowX: "auto",
|
||||
paddingBottom: 0,
|
||||
}}
|
||||
>
|
||||
<List
|
||||
style={{
|
||||
borderRadius: 8,
|
||||
paddingBottom: 0,
|
||||
tableLayout: "auto",
|
||||
display: "table",
|
||||
width: '100%',
|
||||
minWidth: 800,
|
||||
overflowX: "auto",
|
||||
}}>
|
||||
<ListItem style={{width: isSelectedDataStore?"100%":null, borderBottom:isSelectedDataStore?"1px solid #494949":null, display: "table-row"}}>
|
||||
{["Key", "Value", "Actions", "Updated"].map((header, index) => (
|
||||
<ListItemText
|
||||
key={index}
|
||||
primary={header}
|
||||
style={{
|
||||
display: "table-cell",
|
||||
padding: "0px 8px 8px 8px",
|
||||
whiteSpace: "nowrap",
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
borderBottom: "1px solid #494949"
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</ListItem>
|
||||
{listCache === undefined || listCache === null
|
||||
? null
|
||||
: listCache.map((data, index) => {
|
||||
{cachedLoaded === false
|
||||
? [...Array(6)].map((_, rowIndex) => (
|
||||
<ListItem
|
||||
key={rowIndex}
|
||||
style={{
|
||||
display: "table-row",
|
||||
backgroundColor: "#212121",
|
||||
}}
|
||||
>
|
||||
{Array(4)
|
||||
.fill()
|
||||
.map((_, colIndex) => (
|
||||
<ListItemText
|
||||
key={colIndex}
|
||||
style={{
|
||||
display: "table-cell",
|
||||
padding: "8px",
|
||||
}}
|
||||
>
|
||||
<Skeleton
|
||||
variant="text"
|
||||
animation="wave"
|
||||
sx={{
|
||||
backgroundColor: "#1a1a1a",
|
||||
height: "20px",
|
||||
borderRadius: "4px",
|
||||
}}
|
||||
/>
|
||||
</ListItemText>
|
||||
))}
|
||||
</ListItem>
|
||||
))
|
||||
: listCache?.length === 0 ? (
|
||||
<Typography style={{ textAlign: "center", marginTop: 20, marginBottom: 20, minWidth: 1000, }}>
|
||||
No Keys Found
|
||||
</Typography>
|
||||
): listCache?.map((data, index) => {
|
||||
var bgColor = isSelectedDataStore? "#212121":"#27292d";
|
||||
if (index % 2 === 0) {
|
||||
bgColor = isSelectedDataStore? "#1A1A1A":"#1f2023";
|
||||
@@ -421,49 +518,61 @@ const CacheView = (props) => {
|
||||
|
||||
const validate = validateJson(data.value);
|
||||
return (
|
||||
<ListItem key={index} style={{ backgroundColor: bgColor, maxHeight: 300, overflow: "auto", }}>
|
||||
<ListItem key={index} style={{display:'table-row', backgroundColor: bgColor, maxHeight: 300, overflow: "auto", borderBottomLeftRadius: listCache?.length - 1 === index ? 8 : 0, borderBottomRightRadius: listCache?.length - 1 === index ? 8 : 0}}>
|
||||
<ListItemText
|
||||
style={{
|
||||
maxWidth: 200,
|
||||
minWidth: 200,
|
||||
display: "table-cell",
|
||||
overflow: "hidden",
|
||||
padding: 8,
|
||||
verticalAlign: "middle",
|
||||
}}
|
||||
primary={data.key}
|
||||
/>
|
||||
<ListItemText
|
||||
style={{
|
||||
minWidth: 300,
|
||||
maxWidth: 300,
|
||||
// height:200,
|
||||
overflowX: "hidden",
|
||||
display: "table-cell",
|
||||
overflowY: "auto",
|
||||
overflowX: "auto",
|
||||
border: "1px solid rgba(255,255,255,0.7)",
|
||||
borderRadius: 6,
|
||||
backgroundColor: "#151515",
|
||||
maxHeight: 300,
|
||||
verticalAlign: "middle",
|
||||
}}
|
||||
primary={validate.valid ?
|
||||
<ReactJson
|
||||
src={validate.result}
|
||||
theme={theme.palette.jsonTheme}
|
||||
style={theme.palette.reactJsonStyle}
|
||||
collapsed={true}
|
||||
enableClipboard={(copy) => {
|
||||
//handleReactJsonClipboard(copy);
|
||||
}}
|
||||
displayDataTypes={false}
|
||||
onSelect={(select) => {
|
||||
//HandleJsonCopy(showResult, select, data.action.label);
|
||||
//console.log("SELECTED!: ", select);
|
||||
}}
|
||||
name={"value"}
|
||||
/>
|
||||
primary={validate.valid ?
|
||||
<ReactJson
|
||||
src={validate.result}
|
||||
theme={theme.palette.jsonTheme}
|
||||
style={{
|
||||
padding: 5,
|
||||
maxHeight: 300,
|
||||
overflowY: "auto",
|
||||
}}
|
||||
collapsed={true}
|
||||
enableClipboard={(copy) => {
|
||||
// handleReactJsonClipboard(copy);
|
||||
}}
|
||||
collapseStringsAfterLength={theme.palette.jsonCollapseStringsAfterLength}
|
||||
iconStyle={theme.palette.jsonIconStyle}
|
||||
displayDataTypes={false}
|
||||
onSelect={(select) => {
|
||||
// HandleJsonCopy(showResult, select, data.action.label);
|
||||
console.log("SELECTED!: ", select);
|
||||
}}
|
||||
name={"value"}
|
||||
/>
|
||||
:
|
||||
data.value
|
||||
}
|
||||
/>
|
||||
<ListItemText
|
||||
style={{
|
||||
maxWidth: 200,
|
||||
minWidth: 200,
|
||||
marginLeft: 50,
|
||||
display: "table-cell",
|
||||
verticalAlign: "middle",
|
||||
padding: 8
|
||||
}}
|
||||
primary=<span style={{ display: "inline" }}>
|
||||
primary={(
|
||||
<span style={{ display: "inline" }}>
|
||||
<Tooltip
|
||||
title="Edit item"
|
||||
style={{}}
|
||||
@@ -482,9 +591,7 @@ const CacheView = (props) => {
|
||||
setModalOpen(true)
|
||||
}}
|
||||
>
|
||||
<EditIcon
|
||||
style={{ color: "white" }}
|
||||
/>
|
||||
<img src="/icons/editIcon.svg" alt="edit" />
|
||||
</IconButton>
|
||||
</span>
|
||||
</Tooltip>
|
||||
@@ -508,7 +615,6 @@ const CacheView = (props) => {
|
||||
</Tooltip>
|
||||
<Tooltip
|
||||
title={"Delete item"}
|
||||
style={{ marginLeft: 25, }}
|
||||
aria-label={"Delete"}
|
||||
>
|
||||
<span>
|
||||
@@ -519,18 +625,18 @@ const CacheView = (props) => {
|
||||
//deleteFile(orgId);
|
||||
}}
|
||||
>
|
||||
<DeleteIcon
|
||||
style={{ color: "white" }}
|
||||
/>
|
||||
<img src="/icons/deleteIcon.svg" alt="delete" />
|
||||
</IconButton>
|
||||
</span>
|
||||
</Tooltip>
|
||||
</span>
|
||||
)}
|
||||
/>
|
||||
<ListItemText
|
||||
style={{
|
||||
maxWidth: 225,
|
||||
minWidth: 225,
|
||||
display: "table-cell",
|
||||
verticalAlign: "middle",
|
||||
padding: 8
|
||||
}}
|
||||
primary={new Date(data.edited * 1000).toISOString()}
|
||||
/>
|
||||
@@ -538,8 +644,11 @@ const CacheView = (props) => {
|
||||
);
|
||||
})}
|
||||
</List>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
);
|
||||
}
|
||||
export default CacheView;
|
||||
});
|
||||
|
||||
export default memo(CacheView);
|
||||
|
||||
@@ -2,6 +2,7 @@ import React, { useState, useEffect } from "react";
|
||||
import { useInterval } from "react-powerhooks";
|
||||
import { toast } from 'react-toastify';
|
||||
import theme from "../theme.jsx";
|
||||
import WorkflowValidationTimeline from "../components/WorkflowValidationTimeline.jsx"
|
||||
|
||||
import {
|
||||
InputAdornment,
|
||||
@@ -79,7 +80,7 @@ const ConfigureWorkflow = (props) => {
|
||||
useEffect(() => {
|
||||
if (requiredActions.length === 0) {
|
||||
if (setConfigurationFinished !== undefined) {
|
||||
setConfigurationFinished(true)
|
||||
setConfigurationFinished(true)
|
||||
}
|
||||
}
|
||||
}, [requiredActions])
|
||||
@@ -141,17 +142,18 @@ const ConfigureWorkflow = (props) => {
|
||||
|
||||
// Where is this from?
|
||||
if (workflow === undefined || workflow === null || workflow.id === undefined) {
|
||||
return null;
|
||||
//console.log("Workflow is undefined or null: ", workflow)
|
||||
return null
|
||||
}
|
||||
|
||||
if (apps === undefined || apps === null) {
|
||||
console.log("Apps is undefined or null: ", apps)
|
||||
return null;
|
||||
//console.log("Apps is undefined or null: ", apps)
|
||||
return null
|
||||
}
|
||||
|
||||
if (appAuthentication === undefined || appAuthentication === null) {
|
||||
console.log("App authentication is undefined or null: ", appAuthentication)
|
||||
return null;
|
||||
//console.log("App authentication is undefined or null: ", appAuthentication)
|
||||
return null
|
||||
}
|
||||
|
||||
const getApp = (actionId, appId) => {
|
||||
@@ -310,7 +312,7 @@ const ConfigureWorkflow = (props) => {
|
||||
}
|
||||
}
|
||||
|
||||
if (action.authentication_id === "" && app.authentication.required === true && action.parameters !== undefined && action.parameters !== null) {
|
||||
if (action?.authentication_id === "" && app?.authentication?.required === true && action.parameters !== undefined && action.parameters !== null) {
|
||||
// Check if configuration is filled or not
|
||||
var filled = true;
|
||||
for (let [key,keyval] in Object.entries(action.parameters)) {
|
||||
@@ -322,7 +324,7 @@ const ConfigureWorkflow = (props) => {
|
||||
}
|
||||
}
|
||||
|
||||
if (app.authentication.type === "oauth2" || app.authentication.type === "oauth2-app") {
|
||||
if (app?.authentication?.type === "oauth2" || app?.authentication?.type === "oauth2-app") {
|
||||
filled = false
|
||||
|
||||
action.auth_type = "oauth2"
|
||||
@@ -425,10 +427,8 @@ const ConfigureWorkflow = (props) => {
|
||||
trigger.index = key;
|
||||
|
||||
if (trigger.trigger_type === "WEBHOOK") {
|
||||
console.log("Found webhook: ", trigger)
|
||||
|
||||
if (trigger.app_association !== undefined && trigger.app_association.name !== null && trigger.app_association.name !== "") {
|
||||
console.log("Actions: ", newactions)
|
||||
const findapp = trigger.app_association.name.toLowerCase()
|
||||
const foundindex = newactions.findIndex(action => action.app_name.toLowerCase() === findapp)
|
||||
|
||||
@@ -449,9 +449,6 @@ const ConfigureWorkflow = (props) => {
|
||||
|
||||
newactions[foundindex].show_steps = true
|
||||
|
||||
console.log("CHANGED ACTION: ", newactions[foundindex])
|
||||
//console.log("Index: ", newactions[foundindex])
|
||||
|
||||
continue
|
||||
}
|
||||
}
|
||||
@@ -601,7 +598,7 @@ const ConfigureWorkflow = (props) => {
|
||||
<TextField
|
||||
style={{
|
||||
backgroundColor: theme.palette.inputColor,
|
||||
borderRadius: theme.palette.borderRadius,
|
||||
borderRadius: theme.palette?.borderRadius,
|
||||
}}
|
||||
InputProps={{
|
||||
endAdornment: <InputAdornment position="end"></InputAdornment>,
|
||||
@@ -796,15 +793,13 @@ const ConfigureWorkflow = (props) => {
|
||||
}
|
||||
|
||||
parsedName = (parsedName.charAt(0).toUpperCase() + parsedName.slice(1)).replaceAll("_", " ");
|
||||
|
||||
console.log("AUTH Action: ", action)
|
||||
return (
|
||||
<ListItem
|
||||
style={{padding: 0, display: "flex", flexDirection: "column", }}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
border: filled ? `1px solid ${theme.palette.green}` : "1px solid rgba(255,255,255,0.3)", borderRadius: theme.palette.borderRadius, width: "100%", padding: 12, cursor: "pointer",
|
||||
border: filled ? `1px solid ${theme.palette.green}` : "1px solid rgba(255,255,255,0.3)", borderRadius: theme.palette?.borderRadius, width: "100%", padding: 12, cursor: "pointer",
|
||||
}}
|
||||
id="app-config"
|
||||
>
|
||||
@@ -853,7 +848,7 @@ const ConfigureWorkflow = (props) => {
|
||||
{opened ?
|
||||
<div style={{padding: 12, }}>
|
||||
|
||||
{action.app.authentication.type === "oauth2-app" || action.app.authentication.type === "oauth2" || action.auth_type === "oauth2" ?
|
||||
{action.app?.authentication?.type === "oauth2-app" || action.app?.authentication?.type === "oauth2" || action.auth_type === "oauth2" ?
|
||||
<div>
|
||||
<AuthenticationOauth2
|
||||
selectedApp={action.app}
|
||||
@@ -950,7 +945,7 @@ const ConfigureWorkflow = (props) => {
|
||||
)
|
||||
})}
|
||||
|
||||
{action.app.authentication.type !== "oauth2-app" && action.app.authentication.type !== "oauth2" ?
|
||||
{action.app?.authentication?.type !== "oauth2-app" && action.app?.authentication?.type !== "oauth2" ?
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
@@ -1001,7 +996,7 @@ const ConfigureWorkflow = (props) => {
|
||||
justifyContent: "flex-start",
|
||||
backgroundColor: action.auth_done ? theme.palette.surfaceColor : theme.palette.inputColor,
|
||||
color: action.auth_done ? "#686a6c" : "#ffffff",
|
||||
borderRadius: theme.palette.borderRadius,
|
||||
borderRadius: theme.palette?.borderRadius,
|
||||
minWidth: 350,
|
||||
maxHeight: 50,
|
||||
overflow: "hidden",
|
||||
@@ -1041,7 +1036,7 @@ const ConfigureWorkflow = (props) => {
|
||||
>
|
||||
<img
|
||||
alt={action.app_name}
|
||||
style={{ margin: 4, minHeight: 30, maxHeight: 30, borderRadius: theme.palette.borderRadius, }}
|
||||
style={{ margin: 4, minHeight: 30, maxHeight: 30, borderRadius: theme.palette?.borderRadius, }}
|
||||
src={action.large_image}
|
||||
/>
|
||||
<Typography style={{ margin: 0, marginLeft: 10 }} variant="body1">
|
||||
@@ -1061,7 +1056,7 @@ const ConfigureWorkflow = (props) => {
|
||||
justifyContent: "flex-start",
|
||||
backgroundColor: action.auth_done ? theme.palette.surfaceColor : theme.palette.inputColor,
|
||||
color: action.auth_done ? "#686a6c" : "#ffffff",
|
||||
borderRadius: theme.palette.borderRadius,
|
||||
borderRadius: theme.palette?.borderRadius,
|
||||
minWidth: 350,
|
||||
maxHeight: 50,
|
||||
overflow: "hidden",
|
||||
@@ -1092,7 +1087,7 @@ const ConfigureWorkflow = (props) => {
|
||||
>
|
||||
<img
|
||||
alt={action.app_name}
|
||||
style={{ margin: 4, minHeight: 30, maxHeight: 30, borderRadius: theme.palette.borderRadius, }}
|
||||
style={{ margin: 4, minHeight: 30, maxHeight: 30, borderRadius: theme.palette?.borderRadius, }}
|
||||
src={action.large_image}
|
||||
/>
|
||||
<Typography style={{ margin: 0, marginLeft: 10 }} variant="body1">
|
||||
@@ -1112,7 +1107,7 @@ const ConfigureWorkflow = (props) => {
|
||||
justifyContent: "flex-start",
|
||||
backgroundColor: action.auth_done ? theme.palette.surfaceColor : theme.palette.inputColor,
|
||||
color: action.auth_done ? "#686a6c" : "#ffffff",
|
||||
borderRadius: theme.palette.borderRadius,
|
||||
borderRadius: theme.palette?.borderRadius,
|
||||
minWidth: 350,
|
||||
maxHeight: 50,
|
||||
overflow: "hidden",
|
||||
@@ -1127,7 +1122,7 @@ const ConfigureWorkflow = (props) => {
|
||||
>
|
||||
<img
|
||||
alt={action.app_name}
|
||||
style={{ margin: 4, minHeight: 30, maxHeight: 30, borderRadius: theme.palette.borderRadius, }}
|
||||
style={{ margin: 4, minHeight: 30, maxHeight: 30, borderRadius: theme.palette?.borderRadius, }}
|
||||
src={action.large_image}
|
||||
/>
|
||||
<Typography style={{ margin: 0, marginLeft: 10 }} variant="body1">
|
||||
@@ -1279,7 +1274,7 @@ const ConfigureWorkflow = (props) => {
|
||||
const [finishCount, setFinishCount] = useState(0)
|
||||
|
||||
return (
|
||||
<div style={{backgroundColor: hovered ? theme.palette.inputColor : "inherit", border: "1px solid rgba(255,255,255,0.3)", borderRadius: theme.palette.borderRadius, cursor: "pointer", }}
|
||||
<div style={{backgroundColor: hovered ? theme.palette.inputColor : "inherit", border: "1px solid rgba(255,255,255,0.3)", borderRadius: theme.palette?.borderRadius, cursor: "pointer", }}
|
||||
>
|
||||
<div style={{display: "flex", marginLeft: 15, marginTop: 15, marginBottom: 15, }}
|
||||
onClick={() => {
|
||||
@@ -1321,7 +1316,6 @@ const ConfigureWorkflow = (props) => {
|
||||
}
|
||||
|
||||
if (step.type === "authenticate") {
|
||||
console.log("AUTH STEP: ", step)
|
||||
if (data.must_authenticate === true ) {
|
||||
filled = false
|
||||
} else {
|
||||
@@ -1393,9 +1387,23 @@ const ConfigureWorkflow = (props) => {
|
||||
: null
|
||||
}
|
||||
|
||||
<div style={{marginTop: 10, }} />
|
||||
|
||||
{/*
|
||||
<WorkflowValidationTimeline
|
||||
workflow={workflow}
|
||||
|
||||
apps={apps}
|
||||
|
||||
getParents={undefined}
|
||||
execution={undefined}
|
||||
/>
|
||||
<div style={{marginBottom: 10, }} />
|
||||
*/}
|
||||
|
||||
{requiredActions.length > 0 ? (
|
||||
<span>
|
||||
<Typography variant="body2" style={{}}>
|
||||
<Typography variant="body2" color="textSecondary">
|
||||
Please configure the following steps to help us complete your workflow. This can also be done later.
|
||||
</Typography>
|
||||
|
||||
|
||||
@@ -136,6 +136,11 @@ const CreatorGrid = props => {
|
||||
removeQuery("q")
|
||||
refine(event.currentTarget.value)
|
||||
}}
|
||||
onKeyDown={(event) => {
|
||||
if(event.key === "Enter") {
|
||||
event.preventDefault();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{/*isSearchStalled ? 'My search is stalled' : ''*/}
|
||||
</form>
|
||||
@@ -147,6 +152,7 @@ const CreatorGrid = props => {
|
||||
flexWrap: "wrap",
|
||||
alignContent: "space-between",
|
||||
marginTop: 5,
|
||||
padding:"0px 180px",
|
||||
}
|
||||
|
||||
const Hits = ({ hits }) => {
|
||||
|
||||
@@ -0,0 +1,144 @@
|
||||
import React from 'react';
|
||||
import { Bar } from 'react-chartjs-2';
|
||||
import { toast } from "react-toastify";
|
||||
|
||||
export const LoadStats = (globalUrl, cachekey) => {
|
||||
if (globalUrl === undefined) {
|
||||
console.log("Error: Global URL is undefined")
|
||||
return
|
||||
}
|
||||
|
||||
if (cachekey === undefined) {
|
||||
console.log("Error: Cachekey is undefined")
|
||||
return
|
||||
}
|
||||
|
||||
var basedata = {
|
||||
"key": cachekey,
|
||||
"total": 0,
|
||||
"available_keys": [],
|
||||
"labels": [],
|
||||
"datasets": [
|
||||
{
|
||||
"label": "",
|
||||
"data": [],
|
||||
"backgroundColor": [],
|
||||
"barThickness": 15,
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
//const url = `${globalUrl}/api/v1/stats/app_executions_test2`
|
||||
//cachekey = cachekey.replace(" ", "_", -1)
|
||||
const url = `${globalUrl}/api/v1/stats/${cachekey}`
|
||||
return fetch(url, {
|
||||
method: "GET",
|
||||
credentials: "include",
|
||||
})
|
||||
.then((resp) => {
|
||||
return resp.json()
|
||||
}).then((respJson) => {
|
||||
const selectedIndex = 0
|
||||
|
||||
if (respJson.success === true) {
|
||||
for (let entryKey in respJson.entries) {
|
||||
const entry = respJson.entries[entryKey]
|
||||
basedata.labels.push(entry.date)
|
||||
|
||||
basedata.datasets[0].data.push(entry.value)
|
||||
basedata.datasets[0].backgroundColor.push(entry.value > 0 ? "rgba(255,255,255,0.4)" : "red")
|
||||
}
|
||||
|
||||
basedata.available_keys = respJson.available_keys
|
||||
basedata.total = respJson.total
|
||||
|
||||
return basedata
|
||||
} else {
|
||||
console.log("Failed to get stats")
|
||||
return basedata
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
toast("Failed to get stats")
|
||||
return basedata
|
||||
})
|
||||
}
|
||||
|
||||
const DashboardBarchart = (props) => {
|
||||
const { timelineData, title, height, } = props;
|
||||
var inputHeight = 15
|
||||
if (height !== undefined && height !== null) {
|
||||
inputHeight = height
|
||||
}
|
||||
|
||||
const barOptions = {
|
||||
plugins: {
|
||||
tooltip: {
|
||||
enabled: true, // Ensure tooltips are enabled
|
||||
},
|
||||
},
|
||||
tooltips: {
|
||||
mode: 'index',
|
||||
intersect: false,
|
||||
},
|
||||
legend: {
|
||||
display: false
|
||||
},
|
||||
layout: {
|
||||
padding: {
|
||||
top: 0, // Adjust the top padding as needed
|
||||
bottom: -10, // Adjust the bottom padding as needed
|
||||
left: 0, // Adjust the left padding as needed
|
||||
right: 0, // Adjust the right padding as needed
|
||||
},
|
||||
},
|
||||
scales: {
|
||||
y: {
|
||||
beginAtZero: false,
|
||||
},
|
||||
yAxes: [{
|
||||
ticks: {
|
||||
display: false
|
||||
},
|
||||
beginAtZero: false,
|
||||
}],
|
||||
xAxes: [{
|
||||
ticks: {
|
||||
display: false
|
||||
},
|
||||
beginAtZero: false,
|
||||
}]
|
||||
},
|
||||
tooltips: {
|
||||
callbacks: {
|
||||
label: function (tooltipItem, data) {
|
||||
const label = data.labels[tooltipItem.index]
|
||||
return label.split('\n')[0]
|
||||
},
|
||||
afterLabel: function (tooltipItem, data) {
|
||||
const amount = tooltipItem.value === undefined || tooltipItem.value === null ? 0 : tooltipItem.value
|
||||
return `Amount: ${amount}`
|
||||
},
|
||||
title: function () {
|
||||
return title === undefined ? '' : title
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Bar
|
||||
data={timelineData}
|
||||
options={barOptions}
|
||||
height={inputHeight}
|
||||
getElementAtEvent={(elements) => {
|
||||
if (elements && elements.length > 0) {
|
||||
//toast("Click event")
|
||||
console.log("Clicked: ", elements)
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default DashboardBarchart;
|
||||
@@ -0,0 +1,210 @@
|
||||
import React, { useState } from "react";
|
||||
import {
|
||||
Container,
|
||||
Box,
|
||||
TextField,
|
||||
Switch,
|
||||
Typography,
|
||||
Button,
|
||||
CircularProgress,
|
||||
Paper,
|
||||
} from "@mui/material";
|
||||
|
||||
import { toast } from "react-toastify";
|
||||
import theme from '../theme.jsx';
|
||||
import DetectionRuleCard from "../components/DetectionRuleCard.jsx";
|
||||
|
||||
const handleDirectoryChange = (folderDisabled, setFolderDisabled, globalUrl, isTenzirActive) => {
|
||||
|
||||
if (!isTenzirActive) {
|
||||
toast("connect to siem first for global enable/disable to work");
|
||||
return;
|
||||
}
|
||||
|
||||
const action = folderDisabled ? "enable_folder" : "disable_folder";
|
||||
const url = `${globalUrl}/api/v1/detections/${action}`;
|
||||
|
||||
fetch(url, {
|
||||
method: "PUT",
|
||||
credentials: "include",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
})
|
||||
.then((response) =>
|
||||
response.json().then((responseJson) => {
|
||||
if (responseJson["success"] === true) {
|
||||
if (action === "enable_folder") setFolderDisabled(false);
|
||||
else setFolderDisabled(true);
|
||||
} else {
|
||||
//toast(`failed to disable rule`);
|
||||
}
|
||||
})
|
||||
)
|
||||
.catch((error) => {
|
||||
console.log(`Error in ${action} the rule: `, error);
|
||||
toast(`An error occurred while ${action} the rule`);
|
||||
});
|
||||
};
|
||||
|
||||
const Detection = (props) => {
|
||||
const { globalUrl, ruleInfo, folderDisabled, setFolderDisabled, isTenzirActive } = props;
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const handleConnectClick = () => {
|
||||
if (!isTenzirActive) {
|
||||
setLoading(true);
|
||||
const url = `${globalUrl}/api/v1/detections/siem/connect`;
|
||||
|
||||
fetch(url, {
|
||||
method: "GET",
|
||||
credentials: "include",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
})
|
||||
.then((response) =>
|
||||
response.json().then((responseJson) => {
|
||||
if (responseJson["success"] === true) {
|
||||
setTimeout(() => {
|
||||
setLoading(false);
|
||||
window.location.reload();
|
||||
}, 15000);
|
||||
} else {
|
||||
setLoading(false);
|
||||
toast("Failed to connect to SIEM");
|
||||
}
|
||||
})
|
||||
)
|
||||
.catch((error) => {
|
||||
setLoading(false);
|
||||
console.log(`Error in connecting to SIEM: `, error);
|
||||
toast("An error occurred while connecting to SIEM");
|
||||
});
|
||||
} else {
|
||||
console.log("Already connected to SIEM");
|
||||
}
|
||||
};
|
||||
|
||||
const filteredRules = ruleInfo?.filter((rule) =>
|
||||
rule.title.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
rule.description.toLowerCase().includes(searchQuery.toLowerCase())
|
||||
);
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<Paper
|
||||
style={{
|
||||
marginTop: 50,
|
||||
width: "100%",
|
||||
padding: 50,
|
||||
backgroundColor: theme.palette.backgroundColor,
|
||||
borderRadius: theme.palette?.borderRadius,
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
mb: 2,
|
||||
}}
|
||||
>
|
||||
<Typography variant="h6" component="div">
|
||||
Sigma Detection Rules
|
||||
</Typography>
|
||||
<Button
|
||||
variant="contained"
|
||||
onClick={handleConnectClick}
|
||||
disabled={loading} // Disable the button while loading
|
||||
color={isTenzirActive ? "primary" : "secondary"}
|
||||
style={{ }}
|
||||
>
|
||||
{loading ? <CircularProgress size={24} /> : isTenzirActive ? "Connected to siem" : "Connect to siem"}
|
||||
</Button>
|
||||
</Box>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
mb: 2,
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
}}
|
||||
>
|
||||
<TextField
|
||||
label="Search rules"
|
||||
variant="outlined"
|
||||
size="small"
|
||||
sx={{ mr: 2 }}
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
/>
|
||||
{/* <Button
|
||||
color="primary"
|
||||
variant="contained"
|
||||
onClick={() => uploadRef.current.click()}
|
||||
>
|
||||
<PublishIcon /> Upload sigma file
|
||||
</Button>
|
||||
<input
|
||||
hidden
|
||||
type="file"
|
||||
multiple
|
||||
ref={uploadRef}
|
||||
onChange={(event) => {
|
||||
uploadFiles(event.target.files);
|
||||
}}
|
||||
/> */}
|
||||
</Box>
|
||||
<Box sx={{ display: "flex", alignItems: "center" }}>
|
||||
<Typography variant="body2" sx={{ mr: 1 }}>
|
||||
Global disable/enable
|
||||
</Typography>
|
||||
<Switch
|
||||
checked={!folderDisabled}
|
||||
onChange={() =>
|
||||
handleDirectoryChange(folderDisabled, setFolderDisabled, globalUrl, isTenzirActive)
|
||||
}
|
||||
disabled={!isTenzirActive}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
<Box
|
||||
sx={{
|
||||
height: "500px",
|
||||
width: "100%",
|
||||
overflowY: "auto",
|
||||
p: 1,
|
||||
}}
|
||||
>
|
||||
{filteredRules?.length > 0 ?
|
||||
filteredRules.map((card) => {
|
||||
console.log("RULE CARD: ", card);
|
||||
|
||||
return (
|
||||
<DetectionRuleCard
|
||||
key={card.file_id}
|
||||
ruleName={card.title}
|
||||
description={card.description}
|
||||
file_id={card.file_id}
|
||||
globalUrl={globalUrl}
|
||||
folderDisabled={folderDisabled}
|
||||
isTenzirActive={isTenzirActive}
|
||||
{...card}
|
||||
/>
|
||||
)
|
||||
})
|
||||
: null }
|
||||
</Box>
|
||||
</Paper>
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
||||
export default Detection;
|
||||
@@ -0,0 +1,455 @@
|
||||
import React, { useState, useEffect, } from "react";
|
||||
import {
|
||||
Container,
|
||||
Box,
|
||||
TextField,
|
||||
Switch,
|
||||
Typography,
|
||||
Button,
|
||||
CircularProgress,
|
||||
Paper,
|
||||
Divider,
|
||||
IconButton,
|
||||
Tooltip,
|
||||
} from "@mui/material";
|
||||
|
||||
import {
|
||||
OpenInNew as OpenInNewIcon,
|
||||
FmdGood as FmdGoodIcon,
|
||||
} from "@mui/icons-material"
|
||||
|
||||
import { toast } from "react-toastify";
|
||||
import theme from '../theme.jsx';
|
||||
import DetectionRuleCard from "../components/DetectionRuleCard.jsx";
|
||||
import {
|
||||
green,
|
||||
red,
|
||||
grey,
|
||||
} from "../views/AngularWorkflow.jsx"
|
||||
|
||||
import WorkflowValidationTimeline from "../components/WorkflowValidationTimeline.jsx"
|
||||
|
||||
const handleDirectoryChange = (folderDisabled, setFolderDisabled, globalUrl, isDetectionActive) => {
|
||||
if (!isDetectionActive) {
|
||||
toast.warn("Connect to siem first for global enable/disable to work");
|
||||
return;
|
||||
}
|
||||
|
||||
const action = folderDisabled ? "enable_folder" : "disable_folder";
|
||||
const url = `${globalUrl}/api/v1/detections/${action}`;
|
||||
|
||||
fetch(url, {
|
||||
method: "PUT",
|
||||
credentials: "include",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
})
|
||||
.then((response) =>
|
||||
response.json().then((responseJson) => {
|
||||
if (responseJson["success"] === true) {
|
||||
if (action === "enable_folder") setFolderDisabled(false);
|
||||
else setFolderDisabled(true);
|
||||
} else {
|
||||
//toast(`failed to disable rule`);
|
||||
}
|
||||
})
|
||||
)
|
||||
.catch((error) => {
|
||||
console.log(`Error in ${action} the rule: `, error);
|
||||
toast(`An error occurred while ${action} the rule`);
|
||||
});
|
||||
};
|
||||
|
||||
const DetectionExplorer = (props) => {
|
||||
const { globalUrl, userdata, ruleInfo, folderDisabled, setFolderDisabled, detectionInfo, importDetectionFromUrl, rulesLoading, isDetectionActive, setIsDetectionActive, ruleMapping, setRuleMapping, } = props;
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const [workflow, setWorkflow] = useState({})
|
||||
const [detectionWorkflowId, setDetectionWorkflowId] = useState("")
|
||||
const [isDetectionValid, setIsDetectionValid] = useState(false)
|
||||
const [availableDetection, setAvailableDetection] = React.useState([]);
|
||||
const [environmentList, setEnvironmentList] = React.useState([])
|
||||
|
||||
const loadUsecases = () => {
|
||||
const url = `${globalUrl}/api/v1/workflows/usecases`
|
||||
fetch(url, {
|
||||
method: "GET",
|
||||
credentials: "include",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
})
|
||||
.then((response) =>
|
||||
response.json().then((responseJson) => {
|
||||
if (responseJson.success === false) {
|
||||
return
|
||||
}
|
||||
|
||||
if (responseJson.length == 0) {
|
||||
return
|
||||
}
|
||||
|
||||
for (var usecaseCategory in responseJson) {
|
||||
const category = responseJson[usecaseCategory]
|
||||
if (!category.name.toLowerCase().includes("respond") && !category.name.toLowerCase().includes("response")) {
|
||||
continue
|
||||
}
|
||||
|
||||
setAvailableDetection(category.list)
|
||||
break
|
||||
}
|
||||
})
|
||||
)
|
||||
.catch((error) => {
|
||||
console.log(`Error in loading usecases: `, error);
|
||||
//toast(`An error occurred while loading usecases`);
|
||||
})
|
||||
}
|
||||
|
||||
const loadWorkflow = (workflowId) => {
|
||||
const url = `${globalUrl}/api/v1/workflows/${workflowId}`
|
||||
fetch(url, {
|
||||
method: "GET",
|
||||
credentials: "include",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
})
|
||||
.then((response) =>
|
||||
response.json().then((responseJson) => {
|
||||
if (responseJson.id === workflowId) {
|
||||
setWorkflow(responseJson)
|
||||
} else {
|
||||
toast(`Failed to load workflow ${workflowId}`);
|
||||
}
|
||||
}))
|
||||
.catch((error) => {
|
||||
console.log(`Error in loading workflow ${workflowId}: `, error);
|
||||
toast(`An error occurred while loading workflow ${workflowId}`);
|
||||
})
|
||||
}
|
||||
|
||||
const handleConnectClick = () => {
|
||||
if (detectionWorkflowId !== "") {
|
||||
// FIXME: Show the Usecase UI for how to fix the workflow(s)
|
||||
// Instead loading full workflow and showing it directly? Hmm
|
||||
//toast.warn("Please reload the UI to load the detection status")
|
||||
return
|
||||
}
|
||||
|
||||
if (isDetectionActive) {
|
||||
return
|
||||
}
|
||||
|
||||
if (detectionInfo.category === undefined || detectionInfo.category === null) {
|
||||
toast.warn("Detection category not found. Please try again or contact support@shuffler.io if you think this is a bug.")
|
||||
return
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
const url = `${globalUrl}/api/v1/detections/${detectionInfo?.category}/connect`
|
||||
|
||||
fetch(url, {
|
||||
method: "GET",
|
||||
credentials: "include",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
})
|
||||
.then((response) =>
|
||||
response.json().then((responseJson) => {
|
||||
if (responseJson["success"] === true) {
|
||||
setLoading(false)
|
||||
|
||||
if (setIsDetectionActive !== undefined) {
|
||||
setIsDetectionActive(true)
|
||||
}
|
||||
|
||||
if (responseJson.workflow_id !== undefined && responseJson.workflow_id !== null) {
|
||||
setDetectionWorkflowId(responseJson.workflow_id)
|
||||
|
||||
loadWorkflow(responseJson.workflow_id)
|
||||
}
|
||||
|
||||
if (responseJson.workflow_valid !== undefined && responseJson.workflow_valid !== null) {
|
||||
setIsDetectionValid(responseJson.workflow_valid)
|
||||
}
|
||||
} else {
|
||||
if (responseJson.reason !== undefined && responseJson.reason !== null) {
|
||||
toast(responseJson.reason)
|
||||
} else {
|
||||
if (responseJson.workflow_id === "" && responseJson.workflow_valid === false) {
|
||||
toast.info(`Sent job to generate a Detection Workflow and enable ${detectionInfo?.category}. Please wait a minute and reload this UI.`);
|
||||
} else {
|
||||
toast.error(`Failed to connect to ${detectionInfo?.category}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (responseJson.action !== undefined && responseJson.actio !== null && responseJson.action.length > 0) {
|
||||
//if (responseJson.action === "environment_create") {
|
||||
// navigate("/admin?tab=environments")
|
||||
//}
|
||||
}
|
||||
|
||||
setLoading(false);
|
||||
}
|
||||
})
|
||||
)
|
||||
.catch((error) => {
|
||||
setLoading(false);
|
||||
console.log(`Error in connecting to ${detectionInfo?.category}: `, error);
|
||||
toast.error(`An error occurred while connecting to ${detectionInfo?.category}`);
|
||||
});
|
||||
}
|
||||
|
||||
const loadEnvironments = () => {
|
||||
const url = `${globalUrl}/api/v1/getenvironments`
|
||||
fetch(url, {
|
||||
method: "GET",
|
||||
credentials: "include",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
})
|
||||
.then((response) => {
|
||||
return response.json()
|
||||
})
|
||||
.then((responseJson) => {
|
||||
if (responseJson.success === false) {
|
||||
return
|
||||
}
|
||||
|
||||
if (responseJson.length == 0) {
|
||||
return
|
||||
}
|
||||
|
||||
setEnvironmentList(responseJson)
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log(`Error in loading environments: `, error);
|
||||
})
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
loadUsecases()
|
||||
loadEnvironments()
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (detectionInfo === undefined || detectionInfo === null) {
|
||||
return
|
||||
}
|
||||
|
||||
if (detectionInfo.category === undefined || detectionInfo.category === null || detectionInfo.category === "") {
|
||||
return
|
||||
}
|
||||
|
||||
handleConnectClick()
|
||||
}, [detectionInfo])
|
||||
|
||||
const filteredRules = ruleInfo === "default" ? [] : ruleInfo?.filter((rule) =>
|
||||
rule.title.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
rule.description.toLowerCase().includes(searchQuery.toLowerCase())
|
||||
)
|
||||
|
||||
const lakeNodes = environmentList !== undefined && environmentList !== null ? environmentList.filter((env) => env?.data_lake?.enabled === true).length : 0
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<Paper
|
||||
style={{
|
||||
marginTop: 50,
|
||||
width: "100%",
|
||||
padding: 50,
|
||||
backgroundColor: theme.palette.backgroundColor,
|
||||
borderRadius: theme.palette?.borderRadius,
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
mb: 2,
|
||||
}}
|
||||
>
|
||||
<Typography variant="h6" component="div">
|
||||
{detectionInfo?.title} {filteredRules === undefined || filteredRules === null ? null : `(${filteredRules?.length} rules)`}
|
||||
</Typography>
|
||||
|
||||
<div style={{display: "flex", }}>
|
||||
{workflow !== undefined && workflow !== null && workflow.id !== undefined && workflow.id !== null && workflow.id.length > 0 ?
|
||||
<div style={{display: "flex", }}>
|
||||
<div style={{minWidth: 400, maxWidth: 400, }}>
|
||||
<WorkflowValidationTimeline
|
||||
originalWorkflow={workflow}
|
||||
|
||||
apps={[]}
|
||||
getParents={undefined}
|
||||
execution={undefined}
|
||||
|
||||
workflow={workflow}
|
||||
|
||||
showHoverColor={true}
|
||||
globalUrl={globalUrl}
|
||||
userdata={userdata}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<IconButton
|
||||
variant="contained"
|
||||
color="secondary"
|
||||
onClick={() => {
|
||||
window.open(`/workflows/${workflow.id}`, "_blank")
|
||||
}}
|
||||
>
|
||||
<OpenInNewIcon />
|
||||
</IconButton>
|
||||
</div>
|
||||
|
||||
:
|
||||
<Button
|
||||
variant="contained"
|
||||
onClick={() => {
|
||||
handleConnectClick()
|
||||
}}
|
||||
disabled={loading} // Disable the button while loading
|
||||
style={{
|
||||
// Red = workflow exists, validation is false
|
||||
// Green = workflow exists, validation is true
|
||||
// Grey = workflow does not exist
|
||||
backgroundColor: detectionWorkflowId === "" ? grey : isDetectionValid ? green : red,
|
||||
}}
|
||||
>
|
||||
{loading ? <CircularProgress size={24} /> :
|
||||
detectionWorkflowId === "" ? `Connect to ${detectionInfo?.category}` :
|
||||
isDetectionValid ? `Connected to ${detectionInfo?.category}` : `Fix ${detectionInfo?.category} connection`}
|
||||
</Button>
|
||||
}
|
||||
|
||||
{detectionInfo?.category === "SIGMA" || detectionInfo?.category === "SIEM" ?
|
||||
<Tooltip title={`You have ${lakeNodes} available Data Lake node(s)`}>
|
||||
<a href="/admin?tab=environments" style={{textDecoration: "none", color: "inherit", }} target="_blank" rel="noreferrer">
|
||||
<FmdGoodIcon style={{marginLeft: 15, marginTop: 5, color: lakeNodes > 0 ? green : red}} />
|
||||
</a>
|
||||
</Tooltip>
|
||||
: null}
|
||||
</div>
|
||||
|
||||
</Box>
|
||||
{filteredRules?.length > 0 ?
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
mb: 2,
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
minHeight: 50,
|
||||
maxHeight: 50,
|
||||
}}
|
||||
>
|
||||
<TextField
|
||||
label="Search rules"
|
||||
variant="outlined"
|
||||
size="small"
|
||||
sx={{ mr: 2 }}
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
/>
|
||||
</Box>
|
||||
<Box sx={{ display: "flex", alignItems: "center" }}>
|
||||
<Typography variant="body2" sx={{ mr: 1 }}>
|
||||
Global disable/enable
|
||||
</Typography>
|
||||
<Switch
|
||||
checked={!folderDisabled}
|
||||
onChange={() =>
|
||||
handleDirectoryChange(folderDisabled, setFolderDisabled, globalUrl, isDetectionActive)
|
||||
}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
: null}
|
||||
<Divider />
|
||||
<Box
|
||||
sx={{
|
||||
height: "500px",
|
||||
width: "100%",
|
||||
overflowY: "auto",
|
||||
p: 1,
|
||||
}}
|
||||
>
|
||||
|
||||
{filteredRules?.length > 0 ?
|
||||
|
||||
ruleMapping !== undefined && ruleMapping !== null && ruleMapping.value !== undefined && ruleMapping.value !== null ?
|
||||
filteredRules.map((rule, index) => {
|
||||
return (
|
||||
<div style={{marginTop: 5, }}>
|
||||
<DetectionRuleCard
|
||||
globalUrl={globalUrl}
|
||||
key={index}
|
||||
ruleName={rule.file_name}
|
||||
description={rule.description}
|
||||
|
||||
file_id={rule.file_id}
|
||||
globalUrl={globalUrl}
|
||||
folderDisabled={folderDisabled}
|
||||
isDetectionActive={isDetectionActive}
|
||||
|
||||
ruleMapping={ruleMapping}
|
||||
setRuleMapping={setRuleMapping}
|
||||
|
||||
availableDetection={availableDetection}
|
||||
{...rule}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
: null
|
||||
:
|
||||
<div style={{textAlign: "center", }}>
|
||||
{rulesLoading === true ?
|
||||
<Container style={{ display: "flex", justifyContent: "center", alignItems: "center", marginTop: 25, }}>
|
||||
<div>
|
||||
<CircularProgress />
|
||||
<Typography variant="h6" style={{ marginTop: 20 }}>Downloading rules, please wait...</Typography>
|
||||
</div>
|
||||
</Container>
|
||||
:
|
||||
<div>
|
||||
<Typography variant="h6" color="textSecondary" style={{marginTop: 50, }}>
|
||||
No rules loaded yet
|
||||
</Typography>
|
||||
<Button
|
||||
style={{marginTop: 20, }}
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={() => {
|
||||
if (importDetectionFromUrl !== undefined) {
|
||||
importDetectionFromUrl(true, detectionInfo.download_repo)
|
||||
} else {
|
||||
toast("Import function not found. Please contact support@shuffler.io")
|
||||
}
|
||||
}}
|
||||
>
|
||||
Load Default Rules
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</Box>
|
||||
</Paper>
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
||||
export default DetectionExplorer;
|
||||
@@ -0,0 +1,299 @@
|
||||
import React, { useState, useEffect, } from "react";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
IconButton,
|
||||
Typography,
|
||||
Switch,
|
||||
Tooltip,
|
||||
Select,
|
||||
MenuItem,
|
||||
Divider,
|
||||
FormLabel,
|
||||
} from "@mui/material";
|
||||
|
||||
import DashboardBarchart, { LoadStats } from '../components/DashboardBarchart.jsx';
|
||||
import {
|
||||
Edit as EditIcon,
|
||||
} from "@mui/icons-material";
|
||||
import { toast } from "react-toastify";
|
||||
import ShuffleCodeEditor from "../components/ShuffleCodeEditor1.jsx";
|
||||
import theme from '../theme.jsx';
|
||||
|
||||
|
||||
const RuleCard = ({ ruleName, description, file_id, globalUrl, folderDisabled, isTenzirActive, availableDetection, ruleMapping, setRuleMapping, ...otherProps }) => {
|
||||
const [openCodeEditor, setOpenCodeEditor] = React.useState(false);
|
||||
const [fileData, setFileData] = React.useState("");
|
||||
const [isEnabled, setIsEnabled] = React.useState(otherProps.is_enabled);
|
||||
const [filteredBarchart, setFilteredBarchart] = React.useState(null)
|
||||
|
||||
const [responseValue, setResponseValue] = React.useState("No response action")
|
||||
const isCloud = ["localhost:3002", "shuffler.io"].includes(window.location.host);
|
||||
|
||||
console.log("Rulemapping: ", ruleMapping)
|
||||
useEffect(() => {
|
||||
|
||||
//const url = `${globalUrl}/api/v1/stats/app_executions_test2`
|
||||
//const resp = LoadStats(globalUrl, ruleName)
|
||||
//const resp = LoadStats(globalUrl, "app_executions_test2")
|
||||
const resp = LoadStats(globalUrl, "app_executions_cloud")
|
||||
resp.then((data) => {
|
||||
if (data === undefined) {
|
||||
setFilteredBarchart([])
|
||||
} else {
|
||||
setFilteredBarchart(data)
|
||||
}
|
||||
})
|
||||
|
||||
if (ruleMapping !== undefined && ruleMapping !== null && ruleMapping.value !== undefined && ruleMapping.value !== null) {
|
||||
console.log("FIX MAPPING FROM ruleMapping.value: ", ruleMapping)
|
||||
}
|
||||
}, [])
|
||||
|
||||
console.log("Response Value: ", responseValue)
|
||||
|
||||
const handleSwitchChange = (event) => {
|
||||
if (folderDisabled) {
|
||||
toast.warn("Enable the directory to enable individual rules");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isTenzirActive) {
|
||||
toast.warn("Connect to the siem first to enable/disable the rule");
|
||||
return;
|
||||
}
|
||||
|
||||
const newIsEnabled = event.target.checked;
|
||||
toggleRule(file_id, !newIsEnabled, globalUrl, () => {
|
||||
setIsEnabled(newIsEnabled);
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
const UpdateText = (text) => {
|
||||
fetch(`${globalUrl}/api/v1/files/${file_id}/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();
|
||||
})
|
||||
.then((responseJson) => {
|
||||
if (responseJson.success === true) {
|
||||
toast("Successfully updated rule");
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
toast("Error updating file: " + error.toString());
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Card style={{
|
||||
borderRadius: theme.palette?.borderRadius,
|
||||
minHeight: 100,
|
||||
marginBottom: 10,
|
||||
paddingBottom: 0,
|
||||
}}>
|
||||
<CardContent
|
||||
style={{
|
||||
padding: "10px 30px 0px 30px",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
color: "white",
|
||||
}}
|
||||
>
|
||||
<Typography variant="h6">{ruleName.replaceAll("_", " ")} ({filteredBarchart === null || filteredBarchart.total === undefined ? 0 : filteredBarchart.total})</Typography>
|
||||
<div style={{ display: 'flex', alignItems: 'center' }}>
|
||||
|
||||
<Select
|
||||
MenuProps={{
|
||||
disableScrollLock: true,
|
||||
}}
|
||||
labelId="Response Action"
|
||||
value={responseValue}
|
||||
SelectDisplayProps={{
|
||||
style: {
|
||||
color: "rgba(255,255,255,0.4)",
|
||||
},
|
||||
}}
|
||||
fullWidth
|
||||
onChange={(e) => {
|
||||
toast("Changing response: " + e.target.value)
|
||||
console.log("Target: ", e.target.value)
|
||||
|
||||
setResponseValue(e.target.value)
|
||||
|
||||
// FIXME: Handle:
|
||||
// 1. Get the current cache for the detection
|
||||
// 2. Create a new mapping for Detection -> Response
|
||||
}}
|
||||
style={{
|
||||
backgroundColor: theme.palette.inputColor,
|
||||
color: "white",
|
||||
height: 40,
|
||||
borderRadius: theme.palette?.borderRadius,
|
||||
}}
|
||||
>
|
||||
<MenuItem
|
||||
style={{
|
||||
backgroundColor: theme.palette.inputColor,
|
||||
color: "white",
|
||||
}}
|
||||
value="No response action"
|
||||
>
|
||||
<em>No selected response</em>
|
||||
</MenuItem>
|
||||
|
||||
<Divider />
|
||||
|
||||
{availableDetection === undefined || availableDetection === null ? null : availableDetection.map((data, index) => {
|
||||
return (
|
||||
<MenuItem
|
||||
key={index}
|
||||
style={{
|
||||
backgroundColor: theme.palette.inputColor,
|
||||
color: "white",
|
||||
overflowX: "auto",
|
||||
}}
|
||||
value={data.name}
|
||||
>
|
||||
{data.name}
|
||||
</MenuItem>
|
||||
)
|
||||
})}
|
||||
</Select>
|
||||
|
||||
|
||||
<Tooltip title="Edit Rule" placement="top">
|
||||
<IconButton onClick={() => openEditBar(file_id, setOpenCodeEditor, setFileData, globalUrl)}>
|
||||
<EditIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Tooltip title={isEnabled && !folderDisabled ? "Disable Rule" : "Enable Rule"} placement="top">
|
||||
<Switch
|
||||
checked={isEnabled && !folderDisabled}
|
||||
onChange={handleSwitchChange}
|
||||
disabled={false}
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{
|
||||
overflow: 'visible',
|
||||
zIndex: 10,
|
||||
//border: "1px solid rgba(255,255,255,0.3)",
|
||||
borderRadius: theme.palette?.borderRadius,
|
||||
marginTop: 5,
|
||||
|
||||
minHeight: 40,
|
||||
maxHeight: 40,
|
||||
}}>
|
||||
{filteredBarchart === null ? null :
|
||||
<DashboardBarchart
|
||||
timelineData={filteredBarchart}
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
|
||||
{/*
|
||||
<Typography variant="body2" style={{ marginTop: '2%' }}>
|
||||
{description}
|
||||
</Typography>
|
||||
*/}
|
||||
|
||||
<ShuffleCodeEditor
|
||||
isCloud={isCloud}
|
||||
expansionModalOpen={openCodeEditor}
|
||||
setExpansionModalOpen={setOpenCodeEditor}
|
||||
setcodedata={setFileData}
|
||||
codedata={fileData}
|
||||
isFileEditor={true}
|
||||
key={fileData} // https://reactjs.org/docs/reconciliation.html#recursing-on-children
|
||||
runUpdateText={UpdateText}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
const toggleRule = (fileId, isCurrentlyEnabled, globalUrl, callback) => {
|
||||
const action = isCurrentlyEnabled ? "disable" : "enable";
|
||||
const url = `${globalUrl}/api/v1/detections/${fileId}/${action}_rule`;
|
||||
|
||||
fetch(url, {
|
||||
method: "PUT",
|
||||
credentials: "include",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
})
|
||||
.then((response) =>
|
||||
response.json().then((responseJson) => {
|
||||
if (responseJson["success"] === false) {
|
||||
toast(`Failed to ${action} the rule`);
|
||||
} else {
|
||||
toast(`Rule ${action}d successfully`);
|
||||
callback();
|
||||
}
|
||||
})
|
||||
)
|
||||
.catch((error) => {
|
||||
console.log(`Error in ${action}ing the rule: `, error);
|
||||
toast(`An error occurred while ${action}ing the rule`);
|
||||
});
|
||||
};
|
||||
|
||||
const openEditBar = (file_id, setOpenCodeEditor, setFileData, globalUrl) => {
|
||||
getFileContent(file_id, setFileData, globalUrl)
|
||||
|
||||
setOpenCodeEditor(true);
|
||||
};
|
||||
|
||||
const getFileContent = (file_id, setFileData, globalUrl) => {
|
||||
setFileData("");
|
||||
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) => {
|
||||
if (respdata.length === 0) {
|
||||
toast("Failed getting file. Is it deleted?");
|
||||
return;
|
||||
}
|
||||
return respdata
|
||||
})
|
||||
.then((responseData) => {
|
||||
|
||||
setFileData(responseData);
|
||||
})
|
||||
.catch((error) => {
|
||||
toast(error.toString());
|
||||
});
|
||||
};
|
||||
export default RuleCard;
|
||||
@@ -75,7 +75,12 @@ const DiscordChat = props => {
|
||||
fullWidth
|
||||
value={currentRefinement}
|
||||
onChange={(event) => refine(event.currentTarget.value)}
|
||||
placeholder="Search Discord Chats"
|
||||
onKeyDown={(event) => {
|
||||
if(event.key === "Enter") {
|
||||
event.preventDefault();
|
||||
}
|
||||
}}
|
||||
placeholder="Search Discord Chats..."
|
||||
style={{ backgroundColor: theme.palette.inputColor, borderRadius: borderRadius, margin: 10, width: "100%", }}
|
||||
InputProps={{
|
||||
style: {
|
||||
@@ -143,7 +148,7 @@ const DiscordChat = props => {
|
||||
const CustomHits = connectHits(Hits);
|
||||
|
||||
return (
|
||||
<div style={{ width: "100%", textAlign:"center", position: "relative", height: "100%", }}>
|
||||
<div style={{textAlign:"center", position: "relative", height: "100%", padding:"0px 240px" }}>
|
||||
<InstantSearch searchClient={searchClient} indexName="discord_chat">
|
||||
<div style={{ maxWidth: 450, margin: "auto", marginTop: 15, marginBottom: 5, }}>
|
||||
<CustomSearchBox />
|
||||
|
||||
@@ -124,6 +124,11 @@ const DocsGrid = props => {
|
||||
removeQuery("q")
|
||||
refine(event.currentTarget.value)
|
||||
}}
|
||||
onKeyDown={(event) => {
|
||||
if(event.key === "Enter") {
|
||||
event.preventDefault();
|
||||
}
|
||||
}}
|
||||
limit={5}
|
||||
/>
|
||||
{/*isSearchStalled ? 'My search is stalled' : ''*/}
|
||||
@@ -274,7 +279,7 @@ const DocsGrid = props => {
|
||||
</Button>
|
||||
</div>
|
||||
*/}
|
||||
<div style={{width: "100%", position: "relative", height: "100%",}}>
|
||||
<div style={{width: "100%", position: "relative", height: "100%", padding: "0px 180px"}}>
|
||||
<InstantSearch searchClient={searchClient} indexName="documentation">
|
||||
<div style={{maxWidth: 450, margin: "auto", marginTop: 15, marginBottom: 15, }}>
|
||||
<CustomSearchBox />
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,546 @@
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import { Box, Typography, IconButton, CircularProgress, Tooltip } from '@mui/material';
|
||||
import { CheckCircle, Error, ArrowBack, Close, Cached as CachedIcon, Pause as PauseIcon } from '@mui/icons-material';
|
||||
import theme from '../theme.jsx';
|
||||
import ReactJson from "react-json-view-ssr";
|
||||
import { toast } from 'react-toastify';
|
||||
import { validateJson } from "../views/Workflows.jsx";
|
||||
// import HandleJsonCopy from "./ShuffleCodeEditor1";
|
||||
|
||||
const STATUS_CONFIG = {
|
||||
EXECUTING: {
|
||||
color: '#64B5F6',
|
||||
icon: () => <CircularProgress size={16} thickness={4} sx={{ color: '#64B5F6' }} />,
|
||||
label: 'Executing'
|
||||
},
|
||||
SUCCESS: {
|
||||
color: '#4CAF50',
|
||||
icon: () => <CheckCircle sx={{ color: '#4CAF50', fontSize: 16 }} />,
|
||||
label: 'Success'
|
||||
},
|
||||
FINISHED: {
|
||||
color: '#4CAF50',
|
||||
icon: () => <CheckCircle sx={{ color: '#4CAF50', fontSize: 16 }} />,
|
||||
label: 'Finished'
|
||||
},
|
||||
ABORTED: {
|
||||
color: '#F44336',
|
||||
icon: () => <Error sx={{ color: '#F44336', fontSize: 16 }} />,
|
||||
label: 'Aborted'
|
||||
}
|
||||
};
|
||||
|
||||
let to_be_copied = ""
|
||||
|
||||
const handleReactJsonClipboard = (copy) => {
|
||||
toast("Copied JSON path to clipboard, NOT Path")
|
||||
};
|
||||
|
||||
|
||||
const HandleJsonCopy = (base, copy, base_node_name) => {
|
||||
if (typeof copy.name === "string") {
|
||||
copy.name = copy.name.replaceAll(" ", "_");
|
||||
}
|
||||
|
||||
//lol
|
||||
if (typeof base === 'object' || typeof base === 'dict') {
|
||||
base = JSON.stringify(base)
|
||||
}
|
||||
|
||||
if (base_node_name === "execution_argument" || base_node_name === "Execution Argument") {
|
||||
base_node_name = "exec"
|
||||
}
|
||||
|
||||
console.log("COPY: ", base_node_name, copy);
|
||||
|
||||
//var newitem = JSON.parse(base);
|
||||
var newitem = validateJson(base).result
|
||||
to_be_copied = "$" + base_node_name.toLowerCase().replaceAll(" ", "_");
|
||||
for (let copykey in copy.namespace) {
|
||||
if (copy.namespace[copykey].includes("Results for")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (newitem !== undefined && newitem !== null) {
|
||||
newitem = newitem[copy.namespace[copykey]];
|
||||
if (!isNaN(copy.namespace[copykey])) {
|
||||
to_be_copied += ".#";
|
||||
} else {
|
||||
to_be_copied += "." + copy.namespace[copykey];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (newitem !== undefined && newitem !== null) {
|
||||
newitem = newitem[copy.name];
|
||||
if (!isNaN(copy.name)) {
|
||||
to_be_copied += ".#";
|
||||
} else {
|
||||
to_be_copied += "." + copy.name;
|
||||
}
|
||||
}
|
||||
|
||||
to_be_copied.replaceAll(" ", "_");
|
||||
const elementName = "copy_element_shuffle";
|
||||
var copyText = document.getElementById(elementName);
|
||||
if (copyText !== null && copyText !== undefined) {
|
||||
console.log("NAVIGATOR: ", navigator);
|
||||
const clipboard = navigator.clipboard;
|
||||
if (clipboard === undefined) {
|
||||
toast("Can only copy over HTTPS (port 3443)");
|
||||
return;
|
||||
}
|
||||
|
||||
navigator.clipboard.writeText(to_be_copied);
|
||||
copyText.select();
|
||||
copyText.setSelectionRange(0, 99999); /* For mobile devices *
|
||||
|
||||
/* Copy the text inside the text field */
|
||||
document.execCommand("copy");
|
||||
toast("Copied JSON path to clipboard.")
|
||||
console.log("COPYING!");
|
||||
} else {
|
||||
console.log("Couldn't find element ", elementName);
|
||||
}
|
||||
}
|
||||
|
||||
const ExecuteWorkflow = async (executionData, globalUrl) => {
|
||||
try {
|
||||
const workflowData = executionData.workflow;
|
||||
|
||||
// Execute workflow with original parameters
|
||||
await fetch(`${globalUrl}/api/v1/workflows/${workflowData.id}/execute`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json'
|
||||
},
|
||||
credentials: 'include',
|
||||
body: workflowData
|
||||
}).then(response => {
|
||||
window.location.href = `/workflows/${workflowData.id}/code?execution_id=` + response.json().execution_id;
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error re-executing workflow:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const ExecutionsList = ({ executions, onSelectExecution, activeExecutionId }) => {
|
||||
return (
|
||||
<Box>
|
||||
{executions.map((execution) => {
|
||||
const status = STATUS_CONFIG[execution.status] || STATUS_CONFIG.ABORTED;
|
||||
return (
|
||||
<Box
|
||||
key={execution.execution_id}
|
||||
onClick={() => onSelectExecution(execution)}
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
cursor: 'pointer',
|
||||
py: 1,
|
||||
px: 2,
|
||||
borderBottom: '1px solid #2A2A2A',
|
||||
backgroundColor: activeExecutionId === execution.execution_id ?
|
||||
'rgba(255,255,255,0.05)' : 'transparent',
|
||||
'&:hover': {
|
||||
backgroundColor: 'rgba(255,255,255,0.05)'
|
||||
}
|
||||
}}
|
||||
>
|
||||
{status.icon()}
|
||||
|
||||
<Box sx={{ ml: 2, flex: 1, overflow: 'hidden' }}>
|
||||
<Typography variant="body2" sx={{
|
||||
color: '#E0E0E0',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap'
|
||||
}}>
|
||||
{new Date(execution.started_at * 1000).toLocaleString()}
|
||||
</Typography>
|
||||
<Typography variant="caption" sx={{
|
||||
color: status.color
|
||||
}}>
|
||||
{status.label}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
})}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
const ExecutionDetail = ({ execution: initialExecution, onBack, globalUrl, onExecutionUpdate, selectedAction, executeWorkflow }) => {
|
||||
const [execution, setExecution] = useState(initialExecution);
|
||||
const [status, setStatus] = useState(STATUS_CONFIG[execution.status] || STATUS_CONFIG.EXECUTING);
|
||||
const [validResult, setValidResult] = useState("{}")
|
||||
|
||||
const abortExecution = async () => {
|
||||
try {
|
||||
await fetch(`${globalUrl}/api/v1/workflows/${execution.workflow.id}/executions/${execution.execution_id}/abort`, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Accept: "application/json",
|
||||
},
|
||||
credentials: "include",
|
||||
}).then((response) => {
|
||||
if (response.ok) {
|
||||
const updatedExecution = {
|
||||
...execution,
|
||||
status: "ABORTED",
|
||||
};
|
||||
setExecution(updatedExecution);
|
||||
onExecutionUpdate(updatedExecution);
|
||||
}
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.log("Abort error:", error);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setStatus(STATUS_CONFIG[execution.status] || STATUS_CONFIG.EXECUTING);
|
||||
}, [execution]);
|
||||
|
||||
const pollExecutionStatus = useCallback(async () => {
|
||||
try {
|
||||
const response = await fetch(`${globalUrl}/api/v1/streams/results`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
credentials: 'include',
|
||||
body: JSON.stringify({
|
||||
execution_id: execution.execution_id,
|
||||
authorization: execution.authorization,
|
||||
}),
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
const currentStatus = data.results?.[0]?.status || 'EXECUTING';
|
||||
|
||||
const updatedExecution = {
|
||||
...execution,
|
||||
...data,
|
||||
status: currentStatus
|
||||
};
|
||||
|
||||
setExecution(updatedExecution);
|
||||
onExecutionUpdate(updatedExecution);
|
||||
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Polling error:', error);
|
||||
}
|
||||
}, [execution, globalUrl, onExecutionUpdate]);
|
||||
|
||||
useEffect(() => {
|
||||
let pollTimeout;
|
||||
if (execution.status === 'EXECUTING') {
|
||||
pollTimeout = setTimeout(() => pollExecutionStatus(), 3000);
|
||||
}
|
||||
|
||||
if (execution?.results?.length === 1) {
|
||||
setValidResult(JSON.parse(execution?.results[0]?.result || "{}"))
|
||||
}
|
||||
|
||||
return () => clearTimeout(pollTimeout);
|
||||
}, [execution.status, pollExecutionStatus]);
|
||||
|
||||
return (
|
||||
<Box sx={{ height: '100%', display: 'flex', flexDirection: 'column' }}>
|
||||
<Box sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
p: 2,
|
||||
borderBottom: '1px solid #454545'
|
||||
}}>
|
||||
<IconButton onClick={onBack} size="small" sx={{ mr: 2 }}>
|
||||
<ArrowBack fontSize="small" />
|
||||
</IconButton>
|
||||
<Typography variant="subtitle2" sx={{ color: '#E0E0E0', mr: 2 }}>
|
||||
Execution Details
|
||||
</Typography>
|
||||
{status.icon()}
|
||||
<Typography variant="body2" sx={{ ml: 1, color: status.color }}>
|
||||
{status.label}
|
||||
</Typography>
|
||||
|
||||
<Box sx={{ ml: 'auto' }}>
|
||||
{execution.status === "EXECUTING" && (
|
||||
<Tooltip title="Abort workflow" placement="top">
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={abortExecution}
|
||||
sx={{ color: theme.palette.error.main }}
|
||||
>
|
||||
<PauseIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
<Tooltip title="Rerun workflow (uses same startnode as the original)" placement="top">
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={() => {
|
||||
ExecuteWorkflow(
|
||||
execution,
|
||||
globalUrl
|
||||
);
|
||||
}}
|
||||
sx={{ color: theme.palette.primary.main }}
|
||||
>
|
||||
<CachedIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Box sx={{ flex: 1, overflow: 'auto', p: 2 }}>
|
||||
<Box sx={{ mb: 3 }}>
|
||||
<Typography variant="caption" sx={{ color: '#888', display: 'block', mb: 1 }}>
|
||||
Started at
|
||||
</Typography>
|
||||
<Typography variant="body2">
|
||||
{new Date(execution.started_at * 1000).toLocaleString()}
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
<Box sx={{ mb: 3 }}>
|
||||
<Typography variant="caption" sx={{ color: '#888', display: 'block', mb: 1 }}>
|
||||
Execution ID
|
||||
</Typography>
|
||||
<Typography variant="body2" sx={{
|
||||
backgroundColor: '#2A2A2A',
|
||||
p: 1,
|
||||
borderRadius: 1,
|
||||
fontFamily: 'monospace'
|
||||
}}>
|
||||
{execution.execution_id}
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
<Box>
|
||||
<Box>
|
||||
<Typography variant="caption" sx={{ color: '#888', display: 'block', mb: 1 }}>
|
||||
Result
|
||||
</Typography>
|
||||
<Box sx={{
|
||||
backgroundColor: '#2A2A2A',
|
||||
borderRadius: 1,
|
||||
overflow: 'auto',
|
||||
maxHeight: '200px'
|
||||
}}>
|
||||
<pre style={{
|
||||
margin: 0,
|
||||
padding: '1rem',
|
||||
fontSize: '12px',
|
||||
color: status.color,
|
||||
fontFamily: "'JetBrains Mono', monospace"
|
||||
}}>
|
||||
{execution?.status === 'EXECUTING' ? (
|
||||
<Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center', p: 2 }}>
|
||||
<CircularProgress size={24} />
|
||||
<Typography sx={{ ml: 2, color: '#888' }}>Executing...</Typography>
|
||||
</Box>
|
||||
) : (
|
||||
execution?.results?.length === 1 ?
|
||||
<ReactJson
|
||||
src={validResult}
|
||||
theme={theme.palette.jsonTheme}
|
||||
style={{
|
||||
borderRadius: 5,
|
||||
border: `2px solid ${theme.palette.inputColor}`,
|
||||
padding: 10,
|
||||
maxHeight: 450,
|
||||
minheight: 450,
|
||||
overflow: "auto",
|
||||
minWidth: 450,
|
||||
maxWidth: "100%",
|
||||
zIndex: 1200
|
||||
}}
|
||||
enableClipboard={(copy) => {
|
||||
handleReactJsonClipboard(copy);
|
||||
}}
|
||||
collapsed={false}
|
||||
displayDataTypes={false}
|
||||
onSelect={(select) => {
|
||||
var basename = "exec"
|
||||
if (selectedAction !== undefined && selectedAction !== null && Object.keys(selectedAction).length !== 0) {
|
||||
basename = selectedAction.label.toLowerCase().replaceAll(" ", "_")
|
||||
}
|
||||
HandleJsonCopy(validResult, select, basename)
|
||||
}}
|
||||
name={"JSON autocompletion"}
|
||||
/> :
|
||||
<ReactJson
|
||||
src={{}}
|
||||
theme={theme.palette.jsonTheme}
|
||||
style={{
|
||||
borderRadius: 5,
|
||||
border: `2px solid ${theme.palette.inputColor}`,
|
||||
padding: 10,
|
||||
maxHeight: 450,
|
||||
minheight: 450,
|
||||
overflow: "auto",
|
||||
minWidth: 450,
|
||||
maxWidth: "100%",
|
||||
zIndex: 1200
|
||||
}}
|
||||
collapsed={false}
|
||||
enableClipboard={(copy) => { }}
|
||||
displayDataTypes={false}
|
||||
name={"JSON autocompletion"}
|
||||
/>
|
||||
)}
|
||||
</pre>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
const ExecutionPanel = ({
|
||||
workflow,
|
||||
globalUrl,
|
||||
onClose,
|
||||
currentExecution,
|
||||
mainAction
|
||||
}) => {
|
||||
const [executions, setExecutions] = useState([]);
|
||||
const [selectedExecution, setSelectedExecution] = useState(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
const handleExecutionUpdate = useCallback((updatedExecution) => {
|
||||
setExecutions(prevExecutions => {
|
||||
const updatedExecutions = [...prevExecutions];
|
||||
const index = updatedExecutions.findIndex(
|
||||
e => e.execution_id === updatedExecution.execution_id
|
||||
);
|
||||
if (index !== -1) {
|
||||
updatedExecutions[index] = updatedExecution;
|
||||
}
|
||||
return updatedExecutions;
|
||||
});
|
||||
}, []);
|
||||
|
||||
const fetchExecutions = useCallback(async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const response = await fetch(`${globalUrl}/api/v2/workflows/${workflow.id}/executions`, {
|
||||
credentials: 'include',
|
||||
});
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
setExecutions(data.executions);
|
||||
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const executionId = urlParams.get('execution_id');
|
||||
if (executionId) {
|
||||
const execution = data.executions.find(e => e.execution_id === executionId);
|
||||
if (execution) {
|
||||
setSelectedExecution(execution);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch executions:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [workflow.id, globalUrl]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchExecutions();
|
||||
}, [fetchExecutions]);
|
||||
|
||||
useEffect(() => {
|
||||
if (currentExecution?.execution_id) {
|
||||
setExecutions(prev => {
|
||||
const existingIndex = prev.findIndex(e => e.execution_id === currentExecution.execution_id);
|
||||
if (existingIndex === -1) {
|
||||
return [currentExecution, ...prev];
|
||||
}
|
||||
const updated = [...prev];
|
||||
updated[existingIndex] = currentExecution;
|
||||
return updated;
|
||||
});
|
||||
setSelectedExecution(currentExecution);
|
||||
}
|
||||
}, [currentExecution]);
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
position: 'relative',
|
||||
height: "fit-content",
|
||||
backgroundColor: '#1E1E1E',
|
||||
borderTop: '1px solid #454545',
|
||||
overflow: 'hidden',
|
||||
color: '#CCCCCC',
|
||||
fontFamily: "'JetBrains Mono', monospace",
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
}}
|
||||
>
|
||||
{loading && !executions.length ? (
|
||||
<Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center', height: '100%' }}>
|
||||
<CircularProgress size={24} />
|
||||
</Box>
|
||||
) : selectedExecution ? (
|
||||
<ExecutionDetail
|
||||
execution={selectedExecution}
|
||||
onBack={() => {
|
||||
setSelectedExecution(null);
|
||||
const url = new URL(window.location);
|
||||
url.searchParams.delete('execution_id');
|
||||
window.history.pushState({}, '', url);
|
||||
fetchExecutions();
|
||||
}}
|
||||
globalUrl={globalUrl}
|
||||
selecteAction={mainAction}
|
||||
onExecutionUpdate={handleExecutionUpdate}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<Box sx={{
|
||||
p: 2,
|
||||
borderBottom: '1px solid #454545',
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center'
|
||||
}}>
|
||||
<Typography variant="subtitle2" sx={{ color: '#E0E0E0' }}>
|
||||
Execution History
|
||||
</Typography>
|
||||
<IconButton size="small" onClick={onClose} sx={{ color: '#888' }}>
|
||||
<Close fontSize="small" />
|
||||
</IconButton>
|
||||
</Box>
|
||||
<Box sx={{ flex: 1, overflow: 'auto' }}>
|
||||
<ExecutionsList
|
||||
executions={executions}
|
||||
onSelectExecution={(execution) => {
|
||||
setSelectedExecution(execution);
|
||||
const url = new URL(window.location);
|
||||
url.searchParams.set('execution_id', execution.execution_id);
|
||||
window.history.pushState({}, '', url);
|
||||
}}
|
||||
activeExecutionId={currentExecution?.execution_id}
|
||||
/>
|
||||
</Box>
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
export default ExecutionPanel;
|
||||
@@ -227,7 +227,7 @@ const ExploreWorkflow = (props) => {
|
||||
<ArrowBackIosNewIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<div style={{ minWidth: 554, maxWidth: 554, borderRadius: theme.palette.borderRadius, }}>
|
||||
<div style={{ minWidth: 554, maxWidth: 554, borderRadius: theme.palette?.borderRadius, }}>
|
||||
<AliceCarousel
|
||||
style={{ backgroundColor: theme.palette.surfaceColor, minHeight: 750, maxHeight: 750, }}
|
||||
items={formattedCarousel}
|
||||
@@ -320,7 +320,7 @@ const ExploreWorkflow = (props) => {
|
||||
|
||||
<div style={{ marginTop: 0, }}>
|
||||
<div className="thumbs" style={{ display: "flex" }}>
|
||||
<div style={{ minWidth: isMobile ? 300 : 554, maxWidth: isMobile ? 300 : 554, borderRadius: theme.palette.borderRadius, }}>
|
||||
<div style={{ minWidth: isMobile ? 300 : 554, maxWidth: isMobile ? 300 : 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)">
|
||||
|
||||
+482
-378
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,687 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
|
||||
import { toast } from "react-toastify"
|
||||
import theme from '../theme.jsx';
|
||||
import AuthenticationOauth2 from "../components/Oauth2Auth.jsx";
|
||||
import { validateJson, GetIconInfo } from "../views/Workflows.jsx";
|
||||
|
||||
import {
|
||||
Tooltip,
|
||||
Typography,
|
||||
Button,
|
||||
Divider,
|
||||
|
||||
MenuItem,
|
||||
Select,
|
||||
Chip,
|
||||
TextField,
|
||||
CircularProgress,
|
||||
} from "@mui/material"
|
||||
|
||||
import {
|
||||
CheckCircleOutline as CheckCircleOutlineIcon,
|
||||
ErrorOutline as ErrorOutlineIcon,
|
||||
} from "@mui/icons-material"
|
||||
|
||||
import {
|
||||
green,
|
||||
red,
|
||||
} from "../views/AngularWorkflow.jsx"
|
||||
|
||||
const FixWorkflowValidationErrors = (props) => {
|
||||
const { globalUrl, workflow, setWorkflow, setUpdateParent, } = props;
|
||||
|
||||
const [appsLoading, setAppsLoading] = useState(false)
|
||||
const [apps, setApps] = useState([])
|
||||
const [appAuth, setAppAuth] = useState([])
|
||||
const [_, setUpdate] = useState(0)
|
||||
|
||||
const isCloud = window.location.host === "localhost:3002" || window.location.host === "shuffler.io" || window.location.host === "migration.shuffler.io";
|
||||
|
||||
if (workflow === undefined || workflow === null) {
|
||||
console.error("Workflow is undefined")
|
||||
return null
|
||||
}
|
||||
|
||||
if (workflow.validation === undefined || workflow.validation === null) {
|
||||
console.error("Workflow validation is undefined")
|
||||
return null
|
||||
}
|
||||
|
||||
if (workflow.validation.valid === true) {
|
||||
console.error("Workflow is valid - nothing to do for errors")
|
||||
return null
|
||||
}
|
||||
|
||||
if (setWorkflow === undefined || setWorkflow === null) {
|
||||
console.error("No setWorkflow")
|
||||
return null
|
||||
}
|
||||
|
||||
const fetchApps = () => {
|
||||
if (appsLoading === true) {
|
||||
return
|
||||
}
|
||||
|
||||
setAppsLoading(true)
|
||||
|
||||
const url = `${globalUrl}/api/v1/apps`
|
||||
fetch(url,{
|
||||
method: "GET",
|
||||
credentials: "include"
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
setAppsLoading(false)
|
||||
if (data.success === false) {
|
||||
return
|
||||
}
|
||||
|
||||
setApps(data)
|
||||
})
|
||||
.catch(error => {
|
||||
setAppsLoading(false)
|
||||
console.error("Error: ", error)
|
||||
})
|
||||
}
|
||||
|
||||
// Save the workflow as well
|
||||
const saveWorkflow = (workflow) => {
|
||||
if (workflow.id === undefined || workflow.id === null) {
|
||||
toast("Workflow ID is missing during save. Please try again")
|
||||
return
|
||||
}
|
||||
|
||||
const url = `${globalUrl}/api/v1/workflows/${workflow.id}`
|
||||
fetch(url, {
|
||||
method: "PUT",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Accept: "application/json",
|
||||
},
|
||||
credentials: "include",
|
||||
body: JSON.stringify(workflow),
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success === false) {
|
||||
toast("Failed to save workflow")
|
||||
return
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
toast("Failed to save workflow: " + error.toString())
|
||||
})
|
||||
}
|
||||
|
||||
const fetchAuthentication = (reset, updateAction, closeMenu, action_id) => {
|
||||
if (appsLoading === true) {
|
||||
return
|
||||
}
|
||||
|
||||
const url = `${globalUrl}/api/v1/apps/authentication`
|
||||
fetch(url,{
|
||||
method: "GET",
|
||||
credentials: "include"
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success === false) {
|
||||
return
|
||||
}
|
||||
|
||||
const authlist = data.data
|
||||
setAppAuth(authlist)
|
||||
if (updateAction === true) {
|
||||
console.log("Updating action: ", action_id)
|
||||
|
||||
// Find the action in the workflow and set auth for it
|
||||
var foundActionIndex = -1
|
||||
for (var i = 0; i < workflow.actions.length; i++) {
|
||||
if (workflow.actions[i].id === action_id) {
|
||||
foundActionIndex = i
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (foundActionIndex === -1) {
|
||||
console.error("Failed to find action in workflow")
|
||||
return
|
||||
}
|
||||
|
||||
const appId = workflow.actions[foundActionIndex].app_id
|
||||
var lastauth = -1
|
||||
for (var authKey in authlist) {
|
||||
if (authlist[authKey].app_id !== appId) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (authlist[authKey].created > lastauth) {
|
||||
lastauth = authlist[authKey].created
|
||||
} else {
|
||||
continue
|
||||
}
|
||||
|
||||
console.log("FOUND AUTH: ", authlist[authKey])
|
||||
workflow.actions[foundActionIndex].authentication_id = authlist[authKey].id
|
||||
}
|
||||
|
||||
|
||||
if (setWorkflow !== undefined) {
|
||||
setWorkflow(workflow)
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error("Auth loading error: ", error)
|
||||
})
|
||||
}
|
||||
|
||||
if (apps !== undefined && apps !== null && apps.length === 0 && appsLoading === false) {
|
||||
fetchApps()
|
||||
fetchAuthentication()
|
||||
}
|
||||
|
||||
const setSelectedAction = (action) => {
|
||||
if (workflow === undefined || workflow === null) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (workflow.actions === undefined || workflow.actions === null || workflow.actions.length === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (setWorkflow === undefined || setWorkflow === null) {
|
||||
return null
|
||||
}
|
||||
|
||||
for (var i = 0; i < workflow.actions.length; i++) {
|
||||
if (workflow.actions[i].id === action.id) {
|
||||
workflow.actions[i] = action
|
||||
|
||||
// Update any action with the same app_id to have same auth
|
||||
for (var j = 0; j < workflow.actions.length; j++) {
|
||||
if (workflow.actions[j].app_id === action.app_id) {
|
||||
workflow.actions[j].authentication_id = action.authentication_id
|
||||
workflow.actions[j].selectedAuthentication = action.selectedAuthentication
|
||||
}
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
setWorkflow(workflow)
|
||||
}
|
||||
|
||||
const ErrorItem = (props) => {
|
||||
const { apps, error, index } = props
|
||||
|
||||
const [validating, setValidating] = useState(false)
|
||||
const [actionRunInfo, setActionRunInfo] = useState({})
|
||||
if (error === undefined || error === null) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (apps === undefined || apps === null || apps.length === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
const validateApp = (app, action) => {
|
||||
if (validating) {
|
||||
return
|
||||
}
|
||||
|
||||
setValidating(true)
|
||||
|
||||
// FIXME: Run execution:
|
||||
// 1. Should set app authentication validation
|
||||
if (isCloud) {
|
||||
action.environment = "Cloud"
|
||||
} else {
|
||||
action.environment = "Shuffle"
|
||||
}
|
||||
|
||||
/*
|
||||
setExecutionResult({
|
||||
valid: false,
|
||||
result: baseResult,
|
||||
})
|
||||
setExecuting(true);
|
||||
*/
|
||||
|
||||
setActionRunInfo({})
|
||||
const url = `${globalUrl}/api/v1/apps/${app.id}/run?validation=true`
|
||||
fetch(url, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Accept: "application/json",
|
||||
},
|
||||
body: JSON.stringify(action),
|
||||
credentials: "include",
|
||||
})
|
||||
.then((response) => {
|
||||
if (response.status !== 200) {
|
||||
console.log("Status not 200 for stream results :O!");
|
||||
}
|
||||
|
||||
return response.json();
|
||||
})
|
||||
.then((responseJson) => {
|
||||
setValidating(false)
|
||||
|
||||
setActionRunInfo(responseJson)
|
||||
|
||||
//console.log("RESPONSE: ", responseJson)
|
||||
if (
|
||||
responseJson.success === true &&
|
||||
responseJson.result !== null &&
|
||||
responseJson.result !== undefined &&
|
||||
responseJson.result.length > 0
|
||||
) {
|
||||
//toast("
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
toast("Execution error: " + error.toString());
|
||||
setValidating(false)
|
||||
})
|
||||
}
|
||||
|
||||
var authReturn = null
|
||||
var validationReturn = null
|
||||
var foundApp = {
|
||||
"name": "",
|
||||
"id": "",
|
||||
}
|
||||
var foundAction = {
|
||||
"name": "",
|
||||
"label": "",
|
||||
"id": "",
|
||||
"app_id": "",
|
||||
"app_name": "",
|
||||
}
|
||||
|
||||
var selectedImage = null
|
||||
var resolveButton = null
|
||||
if (error.app_id !== undefined && error.app_id !== null) {
|
||||
for (var i = 0; i < apps.length; i++) {
|
||||
if (apps[i].id === error.app_id) {
|
||||
foundApp = apps[i]
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (!foundApp) {
|
||||
toast("Couldn't find relevant app. Is it activated?")
|
||||
return "Failed to find app"
|
||||
}
|
||||
|
||||
selectedImage =
|
||||
<img
|
||||
src={foundApp.large_image}
|
||||
style={{
|
||||
width: 25,
|
||||
height: 25,
|
||||
marginRight: 10,
|
||||
borderRadius: theme.palette?.borderRadius,
|
||||
border: `1px solid ${theme.palette.borderColor}`,
|
||||
}}
|
||||
/>
|
||||
}
|
||||
|
||||
if (error.action_id !== undefined && error.action_id !== null && workflow.actions !== undefined && workflow.actions !== null && workflow.actions.length > 0) {
|
||||
for (var i = 0; i < workflow.actions.length; i++) {
|
||||
if (workflow.actions[i].id === error.action_id) {
|
||||
foundAction = workflow.actions[i]
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const validationIcon = Object.getOwnPropertyNames(actionRunInfo).length === 0 ? null :
|
||||
<Tooltip
|
||||
title={
|
||||
<Typography variant="body1">
|
||||
{actionRunInfo.result}
|
||||
</Typography>
|
||||
}
|
||||
placement="bottom"
|
||||
>
|
||||
{actionRunInfo.validation.valid === true ?
|
||||
<CheckCircleOutlineIcon style={{color: green, marginRight: 10, }} />
|
||||
:
|
||||
<ErrorOutlineIcon style={{color: red, marginRight: 10, }} />
|
||||
}
|
||||
</Tooltip>
|
||||
|
||||
const authenticationType = foundApp.authentication
|
||||
if (error.type === "configuration" || error.type === "authentication") {
|
||||
// FIXME: Check the error
|
||||
if (appAuth === undefined || appAuth === null) {
|
||||
return "Loading auth"
|
||||
}
|
||||
|
||||
var relevantAuthentication = []
|
||||
var foundAuth = {}
|
||||
for (var key in appAuth) {
|
||||
if (appAuth[key].app.id !== error.app_id) {
|
||||
continue
|
||||
}
|
||||
|
||||
foundAuth = appAuth[key]
|
||||
relevantAuthentication.push(appAuth[key])
|
||||
}
|
||||
|
||||
if (foundAction.selectedAuthentication === undefined || foundAction.selectedAuthentication === null) {
|
||||
foundAction.selectedAuthentication = {}
|
||||
}
|
||||
|
||||
console.log("FOUNDACTION: ", foundAction, foundAuth)
|
||||
|
||||
var authFound = false
|
||||
if (foundAuth.id !== undefined && foundAuth.id !== null && foundAuth.id.length > 0) {
|
||||
var authGroups = []
|
||||
// Choose from a dropdown
|
||||
authReturn = <Select
|
||||
MenuProps={{
|
||||
disableScrollLock: true,
|
||||
}}
|
||||
labelId="select-app-auth"
|
||||
value={
|
||||
foundAction.authentication_id === "authgroups" ? "authgroups" :
|
||||
Object.getOwnPropertyNames(foundAction.selectedAuthentication).length === 0
|
||||
? "No selection"
|
||||
: foundAction.selectedAuthentication
|
||||
}
|
||||
SelectDisplayProps={{
|
||||
style: {
|
||||
},
|
||||
}}
|
||||
fullWidth
|
||||
onChange={(e) => {
|
||||
if (e.target.value === "No selection") {
|
||||
foundAction.selectedAuthentication = {};
|
||||
foundAction.authentication_id = "";
|
||||
|
||||
for (let [key,keyval] in Object.entries(foundAction.parameters)) {
|
||||
if (foundAction.parameters[key].configuration === false) {
|
||||
//console.log("FIELDSKIP: ", foundAction.parameters[key].name)
|
||||
continue
|
||||
}
|
||||
|
||||
if (foundAction.parameters[key].name === "url" && authenticationType?.type === "oauth2-app" && foundAction.parameters[key].value.includes("http")) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (foundAction.parameters[key].example !== undefined && foundAction.parameters[key].example !== null && foundAction.parameters[key].example !== "") {
|
||||
if (foundAction.parameters[key].example.toLowerCase().includes("apik") || foundAction.parameters[key].example.toLowerCase().includes("key") || foundAction.parameters[key].example.toLowerCase().includes("pass") || foundAction.parameters[key].example.toLowerCase().includes("****")) {
|
||||
foundAction.parameters[key].value = ""
|
||||
} else {
|
||||
foundAction.parameters[key].value = foundAction.parameters[key].example
|
||||
}
|
||||
|
||||
} else {
|
||||
foundAction.parameters[key].value = ""
|
||||
}
|
||||
}
|
||||
|
||||
setSelectedAction(foundAction)
|
||||
setUpdate(Math.random());
|
||||
|
||||
} else if (e.target.value === "authgroups") {
|
||||
if (authGroups !== undefined && authGroups !== null && authGroups.length === 0) {
|
||||
toast("No auth groups created. Opening window to create one")
|
||||
|
||||
setTimeout(() => {
|
||||
window.open("/admin?tab=app_auth", "_blank")
|
||||
}, 2500)
|
||||
} else {
|
||||
foundAction.selectedAuthentication = {};
|
||||
foundAction.authentication_id = "authgroups"
|
||||
|
||||
for (let [key,keyval] in Object.entries(foundAction.parameters)) {
|
||||
//console.log(foundAction.parameters[key])
|
||||
if (foundAction.parameters[key].configuration) {
|
||||
|
||||
if (foundAction.parameters[key].name === "url" && authenticationType?.type === "oauth2-app") {
|
||||
} else {
|
||||
foundAction.parameters[key].value = "authgroup controlled"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setSelectedAction(foundAction)
|
||||
setUpdate(Math.random())
|
||||
}
|
||||
} else {
|
||||
foundAction.selectedAuthentication = e.target.value
|
||||
foundAction.authentication_id = e.target.value.id
|
||||
|
||||
setSelectedAction(foundAction)
|
||||
setUpdate(Math.random())
|
||||
}
|
||||
}}
|
||||
style={{
|
||||
backgroundColor: theme.palette.inputColor,
|
||||
color: "white",
|
||||
height: 40,
|
||||
borderRadius: theme.palette?.borderRadius,
|
||||
}}
|
||||
>
|
||||
<MenuItem
|
||||
style={{
|
||||
backgroundColor: theme.palette.inputColor,
|
||||
color: "white",
|
||||
}}
|
||||
value="No selection"
|
||||
>
|
||||
{selectedImage}
|
||||
<em>No selection</em>
|
||||
</MenuItem>
|
||||
|
||||
{relevantAuthentication.map((data) => {
|
||||
if (data.last_modified === true) {
|
||||
//console.log("LAST MODIFIED: ", data.label)
|
||||
}
|
||||
|
||||
if (foundAction.authentication_id === data.id) {
|
||||
authFound = true
|
||||
}
|
||||
|
||||
return (
|
||||
<MenuItem
|
||||
key={data.id}
|
||||
disabled={data.id === foundAction.authentication_id}
|
||||
style={{
|
||||
backgroundColor: theme.palette.inputColor,
|
||||
color: "white",
|
||||
overflowX: "auto",
|
||||
}}
|
||||
value={data}
|
||||
>
|
||||
{selectedImage}
|
||||
|
||||
{data.label}
|
||||
</MenuItem>
|
||||
)
|
||||
})}
|
||||
|
||||
{/*
|
||||
<Divider style={{marginTop: 10, marginBottom: 10, }}/>
|
||||
|
||||
<MenuItem
|
||||
disabled
|
||||
style={{
|
||||
backgroundColor: theme.palette.inputColor,
|
||||
color: "white",
|
||||
}}
|
||||
value="authgroups"
|
||||
>
|
||||
<em>Auth Groups</em>
|
||||
</MenuItem>
|
||||
*/}
|
||||
|
||||
</Select>
|
||||
}
|
||||
|
||||
// FIXME: Validate the CURRENT authentication that has been chosen?
|
||||
if (foundApp.authentication === undefined || foundApp.authentication === null) {
|
||||
toast("Authentication error: No authentication found")
|
||||
authReturn = "Failed to find auth"
|
||||
}
|
||||
|
||||
if (authReturn === null && foundApp.authentication.type === "oauth2" || foundApp.authentication.type === "oauth2-app") {
|
||||
authReturn =
|
||||
<AuthenticationOauth2
|
||||
globalUrl={globalUrl}
|
||||
authenticationType={foundApp.authentication}
|
||||
selectedAction={foundAction}
|
||||
|
||||
selectedApp={foundApp}
|
||||
getAppAuthentication={fetchAuthentication}
|
||||
isCloud={true}
|
||||
authButtonOnly={true}
|
||||
/>
|
||||
} else if (authReturn === null) {
|
||||
authReturn = "Other auth - Not implemented"
|
||||
|
||||
}
|
||||
|
||||
validationReturn = authReturn === null || foundAuth.id === undefined || foundAuth.id === null || foundAuth.id.length === 0 || !authFound ? null :
|
||||
<Button
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
color="secondary"
|
||||
style={{
|
||||
height: 35,
|
||||
justifyContent: !validating ? "flex-start" : "center",
|
||||
textTransform: "none",
|
||||
fontSize: 18,
|
||||
borderRadius: theme.palette?.borderRadius,
|
||||
}}
|
||||
onClick={() => {
|
||||
toast("Validating app")
|
||||
validateApp(foundApp, foundAction)
|
||||
}}
|
||||
>
|
||||
{validationIcon}
|
||||
{validating ?
|
||||
<CircularProgress
|
||||
color="secondary"
|
||||
style={{width: 30, height: 30, }}
|
||||
/>
|
||||
:
|
||||
<span>
|
||||
{selectedImage}
|
||||
Validate {foundApp.name.replace("_", " ", -1)}
|
||||
</span>
|
||||
}
|
||||
</Button>
|
||||
|
||||
//resolveButton = !(Object.getOwnPropertyNames(actionRunInfo).length === 0 || actionRunInfo.validation.valid === true) ? null :
|
||||
resolveButton =
|
||||
<Button
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
color="primary"
|
||||
style={{
|
||||
height: 35,
|
||||
textTransform: "none",
|
||||
backgroundColor: green,
|
||||
color: "black",
|
||||
borderRadius: 50,
|
||||
width: 200,
|
||||
margin: "auto",
|
||||
marginTop: 35,
|
||||
}}
|
||||
onClick={() => {
|
||||
toast("Resolving error")
|
||||
console.log("Error: ", error)
|
||||
console.log("Errors: ", workflow.validation)
|
||||
|
||||
// Remove the error from the validation list
|
||||
var newErrors = []
|
||||
for (var workflowErrorKey in workflow.validation.errors) {
|
||||
if (workflow.validation.errors[workflowErrorKey].error !== error.error) {
|
||||
newErrors.push(workflow.validation.errors[workflowErrorKey])
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
workflow.validation.errors = newErrors
|
||||
if (workflow.validation.errors.length === 0) {
|
||||
workflow.validation.valid = true
|
||||
}
|
||||
|
||||
// Sets it in the parent
|
||||
setWorkflow(workflow)
|
||||
if (setUpdateParent !== undefined) {
|
||||
setUpdateParent(Math.random())
|
||||
}
|
||||
|
||||
// Saves the actual workflow with the update(s)
|
||||
saveWorkflow(workflow)
|
||||
}}
|
||||
>
|
||||
Resolve
|
||||
</Button>
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<div>
|
||||
{authReturn}
|
||||
<div style={{marginTop: 5, }} />
|
||||
{validationReturn}
|
||||
|
||||
{resolveButton}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
console.log("Workflow validation: ", workflow.validation)
|
||||
return (
|
||||
<div>
|
||||
{workflow.errors !== undefined && workflow.errors !== null ?
|
||||
<div>
|
||||
General errors: {workflow.errors.length}
|
||||
{workflow.errors.map((error, index) => {
|
||||
return (
|
||||
<div>
|
||||
- {error}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
: null}
|
||||
|
||||
<Divider style={{marginTop: 15, marginBottom: 15, }}/>
|
||||
|
||||
|
||||
{workflow.validation.errors !== undefined && workflow.validation.errors !== null ?
|
||||
<div>
|
||||
Validation errors: {workflow.validation.errors.length}
|
||||
{workflow.validation.errors.map((error, index) => {
|
||||
return (
|
||||
<div>
|
||||
<ErrorItem
|
||||
apps={apps}
|
||||
error={error}
|
||||
index={index}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
: null}
|
||||
|
||||
<Divider style={{marginTop: 15, marginBottom: 15, }} />
|
||||
Apps loaded: {apps.length}
|
||||
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default FixWorkflowValidationErrors
|
||||
@@ -50,8 +50,6 @@ const hoverOutColor = "#e8eaf6"
|
||||
|
||||
const Header = props => {
|
||||
const { globalUrl, setNotifications, notifications, isLoggedIn, removeCookie, homePage, userdata, serverside, } = props;
|
||||
//const theme = useTheme();
|
||||
//const alert = useAlert()
|
||||
|
||||
|
||||
const [HomeHoverColor, setHomeHoverColor] = useState(hoverOutColor);
|
||||
@@ -829,7 +827,7 @@ const { globalUrl, setNotifications, notifications, isLoggedIn, removeCookie, ho
|
||||
},
|
||||
}}
|
||||
style={{
|
||||
borderRadius: theme.palette.borderRadius,
|
||||
borderRadius: theme.palette?.borderRadius,
|
||||
backgroundColor: theme.palette.surfaceColor,
|
||||
marginRight: 15,
|
||||
color: "white",
|
||||
@@ -904,7 +902,7 @@ const { globalUrl, setNotifications, notifications, isLoggedIn, removeCookie, ho
|
||||
|
||||
<Tooltip color="primary" title={parsedTitle} placement="left">
|
||||
<div style={{display: "flex"}}>
|
||||
{isCloud?<Typography variant="body2" style={{borderRadius: theme.palette.borderRadius, float: "left", margin: "0 0 0 0", marginRight: 25, }}>{regiontag}</Typography>:null} {image} <span style={{marginLeft: 8}}>{data.name}</span>
|
||||
{isCloud?<Typography variant="body2" style={{borderRadius: theme.palette?.borderRadius, float: "left", margin: "0 0 0 0", marginRight: 25, }}>{regiontag}</Typography>:null} {image} <span style={{marginLeft: 8}}>{data.name}</span>
|
||||
</div>
|
||||
</Tooltip>
|
||||
</MenuItem>
|
||||
@@ -935,7 +933,7 @@ const { globalUrl, setNotifications, notifications, isLoggedIn, removeCookie, ho
|
||||
null
|
||||
:
|
||||
<Tooltip title={`Amount of executions left: ${userdata.app_execution_usage} / ${userdata.app_execution_limit}. When the limit is reached, you can still use Shuffle normally, but your Workflow triggers may stop working. Reach out to support@shuffler.io to extend this limit.`}>
|
||||
<div style={{maxHeight: 30, minHeight: 30, padding: 8, textAlign: "center", cursor: "pointer", borderRadius: theme.palette.borderRadius, marginRight: 10, marginTop: 12, backgroundColor: theme.palette.surfaceColor, minWidth: 60, maxWidth: 60, border: userdata.app_execution_usage/userdata.app_execution_limit >= 0.9 ? "#f86a3e" : null, }} onClick={() => {
|
||||
<div style={{maxHeight: 30, minHeight: 30, padding: 8, textAlign: "center", cursor: "pointer", borderRadius: theme.palette?.borderRadius, marginRight: 10, marginTop: 12, backgroundColor: theme.palette.surfaceColor, minWidth: 60, maxWidth: 60, border: userdata.app_execution_usage/userdata.app_execution_limit >= 0.9 ? "#f86a3e" : null, }} onClick={() => {
|
||||
console.log(userdata.appe_execution_usage/userdata.app_execution_limit)
|
||||
if (window.drift !== undefined) {
|
||||
window.drift.api.startInteraction({ interactionId: 326905 })
|
||||
|
||||
@@ -11,14 +11,14 @@ import {
|
||||
import HealthBarChart from '../components/HealthBarChart.jsx';
|
||||
|
||||
const HealthPage = (props) => {
|
||||
const { userdata } = props;
|
||||
const { globalUrl, userdata } = props;
|
||||
const [healthData, setHealthData] = useState(null);
|
||||
const [selectedRange, setSelectedRange] = useState('30d');
|
||||
const [filteredData, setFilteredData] = useState([]);
|
||||
const [averageUptime, setAverageUptime] = useState(0);
|
||||
|
||||
const isCloud = window.location.host === "localhost:3002" || window.location.host === "shuffler.io";
|
||||
const globalUrl = `https://shuffler.io`
|
||||
//const globalUrl = `https://shuffler.io`
|
||||
|
||||
console.log("HEALTHPAGE 1")
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -2,7 +2,6 @@ import React, { useState, useEffect } from "react";
|
||||
import ReactGA from 'react-ga4';
|
||||
|
||||
import theme from "../theme.jsx";
|
||||
import { useTheme } from "@mui/styles";
|
||||
import countries from "../components/Countries.jsx";
|
||||
import {
|
||||
Box,
|
||||
@@ -88,7 +87,7 @@ const LicencePopup = (props) => {
|
||||
|
||||
const paperStyle = {
|
||||
padding: 20,
|
||||
borderRadius: theme.palette.borderRadius,
|
||||
borderRadius: theme.palette?.borderRadius,
|
||||
height: "100%",
|
||||
}
|
||||
|
||||
@@ -194,7 +193,7 @@ const LicencePopup = (props) => {
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
style={{ borderRadius: theme.palette.borderRadius, }}
|
||||
style={{ borderRadius: theme.palette?.borderRadius, }}
|
||||
placement="bottom"
|
||||
>
|
||||
<div style={{}}>
|
||||
@@ -320,7 +319,7 @@ const LicencePopup = (props) => {
|
||||
margin: "auto",
|
||||
width: 100,
|
||||
backgroundColor: "white",
|
||||
// borderRadius: theme.palette.borderRadius,
|
||||
// borderRadius: theme.palette?.borderRadius,
|
||||
}}
|
||||
/>
|
||||
: null}
|
||||
@@ -390,7 +389,7 @@ const LicencePopup = (props) => {
|
||||
value={feature.split("Worker License: ")[1]}
|
||||
style={{
|
||||
// backgroundColor: theme.palette.inputColor,
|
||||
// borderRadius: theme.palette.borderRadius,
|
||||
// borderRadius: theme.palette?.borderRadius,
|
||||
}}
|
||||
id={fieldId}
|
||||
onClick={() => { }}
|
||||
@@ -515,16 +514,16 @@ const LicencePopup = (props) => {
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
console.log("New variant: ", shuffleVariant)
|
||||
console.log("New variant: ", shuffleVariant)
|
||||
|
||||
if (shuffleVariant === 1) {
|
||||
setCalculatedCost("$600")
|
||||
setSelectedValue(8)
|
||||
} else {
|
||||
setCalculatedCost("$540")
|
||||
setSelectedValue(300)
|
||||
}
|
||||
}, [shuffleVariant])
|
||||
if (shuffleVariant === 1) {
|
||||
setCalculatedCost("$960")
|
||||
setSelectedValue(8)
|
||||
} else {
|
||||
setCalculatedCost("$960")
|
||||
setSelectedValue(300)
|
||||
}
|
||||
}, [shuffleVariant])
|
||||
|
||||
if (typeof window === 'undefined' || window.location === undefined) {
|
||||
return null
|
||||
@@ -680,7 +679,7 @@ const LicencePopup = (props) => {
|
||||
color: "white",
|
||||
}
|
||||
|
||||
|
||||
console.log("Priceitem: ", shuffleVariant)
|
||||
const isLoggedInHandler = () => {
|
||||
if (calculatedCost === payasyougo) {
|
||||
handlePayasyougo(props.userdata)
|
||||
@@ -690,7 +689,7 @@ const LicencePopup = (props) => {
|
||||
const priceItem = window.location.origin === "https://shuffler.io" ?
|
||||
shuffleVariant === 0 ? "app_executions" : "cores"
|
||||
:
|
||||
shuffleVariant === 0 ? "price_1MROFrDzMUgUjxHShcSxgHO1" : "price_1NXjQqDzMUgUjxHSg690R4FP"
|
||||
shuffleVariant === 0 ? "price_1PZPSSEJjT17t98NLJoTMYja" : "price_1PZPQuEJjT17t98N3yORUtd9"
|
||||
|
||||
const successUrl = `${window.location.origin}/admin?admin_tab=billing&payment=success`
|
||||
const failUrl = `${window.location.origin}/pricing?admin_tab=billing&payment=failure`
|
||||
@@ -824,7 +823,7 @@ const LicencePopup = (props) => {
|
||||
{errorMessage.length > 0 ? <Typography variant="h4">Error: {errorMessage}</Typography> : null}
|
||||
<Card style={{
|
||||
padding: 20,
|
||||
borderRadius: theme.palette.borderRadius,
|
||||
borderRadius: theme.palette?.borderRadius,
|
||||
border: "1px solid #f85a3e",
|
||||
}}>
|
||||
<div>
|
||||
|
||||
@@ -0,0 +1,197 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { Paper, Typography, Box, CircularProgress, TextField, Button } from "@mui/material";
|
||||
import { toast } from "react-toastify";
|
||||
|
||||
const MFASetup = ({ isLoaded, globalUrl, setCookie }) => {
|
||||
const [image2FA, setImage2FA] = useState("");
|
||||
const [secret2FA, setSecret2FA] = useState("");
|
||||
const [mfaCode, setMfaCode] = useState("");
|
||||
const [code, setCode] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
handleGet2FACode();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (isLoaded) {
|
||||
const code = window.location.pathname.split("/")[2];
|
||||
setMfaCode(code);
|
||||
}
|
||||
}, [isLoaded]);
|
||||
|
||||
const handleGet2FACode = () => {
|
||||
if (mfaCode === "") {
|
||||
return;
|
||||
}
|
||||
|
||||
fetch(`${globalUrl}/api/v1/users/${mfaCode}/get2fa`, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Accept: "application/json",
|
||||
},
|
||||
credentials: "include",
|
||||
})
|
||||
.then((response) => {
|
||||
if (response.status === 404) {
|
||||
toast("User not found. Redirecting to login page in 3 seconds...");
|
||||
setTimeout(() => {
|
||||
window.location.pathname = "/login";
|
||||
return;
|
||||
}, 3000);
|
||||
}
|
||||
if (response.status !== 200) {
|
||||
console.log("Status not 200 for apps :O!");
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then((responseJson) => {
|
||||
if (responseJson.success === true) {
|
||||
setImage2FA(responseJson.reason);
|
||||
setSecret2FA(responseJson.extra);
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
toast(error.toString());
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (mfaCode) {
|
||||
handleGet2FACode();
|
||||
}
|
||||
}, [mfaCode]);
|
||||
|
||||
const handleVerify2FA = (mfaCode, code, changeMFAActive) => {
|
||||
const data = {
|
||||
code: code,
|
||||
changeMFAActive: changeMFAActive,
|
||||
};
|
||||
|
||||
toast("Verifying 2fa code. Please wait...");
|
||||
|
||||
fetch(`${globalUrl}/api/v1/users/${mfaCode}/set2fa`, {
|
||||
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 === 500) {
|
||||
toast("Wrong code sent. Please try again.");
|
||||
return;
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then((responseJson) => {
|
||||
if (responseJson.success === true) {
|
||||
toast.success("Successfully setup 2fa. Redirecting in 3 seconds...");
|
||||
for (var key in responseJson["cookies"]) {
|
||||
setCookie(responseJson["cookies"][key].key, responseJson["cookies"][key].value, { path: "/" });
|
||||
}
|
||||
|
||||
const tmpView = new URLSearchParams(window.location.search).get("view");
|
||||
if (tmpView !== undefined && tmpView !== null) {
|
||||
var newUrl = `/${tmpView}`;
|
||||
if (tmpView.startsWith("/")) {
|
||||
newUrl = `${tmpView}`;
|
||||
}
|
||||
window.location.pathname = newUrl;
|
||||
return;
|
||||
}
|
||||
|
||||
if (responseJson.tutorials !== undefined && responseJson.tutorials !== null) {
|
||||
const welcome = responseJson.tutorials.find((element) => element.name === "welcome");
|
||||
if (welcome === undefined || welcome === null) {
|
||||
setTimeout(() => {
|
||||
window.location.pathname = "/welcome";
|
||||
}, 3000);
|
||||
}
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
window.location.pathname = "/workflows";
|
||||
}, 3000);
|
||||
} else {
|
||||
toast("Failed to setup 2fa. Please try again.");
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("Error:", error);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{ paddingTop: 50, margin: "0px auto", width: "500px" }}>
|
||||
<Paper elevation={3} style={{ padding: "30px", backgroundColor: "#212121" }}>
|
||||
<Typography variant="h5" style={{ color: "white", marginBottom: 10, textAlign: "center" }}>
|
||||
Multi-Factor Authentication Setup
|
||||
</Typography>
|
||||
<div style={{ marginTop: 15 }}>
|
||||
<QRCodeSection secret2FA={secret2FA} image2FA={image2FA} />
|
||||
</div>
|
||||
<Typography variant="body1" style={{ color: "white", marginTop: 15 }}>
|
||||
Enter the code from your authenticator app below.
|
||||
</Typography>
|
||||
<TextField
|
||||
variant="outlined"
|
||||
margin="normal"
|
||||
required
|
||||
fullWidth
|
||||
id="code"
|
||||
label="Code"
|
||||
name="code"
|
||||
autoComplete="code"
|
||||
autoFocus
|
||||
onChange={(e) => setCode(e.target.value)}
|
||||
onKeyPress={(e) => {
|
||||
if (e.key === "Enter" && code !== null && code !== "" && code.length === 6) {
|
||||
handleVerify2FA(mfaCode, code, true);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
type="submit"
|
||||
fullWidth
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={() => handleVerify2FA(mfaCode, code, true)}
|
||||
style={{
|
||||
backgroundColor: code === null || code === "" || code.length !== 6 ? "gray" : "#f86743",
|
||||
marginTop: 10,
|
||||
color: "#fff",
|
||||
cursor: code === null || code === "" || code.length !== 6 ? "" : "pointer",
|
||||
}}
|
||||
disabled={code === null || code === "" || code.length !== 6}
|
||||
>
|
||||
Submit
|
||||
</Button>
|
||||
|
||||
</Paper>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const QRCodeSection = ({ secret2FA, image2FA }) => {
|
||||
return (
|
||||
<div style={{ display: "flex", flexDirection: "column", alignItems: "center", justifyContent: "center", height: "100%", width: "100%" }}>
|
||||
{secret2FA && image2FA ? (
|
||||
<div style={{ textAlign: "center" }}>
|
||||
<Typography variant="body2" style={{ color: "white", textAlign: "justify", marginBottom: 10, fontSize: 16 }}>
|
||||
Scan the image below with the two-factor authentication app on your phone. If you can’t use a QR code, use the code {secret2FA} instead.
|
||||
</Typography>
|
||||
<img alt="2FA QR code" src={image2FA} style={{ maxHeight: 200, maxWidth: 200, }} />
|
||||
</div>
|
||||
) : (
|
||||
<CircularProgress style={{ margin: "15px auto" }} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default MFASetup;
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,7 @@
|
||||
import React, {useState} from 'react';
|
||||
import { useTheme } from '@mui/styles';
|
||||
import {isMobile} from "react-device-detect";
|
||||
import ReactGA from 'react-ga4';
|
||||
import theme from '../theme.jsx';
|
||||
|
||||
import {
|
||||
TextField,
|
||||
@@ -12,7 +12,6 @@ import {
|
||||
const Newsletter = (props) => {
|
||||
const { globalUrl, } = props;
|
||||
|
||||
const theme = useTheme();
|
||||
const [email, setEmail] = useState("");
|
||||
const [msg, setMsg] = useState("");
|
||||
const [buttonActive, setButtonActive] = useState(true);
|
||||
|
||||
@@ -96,7 +96,7 @@ const AuthenticationOauth2 = (props) => {
|
||||
autoAuth,
|
||||
authButtonOnly,
|
||||
isLoggedIn,
|
||||
|
||||
org_id,
|
||||
setFinalized,
|
||||
} = props;
|
||||
|
||||
@@ -148,16 +148,14 @@ const AuthenticationOauth2 = (props) => {
|
||||
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;
|
||||
}
|
||||
|
||||
if (selectedApp.authentication === undefined) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const startOauth2Request = (admin_consent) => {
|
||||
// Admin consent also means to add refresh tokens
|
||||
@@ -167,7 +165,7 @@ const AuthenticationOauth2 = (props) => {
|
||||
//console.log("APP: ", selectedApp)
|
||||
if (selectedApp.name.toLowerCase() == "outlook_graph" || selectedApp.name.toLowerCase() == "outlook_office365") {
|
||||
handleOauth2Request(
|
||||
"efe4c3fe-84a1-4821-a84f-23a6cfe8e72d",
|
||||
"fd55c175-aa30-4fa6-b303-09a29fb3f750",
|
||||
"",
|
||||
"https://graph.microsoft.com",
|
||||
["Mail.ReadWrite", "Mail.Send", "offline_access"],
|
||||
@@ -299,12 +297,11 @@ const AuthenticationOauth2 = (props) => {
|
||||
|
||||
const handleOauth2Request = (client_id, client_secret, oauth_url, scopes, admin_consent, prompt, skipScopeReplace) => {
|
||||
|
||||
console.log("SKIP SCOPE: ", skipScopeReplace)
|
||||
if (skipScopeReplace === false || skipScopeReplace === undefined) {
|
||||
|
||||
console.log("Selected scopes: ", selectedScopes)
|
||||
if (selectedScopes !== undefined && selectedScopes !== null && selectedScopes.length > 0) {
|
||||
toast("Using your scopes instead of the default ones")
|
||||
//toast("Using your scopes instead of the default ones")
|
||||
scopes = selectedScopes
|
||||
}
|
||||
}
|
||||
@@ -453,6 +450,8 @@ const AuthenticationOauth2 = (props) => {
|
||||
if (orgId !== undefined && orgId !== null && orgId.length > 0) {
|
||||
console.log("Adding org_id from user side")
|
||||
state += `%26org_id%3d${orgId}`;
|
||||
}else{
|
||||
state += `%26org_id%3d${org_id}`
|
||||
}
|
||||
|
||||
if (oauth_url !== undefined && oauth_url !== null && oauth_url.length > 0) {
|
||||
@@ -461,42 +460,56 @@ const AuthenticationOauth2 = (props) => {
|
||||
}
|
||||
|
||||
|
||||
if (
|
||||
authenticationType.refresh_uri !== undefined &&
|
||||
authenticationType.refresh_uri !== null &&
|
||||
authenticationType.refresh_uri.length > 0
|
||||
) {
|
||||
state += `%26refresh_uri%3d${authenticationType.refresh_uri}`;
|
||||
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}`;
|
||||
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`;
|
||||
if (workflow?.org_id !== undefined && workflow?.org_id !== null && workflow?.org_id.length > 0) {
|
||||
state += `%26org_id%3d${workflow.org_id}`
|
||||
}
|
||||
|
||||
// FIXME: Should this be =consent?
|
||||
var defaultPrompt = "login"
|
||||
if (prompt !== undefined && prompt !== null && prompt.length > 0) {
|
||||
defaultPrompt = prompt
|
||||
}
|
||||
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`;
|
||||
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`;
|
||||
}
|
||||
|
||||
if (url !== undefined && url !== null && url.length > 0) {
|
||||
if (url.toLowerCase().includes("{tenant")) {
|
||||
// Check location of {tenant, then find the next } and replace with 'common'. Make sure next } is AFTER {tenant
|
||||
try {
|
||||
const tenantIndex = url.toLowerCase().indexOf("{tenant")
|
||||
const substring = url.substring(tenantIndex)
|
||||
const nextBracket = substring.indexOf("}")
|
||||
const newUrl = url.substring(0, tenantIndex) + "common" + url.substring(tenantIndex + nextBracket + 1)
|
||||
url = newUrl
|
||||
} catch (e) {
|
||||
console.log("Failed to replace {tenant} with common: ", e)
|
||||
}
|
||||
|
||||
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`;
|
||||
}
|
||||
}
|
||||
|
||||
// Force new consent
|
||||
/*
|
||||
console.log("OAUTH2 URL: ", url)
|
||||
return
|
||||
*/
|
||||
|
||||
|
||||
// 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
|
||||
// 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)
|
||||
@@ -511,7 +524,12 @@ const AuthenticationOauth2 = (props) => {
|
||||
//alert('"Secure Payment" window closed!');
|
||||
|
||||
if (getAppAuthentication !== undefined) {
|
||||
getAppAuthentication(true, true, true);
|
||||
// This should be orgId, not action Id as to load auth properly
|
||||
if (workflow !== undefined && workflow !== null && workflow.org_id !== undefined && workflow.org_id !== null && workflow.org_id.length > 0) {
|
||||
getAppAuthentication(true, true, true, workflow.org_id)
|
||||
} else {
|
||||
getAppAuthentication(true, true, true)
|
||||
}
|
||||
}
|
||||
|
||||
toast("Authentication successful!")
|
||||
@@ -525,7 +543,7 @@ const AuthenticationOauth2 = (props) => {
|
||||
setFinalized(true)
|
||||
}
|
||||
} else {
|
||||
console.log("Not closed")
|
||||
//console.log("Not closed")
|
||||
}
|
||||
}, 1000);
|
||||
//do {
|
||||
@@ -678,7 +696,7 @@ const AuthenticationOauth2 = (props) => {
|
||||
justifyContent: "flex-start",
|
||||
backgroundColor: "#ffffff",
|
||||
color: "#2f2f2f",
|
||||
borderRadius: theme.palette.borderRadius,
|
||||
borderRadius: theme.palette?.borderRadius,
|
||||
minWidth: 300,
|
||||
maxWidth: 300,
|
||||
maxHeight: 50,
|
||||
@@ -703,7 +721,7 @@ const AuthenticationOauth2 = (props) => {
|
||||
<span style={{display: "flex"}}>
|
||||
<img
|
||||
alt={selectedAction.app_name}
|
||||
style={{ margin: 4, minHeight: 30, maxHeight: 30, borderRadius: theme.palette.borderRadius, }}
|
||||
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">
|
||||
@@ -726,7 +744,7 @@ const AuthenticationOauth2 = (props) => {
|
||||
</DialogTitle>
|
||||
<DialogContent>
|
||||
<span style={{}}>
|
||||
Oauth2 requires a client ID and secret to authenticate, defined in the remote system. {authenticationType.type === "oauth2-app" ? null : <span>Your redirect URL is <b>{window.location.origin}/set_authentication</b> - </span>}
|
||||
Oauth2 requires a client ID and secret to authenticate, defined in the remote system. <span>Your redirect URL is <b>{window.location.origin}/set_authentication</b> - </span>
|
||||
<a
|
||||
target="_blank"
|
||||
rel="norefferer"
|
||||
@@ -739,7 +757,7 @@ const AuthenticationOauth2 = (props) => {
|
||||
<div />
|
||||
</span>
|
||||
|
||||
{isCloud && registeredApps.includes(selectedApp.name.toLowerCase()) ?
|
||||
{isCloud && registeredApps?.includes(selectedApp?.name?.replaceAll(" ", "_").toLowerCase()) ?
|
||||
<span>
|
||||
<span style={{display: "flex"}}>
|
||||
{autoAuthButton}
|
||||
@@ -785,7 +803,7 @@ const AuthenticationOauth2 = (props) => {
|
||||
</span>
|
||||
: null}
|
||||
{/*<TextField
|
||||
style={{backgroundColor: theme.palette.inputColor, borderRadius: theme.palette.borderRadius,}}
|
||||
style={{backgroundColor: theme.palette.inputColor, borderRadius: theme.palette?.borderRadius,}}
|
||||
InputProps={{
|
||||
style:{
|
||||
},
|
||||
@@ -817,13 +835,14 @@ const AuthenticationOauth2 = (props) => {
|
||||
setOauthUrl(data.value);
|
||||
}
|
||||
|
||||
const defaultValue = data.name === "url" && authenticationType.token_uri !== undefined && authenticationType.token_uri !== null && authenticationType.token_uri.length > 0 && (authenticationType.authorizationUrl === undefined || authenticationType.authorizationUrl === null || authenticationType.authorizationUrl.length === 0) && authenticationType.type === "oauth2-app" ? authenticationType.token_uri : data.value === undefined || data.value === null ? "" : data.value
|
||||
const isNormalOauth = authenticationType.redirect_uri !== undefined && authenticationType.redirect_uri !== null && authenticationType.redirect_uri.length > 0
|
||||
|
||||
const defaultValue = !isNormalOauth && data.name === "url" && authenticationType.token_uri !== undefined && authenticationType.token_uri !== null && authenticationType.token_uri.length > 0 && (authenticationType.authorizationUrl === undefined || authenticationType.authorizationUrl === null || authenticationType.authorizationUrl.length === 0) && authenticationType.type === "oauth2-app" ? authenticationType.token_uri : data.value === undefined || data.value === null ? "" : data.value
|
||||
|
||||
const fieldname = data.name === "url" && authenticationType.grant_type !== undefined && authenticationType.grant_type !== null && authenticationType.grant_type.length > 0 && authenticationType.type === "oauth2-app" ? "Token URL" : data.name
|
||||
const fieldname = !isNormalOauth && data.name === "url" && authenticationType.grant_type !== undefined && authenticationType.grant_type !== null && authenticationType.grant_type.length > 0 && authenticationType.type === "oauth2-app" ? "Token URL" : data.name
|
||||
|
||||
return (
|
||||
<div key={index} style={{ marginTop: authenticationType.type === "oauth2-app" ? 10 : 0, }}>
|
||||
<div key={index} style={{ marginTop: !isNormalOauth && authenticationType.type === "oauth2-app" ? 10 : 0, }}>
|
||||
<LockOpenIcon style={{ marginRight: 10 }} />
|
||||
|
||||
<b>{fieldname}</b>
|
||||
@@ -875,7 +894,7 @@ const AuthenticationOauth2 = (props) => {
|
||||
<TextField
|
||||
style={{
|
||||
backgroundColor: theme.palette.inputColor,
|
||||
borderRadius: theme.palette.borderRadius,
|
||||
borderRadius: theme.palette?.borderRadius,
|
||||
}}
|
||||
InputProps={{
|
||||
style: {
|
||||
@@ -906,7 +925,7 @@ const AuthenticationOauth2 = (props) => {
|
||||
style={{
|
||||
marginTop: 20,
|
||||
backgroundColor: theme.palette.inputColor,
|
||||
borderRadius: theme.palette.borderRadius,
|
||||
borderRadius: theme.palette?.borderRadius,
|
||||
}}
|
||||
InputProps={{
|
||||
style: {
|
||||
@@ -924,7 +943,7 @@ const AuthenticationOauth2 = (props) => {
|
||||
<TextField
|
||||
style={{
|
||||
backgroundColor: theme.palette.inputColor,
|
||||
borderRadius: theme.palette.borderRadius,
|
||||
borderRadius: theme.palette?.borderRadius,
|
||||
marginBottom: 10,
|
||||
}}
|
||||
InputProps={{
|
||||
@@ -946,7 +965,7 @@ const AuthenticationOauth2 = (props) => {
|
||||
<TextField
|
||||
style={{
|
||||
backgroundColor: theme.palette.inputColor,
|
||||
borderRadius: theme.palette.borderRadius,
|
||||
borderRadius: theme.palette?.borderRadius,
|
||||
}}
|
||||
InputProps={{
|
||||
style: {
|
||||
@@ -964,7 +983,7 @@ const AuthenticationOauth2 = (props) => {
|
||||
<TextField
|
||||
style={{
|
||||
backgroundColor: theme.palette.inputColor,
|
||||
borderRadius: theme.palette.borderRadius,
|
||||
borderRadius: theme.palette?.borderRadius,
|
||||
marginBottom: 10,
|
||||
}}
|
||||
InputProps={{
|
||||
@@ -997,7 +1016,6 @@ const AuthenticationOauth2 = (props) => {
|
||||
color: "white",
|
||||
padding: 5,
|
||||
minWidth: 300,
|
||||
maxWidth: 300,
|
||||
}}
|
||||
onChange={(e, value) => {
|
||||
//handleScopeChange(e)
|
||||
@@ -1044,7 +1062,7 @@ const AuthenticationOauth2 = (props) => {
|
||||
style={{
|
||||
marginBottom: 40,
|
||||
marginTop: 20,
|
||||
borderRadius: theme.palette.borderRadius,
|
||||
borderRadius: theme.palette?.borderRadius,
|
||||
}}
|
||||
disabled={
|
||||
clientSecret.length === 0 || clientId.length === 0 || buttonClicked || (allscopes.length !== 0 && selectedScopes.length === 0)
|
||||
@@ -1075,7 +1093,7 @@ const AuthenticationOauth2 = (props) => {
|
||||
<Button
|
||||
style={{
|
||||
marginLeft: 10,
|
||||
borderRadius: theme.palette.borderRadius,
|
||||
borderRadius: theme.palette?.borderRadius,
|
||||
}}
|
||||
disabled={clientSecret.length === 0 || clientId.length === 0}
|
||||
variant="text"
|
||||
|
||||
@@ -110,8 +110,30 @@ const OrgHeader = (props) => {
|
||||
orgDescription,
|
||||
selectedOrganization.id,
|
||||
selectedOrganization.image,
|
||||
{},
|
||||
{},
|
||||
{
|
||||
app_download_repo: selectedOrganization?.defaults?.app_download_repo,
|
||||
app_download_branch: selectedOrganization?.defaults?.app_download_branch,
|
||||
workflow_download_repo: selectedOrganization?.defaults?.workflow_download_repo,
|
||||
workflow_download_branch: selectedOrganization?.defaults?.workflow_download_branch,
|
||||
notification_workflow: selectedOrganization?.defaults?.notification_workflow,
|
||||
documentation_reference: selectedOrganization?.defaults?.documentation_reference,
|
||||
workflow_upload_repo: selectedOrganization?.defaults?.workflow_upload_repo,
|
||||
workflow_upload_branch: selectedOrganization?.defaults?.workflow_upload_branch,
|
||||
workflow_upload_username: selectedOrganization?.defaults?.workflow_upload_username,
|
||||
workflow_upload_token: selectedOrganization?.defaults?.workflow_upload_token,
|
||||
newsletter: selectedOrganization?.defaults?.newsletter,
|
||||
weekly_recommendations: selectedOrganization?.defaults?.weekly_recommendations,
|
||||
},
|
||||
{
|
||||
sso_entrypoint: selectedOrganization?.sso_config?.sso_entrypoint,
|
||||
sso_certificate: selectedOrganization?.sso_config?.sso_certificate,
|
||||
client_id: selectedOrganization?.sso_config?.client_id,
|
||||
client_secret: selectedOrganization?.sso_config?.client_secret,
|
||||
openid_authorization: selectedOrganization?.sso_config?.openid_authorization,
|
||||
openid_token: selectedOrganization?.sso_config?.openid_token,
|
||||
SSORequired: selectedOrganization?.sso_config?.SSORequired,
|
||||
auto_provision: selectedOrganization?.sso_config?.auto_provision,
|
||||
},
|
||||
[],
|
||||
)
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -2,7 +2,7 @@ import React, {useState, useEffect, useLayoutEffect} from 'react';
|
||||
|
||||
import Draggable from "react-draggable";
|
||||
import {
|
||||
Paper
|
||||
Paper
|
||||
} from "@mui/material";
|
||||
|
||||
const PaperComponent = (props) => {
|
||||
@@ -11,7 +11,9 @@ const PaperComponent = (props) => {
|
||||
handle="#draggable-dialog-title"
|
||||
cancel={'[class*="MuiDialogContent-root"]'}
|
||||
>
|
||||
<Paper {...props} />
|
||||
<Paper
|
||||
{...props}
|
||||
/>
|
||||
</Draggable>
|
||||
)
|
||||
}
|
||||
|
||||
+2265
-1916
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,4 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import React, { useState, useEffect, useContext, memo } from "react";
|
||||
|
||||
import { toast } from "react-toastify";
|
||||
import theme from "../theme.jsx";
|
||||
@@ -13,22 +13,26 @@ import {
|
||||
Card,
|
||||
Chip,
|
||||
Switch,
|
||||
Skeleton,
|
||||
} from "@mui/material";
|
||||
import { Context } from "../context/ContextApi.jsx";
|
||||
|
||||
import { useNavigate, Link } from "react-router-dom";
|
||||
import Priority from "../components/Priority.jsx";
|
||||
import { constrainMatrix } from "reaviz";
|
||||
//import { useAlert
|
||||
|
||||
const Priorities = (props) => {
|
||||
const Priorities = memo((props) => {
|
||||
const { globalUrl, userdata,clickedFromOrgTab, serverside, billingInfo, stripeKey, checkLogin, setAdminTab, setCurTab, notifications, setNotifications, } = props;
|
||||
|
||||
const [showDismissed, setShowDismissed] = React.useState(false);
|
||||
const [showRead, setShowRead] = React.useState(false);
|
||||
const [appFramework, setAppFramework] = React.useState({});
|
||||
|
||||
const [selectedWorkflow, setSelectedWorkflow] = React.useState("NO HIGHLIGHT");
|
||||
const [selectedExecutionId, setSelectedExecutionId] = React.useState("NO HIGHLIGHT");
|
||||
const [highlightKMS, setHighlightKMS] = React.useState(false)
|
||||
|
||||
let navigate = useNavigate();
|
||||
|
||||
useEffect(() => {
|
||||
getFramework()
|
||||
|
||||
@@ -36,6 +40,12 @@ const Priorities = (props) => {
|
||||
const urlParams = new URLSearchParams(window.location.search)
|
||||
const workflow = urlParams.get("workflow")
|
||||
const execution_id = urlParams.get("execution_id")
|
||||
const kms = urlParams.get("kms")
|
||||
|
||||
if (kms !== null && kms !== undefined && kms.length > 0 && kms === "true") {
|
||||
toast.info("KMS-related notifications are highlighted.")
|
||||
setHighlightKMS(true)
|
||||
}
|
||||
|
||||
if (execution_id !== null) {
|
||||
setSelectedExecutionId(execution_id)
|
||||
@@ -209,176 +219,16 @@ const Priorities = (props) => {
|
||||
const notificationWidth = "100%"
|
||||
const imagesize = 22
|
||||
const boxColor = "#86c142"
|
||||
const NotificationItem = (props) => {
|
||||
const {data} = props
|
||||
|
||||
var image = "";
|
||||
var orgName = "";
|
||||
var orgId = "";
|
||||
|
||||
|
||||
const highlighted = selectedExecutionId === "" && selectedWorkflow === "" ? false : data.reference_url === undefined || data.reference_url === null || data.reference_url.length === 0 ? false : data.reference_url.includes(selectedExecutionId) || data.reference_url.includes(selectedWorkflow)
|
||||
|
||||
if (userdata.orgs !== undefined) {
|
||||
const foundOrg = userdata.orgs.find((org) => org.id === data["org_id"]);
|
||||
if (foundOrg !== undefined && foundOrg !== null) {
|
||||
//position: "absolute", bottom: 5, right: -5,
|
||||
const imageStyle = {
|
||||
width: imagesize,
|
||||
height: imagesize,
|
||||
pointerEvents: "none",
|
||||
marginLeft:
|
||||
data.creator_org !== undefined && data.creator_org.length > 0
|
||||
? 20
|
||||
: 0,
|
||||
borderRadius: 10,
|
||||
border:
|
||||
foundOrg.id === userdata.active_org.id
|
||||
? `3px solid ${boxColor}`
|
||||
: null,
|
||||
cursor: "pointer",
|
||||
marginRight: 10,
|
||||
};
|
||||
|
||||
image =
|
||||
foundOrg.image === "" ? (
|
||||
<img
|
||||
alt={foundOrg.name}
|
||||
src={theme.palette.defaultImage}
|
||||
style={imageStyle}
|
||||
/>
|
||||
) : (
|
||||
<img
|
||||
alt={foundOrg.name}
|
||||
src={foundOrg.image}
|
||||
style={imageStyle}
|
||||
onClick={() => {}}
|
||||
/>
|
||||
);
|
||||
|
||||
orgName = foundOrg.name;
|
||||
orgId = foundOrg.id;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Paper
|
||||
style={{
|
||||
backgroundColor: theme.palette.platformColor,
|
||||
width: clickedFromOrgTab ? null :notificationWidth,
|
||||
padding: 30,
|
||||
borderBottom: "1px solid rgba(255,255,255,0.4)",
|
||||
marginBottom: 20,
|
||||
border: highlighted ? "2px solid #f85a3e" : null,
|
||||
borderRadius: theme.palette.borderRadius,
|
||||
}}
|
||||
>
|
||||
<div style={{display: "flex", }}>
|
||||
{data.amount === 1 && data.read === false ?
|
||||
<Chip
|
||||
label={"First seen"}
|
||||
variant="contained"
|
||||
color="primary"
|
||||
style={{marginRight: 15, height: 25, }}
|
||||
/>
|
||||
: null}
|
||||
{data.ignored === true ?
|
||||
<Chip
|
||||
label={"Disabled"}
|
||||
variant="outlined"
|
||||
color="primary"
|
||||
style={{marginRight: 15, height: 25, }}
|
||||
/>
|
||||
: null}
|
||||
{data.read === false ?
|
||||
<Chip
|
||||
label={"Unread"}
|
||||
variant="outlined"
|
||||
color="primary"
|
||||
style={{marginRight: 15, height: 25, }}
|
||||
/>
|
||||
:
|
||||
<Chip
|
||||
label={"Read"}
|
||||
variant="outlined"
|
||||
color="secondary"
|
||||
style={{marginRight: 15, height: 25, }}
|
||||
/>
|
||||
}
|
||||
<Typography variant="body1" color="textPrimary">
|
||||
{data.title}
|
||||
</Typography >
|
||||
</div>
|
||||
|
||||
{data.image !== undefined && data.image !== null && data.image.length > 0 ?
|
||||
<img alt={data.title} src={data.image} style={{height: 100, width: 100, }} />
|
||||
:
|
||||
null
|
||||
}
|
||||
<Typography variant="body2" color="textSecondary" style={{marginTop: 10, maxHeight: 200, overflowX: "hidden", overflowY: "auto", }}>
|
||||
{data.description}
|
||||
</Typography >
|
||||
<div style={{ display: "flex" }}>
|
||||
<ButtonGroup>
|
||||
<Button
|
||||
color="secondary"
|
||||
variant="outlined"
|
||||
style={{ marginTop: 15 }}
|
||||
disabled={data.reference_url === undefined || data.reference_url === null || data.reference_url.length === 0}
|
||||
onClick={() => {
|
||||
window.open(data.reference_url, "_blank")
|
||||
}}
|
||||
>
|
||||
Explore
|
||||
</Button>
|
||||
{data.read === false ? (
|
||||
<Button
|
||||
color="secondary"
|
||||
variant="outlined"
|
||||
style={{ marginTop: 15 }}
|
||||
onClick={() => {
|
||||
dismissNotification(data.id);
|
||||
}}
|
||||
>
|
||||
Dismiss
|
||||
</Button>
|
||||
) : null}
|
||||
<Tooltip title="Disabling a notification makes it so similar notifications to this one will NOT be re-opened. It will NOT forward notifications to your notification workflow, but WILL still keep counting." placement="top">
|
||||
<Button
|
||||
color="secondary"
|
||||
variant={data.ignored === true ? "contained" : "outlined"}
|
||||
style={{ marginTop: 15, }}
|
||||
onClick={() => {
|
||||
if (data.ignored === true) {
|
||||
dismissNotification(data.id, false)
|
||||
} else {
|
||||
dismissNotification(data.id, true)
|
||||
}
|
||||
}}
|
||||
>
|
||||
{data.ignored === true ? "Re-enable" : "Disable"}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</ButtonGroup>
|
||||
|
||||
<Typography variant="body2" color="textSecondary" style={{marginLeft: 20, marginTop: 20, }}>
|
||||
<b>First seen</b>: {new Date(data.created_at * 1000).toISOString().slice(0, 19)}
|
||||
</Typography >
|
||||
<Typography variant="body2" color="textSecondary" style={{marginLeft: 20, marginTop: 20, }}>
|
||||
<b>Last seen</b>: {new Date(data.updated_at * 1000).toISOString().slice(0, 19)}
|
||||
</Typography >
|
||||
<Typography variant="body2" color="textSecondary" style={{marginLeft: 20, marginTop: 20, }}>
|
||||
<b>Times seen</b>: {data.amount}
|
||||
</Typography >
|
||||
</div>
|
||||
</Paper>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{width: clickedFromOrgTab ? 1030:1000, padding: clickedFromOrgTab ? 27:null, height: clickedFromOrgTab ? "auto":null, backgroundColor: clickedFromOrgTab ? '#212121':null, borderRadius: clickedFromOrgTab ? '16px':null, }}>
|
||||
<h2 style={{ display: clickedFromOrgTab?null:"inline", marginBottom: clickedFromOrgTab? 8:null, marginTop: clickedFromOrgTab?40:null, color: clickedFromOrgTab?"#ffffff":null }}>Notifications</h2>
|
||||
<span style={{ marginLeft: clickedFromOrgTab?null:25, color: clickedFromOrgTab?"#9E9E9E":null, }}>
|
||||
<div style={{width: "100%", height: "100%", boxSizing: 'border-box', transition: 'width 0.3s ease', padding: clickedFromOrgTab ? "27px 10px 19px 27px":null, height: clickedFromOrgTab ? "auto":null, minHeight: 843, backgroundColor: clickedFromOrgTab ? '#212121':null, borderRadius: clickedFromOrgTab ? '16px':null, }}>
|
||||
<div style={{ maxHeight: 1700, overflowY: "auto", width: '100%', scrollbarColor: '#494949 transparent', scrollbarWidth: 'thin'}}>
|
||||
<div style={{maxWidth: "calc(100% - 20px)"}}>
|
||||
<Typography style={{ fontSize: 24, fontWeight: 'bold', display: clickedFromOrgTab?null:"inline", marginBottom: clickedFromOrgTab? 8:null, color: clickedFromOrgTab?"#ffffff":null }}>Notifications ({
|
||||
notifications?.filter((notification) => showRead === true || notification.read === false).length
|
||||
})</Typography>
|
||||
|
||||
<span style={{ fontSize: 16, marginLeft: clickedFromOrgTab?null:25, color: clickedFromOrgTab?"#9E9E9E":null, }}>
|
||||
Notifications help you find potential problems with your workflows and apps.
|
||||
<a
|
||||
target="_blank"
|
||||
@@ -411,24 +261,11 @@ const Priorities = (props) => {
|
||||
</Button>
|
||||
) : null}
|
||||
</div>
|
||||
{notifications === null || notifications === undefined || notifications.length === 0 ? null :
|
||||
<div>
|
||||
{notifications.map((notification, index) => {
|
||||
if (showRead === false && notification.read === true) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<NotificationItem data={notification} key={index} />
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
}
|
||||
<NotificationComponent notifications={notifications} showRead={showRead} selectedExecutionId={selectedExecutionId} selectedWorkflow={selectedWorkflow} highlightKMS={highlightKMS} userdata={userdata} imagesize={imagesize} boxColor={boxColor} clickedFromOrgTab={clickedFromOrgTab} notificationWidth={notificationWidth} dismissNotification={dismissNotification}/>
|
||||
|
||||
{clickedFromOrgTab? null : <Divider style={{marginTop: 50, marginBottom: 50, }} />}
|
||||
|
||||
<h2 style={{ display: clickedFromOrgTab ? null:"inline", marginBottom: clickedFromOrgTab ? 8:null, marginTop: clickedFromOrgTab ?0:null, color: clickedFromOrgTab ? "#ffffff" : null }}>Suggestions</h2>
|
||||
<span style={{ color: clickedFromOrgTab ?"#9E9E9E":null,marginLeft: clickedFromOrgTab ?null:25 }}>
|
||||
<h2 style={{ display: clickedFromOrgTab ? null:"inline", marginBottom: clickedFromOrgTab ? 8:null, marginTop: clickedFromOrgTab ? 30 :null, color: clickedFromOrgTab ? "#ffffff" : null }}>Suggestions</h2>
|
||||
<span style={{ fontSize: 16, color: clickedFromOrgTab ?"#9E9E9E":null,marginLeft: clickedFromOrgTab ?null:25 }}>
|
||||
Suggestions are tasks identified by Shuffle to help you discover ways to protect your and customers' company. <br/>These range from simple configurations in Shuffle to Usecases you may have missed.
|
||||
<a
|
||||
target="_blank"
|
||||
@@ -470,9 +307,232 @@ const Priorities = (props) => {
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
export default Priorities;
|
||||
|
||||
|
||||
const NotificationItem = memo((props) => {
|
||||
const {data, selectedExecutionId, selectedWorkflow, highlightKMS, userdata, imagesize, boxColor, clickedFromOrgTab, notificationWidth, dismissNotification} = props
|
||||
|
||||
var image = "";
|
||||
var orgName = "";
|
||||
var orgId = "";
|
||||
|
||||
|
||||
var highlighted = selectedExecutionId === "" && selectedWorkflow === "" ? false : data.reference_url === undefined || data.reference_url === null || data.reference_url.length === 0 ? false : data.reference_url.includes(selectedExecutionId) || data.reference_url.includes(selectedWorkflow)
|
||||
|
||||
if (!highlighted && highlightKMS) {
|
||||
if (data.title !== undefined && data.title !== null && data.title.toLowerCase().includes("kms")) {
|
||||
highlighted = true
|
||||
} else if (data.description !== undefined && data.description !== null && data.description.toLowerCase().includes("kms")) {
|
||||
highlighted = true
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if (userdata.orgs !== undefined) {
|
||||
const foundOrg = userdata.orgs.find((org) => org.id === data["org_id"]);
|
||||
if (foundOrg !== undefined && foundOrg !== null) {
|
||||
//position: "absolute", bottom: 5, right: -5,
|
||||
const imageStyle = {
|
||||
width: imagesize,
|
||||
height: imagesize,
|
||||
pointerEvents: "none",
|
||||
marginLeft:
|
||||
data.creator_org !== undefined && data.creator_org.length > 0
|
||||
? 20
|
||||
: 0,
|
||||
borderRadius: 10,
|
||||
border:
|
||||
foundOrg.id === userdata.active_org.id
|
||||
? `3px solid ${boxColor}`
|
||||
: null,
|
||||
cursor: "pointer",
|
||||
marginRight: 10,
|
||||
};
|
||||
|
||||
image =
|
||||
foundOrg.image === "" ? (
|
||||
<img
|
||||
alt={foundOrg.name}
|
||||
src={theme.palette.defaultImage}
|
||||
style={imageStyle}
|
||||
/>
|
||||
) : (
|
||||
<img
|
||||
alt={foundOrg.name}
|
||||
src={foundOrg.image}
|
||||
style={imageStyle}
|
||||
onClick={() => {}}
|
||||
/>
|
||||
);
|
||||
|
||||
orgName = foundOrg.name;
|
||||
orgId = foundOrg.id;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Paper
|
||||
style={{
|
||||
backgroundColor: theme.palette.inputColor.backgroundColor,
|
||||
width: clickedFromOrgTab ? null :notificationWidth,
|
||||
padding: 30,
|
||||
borderBottom: "1px solid rgba(255,255,255,0.4)",
|
||||
marginBottom: 20,
|
||||
border: highlighted ? "2px solid #f85a3e" : null,
|
||||
borderRadius: theme.palette?.borderRadius,
|
||||
}}
|
||||
>
|
||||
<div style={{display: "flex", }}>
|
||||
{data.amount === 1 && data.read === false ?
|
||||
<Chip
|
||||
label={"First seen"}
|
||||
variant="contained"
|
||||
color="primary"
|
||||
style={{marginRight: 15, height: 25, }}
|
||||
/>
|
||||
: null}
|
||||
{data.ignored === true ?
|
||||
<Chip
|
||||
label={"Disabled"}
|
||||
variant="outlined"
|
||||
color="primary"
|
||||
style={{marginRight: 15, height: 25, }}
|
||||
/>
|
||||
: null}
|
||||
{data.read === false ?
|
||||
<Chip
|
||||
label={"Unread"}
|
||||
variant="outlined"
|
||||
color="primary"
|
||||
style={{marginRight: 15, height: 25, }}
|
||||
/>
|
||||
:
|
||||
<Chip
|
||||
label={"Read"}
|
||||
variant="outlined"
|
||||
color="secondary"
|
||||
style={{marginRight: 15, height: 25, }}
|
||||
/>
|
||||
}
|
||||
<Typography variant="body1" color="textPrimary" style={{ wordWrap: "break-word", overflow: "hidden", textOverflow: "ellipsis" }}>
|
||||
{data.title}
|
||||
</Typography >
|
||||
</div>
|
||||
|
||||
{data.image !== undefined && data.image !== null && data.image.length > 0 ?
|
||||
<img alt={data.title} src={data.image} style={{height: 100, width: 100, }} />
|
||||
:
|
||||
null
|
||||
}
|
||||
<Typography variant="body2" color="textSecondary" style={{ marginTop: 10, maxHeight: 200, overflowX: "hidden", overflowY: "auto", wordWrap: "break-word" }}>
|
||||
{data.description}
|
||||
</Typography >
|
||||
<div style={{ display: "flex" }}>
|
||||
<ButtonGroup>
|
||||
<Button
|
||||
color="secondary"
|
||||
variant="outlined"
|
||||
style={{ marginTop: 15 }}
|
||||
disabled={data.reference_url === undefined || data.reference_url === null || data.reference_url.length === 0}
|
||||
onClick={() => {
|
||||
window.open(data.reference_url, "_blank")
|
||||
}}
|
||||
>
|
||||
Explore
|
||||
</Button>
|
||||
{data.read === false ? (
|
||||
<Button
|
||||
color="secondary"
|
||||
variant="outlined"
|
||||
style={{ marginTop: 15 }}
|
||||
onClick={() => {
|
||||
dismissNotification(data.id);
|
||||
}}
|
||||
>
|
||||
Dismiss
|
||||
</Button>
|
||||
) : null}
|
||||
<Tooltip title="Disabling a notification makes it so similar notifications to this one will NOT be re-opened. It will NOT forward notifications to your notification workflow, but WILL still keep counting." placement="top">
|
||||
<Button
|
||||
color="secondary"
|
||||
variant={data.ignored === true ? "contained" : "outlined"}
|
||||
style={{ marginTop: 15, }}
|
||||
onClick={() => {
|
||||
if (data.ignored === true) {
|
||||
dismissNotification(data.id, false)
|
||||
} else {
|
||||
dismissNotification(data.id, true)
|
||||
}
|
||||
}}
|
||||
>
|
||||
{data.ignored === true ? "Re-enable" : "Disable"}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</ButtonGroup>
|
||||
|
||||
<Typography variant="body2" color="textSecondary" style={{ marginLeft: 20, marginTop: 20, wordWrap: "break-word", overflow: "hidden", textOverflow: "ellipsis" }}>
|
||||
<b>First seen</b>: {new Date(data.created_at * 1000).toISOString().slice(0, 19)}
|
||||
</Typography >
|
||||
|
||||
<Typography variant="body2" color="textSecondary" style={{ marginLeft: 20, marginTop: 20, wordWrap: "break-word", overflow: "hidden", textOverflow: "ellipsis" }}>
|
||||
<b>Last seen</b>: {new Date(data.updated_at * 1000).toISOString().slice(0, 19)}
|
||||
</Typography >
|
||||
|
||||
<Typography variant="body2" color="textSecondary" style={{ marginLeft: 20, marginTop: 20, wordWrap: "break-word", overflow: "hidden", textOverflow: "ellipsis" }}>
|
||||
<b>Times seen</b>: {data.amount}
|
||||
</Typography >
|
||||
</div>
|
||||
</Paper>
|
||||
);
|
||||
})
|
||||
|
||||
|
||||
const NotificationComponent = memo(({notifications, showRead, selectedExecutionId, selectedWorkflow, highlightKMS, userdata, imagesize, boxColor, clickedFromOrgTab, notificationWidth, dismissNotification}) => {
|
||||
|
||||
return(
|
||||
<div>
|
||||
{notifications === null || notifications === undefined || notifications?.length === 0 ? (
|
||||
null
|
||||
) :
|
||||
<div>
|
||||
{notifications?.map((notification, index) => {
|
||||
if (showRead === false && notification.read === true) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<NotificationItem data={notification} key={index} selectedExecutionId={selectedExecutionId} selectedWorkflow={selectedWorkflow} highlightKMS={highlightKMS} userdata={userdata} imagesize={imagesize} boxColor={boxColor} clickedFromOrgTab={clickedFromOrgTab} notificationWidth={notificationWidth} dismissNotification={dismissNotification} />
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
)
|
||||
})
|
||||
|
||||
// const PaddingWrapper = memo(({children, clickedFromOrgTab}) => {
|
||||
|
||||
// const { leftSideBarOpenByClick } = useContext(Context)
|
||||
|
||||
// return(
|
||||
// <div style={{width: leftSideBarOpenByClick ? 950 : 1030,transition: 'width 0.3s ease', padding: clickedFromOrgTab ? "27px 10px 19px 27px":null, height: clickedFromOrgTab ? "auto":null, minHeight: 843, backgroundColor: clickedFromOrgTab ? '#212121':null, borderRadius: clickedFromOrgTab ? '16px':null, }}>
|
||||
// {children}
|
||||
// </div>
|
||||
// )
|
||||
// })
|
||||
|
||||
// const Wrapper = memo(({children, clickedFromOrgTab}) => {
|
||||
|
||||
// return(
|
||||
// <PaddingWrapper clickedFromOrgTab={clickedFromOrgTab}>
|
||||
// {children}
|
||||
// </PaddingWrapper>
|
||||
// )
|
||||
// })
|
||||
|
||||
@@ -24,9 +24,16 @@ import {
|
||||
const Priority = (props) => {
|
||||
const { globalUrl, clickedFromOrgTab,userdata, serverside, priority, checkLogin, setAdminTab, setCurTab, appFramework, } = props;
|
||||
|
||||
const isCloud = window.location.host === "localhost:3002" || window.location.host === "shuffler.io";
|
||||
const isCloud = (window.location.host === "localhost:3002" || window.location.host === "shuffler.io") ? true : (process.env.IS_SSR === "true");
|
||||
let navigate = useNavigate();
|
||||
|
||||
if (window.location.pathname === "/workflows") {
|
||||
const hidePriorities = localStorage.getItem("hidePriorities", "true")
|
||||
if (hidePriorities === "true") {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
var realignedSrc = false
|
||||
var realignedDst = false
|
||||
let newdescription = priority.description
|
||||
@@ -114,7 +121,7 @@ const Priority = (props) => {
|
||||
const srcSize = realignedSrc ? 35 : 30
|
||||
const dstSize = realignedDst ? 35 : 30
|
||||
return (
|
||||
<div style={{border: priority.active === false ? "1px solid #000000" : priority.severity === 1 ? "1px solid #f85a3e" : clickedFromOrgTab ?null:"1px solid rgba(255,255,255,0.3)", borderRadius: theme.palette.borderRadius, marginTop: 10, marginBottom: 10, padding: clickedFromOrgTab ? 24:15, textAlign: "center", minHeight: isCloud ? 70 : 100, maxHeight: isCloud ? 70 : 100, textAlign: "left", backgroundColor: clickedFromOrgTab ? "#1A1A1A": theme.palette.surfaceColor, display: "flex", }}>
|
||||
<div style={{border: priority.active === false ? "1px solid #000000" : priority.severity === 1 ? "1px solid #f85a3e" : clickedFromOrgTab ?null:"1px solid rgba(255,255,255,0.3)", borderRadius: theme.palette?.borderRadius, marginTop: 10, marginBottom: 10, padding: clickedFromOrgTab ? 24:15, textAlign: "center", minHeight: isCloud ? 70 : 100, maxHeight: isCloud ? 70 : 100, textAlign: "left", backgroundColor: clickedFromOrgTab ? "#1A1A1A": 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}
|
||||
@@ -124,7 +131,7 @@ const Priority = (props) => {
|
||||
</span>
|
||||
{priority.type === "usecase" && priority.description.includes("&") ?
|
||||
<span style={{display: "flex", marginTop: 10, }}>
|
||||
<img src={newdescription.split("&")[1]} alt={priority.name} style={{height: srcSize, width: srcSize, marginRight: realignedSrc ? isCloud ? 0 : -10 : 10, borderRadius: theme.palette.borderRadius-3, marginTop: realignedSrc ? 5 : 0 }} />
|
||||
<img src={newdescription.split("&")[1]} alt={priority.name} style={{height: srcSize, width: srcSize, marginRight: realignedSrc ? isCloud ? 0 : -10 : 10, borderRadius: theme.palette?.borderRadius-3, marginTop: realignedSrc ? 5 : 0 }} />
|
||||
<Typography variant="body2" color="textSecondary" style={{marginTop: 3, }}>
|
||||
{newdescription.split("&")[0]}
|
||||
</Typography>
|
||||
@@ -132,7 +139,7 @@ const Priority = (props) => {
|
||||
{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: dstSize, width: dstSize, marginRight: realignedDst ? -5 : 10, borderRadius: theme.palette.borderRadius-3, marginTop: realignedDst ? 5 : 0 }} />
|
||||
<img src={newdescription.split("&")[3]} alt={priority.name+"2"} style={{height: dstSize, width: dstSize, marginRight: realignedDst ? -5 : 10, borderRadius: theme.palette?.borderRadius-3, marginTop: realignedDst ? 5 : 0 }} />
|
||||
<Typography variant="body2" color="textSecondary" style={{marginTop: 3}}>
|
||||
{newdescription.split("&")[2]}
|
||||
</Typography>
|
||||
@@ -176,6 +183,20 @@ const Priority = (props) => {
|
||||
<Button style={{borderRadius: 25, fontSize:16, boxShadow: clickedFromOrgTab ? "none":null,textTransform: clickedFromOrgTab ? 'capitalize':null, width: 100, height: 50, marginTop: 8, }} variant="text" color="secondary" onClick={() => {
|
||||
// dismiss -> get envs
|
||||
changeRecommendation(priority, "dismiss")
|
||||
|
||||
// Check window location if it's /workflows
|
||||
if (window.location.pathname === "/workflows") {
|
||||
// Set local storage to hide priorities for now
|
||||
localStorage.setItem("hidePriorities", "true")
|
||||
}
|
||||
|
||||
if (isCloud) {
|
||||
ReactGA.event({
|
||||
category: "recommendation",
|
||||
action: `dismiss_${priority.name}`,
|
||||
label: "",
|
||||
})
|
||||
}
|
||||
}}>
|
||||
Dismiss
|
||||
</Button>
|
||||
|
||||
@@ -0,0 +1,155 @@
|
||||
import React from "react"
|
||||
|
||||
import { Link } from "react-router-dom";
|
||||
import {
|
||||
Avatar,
|
||||
Box,
|
||||
Button,
|
||||
Typography,
|
||||
Tooltip,
|
||||
} from "@mui/material"
|
||||
import { useNavigate } from "react-router";
|
||||
import theme from "../theme.jsx";
|
||||
|
||||
import {
|
||||
Lock as LockIcon,
|
||||
} from '@mui/icons-material';
|
||||
|
||||
// onclickHandler = function override from parent onclick
|
||||
const RecentWorkflow = ({ workflow, onclickHandler, leftNavOpen, currentWorkflowId, }) => {
|
||||
|
||||
const navigate = useNavigate();
|
||||
|
||||
const [hovered, setHovered] = React.useState(false)
|
||||
if (workflow === undefined || workflow === null) {
|
||||
console.log("No workflow")
|
||||
return null
|
||||
}
|
||||
|
||||
/*
|
||||
* Note for @Lalit:
|
||||
*
|
||||
* When you want to make a list of something that is complex,
|
||||
* make a component. This way, you can easily manage
|
||||
* the logic, and we can actually reuse it. This component is used
|
||||
* multiple places, so do make sure to not break it randomly.
|
||||
*/
|
||||
|
||||
const expandLeftNav = leftNavOpen === true || leftNavOpen === undefined ? true : false
|
||||
|
||||
// Check if workflow.input_markdown has an image in it
|
||||
// If it does, show it as the main thing
|
||||
var relevantImageUrl = ""
|
||||
if (workflow?.form_control?.input_markdown !== undefined && workflow?.form_control?.input_markdown !== null && workflow?.form_control?.input_markdown !== "") {
|
||||
// Look for <img> tag or  markdown
|
||||
// html > markdown
|
||||
const imgTag = workflow?.form_control?.input_markdown.match(/<img[^>]+>/g)
|
||||
|
||||
if (imgTag !== null) {
|
||||
const src = imgTag[0].match(/src="([^"]+)"/)
|
||||
if (src !== null) {
|
||||
relevantImageUrl = src[1]
|
||||
}
|
||||
} else {
|
||||
const markdownTag = workflow?.form_control?.input_markdown.match(/!\[.*\]\(.*\)/g)
|
||||
|
||||
if (markdownTag !== null) {
|
||||
const src = markdownTag[0].match(/\(([^)]+)\)/)
|
||||
if (src !== null) {
|
||||
relevantImageUrl = src[1]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
onMouseEnter={() => setHovered(true)}
|
||||
onMouseLeave={() => setHovered(false)}
|
||||
>
|
||||
<Link to={`/workflows/` + workflow?.id} style={{textDecoration: "none"}}>
|
||||
<Button
|
||||
onClick={(e) => {
|
||||
if (onclickHandler !== undefined) {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
onclickHandler()
|
||||
}
|
||||
}}
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
textTransform: "none",
|
||||
width: "100%",
|
||||
justifyContent: "flex-start",
|
||||
textAlign: "left",
|
||||
opacity: expandLeftNav ? 1 : 0,
|
||||
transition: "opacity 0.1s",
|
||||
|
||||
borderRadius: theme.palette?.borderRadius,
|
||||
backgroundColor: hovered || currentWorkflowId === workflow.id ? "#1f1f1f" : "transparent",
|
||||
}}
|
||||
disableRipple
|
||||
>
|
||||
<Box
|
||||
style={{
|
||||
display: "flex",
|
||||
marginRight: "auto",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
|
||||
{relevantImageUrl !== undefined && relevantImageUrl !== null && relevantImageUrl !== "" ?
|
||||
<Avatar
|
||||
alt={workflow?.name}
|
||||
src={relevantImageUrl}
|
||||
style={{ width: 24, height: 24, marginRight: 5, }}
|
||||
/>
|
||||
:
|
||||
workflow?.apps?.slice(0, 2).map((data, index) => (
|
||||
<Box
|
||||
key={index}
|
||||
style={{
|
||||
position: "relative",
|
||||
marginLeft: index === 1 ? -8 : 0,
|
||||
}}
|
||||
>
|
||||
<Avatar
|
||||
alt={data.app_name}
|
||||
src={
|
||||
data.large_image
|
||||
? data.large_image
|
||||
: "/images/no_image.png"
|
||||
}
|
||||
style={{ width: 24, height: 24 }}
|
||||
/>
|
||||
</Box>
|
||||
))}
|
||||
<Typography
|
||||
style={{
|
||||
color: "#CDCDCD",
|
||||
fontSize: 16,
|
||||
marginLeft: 8,
|
||||
maxWidth: 180,
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
whiteSpace: "nowrap",
|
||||
}}
|
||||
>
|
||||
{workflow?.name}
|
||||
</Typography>
|
||||
|
||||
{onclickHandler !== undefined && workflow.sharing !== "form" ?
|
||||
<Tooltip title="Private Org Form" placement="right">
|
||||
<LockIcon style={{height: 15, width: 15, color: "grey", position: "absolute", left: -17, }}/>
|
||||
</Tooltip>
|
||||
: null
|
||||
}
|
||||
</Box>
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default RecentWorkflow
|
||||
@@ -35,6 +35,7 @@ import {
|
||||
PlayArrow as PlayArrowIcon,
|
||||
Insights as InsightsIcon,
|
||||
Replay as ReplayIcon,
|
||||
EditNote as EditNoteIcon,
|
||||
} from '@mui/icons-material';
|
||||
|
||||
import { DataGrid, GridColDef, GridValueGetterParams } from '@mui/x-data-grid'
|
||||
@@ -76,6 +77,10 @@ const RuntimeDebugger = (props) => {
|
||||
{"id": "", "name": "All Workflows",}
|
||||
])
|
||||
|
||||
if (document != undefined) {
|
||||
document.title = "Workflow Run Debugger"
|
||||
}
|
||||
|
||||
// Shitty workflow search on purpose :)
|
||||
const handleWorkflowUsageCount = (workflows) => {
|
||||
if (workflows === undefined || workflows === null || workflows.length === 0) {
|
||||
@@ -219,7 +224,7 @@ const RuntimeDebugger = (props) => {
|
||||
}
|
||||
|
||||
|
||||
const getAvailableWorkflows = () => {
|
||||
const getAvailableWorkflows = (workflowId) => {
|
||||
fetch(globalUrl + "/api/v1/workflows", {
|
||||
method: "GET",
|
||||
headers: {
|
||||
@@ -240,6 +245,15 @@ const RuntimeDebugger = (props) => {
|
||||
var foundWorkflows = [{"id": "", "name": "All Workflows",}]
|
||||
foundWorkflows.push(...responseJson)
|
||||
setWorkflows(foundWorkflows)
|
||||
|
||||
if (workflowId !== undefined && workflowId !== null && workflowId !== "" && workflowId.length === 36) {
|
||||
for (var key in responseJson) {
|
||||
if (responseJson[key].id === workflowId) {
|
||||
setWorkflow(responseJson[key])
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
@@ -248,7 +262,6 @@ const RuntimeDebugger = (props) => {
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
getAvailableWorkflows()
|
||||
|
||||
// Find workflow_id in url query
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
@@ -265,6 +278,8 @@ const RuntimeDebugger = (props) => {
|
||||
}
|
||||
}
|
||||
|
||||
getAvailableWorkflows(workflowId)
|
||||
|
||||
const foundStatus = urlParams.get('status');
|
||||
if (foundStatus !== undefined && foundStatus !== null && foundStatus !== "") {
|
||||
setStatus(foundStatus)
|
||||
@@ -314,15 +329,17 @@ const RuntimeDebugger = (props) => {
|
||||
|
||||
var source = params.row.execution_source
|
||||
if (source === "schedule") {
|
||||
foundSource = <img src={alltriggers[1].large_image} alt="schedule" style={{borderRadius: theme.palette.borderRadius, height: imageSize, width: imageSize, }} />
|
||||
foundSource = <img src={alltriggers[1].large_image} alt="schedule" style={{borderRadius: theme.palette?.borderRadius, height: imageSize, width: imageSize, }} />
|
||||
} else if (source === "webhook") {
|
||||
foundSource = <img src={alltriggers[0].large_image} alt="webhook" style={{borderRadius: theme.palette.borderRadius, height: imageSize, width: imageSize, }} />
|
||||
foundSource = <img src={alltriggers[0].large_image} alt="webhook" style={{borderRadius: theme.palette?.borderRadius, height: imageSize, width: imageSize, }} />
|
||||
} else if (source === "subflow" || source.length === 36) {
|
||||
foundSource = <img src={alltriggers[4].large_image} alt="subflow" style={{borderRadius: theme.palette.borderRadius, height: imageSize, width: imageSize, }} />
|
||||
foundSource = <img src={alltriggers[3].large_image} alt="subflow" style={{borderRadius: theme.palette?.borderRadius, height: imageSize, width: imageSize, }} />
|
||||
source = "subflow"
|
||||
} else if (source === "rerun" || source.length === 36) {
|
||||
foundSource = <ReplayIcon style={{color: theme.palette.primary.secondary, height: imageSize, width: imageSize, }} />
|
||||
source = "rerun of a previous run"
|
||||
} else if (source === "form") {
|
||||
foundSource = <EditNoteIcon style={{color: theme.palette.primary.secondary, height: imageSize, width: imageSize, }} />
|
||||
} else {
|
||||
source = "manual"
|
||||
}
|
||||
@@ -888,13 +905,13 @@ const RuntimeDebugger = (props) => {
|
||||
{userdata.support === true ?
|
||||
<Button
|
||||
variant={ignoreOrg ? "contained" : "outlined"}
|
||||
color="primary"
|
||||
style={{maxHeight: 40, marginTop: 25, }}
|
||||
color="secondary"
|
||||
style={{marginLeft: 100, maxHeight: 40, marginTop: 25, }}
|
||||
onClick={() => {
|
||||
setIgnoreOrg(!ignoreOrg)
|
||||
}}
|
||||
>
|
||||
{ignoreOrg ? "Ignoring Org" : "Ignore Org"}
|
||||
{ignoreOrg ? "Ignoring Org" : "Ignore Org (Support Only)"}
|
||||
</Button>
|
||||
: null}
|
||||
</div>
|
||||
@@ -942,11 +959,8 @@ const RuntimeDebugger = (props) => {
|
||||
},
|
||||
}}
|
||||
getOptionLabel={(option) => {
|
||||
if (
|
||||
option === undefined ||
|
||||
option === null ||
|
||||
option.name === undefined ||
|
||||
option.name === null
|
||||
if (option === undefined || option === null ||
|
||||
option.name === undefined || option.name === null
|
||||
) {
|
||||
return "No Workflow Selected";
|
||||
}
|
||||
@@ -961,7 +975,7 @@ const RuntimeDebugger = (props) => {
|
||||
style={{
|
||||
backgroundColor: theme.palette.inputColor,
|
||||
height: 50,
|
||||
borderRadius: theme.palette.borderRadius,
|
||||
borderRadius: theme.palette?.borderRadius,
|
||||
marginTop: 5,
|
||||
marginLeft: 5,
|
||||
}}
|
||||
@@ -995,13 +1009,13 @@ const RuntimeDebugger = (props) => {
|
||||
<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, }} />
|
||||
<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,
|
||||
@@ -1023,7 +1037,7 @@ const RuntimeDebugger = (props) => {
|
||||
<TextField
|
||||
style={{
|
||||
backgroundColor: theme.palette.inputColor,
|
||||
borderRadius: theme.palette.borderRadius,
|
||||
borderRadius: theme.palette?.borderRadius,
|
||||
}}
|
||||
{...params}
|
||||
label="Workflow"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import React, { useState, useEffect, useRef, useContext } from 'react';
|
||||
|
||||
import theme from '../theme.jsx';
|
||||
import { useNavigate, Link, useParams } from "react-router-dom";
|
||||
@@ -33,6 +33,8 @@ import {
|
||||
AvatarGroup,
|
||||
} from "@mui/material"
|
||||
|
||||
import { Context } from '../context/ContextApi.jsx';
|
||||
|
||||
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';
|
||||
@@ -47,8 +49,9 @@ const chipStyle = {
|
||||
|
||||
const searchClient = algoliasearch("JNSS5CFDZZ", "db08e40265e2941b9a7d8f644b6e5240")
|
||||
const SearchData = props => {
|
||||
const { serverside, globalUrl, userdata, setModalOpen, modalOpen } = props
|
||||
const { serverside, globalUrl, userdata } = props
|
||||
let navigate = useNavigate();
|
||||
const { searchBarModalOpen, setSearchBarModalOpen } = useContext(Context);
|
||||
const borderRadius = 3
|
||||
const node = useRef()
|
||||
const [searchOpen, setSearchOpen] = useState(false)
|
||||
@@ -56,8 +59,8 @@ const SearchData = props => {
|
||||
const [value, setValue] = useState("");
|
||||
|
||||
const handleLinkClick = () => {
|
||||
if (modalOpen) {
|
||||
setModalOpen(false); // Assuming setModalOpen is defined correctly
|
||||
if (searchBarModalOpen) {
|
||||
setSearchBarModalOpen(false); // Assuming setModalOpen is defined correctly
|
||||
} else {
|
||||
console.log("Condition not met, staying on the same page");
|
||||
}
|
||||
@@ -71,7 +74,7 @@ const SearchData = props => {
|
||||
// return null
|
||||
//}
|
||||
|
||||
const isCloud = window.location.host === "localhost:3002" || window.location.host === "shuffler.io";
|
||||
const isCloud = (window.location.host === "localhost:3002" || window.location.host === "shuffler.io") ? true : (process.env.IS_SSR === "true");
|
||||
// if (window.location.pathname === "/docs" || window.location.pathname === "/apps" || window.location.pathname === "/usecases" ) {
|
||||
// setModalOpen(false)
|
||||
// }
|
||||
@@ -88,14 +91,14 @@ const SearchData = props => {
|
||||
|
||||
const textFieldRef = useRef(null);
|
||||
const keyPressHandler = (e) => {
|
||||
if (e.which === 13) {
|
||||
if (e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
// navigate(`/search?q=${currentRefinement}`, { state: value, replace: true });
|
||||
// setModalOpen(false);
|
||||
const trimmedValue = inputValue.trim();
|
||||
if (trimmedValue !== '') {
|
||||
e.preventDefault();
|
||||
navigate(`/search?q=${trimmedValue}`, { state: trimmedValue, replace: true });
|
||||
setModalOpen(false);
|
||||
setSearchBarModalOpen(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -249,7 +252,7 @@ const SearchData = props => {
|
||||
<Link key={hit.objectID} to={parsedUrl} rel="noopener noreferrer" style={{ textDecoration: "none", color: "white", }} onClick={(event) => {
|
||||
//console.log("CLICK")
|
||||
setSearchOpen(true)
|
||||
setModalOpen(false)
|
||||
setSearchBarModalOpen(false)
|
||||
aa('init', {
|
||||
appId: searchClient.appId,
|
||||
apiKey: searchClient.transporter.queryParameters["x-algolia-api-key"]
|
||||
@@ -498,7 +501,7 @@ const SearchData = props => {
|
||||
return (
|
||||
<Link key={hit.objectID} to={parsedUrl} style={{ textDecoration: "none", color: "white", }} onClick={(event) => {
|
||||
setSearchOpen(true)
|
||||
setModalOpen(false)
|
||||
setSearchBarModalOpen(false)
|
||||
aa('init', {
|
||||
appId: searchClient.appId,
|
||||
apiKey: searchClient.transporter.queryParameters["x-algolia-api-key"]
|
||||
@@ -547,8 +550,6 @@ const SearchData = props => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
|
||||
console.log("OBJECT CHANGE: ", hit.objectID)
|
||||
|
||||
// This does nothing rofl
|
||||
if (userdata.active_apps === undefined || userdata.active_apps === null) {
|
||||
activateApp(hit.name, hit.objectID, "activate")
|
||||
@@ -702,7 +703,7 @@ const SearchData = props => {
|
||||
|
||||
console.log("CLICK")
|
||||
setSearchOpen(true)
|
||||
setModalOpen(false)
|
||||
setSearchBarModalOpen(false)
|
||||
}}>
|
||||
<ListItem key={hit.objectID} style={innerlistitemStyle} onMouseOver={() => {
|
||||
setMouseHoverIndex(index)
|
||||
@@ -873,10 +874,11 @@ const SearchData = props => {
|
||||
</List>
|
||||
</Grid>
|
||||
<Grid style={{ textAlign: "end", width: "100%", textTransform: 'capitalize', }}>
|
||||
<Button style={{ textAlign: "center", textTransform: 'capitalize' }}
|
||||
onClick={() => { window.location = "/search"; }} >
|
||||
See More
|
||||
</Button>
|
||||
<Link to="/search" style={{ textDecoration: "none", color: "#f85a3e" }}>
|
||||
<Button style={{ textAlign: "center", textTransform: 'capitalize' }}>
|
||||
See More
|
||||
</Button>
|
||||
</Link>
|
||||
</Grid>
|
||||
</Grid>
|
||||
) : null
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import React, { useState, useEffect, useRef, useContext } from 'react';
|
||||
|
||||
import theme from '../theme.jsx';
|
||||
import { useNavigate, Link, useParams } from "react-router-dom";
|
||||
import SearchBox from "../components/SearchData.jsx";
|
||||
|
||||
import { Context } from '../context/ContextApi.jsx';
|
||||
|
||||
import {
|
||||
Chip,
|
||||
IconButton,
|
||||
@@ -45,20 +47,22 @@ const chipStyle = {
|
||||
const SearchField = props => {
|
||||
const { serverside, userdata, isMobile, isLoaded, globalUrl, isHeader, isLoggedIn, small, rounded } = props
|
||||
|
||||
const {searchBarModalOpen, setSearchBarModalOpen} = useContext(Context);
|
||||
|
||||
let navigate = useNavigate();
|
||||
const borderRadius = 3
|
||||
const node = useRef()
|
||||
const [searchOpen, setSearchOpen] = useState(false)
|
||||
const [modalOpen, setModalOpen] = React.useState(false);
|
||||
// const [modalOpen, setModalOpen] = React.useState(false);
|
||||
const [oldPath, setOldPath] = useState("")
|
||||
const [value, setValue] = useState("");
|
||||
useEffect(() => {
|
||||
Mousetrap.bind(['command+k', 'ctrl+k'], () => {
|
||||
setModalOpen(true);
|
||||
setSearchBarModalOpen(true);
|
||||
return false; // Prevent the default action
|
||||
});
|
||||
Mousetrap.bind(['esc'], () => {
|
||||
setModalOpen(false);
|
||||
setSearchBarModalOpen(false);
|
||||
return false; // Prevent the default action
|
||||
});
|
||||
|
||||
@@ -72,9 +76,9 @@ const SearchField = props => {
|
||||
// console.log("key:", dataValue.key),
|
||||
//console.log("value:",dataValue.value),
|
||||
<Dialog
|
||||
open={modalOpen}
|
||||
open={searchBarModalOpen}
|
||||
onClose={() => {
|
||||
setModalOpen(false);
|
||||
setSearchBarModalOpen(false);
|
||||
}}
|
||||
PaperProps={{
|
||||
style: {
|
||||
@@ -92,12 +96,12 @@ const SearchField = props => {
|
||||
{isHeader ? <div style={{ display: "flex"}}>
|
||||
<DialogTitle style={{ marginTop: 15, marginLeft: 5, color: "var(--Paragraph-text, #C8C8C8)" }} >Search for Docs, Apps, Workflows and more</DialogTitle>
|
||||
<Button color="secondary" fullWidth style={{ marginLeft:180, }} onClick={() => {
|
||||
setModalOpen(false);
|
||||
setSearchBarModalOpen(false);
|
||||
}}><CloseIcon /></Button>
|
||||
</div>
|
||||
: null}
|
||||
<DialogContent className='dialog-content' style={{}}>
|
||||
<SearchBox globalUrl={globalUrl} setModalOpen={setModalOpen} modalOpen={modalOpen} serverside={serverside} userdata={userdata} />
|
||||
<SearchBox globalUrl={globalUrl} serverside={serverside} userdata={userdata} />
|
||||
</DialogContent>
|
||||
<Divider style={{overflow: "hidden"}}/>
|
||||
<span style={{display:"flex", width:"100%", height:30}}>
|
||||
@@ -124,10 +128,10 @@ const SearchField = props => {
|
||||
);
|
||||
|
||||
return (
|
||||
<div style={{ marginTop: "auto", marginLeft: !isLoggedIn ? 0 : 130 }}>
|
||||
<div style={{ marginTop: "auto", marginLeft: !isLoggedIn ? 0: "auto", marginRight: !isLoggedIn ? 0 : "auto", width: !isLoggedIn ? "auto" : 410, }}>
|
||||
{modalView}
|
||||
<TextField
|
||||
style={{ backgroundColor: "#212121", height: 48, borderRadius: rounded === true ? 25 : theme.palette.borderRadius, minWidth: fieldWidth, maxWidth: fieldWidth, }}
|
||||
style={{ backgroundColor: "#212121", height: 48, borderRadius: rounded === true ? 25 : theme.palette?.borderRadius, minWidth: fieldWidth, maxWidth: fieldWidth, }}
|
||||
InputProps={{
|
||||
style: {
|
||||
color: "white",
|
||||
@@ -155,7 +159,7 @@ const SearchField = props => {
|
||||
color="primary"
|
||||
placeholder="Search Apps, Workflows, Docs..."
|
||||
onClick={(event) => {
|
||||
setModalOpen(true)
|
||||
setSearchBarModalOpen(true)
|
||||
}}
|
||||
limit={5}
|
||||
/>
|
||||
|
||||
@@ -40,11 +40,12 @@ import {
|
||||
|
||||
Close as CloseIcon,
|
||||
DragIndicator as DragIndicatorIcon,
|
||||
RestartAlt as RestartAltIcon,
|
||||
} from '@mui/icons-material';
|
||||
|
||||
|
||||
import { validateJson } from "../views/Workflows.jsx";
|
||||
import ReactJson from "react-json-view";
|
||||
import ReactJson from "react-json-view-ssr";
|
||||
import PaperComponent from "../components/PaperComponent.jsx";
|
||||
|
||||
import { padding, textAlign } from '@mui/system';
|
||||
@@ -56,6 +57,7 @@ import { tags as t } from '@lezer/highlight';
|
||||
import AceEditor from "react-ace";
|
||||
import ace from "ace-builds";
|
||||
import 'ace-builds/src-noconflict/mode-python';
|
||||
import 'ace-builds/src-noconflict/mode-json';
|
||||
//import 'ace-builds/src-noconflict/theme-twilight';
|
||||
//import 'ace-builds/src-noconflict/theme-solarized_dark';
|
||||
import 'ace-builds/src-noconflict/theme-gruvbox';
|
||||
@@ -105,12 +107,16 @@ const CodeEditor = (props) => {
|
||||
selectedAction ,
|
||||
workflowExecutions,
|
||||
getParents,
|
||||
|
||||
activeDialog,
|
||||
setActiveDialog,
|
||||
fieldname,
|
||||
contentLoading,
|
||||
editorData,
|
||||
|
||||
setAiQueryModalOpen,
|
||||
fullScreenMode
|
||||
} = props
|
||||
|
||||
|
||||
|
||||
const [localcodedata, setlocalcodedata] = React.useState(codedata === undefined || codedata === null || codedata.length === 0 ? "" : codedata);
|
||||
|
||||
//const { setContainer } = useCodeMirror({
|
||||
@@ -163,6 +169,11 @@ const CodeEditor = (props) => {
|
||||
setMenuPosition(null);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
highlight_variables(localcodedata)
|
||||
expectedOutput(localcodedata)
|
||||
}, [localcodedata])
|
||||
|
||||
let navigate = useNavigate();
|
||||
|
||||
useEffect(() => {
|
||||
@@ -627,8 +638,8 @@ const CodeEditor = (props) => {
|
||||
newMarkers.push({
|
||||
startRow: i,
|
||||
startCol: startCh,
|
||||
endRow: i+1,
|
||||
endCol: endCh+1,
|
||||
endRow: i,
|
||||
endCol: endCh,
|
||||
className: correctVariable ? "good-marker" : "bad-marker",
|
||||
type: "text",
|
||||
})
|
||||
@@ -811,11 +822,7 @@ const CodeEditor = (props) => {
|
||||
}
|
||||
|
||||
const handleItemClick = (values) => {
|
||||
if (
|
||||
values === undefined ||
|
||||
values === null ||
|
||||
values.length === 0
|
||||
) {
|
||||
if (values === undefined || values === null || values.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -830,7 +837,11 @@ const CodeEditor = (props) => {
|
||||
toComplete += values[key].autocomplete;
|
||||
}
|
||||
|
||||
setlocalcodedata(localcodedata+toComplete)
|
||||
|
||||
handleClick({
|
||||
"value": toComplete
|
||||
})
|
||||
//setlocalcodedata(localcodedata+toComplete)
|
||||
setMenuPosition(null)
|
||||
}
|
||||
|
||||
@@ -839,10 +850,37 @@ const CodeEditor = (props) => {
|
||||
return
|
||||
}
|
||||
|
||||
if (!item.value.includes("{%") && !item.value.includes("{{")) {
|
||||
setlocalcodedata(localcodedata+" | "+item.value+" }}")
|
||||
} else {
|
||||
setlocalcodedata(localcodedata+item.value)
|
||||
// Injects it in the right spot instead of random
|
||||
var edited = false
|
||||
if (currentCharacter !== undefined && currentCharacter !== null && currentCharacter !== -1 && currentLine !== undefined && currentLine !== null && currentLine !== -1) {
|
||||
// Input at the right spot
|
||||
var codedatasplit = localcodedata.split('\n')
|
||||
if (codedatasplit.length > currentLine) {
|
||||
var currentLineData = codedatasplit[currentLine]
|
||||
|
||||
// Remove newlines from item.value
|
||||
if (item.value.includes("% python %")) {
|
||||
item.value = item.value.replaceAll("\n", ";")
|
||||
item.value = item.value.replaceAll("python %};", "python %}")
|
||||
} else {
|
||||
item.value = item.value.replaceAll("\n", "")
|
||||
}
|
||||
|
||||
currentLineData = currentLineData.slice(0, currentCharacter) + item.value + currentLineData.slice(currentCharacter)
|
||||
codedatasplit[currentLine] = currentLineData
|
||||
|
||||
setlocalcodedata(codedatasplit.join('\n'))
|
||||
|
||||
edited = true
|
||||
}
|
||||
}
|
||||
|
||||
if (edited === false) {
|
||||
if (!item.value.includes("{%") && !item.value.includes("{{")) {
|
||||
setlocalcodedata(localcodedata+" | "+item.value+" }}")
|
||||
} else {
|
||||
setlocalcodedata(localcodedata+item.value)
|
||||
}
|
||||
}
|
||||
|
||||
setAnchorEl(null)
|
||||
@@ -858,8 +896,8 @@ const CodeEditor = (props) => {
|
||||
// Shuffle Tools 1.2.0 (in most cases?)
|
||||
const appid = toolsAppId !== undefined && toolsAppId !== null && toolsAppId.length > 0 ? toolsAppId : "3e2bdf9d5069fe3f4746c29d68785a6a"
|
||||
|
||||
const actionname = selectedAction.name === "execute_python" && !inputdata.replaceAll(" ", "").includes("{%python%}") ? "execute_python" : "repeat_back_to_me"
|
||||
const params = actionname === "execute_python" ? [{"name": "code", "value":inputdata}] : [{"name":"call", "value": inputdata}]
|
||||
const actionname = selectedAction.name === "execute_python" && !inputdata.replaceAll(" ", "").includes("{%python%}") ? "execute_python" : selectedAction.name === "execute_bash" ? "execute_bash" : "repeat_back_to_me"
|
||||
const params = actionname === "execute_python" ? [{"name": "code", "value":inputdata}] : actionname === "execute_bash" ? [{"name": "code", "value":inputdata}, {"name": "shuffle_input", "value": "", }] : [{"name":"call", "value": inputdata}]
|
||||
|
||||
const actiondata = {"description":"Repeats the call parameter","id":"","name":actionname,"label":"","node_type":"","environment":"","sharing":false,"private_id":"","public_id":"","app_id": appid,"tags":null,"authentication":[],"tested":false,"parameters": params, "execution_variable":{"description":"","id":"","name":"","value":""},"returns":{"description":"","example":"","id":"","schema":{"type":"string"}},"authentication_id":"","example":"","auth_not_required":false,"source_workflow":"","run_magic_output":false,"run_magic_input":false,"execution_delay":0,"app_name":"Shuffle Tools","app_version":"1.2.0","selectedAuthentication":{}}
|
||||
|
||||
@@ -946,23 +984,77 @@ const CodeEditor = (props) => {
|
||||
|
||||
|
||||
// Define a custom completer for the Ace Editor
|
||||
const customVariables = availableVariables
|
||||
const customCompleter = {
|
||||
getCompletions: function(editor, session, pos, prefix, callback) {
|
||||
callback(null, customVariables.map((variable) => ({
|
||||
caption: variable,
|
||||
value: variable,
|
||||
meta: 'custom',
|
||||
})));
|
||||
console.log("CUSTOM COMPLETER: ", prefix)
|
||||
|
||||
callback(null, availableVariables.map((variable) => {
|
||||
console.log("CUSTOM VAR: ", variable)
|
||||
|
||||
return ({
|
||||
caption: variable,
|
||||
value: variable,
|
||||
meta: 'custom',
|
||||
})
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
if (fullScreenMode) {
|
||||
return (
|
||||
<AceEditor
|
||||
mode="python"
|
||||
theme="gruvbox"
|
||||
value={localcodedata}
|
||||
onChange={(value, editor) => {
|
||||
// setlocalcodedata(value)
|
||||
// expectedOutput(value)
|
||||
// highlight_variables(value,editor)
|
||||
setlocalcodedata(value)
|
||||
setcodedata(value)
|
||||
}}
|
||||
name="python-editor"
|
||||
fontSize={14}
|
||||
width="100%"
|
||||
height="100%"
|
||||
showPrintMargin={false}
|
||||
showGutter={true}
|
||||
markers={markers}
|
||||
highlightActiveLine={false}
|
||||
|
||||
enableBasicAutocompletion={true}
|
||||
completers={[customCompleter]}
|
||||
|
||||
style={{
|
||||
wordBreak: "break-word",
|
||||
marginTop: 0,
|
||||
paddingBottom: 10,
|
||||
overflowY: "auto",
|
||||
whiteSpace: "pre-wrap",
|
||||
wordWrap: "break-word",
|
||||
backgroundColor: "rgba(40,40,40,1)",
|
||||
zIndex: activeDialog === "codeeditor" ? 1200 : 1100,
|
||||
}}
|
||||
|
||||
setOptions={{
|
||||
enableBasicAutocompletion: true,
|
||||
enableLiveAutocompletion: true,
|
||||
enableSnippets: true,
|
||||
showLineNumbers: true,
|
||||
tabSize: 4,
|
||||
fontFamily: "'JetBrains Mono', Consolas, monospace",
|
||||
useSoftTabs: true
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
aria-labelledby="draggable-code-modal"
|
||||
disableBackdropClick={true}
|
||||
aria-labelledby="draggable-dialog-title"
|
||||
// disableBackdropClick={true}
|
||||
disableEnforceFocus={true}
|
||||
//style={{ pointerEvents: "none" }}
|
||||
style={{ pointerEvents: "none", zIndex: activeDialog === "codeeditor" ? 1200 : 1100}}
|
||||
hideBackdrop={true}
|
||||
open={expansionModalOpen}
|
||||
onClose={() => {
|
||||
@@ -977,8 +1069,14 @@ const CodeEditor = (props) => {
|
||||
}}
|
||||
PaperComponent={PaperComponent}
|
||||
PaperProps={{
|
||||
onClick: () => {
|
||||
if (setActiveDialog !== undefined) {
|
||||
setActiveDialog("codeeditor")
|
||||
}
|
||||
},
|
||||
style: {
|
||||
zIndex: 12501,
|
||||
// zIndex: 12501,
|
||||
pointerEvents: "auto",
|
||||
color: "white",
|
||||
minWidth: isMobile ? "100%" : isFileEditor ? 650 : "80%",
|
||||
maxWidth: isMobile ? "100%" : isFileEditor ? 650 : 1100,
|
||||
@@ -986,9 +1084,22 @@ const CodeEditor = (props) => {
|
||||
maxHeight: isMobile ? "100%" : 700,
|
||||
border: theme.palette.defaultBorder,
|
||||
padding: isMobile ? "25px 10px 25px 10px" : 25,
|
||||
zoom: 0.8,
|
||||
backgroundColor: "black",
|
||||
},
|
||||
}}
|
||||
>
|
||||
|
||||
{contentLoading === true ?
|
||||
<Tooltip
|
||||
color="primary"
|
||||
title={`The File content is loading. Please wait a moment.`}
|
||||
placement="top"
|
||||
>
|
||||
<CircularProgress style={{position: "absolute", right: 106, top: 6, }}/>
|
||||
</Tooltip>
|
||||
: null}
|
||||
|
||||
<Tooltip
|
||||
color="primary"
|
||||
title={`Move window`}
|
||||
@@ -1071,10 +1182,15 @@ const CodeEditor = (props) => {
|
||||
*/}
|
||||
{ isFileEditor ? null :
|
||||
<div style={{display: "flex", maxHeight: 40, }}>
|
||||
{selectedAction.name === "execute_python" ?
|
||||
{selectedAction?.name === "execute_python" ?
|
||||
<Typography variant="body1" style={{marginTop: 5, }}>
|
||||
Run Python Code
|
||||
</Typography>
|
||||
:
|
||||
selectedAction.name === "execute_bash" ?
|
||||
<Typography variant="body1" style={{marginTop: 5, }}>
|
||||
Run Bash Code
|
||||
</Typography>
|
||||
:
|
||||
<div style={{display: "flex", }}>
|
||||
<Button
|
||||
@@ -1236,7 +1352,7 @@ const CodeEditor = (props) => {
|
||||
maxHeight: 650,
|
||||
}}
|
||||
>
|
||||
{actionlist.map((innerdata) => {
|
||||
{actionlist?.map((innerdata) => {
|
||||
const icon =
|
||||
innerdata.type === "action" ? (
|
||||
<AppsIcon style={{ marginRight: 10 }} />
|
||||
@@ -1321,6 +1437,8 @@ const CodeEditor = (props) => {
|
||||
onClick={() => {
|
||||
console.log("CLICKED: ", innerdata);
|
||||
console.log(innerdata.example)
|
||||
|
||||
//const handleClick = (item) => {
|
||||
handleItemClick([innerdata]);
|
||||
}}
|
||||
>
|
||||
@@ -1465,14 +1583,37 @@ const CodeEditor = (props) => {
|
||||
width: 50,
|
||||
marginLeft: 100,
|
||||
}}
|
||||
disabled={editorData === undefined || editorData.example === undefined || editorData.example === null || editorData.example.length === 0}
|
||||
onClick={() => {
|
||||
setlocalcodedata(editorData.example)
|
||||
}}
|
||||
color="secondary"
|
||||
>
|
||||
<Tooltip
|
||||
title={"Reset to example body"}
|
||||
placement="top"
|
||||
>
|
||||
<RestartAltIcon />
|
||||
</Tooltip>
|
||||
</IconButton>
|
||||
<IconButton
|
||||
style={{
|
||||
height: 50,
|
||||
width: 50,
|
||||
marginLeft: 0,
|
||||
}}
|
||||
disabled={isAiLoading}
|
||||
onClick={() => {
|
||||
autoFormat(localcodedata)
|
||||
if (setAiQueryModalOpen !== undefined) {
|
||||
setAiQueryModalOpen(true)
|
||||
} else {
|
||||
autoFormat(localcodedata)
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Tooltip
|
||||
color="primary"
|
||||
title={"Auto format data"}
|
||||
title={"Format with AI"}
|
||||
placement="top"
|
||||
>
|
||||
{isAiLoading ?
|
||||
@@ -1487,7 +1628,7 @@ const CodeEditor = (props) => {
|
||||
}
|
||||
|
||||
<div style={{
|
||||
borderRadius: theme.palette.borderRadius,
|
||||
borderRadius: theme.palette?.borderRadius,
|
||||
position: "relative",
|
||||
paddingTop: 0,
|
||||
// minHeight: 548,
|
||||
@@ -1495,8 +1636,10 @@ const CodeEditor = (props) => {
|
||||
}}>
|
||||
{(availableVariables !== undefined && availableVariables !== null && availableVariables.length > 0) || isFileEditor ? (
|
||||
<AceEditor
|
||||
id="shuffle-codeeditor"
|
||||
name="shuffle-codeeditor"
|
||||
value={localcodedata}
|
||||
mode={selectedAction === undefined ? "" : selectedAction.name === "execute_python" ? "python" : ""}
|
||||
mode={selectedAction === undefined ? "json" : selectedAction.name === "execute_python" ? "python" : selectedAction.name === "execute_bash" ? "bash" : "json"}
|
||||
theme="gruvbox"
|
||||
height={isFileEditor ? 450 : 550}
|
||||
width={isFileEditor ? 650 : "100%"}
|
||||
@@ -1515,17 +1658,16 @@ const CodeEditor = (props) => {
|
||||
whiteSpace: "pre-wrap",
|
||||
wordWrap: "break-word",
|
||||
backgroundColor: "rgba(40,40,40,1)",
|
||||
zIndex: activeDialog === "codeeditor" ? 1200 : 1100,
|
||||
}}
|
||||
onLoad={(editor) => {
|
||||
highlight_variables(localcodedata)
|
||||
}}
|
||||
onCursorChange={(cursorPosition, editor, value) => {
|
||||
setCurrentCharacter(cursorPosition.column)
|
||||
setCurrentLine(cursorPosition.row)
|
||||
setCurrentCharacter(cursorPosition.cursor.column)
|
||||
setCurrentLine(cursorPosition.cursor.row)
|
||||
findIndex(cursorPosition.row, cursorPosition.column)
|
||||
|
||||
//highlight_variables(localcodedata)
|
||||
//console.log("VALUE CURSOR: ", value)
|
||||
}}
|
||||
onChange={(value, editor) => {
|
||||
// setlocalcodedata(value)
|
||||
@@ -1559,7 +1701,7 @@ const CodeEditor = (props) => {
|
||||
</div>
|
||||
|
||||
{isFileEditor ? null :
|
||||
<div style={{flex: 1, marginLeft: 5, borderLeft: "1px solid rgba(255,255,255,0.3)", paddingLeft: 5, }}>
|
||||
<div style={{flex: 1, marginLeft: 5, borderLeft: "1px solid rgba(255,255,255,0.3)", paddingLeft: 5, overflow: "hidden", }}>
|
||||
<div>
|
||||
{isMobile ? null :
|
||||
<DialogTitle
|
||||
@@ -1567,11 +1709,13 @@ const CodeEditor = (props) => {
|
||||
paddingLeft: 10,
|
||||
paddingTop: 0,
|
||||
display: "flex",
|
||||
cursor: "move"
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
<span style={{color: "white"}}>
|
||||
Expected Output
|
||||
|
||||
{selectedAction === undefined ? "" : selectedAction.name === "execute_python" || selectedAction.name === "execute_bash" ? "Code to run" : `Expected Output for '${selectedAction.name}'`}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -1588,7 +1732,7 @@ const CodeEditor = (props) => {
|
||||
border: `1px solid ${theme.palette.primary.main}`,
|
||||
position: "absolute",
|
||||
top: 24,
|
||||
right: 65,
|
||||
right: 100,
|
||||
maxHeight: 35,
|
||||
minWidth: 70,
|
||||
}}
|
||||
@@ -1599,7 +1743,7 @@ const CodeEditor = (props) => {
|
||||
{executing ?
|
||||
<CircularProgress style={{height: 18, width: 18, }} />
|
||||
:
|
||||
<span>Try it <PlayArrowIcon style={{height: 18, width: 18, marginBottom: -4, marginLeft: 5, }} /> </span>
|
||||
<span>{selectedAction === undefined ? "" : selectedAction.name === "execute_python" ? "Run Python Code" : selectedAction.name === "execute_bash" ? "Run Bash" : "Try it"}<PlayArrowIcon style={{height: 18, width: 18, marginBottom: -4, marginLeft: 5, }} /> </span>
|
||||
}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
@@ -1618,6 +1762,7 @@ const CodeEditor = (props) => {
|
||||
overflow: "auto",
|
||||
minWidth: 450,
|
||||
maxWidth: "100%",
|
||||
zIndex: activeDialog === "codeeditor" ? 1200 : 1100,
|
||||
}}
|
||||
collapsed={false}
|
||||
enableClipboard={(copy) => {
|
||||
@@ -1645,11 +1790,12 @@ const CodeEditor = (props) => {
|
||||
padding: 10,
|
||||
marginTop: -2,
|
||||
border: `2px solid ${theme.palette.inputColor}`,
|
||||
borderRadius: theme.palette.borderRadius,
|
||||
borderRadius: theme.palette?.borderRadius,
|
||||
maxHeight: 450,
|
||||
minHeight: 450,
|
||||
overflow: "auto",
|
||||
wordWrap: "anywhere",
|
||||
zIndex: activeDialog === "codeeditor" ? 1200 : 1100,
|
||||
}}
|
||||
>
|
||||
{expOutput}
|
||||
|
||||
@@ -102,7 +102,7 @@ const SuggestedWorkflows = (props) => {
|
||||
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={() => {
|
||||
<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)
|
||||
@@ -140,7 +140,7 @@ const SuggestedWorkflows = (props) => {
|
||||
|
||||
//<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,}}>
|
||||
<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={() => {
|
||||
@@ -193,7 +193,7 @@ const SuggestedWorkflows = (props) => {
|
||||
apps={apps}
|
||||
/>
|
||||
</Dialog>
|
||||
<div style={{minWidth: 250, maxWidth: 250, padding: 15, borderRadius: theme.palette.borderRadius, position: "relative", }}>
|
||||
<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>
|
||||
|
||||
@@ -349,7 +349,7 @@ const UsecaseSearch = (props) => {
|
||||
const [selectedAction, setSelectedAction] = React.useState({});
|
||||
const [firstRequest, setFirstRequest] = React.useState(true);
|
||||
|
||||
const isCloud = window.location.host === "localhost:3002" || window.location.host === "shuffler.io";
|
||||
const isCloud = (window.location.host === "localhost:3002" || window.location.host === "shuffler.io") ? true : (process.env.IS_SSR === "true");
|
||||
//const alert = useAlert()
|
||||
|
||||
useEffect(() => {
|
||||
@@ -594,7 +594,7 @@ const UsecaseSearch = (props) => {
|
||||
width: 30,
|
||||
height: 30,
|
||||
border: "2px solid rgba(255,255,255,0.6)",
|
||||
borderRadius: theme.palette.borderRadius,
|
||||
borderRadius: theme.palette?.borderRadius,
|
||||
maxHeight: 30,
|
||||
maxWidth: 30,
|
||||
overflow: "hidden",
|
||||
@@ -616,7 +616,7 @@ const UsecaseSearch = (props) => {
|
||||
width: 30,
|
||||
height: 30,
|
||||
border: "2px solid rgba(255,255,255,0.6)",
|
||||
borderRadius: theme.palette.borderRadius,
|
||||
borderRadius: theme.palette?.borderRadius,
|
||||
maxWidth: 30,
|
||||
maxHeight: 30,
|
||||
overflow: "hidden",
|
||||
@@ -1266,9 +1266,11 @@ const UsecaseSearch = (props) => {
|
||||
.then((responseJson) => {
|
||||
if (responseJson.success === false) {
|
||||
var msgString = "Failed to activate the app"
|
||||
|
||||
if (responseJson.reason !== undefined) {
|
||||
msgString += ": " + responseJson.reason
|
||||
}
|
||||
|
||||
toast(msgString)
|
||||
} else {
|
||||
//toast("App activated for your organization! Refresh the page to use the app.")
|
||||
@@ -1388,7 +1390,7 @@ const UsecaseSearch = (props) => {
|
||||
<Typography variant="body1" style={{color: "rgba(255,255,255,0.5)", marginRight: 20, marginTop: 13, }}>
|
||||
{startText}
|
||||
</Typography>
|
||||
<div style={{border: `1px solid ${borderColor}`, backgroundColor: theme.palette.surfaceColor, width: miditem === true ? "65%" : "85%", marginLeft: miditem === true ? 125 : 0, borderRadius: expanded ? theme.palette.borderRadius : 50, maxHeight: expanded || hasError ? 500 : 50, minHeight: 50, }}>
|
||||
<div style={{border: `1px solid ${borderColor}`, backgroundColor: theme.palette.surfaceColor, width: miditem === true ? "65%" : "85%", marginLeft: miditem === true ? 125 : 0, borderRadius: expanded ? theme.palette?.borderRadius : 50, maxHeight: expanded || hasError ? 500 : 50, minHeight: 50, }}>
|
||||
|
||||
{selectionOpen === true ?
|
||||
<AppsearchPopout
|
||||
@@ -1560,7 +1562,7 @@ const UsecaseSearch = (props) => {
|
||||
// <b>{defaultSearch}: {allusecases[usecaseIndex].name}</b>
|
||||
//console.log("UseCase: ", usecases)
|
||||
return (
|
||||
<div style={{maxWidth: "100%", minWidth: "100%", border: "1px solid rgba(255,255,255,0)", borderRadius: theme.palette.borderRadius,}}>
|
||||
<div style={{maxWidth: "100%", minWidth: "100%", border: "1px solid rgba(255,255,255,0)", borderRadius: theme.palette?.borderRadius,}}>
|
||||
{configureWorkflowModal}
|
||||
{authenticationModal}
|
||||
{showTitle !== false && defaultSearch !== undefined ?
|
||||
|
||||
@@ -64,6 +64,7 @@ const WelcomeForm = (props) => {
|
||||
discoveryWrapper,
|
||||
setDiscoveryWrapper,
|
||||
appFramework,
|
||||
setAppFramework,
|
||||
getFramework,
|
||||
activeStep,
|
||||
setActiveStep,
|
||||
@@ -160,7 +161,7 @@ const WelcomeForm = (props) => {
|
||||
const [clickdiff, setclickdiff] = useState(0);
|
||||
const [mouseHoverIndex, setMouseHoverIndex] = useState(-1)
|
||||
|
||||
const isCloud = window.location.host === "localhost:3002" || window.location.host === "shuffler.io";
|
||||
const isCloud = (window.location.host === "localhost:3002" || window.location.host === "shuffler.io") ? true : (process.env.IS_SSR === "true");
|
||||
//const alert = useAlert();
|
||||
let navigate = useNavigate();
|
||||
|
||||
@@ -435,20 +436,6 @@ const WelcomeForm = (props) => {
|
||||
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 handleReset = () => {
|
||||
setActiveStep(0);
|
||||
@@ -645,6 +632,7 @@ const WelcomeForm = (props) => {
|
||||
globalUrl={globalUrl}
|
||||
userdata={userdata}
|
||||
appFramework={appFramework}
|
||||
setAppFramework={setAppFramework}
|
||||
setActiveStep={setActiveStep}
|
||||
defaultSearch={defaultSearch}
|
||||
setDefaultSearch={setDefaultSearch}
|
||||
@@ -660,7 +648,7 @@ const WelcomeForm = (props) => {
|
||||
|
||||
<div style={{ marginTop: 0, }}>
|
||||
<div className="thumbs" style={{ display: "flex" }}>
|
||||
<div style={{ minWidth: isMobile ? 300 : 554, maxWidth: isMobile ? 300 : 554, borderRadius: theme.palette.borderRadius, }}>
|
||||
<div style={{ minWidth: isMobile ? 300 : 554, maxWidth: isMobile ? 300 : 554, borderRadius: theme.palette?.borderRadius, }}>
|
||||
<ExploreWorkflow
|
||||
globalUrl={globalUrl}
|
||||
isLoggedIn={isLoggedIn}
|
||||
|
||||
@@ -28,7 +28,7 @@ const searchClient = algoliasearch("JNSS5CFDZZ", "db08e40265e2941b9a7d8f644b6e52
|
||||
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 isCloud = (window.location.host === "localhost:3002" || window.location.host === "shuffler.io") ? true : (process.env.IS_SSR === "true");
|
||||
const rowHandler = maxRows === undefined || maxRows === null ? 50 : maxRows
|
||||
const xs = parsedXs === undefined || parsedXs === null ? isMobile ? 6 : 4 : parsedXs
|
||||
//const [apps, setApps] = React.useState([]);
|
||||
@@ -204,6 +204,9 @@ const AppGrid = props => {
|
||||
style={{backgroundColor: theme.palette.inputColor, borderRadius: borderRadius, margin: 10, width: "100%",}}
|
||||
InputProps={{
|
||||
style:{
|
||||
color: "white",
|
||||
fontSize: "1em",
|
||||
height: 50,
|
||||
},
|
||||
startAdornment: (
|
||||
<InputAdornment position="start">
|
||||
@@ -221,6 +224,11 @@ const AppGrid = props => {
|
||||
removeQuery("q")
|
||||
refine(event.currentTarget.value)
|
||||
}}
|
||||
onKeyDown={(event) => {
|
||||
if(event.key === "Enter") {
|
||||
event.preventDefault();
|
||||
}
|
||||
}}
|
||||
limit={5}
|
||||
/>
|
||||
: null}
|
||||
@@ -233,6 +241,8 @@ const AppGrid = props => {
|
||||
flexWrap: "wrap",
|
||||
alignContent: "space-between",
|
||||
marginTop: 5,
|
||||
padding: "0px 180px",
|
||||
width:"auto"
|
||||
}
|
||||
|
||||
var workflowDelay = -50
|
||||
|
||||
@@ -47,7 +47,7 @@ const WorkflowTemplatePopup = (props) => {
|
||||
|
||||
const [requestSent, setRequestSent] = React.useState(false)
|
||||
|
||||
const isCloud = window.location.host === "localhost:3002" || window.location.host === "shuffler.io";
|
||||
const isCloud = (window.location.host === "localhost:3002" || window.location.host === "shuffler.io") ? true : (process.env.IS_SSR === "true");
|
||||
let navigate = useNavigate();
|
||||
useEffect(() => {
|
||||
if (modalOpen !== true) {
|
||||
@@ -585,7 +585,7 @@ const WorkflowTemplatePopup = (props) => {
|
||||
{/*errorMessage === "" && configurationFinished === true && workflow.id !== undefined && workflowLoading === false ?
|
||||
<Tooltip title="Click to explore the workflow" placement="top">
|
||||
<span
|
||||
style={{position: "fixed", display: "flex", right: "10%", top: "20%", border: "1px solid rgba(255,255,255,0.3)", borderRadius: theme.palette.borderRadius, padding: "15px 30px 15px 30px", backgroundColor: theme.palette.platformColor, cursor: "pointer", }}
|
||||
style={{position: "fixed", display: "flex", right: "10%", top: "20%", border: "1px solid rgba(255,255,255,0.3)", borderRadius: theme.palette?.borderRadius, padding: "15px 30px 15px 30px", backgroundColor: theme.palette.platformColor, cursor: "pointer", }}
|
||||
onClick={() => {
|
||||
// Open in new tab
|
||||
window.open("/workflows/" + workflow.id, "_blank")
|
||||
|
||||
@@ -0,0 +1,901 @@
|
||||
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 AppSearchButtons from "../components/AppSearchButtons.jsx";
|
||||
import { isMobile } from "react-device-detect";
|
||||
import RenderCytoscape from "../components/RenderCytoscape.jsx";
|
||||
import {
|
||||
Button,
|
||||
Typography,
|
||||
Dialog,
|
||||
DialogTitle,
|
||||
DialogContent,
|
||||
DialogActions,
|
||||
Drawer,
|
||||
CircularProgress,
|
||||
Fade,
|
||||
IconButton,
|
||||
Tooltip,
|
||||
} from "@mui/material";
|
||||
|
||||
import {
|
||||
Check as CheckIcon,
|
||||
TrendingFlat as TrendingFlatIcon,
|
||||
Close as CloseIcon,
|
||||
East as EastIcon,
|
||||
Interests as InterestsIcon,
|
||||
} from '@mui/icons-material';
|
||||
|
||||
import {
|
||||
green,
|
||||
yellow,
|
||||
red,
|
||||
grey,
|
||||
} from "../views/AngularWorkflow.jsx"
|
||||
|
||||
import WorkflowTemplatePopup2 from "./WorkflowTemplatePopup.jsx";
|
||||
import ConfigureWorkflow from "../components/ConfigureWorkflow.jsx";
|
||||
import WorkflowValidationTimeline from "../components/WorkflowValidationTimeline.jsx";
|
||||
import FixWorkflowValidationErrors from "../components/FixWorkflowValidationErrors.jsx";
|
||||
|
||||
const WorkflowTemplatePopup = (props) => {
|
||||
const {
|
||||
userdata, appFramework, globalUrl, img1, srcapp, img2, dstapp, title, description, visualOnly, apps, isLoggedIn, isHomePage, getAppFramework, showTryit, shownColor, workflowBuilt, usecaseDetails,
|
||||
|
||||
isModalOpenDefault,
|
||||
setIsClicked,
|
||||
inputWorkflowId,
|
||||
} = props;
|
||||
|
||||
const [isActive, setIsActive] = useState(workflowBuilt === true);
|
||||
const [isHovered, setIsHovered] = useState(false);
|
||||
const [modalOpen, setModalOpen] = useState(isModalOpenDefault === true ? true : false)
|
||||
const [errorMessage, setErrorMessage] = useState("");
|
||||
const [workflowLoading, setWorkflowLoading] = useState(false)
|
||||
const [showLoginButton, setShowLoginButton] = useState(false);
|
||||
const [appAuthentication, setAppAuthentication] = React.useState(undefined);
|
||||
const [missingSource, setMissingSource] = React.useState(undefined)
|
||||
const [missingDestination, setMissingDestination] = React.useState(undefined);
|
||||
const [configurationFinished, setConfigurationFinished] = React.useState(false)
|
||||
const [appSetupDone, setAppSetupDone] = React.useState(false)
|
||||
|
||||
const [requestSent, setRequestSent] = React.useState(false)
|
||||
const [showTryitOut, setShowTryitout] = React.useState(showTryit === true ? true : false)
|
||||
|
||||
const [loadingWorkflow, setLoadingWorkflow] = React.useState(false)
|
||||
const [workflow, setWorkflow] = useState({});
|
||||
const [_, setUpdate] = useState(0)
|
||||
|
||||
const fetchWorkflow = (id) => {
|
||||
if (id === undefined || id === null || id === "") {
|
||||
return
|
||||
}
|
||||
|
||||
if (loadingWorkflow === true) {
|
||||
return
|
||||
}
|
||||
|
||||
setLoadingWorkflow(true)
|
||||
const url = `${globalUrl}/api/v1/workflows/${id}`
|
||||
fetch(url, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Accept: "application/json",
|
||||
},
|
||||
credentials: "include",
|
||||
})
|
||||
.then((response) => {
|
||||
setLoadingWorkflow(false)
|
||||
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 ", id)
|
||||
} else {
|
||||
setWorkflow(responseJson)
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log("err in framework: ", error.toString());
|
||||
setLoadingWorkflow(false)
|
||||
})
|
||||
|
||||
|
||||
}
|
||||
|
||||
if (inputWorkflowId !== undefined && inputWorkflowId !== null && inputWorkflowId !== "" && workflow.id !== inputWorkflowId) {
|
||||
fetchWorkflow(inputWorkflowId)
|
||||
}
|
||||
|
||||
const isCloud = window.location.host === "localhost:3002" || window.location.host === "shuffler.io";
|
||||
let navigate = useNavigate();
|
||||
useEffect(() => {
|
||||
if (modalOpen !== true) {
|
||||
if (workflowLoading === true) {
|
||||
setWorkflowLoading(false)
|
||||
}
|
||||
|
||||
//console.log("Modal is not open, so we are not doing anything.")
|
||||
return
|
||||
}
|
||||
|
||||
if (workflowLoading !== true) {
|
||||
//console.log("Workflow loading is false, so we can try to get the workflow.")
|
||||
return
|
||||
}
|
||||
|
||||
console.log("DEBUG: Skipped direct generation without Try it for now.")
|
||||
|
||||
/*
|
||||
if (!srcapp.includes(":default") && !dstapp.includes(":default")) {
|
||||
if (appSetupDone === false && setAppSetupDone !== undefined) {
|
||||
setAppSetupDone(true)
|
||||
}
|
||||
|
||||
getGeneratedWorkflow()
|
||||
}
|
||||
|
||||
if (missingSource !== undefined && missingDestination !== undefined) {
|
||||
if (appSetupDone === false && setAppSetupDone !== undefined) {
|
||||
setAppSetupDone(true)
|
||||
}
|
||||
}
|
||||
|
||||
if (getAppFramework !== undefined) {
|
||||
setTimeout(() => {
|
||||
getAppFramework()
|
||||
}, 500)
|
||||
}
|
||||
*/
|
||||
}, [modalOpen, missingSource, missingDestination])
|
||||
|
||||
useEffect(() => {
|
||||
//console.log("IN USEEFFECT FOR CONFIG: ", configurationFinished)
|
||||
if (configurationFinished === true && workflow.id !== undefined && workflow.id !== null && workflow.id !== "") {
|
||||
//toast.success("Generation Successful")
|
||||
|
||||
/*
|
||||
setTimeout(() => {
|
||||
navigate("/workflows/" + workflow.id)
|
||||
}, 2000)
|
||||
*/
|
||||
}
|
||||
}, [configurationFinished, workflow])
|
||||
|
||||
const imageSize = 32
|
||||
const defaultBorder = "1px solid rgba(255,255,255,0.6)"
|
||||
const imagestyleWrapper = {
|
||||
height: imageSize,
|
||||
width: imageSize,
|
||||
borderRadius: imageSize,
|
||||
border: isHomePage ? null : defaultBorder,
|
||||
overflow: "hidden",
|
||||
display: "flex",
|
||||
|
||||
backgroundColor: theme.palette.inputColor,
|
||||
}
|
||||
|
||||
const imagestyleWrapperDefault = {
|
||||
height: imageSize,
|
||||
width: imageSize,
|
||||
borderRadius: imageSize,
|
||||
border: isHomePage ? null : defaultBorder,
|
||||
overflow: "hidden",
|
||||
display: "flex",
|
||||
|
||||
backgroundColor: theme.palette.inputColor,
|
||||
}
|
||||
|
||||
const imagestyle = {
|
||||
height: imageSize,
|
||||
width: imageSize,
|
||||
borderRadius: imageSize,
|
||||
//border: isHomePage ? null : defaultBorder,
|
||||
overflow: "hidden",
|
||||
|
||||
backgroundColor: theme.palette.inputColor,
|
||||
}
|
||||
|
||||
const imagestyleDefault = {
|
||||
display: "block",
|
||||
marginLeft: 9,
|
||||
marginTop: 9,
|
||||
height: imageSize,
|
||||
width: "auto",
|
||||
|
||||
backgroundColor: theme.palette.inputColor,
|
||||
}
|
||||
|
||||
if (modalOpen === false && (title === undefined || title === null || title === "")) {
|
||||
if (setIsClicked !== undefined) {
|
||||
setIsClicked(false)
|
||||
}
|
||||
|
||||
console.log("No title for workflow template popup!");
|
||||
return null
|
||||
}
|
||||
|
||||
|
||||
const loadAppAuth = () => {
|
||||
// Check if it exists, and has keys
|
||||
//
|
||||
if (userdata === undefined || userdata === null || Object.keys(userdata).length === 0) {
|
||||
setErrorMessage("You need to be logged in to try the pre-built Workflow Templates.")
|
||||
setShowLoginButton(true)
|
||||
|
||||
// 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());
|
||||
});
|
||||
}
|
||||
|
||||
// Can create and set workflows
|
||||
const reloadWorkflow = (workflow_id) => {
|
||||
|
||||
const new_url = `${globalUrl}/api/v1/workflows/${workflow_id}`
|
||||
return fetch(new_url, {
|
||||
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;
|
||||
}
|
||||
//setSubmitLoading(false);
|
||||
|
||||
return response.json();
|
||||
})
|
||||
.then((responseJson) => {
|
||||
if (responseJson.success === false) {
|
||||
if (responseJson.reason !== undefined) {
|
||||
toast("Error setting workflow: ", responseJson.reason)
|
||||
} else {
|
||||
toast("Error setting workflow.")
|
||||
}
|
||||
|
||||
return
|
||||
} else if (responseJson.id !== undefined && responseJson.id !== null && responseJson.id !== "") {
|
||||
setWorkflow(responseJson)
|
||||
}
|
||||
|
||||
return responseJson;
|
||||
})
|
||||
.catch((error) => {
|
||||
toast("Failed reloading configured workflow: ", error.toString());
|
||||
});
|
||||
};
|
||||
|
||||
// Can create and set workflows
|
||||
const saveWorkflow = (workflowdata) => {
|
||||
|
||||
const new_url = `${globalUrl}/api/v1/workflows?set_auth=true`
|
||||
return fetch(new_url, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Accept: "application/json",
|
||||
},
|
||||
body: JSON.stringify(workflowdata),
|
||||
credentials: "include",
|
||||
})
|
||||
.then((response) => {
|
||||
if (response.status !== 200) {
|
||||
console.log("Status not 200 for workflows :O!");
|
||||
return;
|
||||
}
|
||||
//setSubmitLoading(false);
|
||||
|
||||
return response.json();
|
||||
})
|
||||
.then((responseJson) => {
|
||||
if (responseJson.success === false) {
|
||||
if (responseJson.reason !== undefined) {
|
||||
toast("Error setting workflow: ", responseJson.reason)
|
||||
} else {
|
||||
toast("Error setting workflow.")
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// In case it got a new id, this is to make sure it loads with the correct config
|
||||
if (responseJson.id !== undefined && responseJson.id !== null && responseJson.id !== "") {
|
||||
reloadWorkflow(responseJson.id)
|
||||
}
|
||||
|
||||
return responseJson;
|
||||
})
|
||||
.catch((error) => {
|
||||
toast("Failed generating workflow: ", 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 (requestSent === true) {
|
||||
return
|
||||
}
|
||||
|
||||
console.log("SRCAPP: ", srcapp, "DSTAPP: ", dstapp)
|
||||
if (srcapp === undefined || srcapp === null) {
|
||||
srcapp = ""
|
||||
}
|
||||
|
||||
if ((srcapp !== undefined && srcapp !== null && srcapp.includes(":default")) || (dstapp !== undefined && dstapp !== null && dstapp.includes(":default"))) {
|
||||
toast("You need to select both a source and destination app before generating this workflow.")
|
||||
|
||||
if (srcapp !== undefined && srcapp !== null && srcapp.includes(":default")) {
|
||||
setMissingSource({
|
||||
"type": srcapp.split(":")[0],
|
||||
})
|
||||
}
|
||||
|
||||
if (dstapp !== undefined && dstapp !== null && dstapp.includes(":default")) {
|
||||
setMissingDestination({
|
||||
"type": dstapp.split(":")[0],
|
||||
})
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
setWorkflowLoading(true)
|
||||
|
||||
const newsrcapp = srcapp
|
||||
const newdstapp = dstapp
|
||||
|
||||
const mergedata = {
|
||||
name: title,
|
||||
id: "",
|
||||
source: {
|
||||
app_name: newsrcapp,
|
||||
},
|
||||
middle: [],
|
||||
destination: {
|
||||
app_name: newdstapp,
|
||||
},
|
||||
}
|
||||
|
||||
setRequestSent(true)
|
||||
const url = isCloud ? `${globalUrl}/api/v1/workflows/merge` : `https://shuffler.io/api/v1/workflows/merge`
|
||||
fetch(url, {
|
||||
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!");
|
||||
setRequestSent(false)
|
||||
}
|
||||
|
||||
setWorkflowLoading(false)
|
||||
return response.json();
|
||||
})
|
||||
.then((responseJson) => {
|
||||
if (responseJson.id !== undefined && responseJson.id !== null && responseJson.id !== "" && responseJson.name !== undefined && responseJson.name !== null && responseJson.name !== "") {
|
||||
console.log("Success in workflow template (prebuilt): ", responseJson);
|
||||
setWorkflow(responseJson)
|
||||
|
||||
// Sets it in the database properly
|
||||
saveWorkflow(responseJson)
|
||||
return
|
||||
}
|
||||
|
||||
if (responseJson.success === false) {
|
||||
//console.log("Error in workflow template: ", responseJson.error);
|
||||
setRequestSent(false)
|
||||
|
||||
const defaultMessage = "Error: Failed to generate workflow the workflow - the Shuffle team has been notified. Contact support@shuffler.io if you want manual help building this usecase until the AI system is handled."
|
||||
if (responseJson.reason !== undefined && responseJson.reason !== null && responseJson.reason !== "") {
|
||||
setErrorMessage(defaultMessage + "\n\n" + responseJson.reason)
|
||||
} else {
|
||||
setErrorMessage(defaultMessage)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
fetchWorkflow(responseJson.workflow_id)
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log("err in framework: ", error.toString());
|
||||
setRequestSent(false)
|
||||
setWorkflowLoading(false)
|
||||
})
|
||||
}
|
||||
|
||||
if (modalOpen === true && !srcapp?.includes(":default") && !dstapp?.includes(":default")) {
|
||||
if (appSetupDone === false && setAppSetupDone !== undefined) {
|
||||
setAppSetupDone(true)
|
||||
}
|
||||
|
||||
// No autoruns anymore without clicking "Try it"
|
||||
if (workflow.id === undefined && workflowLoading === false && errorMessage === "") {
|
||||
//getGeneratedWorkflow()
|
||||
}
|
||||
}
|
||||
|
||||
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 = () => {
|
||||
if (modalOpen === false) {
|
||||
return null
|
||||
}
|
||||
|
||||
const divHeight = 500
|
||||
const divWidth = 500
|
||||
|
||||
return (
|
||||
<Drawer
|
||||
anchor={"right"}
|
||||
open={modalOpen}
|
||||
onClose={() => {
|
||||
setModalOpen(false);
|
||||
|
||||
if (setIsClicked !== undefined) {
|
||||
setIsClicked(false)
|
||||
}
|
||||
}}
|
||||
PaperProps={{
|
||||
style: {
|
||||
backgroundColor: "black",
|
||||
color: "white",
|
||||
minWidth: isHomePage ? null : isMobile ? 300 : 850,
|
||||
maxWidth: isHomePage ? null : isMobile ? 300 : 850,
|
||||
paddingTop: isMobile ? null : 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: isHomePage ? null : isMobile ? null : 75, maxWidth: 470, }}>
|
||||
<Typography variant="h4" style={{ fontSize: isMobile ? 20 : null}}>
|
||||
<b>Configure Workflow</b>
|
||||
</Typography>
|
||||
|
||||
{title === undefined || title === null || title === "" ? null :
|
||||
<span>
|
||||
<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}
|
||||
|
||||
workflowBuilt={workflowBuilt}
|
||||
shownColor={shownColor}
|
||||
/>
|
||||
|
||||
</div>
|
||||
</span>
|
||||
}
|
||||
|
||||
<div style={{marginTop: 15, }}>
|
||||
{/* Fix the timeline when errors are fixed.. how? */}
|
||||
<WorkflowValidationTimeline
|
||||
workflow={workflow}
|
||||
/>
|
||||
|
||||
<FixWorkflowValidationErrors
|
||||
globalUrl={globalUrl}
|
||||
workflow={workflow}
|
||||
setWorkflow={setWorkflow}
|
||||
|
||||
setUpdateParent={setUpdate}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{workflowLoading === true ?
|
||||
<div style={{marginTop: 75, textAlign: "center", }}>
|
||||
<Typography variant="h4"> Generating the Workflow...
|
||||
</Typography>
|
||||
<CircularProgress style={{marginLeft: 0, marginTop: 25, }}/>
|
||||
</div>
|
||||
:
|
||||
<div>
|
||||
{usecaseDetails === undefined ? null :
|
||||
<Typography variant="h6" style={{marginTop: 75, }}>
|
||||
{usecaseDetails?.description}
|
||||
</Typography>
|
||||
}
|
||||
<Typography variant="h6" style={{marginTop: 75, }}>
|
||||
{errorMessage !== "" ? errorMessage : ""}
|
||||
</Typography>
|
||||
{showLoginButton ?
|
||||
<Link to="/register?message=Please login to create workflows&view=usecases"
|
||||
style={{
|
||||
textDecoration: 'none',
|
||||
marginBottom: 50,
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
style={{
|
||||
display: "flex",
|
||||
fontSize: 18,
|
||||
color: "rgba(255, 132, 68, 1)",
|
||||
marginTop: 32,
|
||||
fontFamily: "var(--zds-typography-base,Inter,Helvetica,arial,sans-serif)",
|
||||
fontWeight: 550,
|
||||
}}
|
||||
>
|
||||
Sign up
|
||||
<EastIcon style={{ marginTop: 3, marginLeft: 7 }} />
|
||||
</Typography>
|
||||
</Link>
|
||||
:
|
||||
!showTryitOut && !isActive ?
|
||||
<Button
|
||||
variant="outlined"
|
||||
style={{
|
||||
textTransform: "none",
|
||||
}}
|
||||
onClick={() => {
|
||||
//setWorkflowLoading(true)
|
||||
getGeneratedWorkflow()
|
||||
loadAppAuth()
|
||||
}}
|
||||
>
|
||||
Try this usecase <TrendingFlatIcon style={{ }} />
|
||||
</Button>
|
||||
: null}
|
||||
</div>
|
||||
}
|
||||
|
||||
{!isLoggedIn ? null :
|
||||
<div>
|
||||
{(appSetupDone === false && missingSource !== undefined || missingDestination !== undefined) ?
|
||||
<Typography variant="body1" style={{marginTop: 75, marginBottom: 10, }}>
|
||||
{"Find relevant Apps for this Usecase"}
|
||||
</Typography>
|
||||
: null}
|
||||
|
||||
{(missingSource !== undefined) ?
|
||||
<div style={{}}>
|
||||
<AppSearchButtons
|
||||
globalUrl={globalUrl}
|
||||
appFramework={appFramework}
|
||||
|
||||
appType={missingSource.type}
|
||||
AppImage={missingSource.image}
|
||||
|
||||
setMissing={setMissingSource}
|
||||
|
||||
getAppFramework={getAppFramework}
|
||||
/>
|
||||
</div>
|
||||
: null}
|
||||
|
||||
{(missingDestination !== undefined) ?
|
||||
<div style={{}}>
|
||||
<AppSearchButtons
|
||||
globalUrl={globalUrl}
|
||||
appFramework={appFramework}
|
||||
|
||||
appType={missingDestination.type}
|
||||
AppImage={missingDestination.image}
|
||||
|
||||
setMissing={setMissingDestination}
|
||||
|
||||
getAppFramework={getAppFramework}
|
||||
/>
|
||||
</div>
|
||||
: null}
|
||||
</div>
|
||||
}
|
||||
|
||||
<ConfigureWorkflow
|
||||
userdata={userdata}
|
||||
theme={theme}
|
||||
globalUrl={globalUrl}
|
||||
appAuthentication={appAuthentication}
|
||||
setAppAuthentication={setAppAuthentication}
|
||||
|
||||
workflow={workflow}
|
||||
apps={apps}
|
||||
|
||||
setConfigurationFinished={setConfigurationFinished}
|
||||
/>
|
||||
|
||||
</DialogContent>
|
||||
</Drawer>
|
||||
)
|
||||
}
|
||||
|
||||
if (isModalOpenDefault === true) {
|
||||
return <ModalView />
|
||||
}
|
||||
|
||||
var parsedTitle = title !== undefined && title !== null ? title : ""
|
||||
const maxlength = 50
|
||||
if (title !== undefined && title !== null && title.length > maxlength) {
|
||||
parsedTitle = title.substring(0, maxlength) + "..."
|
||||
}
|
||||
|
||||
parsedTitle = parsedTitle.replaceAll("_", " ")
|
||||
|
||||
const parsedDescription = description !== undefined && description !== null ? description.replaceAll("_", " ") : ""
|
||||
|
||||
const boxHeight = 104
|
||||
const highlightColor = shownColor !== undefined && shownColor !== null && shownColor !== "" ? shownColor : "#f85a3e"
|
||||
|
||||
var hasInterest = false
|
||||
if (userdata.interests !== undefined && userdata.interests !== null && userdata.interests.length > 0) {
|
||||
const comparisonTitle = title === undefined || title === null ? "" : title.trim().toLowerCase().replaceAll(" ", "_")
|
||||
for (var interestkey in userdata.interests) {
|
||||
if (userdata.interests[interestkey].name === undefined || userdata.interests[interestkey].name === null || userdata.interests[interestkey].name === "") {
|
||||
continue
|
||||
}
|
||||
|
||||
if (modalOpen) {
|
||||
console.log("COMPARE: ", userdata.interests[interestkey].name.trim().toLowerCase().replaceAll(" ", "_"), comparisonTitle)
|
||||
}
|
||||
|
||||
if (userdata.interests[interestkey].name.trim().toLowerCase().replaceAll(" ", "_") === comparisonTitle) {
|
||||
if (modalOpen) {
|
||||
console.log("FOUND: ", comparisonTitle)
|
||||
}
|
||||
|
||||
hasInterest = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const borderStyle = isHomePage ? null : isHovered && isActive ? errorMessage !== "" ? "1px solid red" : `2px solid ${theme.palette.green}` : isHovered ? `1px solid ${highlightColor}` : "1px solid rgba(33, 33, 33, 1)"
|
||||
|
||||
return (
|
||||
<div style={{ display: "flex", height: boxHeight, borderRadius: theme.palette?.borderRadius, justifyContent: isMobile ? null : "center" }}
|
||||
>
|
||||
<ModalView />
|
||||
|
||||
<div
|
||||
// variant={isActive === 1 ? "contained" : "outlined"}
|
||||
color="secondary"
|
||||
disabled={visualOnly === true}
|
||||
style={{
|
||||
width: isHomePage? isMobile ? null : "100%" : "99%",
|
||||
borderRadius: 8,
|
||||
textTransform: "none",
|
||||
backgroundColor: isHomePage ? null : theme.palette.inputColor,
|
||||
border: borderStyle,
|
||||
cursor: isActive ? errorMessage !== "" ? "not-allowed" : "pointer" : "pointer",
|
||||
position: "relative",
|
||||
|
||||
}}
|
||||
onMouseEnter={() => {
|
||||
setIsHovered(true)
|
||||
|
||||
setShowTryitout(true)
|
||||
}}
|
||||
onMouseLeave={() => {
|
||||
setIsHovered(false)
|
||||
|
||||
if (showTryit !== true) {
|
||||
setShowTryitout(false)
|
||||
}
|
||||
}}
|
||||
onClick={() => {
|
||||
if (visualOnly === true) {
|
||||
console.log("Not showing more than visuals.")
|
||||
return
|
||||
}
|
||||
|
||||
if (!isLoggedIn) {
|
||||
loadAppAuth()
|
||||
setModalOpen(true)
|
||||
} else if (isLoggedIn && errorMessage !== "") {
|
||||
toast.error("Already failed to generate a workflow for this usecase. Please try again later or contact support@shuffler.io.")
|
||||
|
||||
setModalOpen(true)
|
||||
} else if (isActive) {
|
||||
// toast.success("Workflow already generated. Please try another workflow template!")
|
||||
|
||||
// FIXME: Remove these?
|
||||
loadAppAuth()
|
||||
setModalOpen(true)
|
||||
//getGeneratedWorkflow()
|
||||
} else {
|
||||
setModalOpen(true)
|
||||
//setWorkflowLoading(false)
|
||||
}
|
||||
}}
|
||||
>
|
||||
|
||||
<div style={{display: "flex", }}>
|
||||
{shownColor !== undefined && shownColor !== null && shownColor !== "" ?
|
||||
<div style={{position: "absolute", left: 0, height: boxHeight-2, width: 4, backgroundColor: shownColor, borderTopLeftRadius: 8, borderBottomLeftRadius: 8, }} />
|
||||
: null}
|
||||
|
||||
<div style={{ display: "flex", itemAlign: "left", textAlign: "left", }}>
|
||||
<div style={{display: "flex", flex: 1, marginLeft: 25, marginTop: showTryitOut && !isActive ? 14 : 30, }}>
|
||||
<div style={{zIndex: 51}}>
|
||||
{img1 !== undefined && img1 !== "" && srcapp !== undefined && srcapp !== "" ?
|
||||
<Tooltip title={srcapp.replaceAll(":default", "").replaceAll("_", " ").replaceAll(" API", "")} placement="top">
|
||||
<div style={srcapp !== undefined && srcapp.includes(":default") ? imagestyleWrapperDefault : imagestyleWrapper}>
|
||||
<img src={img1} style={srcapp !== undefined && srcapp.includes(":default") ? imagestyleDefault : imagestyle} />
|
||||
</div>
|
||||
</Tooltip>
|
||||
:
|
||||
<div style={{width: 50, }} />
|
||||
}
|
||||
</div>
|
||||
|
||||
|
||||
{img2 !== undefined && img2 !== "" && dstapp !== undefined && dstapp !== "" ?
|
||||
<Tooltip title={dstapp.replaceAll(":default", "").replaceAll("_", " ").replaceAll(" API", "")} placement="top">
|
||||
<div style={{display: "flex", position: "relative", left: -10, }}>
|
||||
<div style={dstapp !== undefined && dstapp.includes(":default") ? imagestyleWrapperDefault : imagestyleWrapper}>
|
||||
<img src={img2} style={dstapp !== undefined && dstapp.includes(":default") ? imagestyleDefault : imagestyle} />
|
||||
</div>
|
||||
</div>
|
||||
</Tooltip>
|
||||
:
|
||||
<div style={{width: 0, }} />
|
||||
}
|
||||
|
||||
</div>
|
||||
<div style={{ marginLeft: 20, overflow: "hidden", maxHeight: 30, marginTop: showTryitOut && !isActive ? 8 : 23, }}>
|
||||
<Typography variant="body1" style={{ marginTop: parsedDescription.length === 0 ? 10 : 0, fontSize: isMobile ? 13 : 16, fontWeight: isHomePage ? 600 : null, textTransform: 'capitalize', color: isHomePage ? "var(--White-text, #F1F1F1)" : "rgba(241, 241, 241, 1)"}} >
|
||||
<b>{parsedTitle}</b>
|
||||
</Typography>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
{isActive === true && errorMessage === "" ?
|
||||
<Tooltip title="You already have workflows that are based on this usecase" placement="top">
|
||||
<CheckIcon color="primary" sx={{ borderRadius: 4 }} style={{ position: "absolute", color: theme.palette.green, top: 10, right: 10, }} />
|
||||
</Tooltip>
|
||||
: ""}
|
||||
|
||||
{!isActive && hasInterest === true ?
|
||||
<Tooltip title="Your team has shown interest in this usecase previously." placement="top">
|
||||
<InterestsIcon color="primary" sx={{ borderRadius: 4 }} style={{ position: "absolute", color: "rgba(254, 204, 0, 0.5)", top: 10, right: 10, }} />
|
||||
</Tooltip>
|
||||
: null}
|
||||
</div>
|
||||
|
||||
|
||||
{showTryitOut && !isActive ?
|
||||
<Fade in={showTryitOut} timeout={300}>
|
||||
<Button
|
||||
variant="text"
|
||||
style={{
|
||||
textTransform: "none",
|
||||
marginTop: 8,
|
||||
marginLeft: 15,
|
||||
}}
|
||||
onClick={() => {
|
||||
//setWorkflowLoading(true)
|
||||
getGeneratedWorkflow()
|
||||
loadAppAuth()
|
||||
}}
|
||||
>
|
||||
Try it out <TrendingFlatIcon style={{ }} />
|
||||
</Button>
|
||||
</Fade>
|
||||
: null}
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default WorkflowTemplatePopup
|
||||
@@ -0,0 +1,694 @@
|
||||
import React, { useState, } from "react";
|
||||
import { makeStyles, createStyles } from "@mui/styles";
|
||||
import { toast } from "react-toastify"
|
||||
|
||||
import {
|
||||
Tooltip,
|
||||
Chip,
|
||||
Typography,
|
||||
IconButton,
|
||||
|
||||
Avatar,
|
||||
AvatarGroup,
|
||||
} from "@mui/material"
|
||||
|
||||
import {
|
||||
ErrorOutline as ErrorOutlineIcon,
|
||||
} from "@mui/icons-material"
|
||||
|
||||
import {
|
||||
green,
|
||||
yellow,
|
||||
red,
|
||||
grey,
|
||||
} from "../views/AngularWorkflow.jsx"
|
||||
|
||||
import WorkflowTemplatePopup2 from "../components/WorkflowTemplatePopup2.jsx"
|
||||
import { validateJson, GetIconInfo } from "../views/Workflows.jsx";
|
||||
import theme from "../theme.jsx";
|
||||
const itemHeight = 24
|
||||
|
||||
export const getParentNodes = (workflow, action) => {
|
||||
if (action === undefined || action === null) {
|
||||
return []
|
||||
}
|
||||
|
||||
if (workflow.actions === undefined || workflow.actions === null) {
|
||||
workflow.actions = []
|
||||
}
|
||||
|
||||
if (workflow.triggers === undefined || workflow.triggers === null) {
|
||||
workflow.triggers = []
|
||||
}
|
||||
|
||||
if (workflow.branches === undefined || workflow.branches === null) {
|
||||
workflow.branches = []
|
||||
}
|
||||
|
||||
var allkeys = [action.id];
|
||||
var handled = [];
|
||||
var results = [];
|
||||
|
||||
// maxiter = max amount of parent nodes to loop
|
||||
// also handles breaks if there are issues
|
||||
var iterations = 0;
|
||||
var maxiter = 10;
|
||||
while (true) {
|
||||
for (let parentkey in allkeys) {
|
||||
if (allkeys[parentkey] === undefined) {
|
||||
continue
|
||||
}
|
||||
|
||||
var currentnode = workflow.actions.find((element) => element.id === allkeys[parentkey])
|
||||
if (currentnode === undefined) {
|
||||
currentnode = workflow.triggers.find((element) => element.id === allkeys[parentkey])
|
||||
|
||||
if (currentnode === undefined) {
|
||||
//console.log("Could not find parent node for: ", allkeys[parentkey])
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
if (handled.includes(currentnode.id)) {
|
||||
continue
|
||||
} else {
|
||||
handled.push(currentnode.id);
|
||||
results.push(currentnode);
|
||||
}
|
||||
|
||||
// Get the name / label here too?
|
||||
if (currentnode.length === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// FIXME: This part is only handling first level,
|
||||
// but needs to recurse
|
||||
var incomingEdges = []
|
||||
for (var branchkey in workflow.branches) {
|
||||
const branch = workflow.branches[branchkey]
|
||||
if (branch.destination_id !== currentnode.id) {
|
||||
continue
|
||||
}
|
||||
|
||||
// Go up in the levels
|
||||
const parents = getParentNodes(workflow, {
|
||||
id: branch.source_id,
|
||||
})
|
||||
if (parents.length > 0) {
|
||||
incomingEdges = incomingEdges.concat(parents)
|
||||
}
|
||||
|
||||
incomingEdges.push(branch)
|
||||
}
|
||||
|
||||
for (let i = 0; i < incomingEdges.length; i++) {
|
||||
var tmp = incomingEdges[i];
|
||||
if (tmp.decorator === true) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (!allkeys.includes(tmp.source_id)) {
|
||||
allkeys.push(tmp.source_id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (results.length === allkeys.length || iterations === maxiter) {
|
||||
break
|
||||
}
|
||||
|
||||
iterations += 1
|
||||
}
|
||||
|
||||
// Remove on the end as we don't want to remove everything
|
||||
results = results.filter((data) => data.id !== action.id)
|
||||
results = results.filter((data) => data.type === "ACTION" || data.app_name === "Shuffle Workflow" || data.app_name === "User Input" || data.app_name === "shuffle-subflow")
|
||||
results.push({ label: "Execution Argument", type: "INTERNAL" })
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
const WorkflowValidationTimeline = (props) => {
|
||||
const { globalUrl, userdata, workflow, originalWorkflow, apps, getParents, execution, showHoverColor, } = props
|
||||
|
||||
const [hovering, setHovering] = useState(false)
|
||||
const [decidedColor, setDecidedColor] = useState(grey)
|
||||
const [isClicked, setIsClicked] = useState(false)
|
||||
|
||||
const showMiddle = false
|
||||
if (workflow === undefined || workflow === null) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (workflow.validation === undefined || workflow.validation === null) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (workflow.actions === undefined || workflow.actions === null) {
|
||||
workflow.actions = []
|
||||
}
|
||||
|
||||
if (workflow.triggers === undefined || workflow.triggers === null) {
|
||||
workflow.triggers = []
|
||||
}
|
||||
|
||||
if (workflow.branches === undefined || workflow.branches === null) {
|
||||
workflow.branches = []
|
||||
}
|
||||
|
||||
var results = []
|
||||
if (execution !== undefined) {
|
||||
results = execution.results
|
||||
}
|
||||
|
||||
// 1. Find startnode
|
||||
// 2. Map childnodes from it
|
||||
var startnodeId = workflow.start
|
||||
|
||||
if (execution !== undefined && execution !== null) {
|
||||
startnodeId = execution.start
|
||||
}
|
||||
|
||||
// Find parent of startnodeId and if it's a webhook
|
||||
var relevantactions = []
|
||||
for (var key in workflow.branches) {
|
||||
const branch = workflow.branches[key]
|
||||
if (branch.destination_id !== startnodeId) {
|
||||
continue
|
||||
}
|
||||
|
||||
for (var triggerkey in workflow.triggers) {
|
||||
const trigger = workflow.triggers[triggerkey]
|
||||
if (trigger.trigger_type !== "WEBHOOK") {
|
||||
continue
|
||||
}
|
||||
|
||||
if (trigger.id === branch.source_id) {
|
||||
trigger.order = -1
|
||||
relevantactions.push(trigger)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (var key in workflow.actions) {
|
||||
const action = workflow.actions[key]
|
||||
if (action.id === startnodeId) {
|
||||
action.order = 0
|
||||
relevantactions.push(action)
|
||||
continue
|
||||
}
|
||||
|
||||
var parents = []
|
||||
if (getParents !== undefined) {
|
||||
parents = getParents(action)
|
||||
} else {
|
||||
parents = getParentNodes(workflow, action)
|
||||
}
|
||||
|
||||
if (action.app_name === "Integration Framework" || action.app_name === "Integration") {
|
||||
for (var paramkey in action.parameters) {
|
||||
const param = action.parameters[paramkey]
|
||||
if (param.name === "app_name") {
|
||||
action.app_name = param.value.charAt(0).toUpperCase() + param.value.slice(1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//const parents = getParentNodes(workflow, action)
|
||||
//console.log("PARENTS", key, parents)
|
||||
if (parents !== undefined && parents !== null && parents.length > 0) {
|
||||
const parentfound = parents.find((element) => element.id === startnodeId)
|
||||
if (parentfound !== undefined) {
|
||||
|
||||
// FIXME: add order here based on how many steps away from the startnode
|
||||
// This just has the parent count
|
||||
action.order = parents.length
|
||||
|
||||
relevantactions.push(action)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (getParents === undefined) {
|
||||
var newactions = []
|
||||
for (var key in workflow.triggers) {
|
||||
const trigger = workflow.triggers[key]
|
||||
if (trigger.trigger_type !== "SUBFLOW" && trigger.trigger_type !== "USERINPUT") {
|
||||
continue
|
||||
}
|
||||
|
||||
for (var branchkey in workflow.branches) {
|
||||
const branch = workflow.branches[branchkey]
|
||||
|
||||
// Checking for OUTBOUND branches from it.
|
||||
// This will mean it's NOT the last node and is easy to visualize
|
||||
if (branch.source_id !== trigger.id) {
|
||||
continue
|
||||
}
|
||||
|
||||
trigger.order = 2
|
||||
}
|
||||
|
||||
|
||||
// Just in case (:
|
||||
if (workflow.actions.find((element) => element.id === trigger.id) === undefined) {
|
||||
newactions.push(trigger)
|
||||
//workflow.actions.push(trigger)
|
||||
}
|
||||
}
|
||||
|
||||
relevantactions.push(...newactions)
|
||||
}
|
||||
|
||||
if (relevantactions.length <= 1) {
|
||||
return null
|
||||
}
|
||||
|
||||
// Sort according to how many parents a node has. MAY be wrong~
|
||||
relevantactions.sort((a, b) => {
|
||||
if (a.order === undefined) {
|
||||
return 1
|
||||
}
|
||||
|
||||
if (b.order === undefined) {
|
||||
return -1
|
||||
}
|
||||
|
||||
return a.order - b.order
|
||||
})
|
||||
|
||||
// FIXME: Add other relevant items as well from subflows (?)
|
||||
var nodecolor = grey
|
||||
var branchcolor = grey
|
||||
var skipped = false
|
||||
|
||||
var previousTools = false
|
||||
var scheduleNotStarted = false
|
||||
|
||||
if (workflow.validation !== undefined && workflow.validation !== null && workflow.validation.validation_ran === false) {
|
||||
console.log("Validation didn't run. Why?")
|
||||
return null
|
||||
}
|
||||
|
||||
if (workflow.validation !== undefined && workflow.validation !== null && workflow.validation.errors !== undefined && workflow.validation.errors !== null && workflow.validation.errors.length > 0) {
|
||||
var newErrors = []
|
||||
for (var key in workflow.validation.errors) {
|
||||
const error = workflow.validation.errors[key]
|
||||
if (error.type === "SCHEDULE") {
|
||||
scheduleNotStarted = true
|
||||
continue
|
||||
}
|
||||
|
||||
newErrors.push(error)
|
||||
}
|
||||
|
||||
workflow.validation.errors = newErrors
|
||||
}
|
||||
|
||||
// Use this variable to control visualization
|
||||
//const showMiddle = false
|
||||
// border: workflow.validation.valid ? `2px solid ${green}` : "1px solid rgba(255,255,255,0.4)",
|
||||
var middleError = ""
|
||||
var startBranchColor = ""
|
||||
var middleBranchColor = ""
|
||||
|
||||
|
||||
const showHoverForClick = showHoverColor === true ? true : false
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
padding: "10px 5px 10px 5px",
|
||||
borderRadius: theme.palette?.borderRadius,
|
||||
|
||||
border: hovering === true && showHoverForClick === true ? `1px solid ${decidedColor}` : "1px solid rgba(255,255,255,0.0)",
|
||||
cursor: hovering === true && showHoverForClick === true ? "pointer" : "default",
|
||||
}}
|
||||
onMouseEnter={() => {
|
||||
if (isClicked === false) {
|
||||
setHovering(true)
|
||||
}
|
||||
}}
|
||||
onMouseLeave={() => {
|
||||
if (isClicked === false) {
|
||||
setHovering(false)
|
||||
}
|
||||
}}
|
||||
onClick={() => {
|
||||
if (showHoverForClick === true) {
|
||||
setIsClicked(true)
|
||||
}
|
||||
}}
|
||||
>
|
||||
|
||||
{isClicked === false ? null :
|
||||
<WorkflowTemplatePopup2
|
||||
globalUrl={globalUrl}
|
||||
userdata={userdata}
|
||||
|
||||
isModalOpenDefault={isClicked}
|
||||
workflowBuilt={true}
|
||||
setIsClicked={setIsClicked}
|
||||
inputWorkflowId={workflow.id}
|
||||
/>
|
||||
}
|
||||
|
||||
<div style={{display: "flex", justifyContent: "center", alignItems: "center"}}>
|
||||
|
||||
{scheduleNotStarted === true ?
|
||||
null
|
||||
: null}
|
||||
|
||||
{relevantactions.map((action, index) => {
|
||||
action.result = {}
|
||||
if (results !== undefined) {
|
||||
const foundResult = results.find((element) => element.action.id === action.id)
|
||||
if (foundResult !== undefined) {
|
||||
action.result = foundResult
|
||||
|
||||
action.status = foundResult.status
|
||||
}
|
||||
}
|
||||
|
||||
const lastitem = index === relevantactions.length - 1
|
||||
if (!lastitem) {
|
||||
if (action.app_name === "Shuffle Tools") {
|
||||
if (action.status === "SUCCESS") {
|
||||
branchcolor = red
|
||||
|
||||
// Check action.result for the actual status
|
||||
const validate = validateJson(action.result.result)
|
||||
if (validate.valid) {
|
||||
if (validate.result.success === true) {
|
||||
nodecolor = green
|
||||
branchcolor = green
|
||||
} else {
|
||||
nodecolor = grey
|
||||
branchcolor = grey
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
} else if (action.status === "SKIPPED") {
|
||||
branchcolor = grey
|
||||
} else {
|
||||
// FIXME: How do we handle this?
|
||||
if (action.status === undefined) {
|
||||
nodecolor = grey
|
||||
branchcolor = grey
|
||||
} else {
|
||||
nodecolor = red
|
||||
branchcolor = red
|
||||
}
|
||||
}
|
||||
|
||||
previousTools = true
|
||||
|
||||
if (startnodeId !== action.id) {
|
||||
//return null
|
||||
}
|
||||
} else {
|
||||
if (action.status === "SUCCESS") {
|
||||
nodecolor = green
|
||||
} else if (action.status === "SKIPPED") {
|
||||
nodecolor = grey
|
||||
} else {
|
||||
if (action.status === undefined) {
|
||||
nodecolor = green
|
||||
} else {
|
||||
nodecolor = red
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
nodecolor = grey
|
||||
}
|
||||
|
||||
if (action.status === "SKIPPED") {
|
||||
skipped = true
|
||||
}
|
||||
|
||||
var image = ""
|
||||
if (action.large_image !== undefined && action.large_image !== null && action.large_image !== "") {
|
||||
image = action.large_image
|
||||
} else {
|
||||
if (originalWorkflow !== undefined) {
|
||||
for (var key in originalWorkflow.actions) {
|
||||
if (originalWorkflow.actions[key].id === action.id) {
|
||||
image = originalWorkflow.actions[key].large_image
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (image === "") {
|
||||
for (var key in originalWorkflow.triggers) {
|
||||
if (originalWorkflow.triggers[key].id === action.id) {
|
||||
image = originalWorkflow.triggers[key].large_image
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var founderror = ""
|
||||
//console.log("WORKFLOW VALIDATION: ", workflow.validation)
|
||||
if (workflow.validation !== undefined && workflow.validation !== null && workflow.validation.errors !== undefined && workflow.validation.errors !== null) {
|
||||
const foundError = workflow.validation.errors.find((element) => element.action_id === action.id)
|
||||
if (foundError !== undefined) {
|
||||
founderror = foundError.error
|
||||
nodecolor = red
|
||||
branchcolor = red
|
||||
} else {
|
||||
//console.log("NO ERROR: ", action.id)
|
||||
}
|
||||
}
|
||||
|
||||
var appgroup = []
|
||||
if (action.app_name === "shuffle-subflow") {
|
||||
if (action.status === "SUCCESS") {
|
||||
nodecolor = green
|
||||
branchcolor = green
|
||||
}
|
||||
|
||||
if (workflow.validation.subflow_apps !== undefined && workflow.validation.subflow_apps !== null && workflow.validation.subflow_apps.length > 0) {
|
||||
nodecolor = red
|
||||
branchcolor = red
|
||||
|
||||
for (var subflowkey in workflow.validation.subflow_apps) {
|
||||
const subflowApp = workflow.validation.subflow_apps[subflowkey]
|
||||
founderror += "- " + subflowApp.error+"\n"
|
||||
|
||||
if (subflowApp.error === action.id) {
|
||||
appgroup.push(subflowApp)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!showMiddle && relevantactions.length > 2 && index > 0 && index === relevantactions.length - 2) {
|
||||
if (founderror.length > 0) {
|
||||
middleError += founderror+"\n"
|
||||
|
||||
middleBranchColor = branchcolor
|
||||
}
|
||||
|
||||
if (index === relevantactions.length-2 && relevantactions.length > 2) {
|
||||
|
||||
const selectedIcon = middleError.length > 0 ?
|
||||
<Tooltip title={
|
||||
<Typography variant="body1" style={{margin: 5, whiteSpace: "pre-line", }}>
|
||||
{middleError}
|
||||
</Typography>
|
||||
}>
|
||||
<IconButton style={{width: 30, height: 30, backgroundColor: "rgba(255,255,255,0.0)", borderRadius: 30, marginTop: 2, }}>
|
||||
<ErrorOutlineIcon style={{color: "red", }} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
: null
|
||||
|
||||
return selectedIcon
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
// Returns for anything non-middle
|
||||
if (relevantactions.length > 2 && index >= 1 && index < relevantactions.length - 2) {
|
||||
if (founderror.length > 0) {
|
||||
middleError += founderror+"\n"
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
if (skipped && !lastitem) {
|
||||
nodecolor = grey
|
||||
branchcolor = grey
|
||||
}
|
||||
|
||||
if (previousTools) {
|
||||
previousTools = false
|
||||
} else {
|
||||
branchcolor = nodecolor
|
||||
}
|
||||
|
||||
if (action.trigger_type === "WEBHOOK") {
|
||||
nodecolor = green
|
||||
branchcolor = green
|
||||
}
|
||||
|
||||
|
||||
|
||||
var flex = index !== 0 && index !== relevantactions.length - 1 ? 1 : 3
|
||||
if (nodecolor === green) {
|
||||
branchcolor = green
|
||||
} else if (nodecolor === yellow) {
|
||||
branchcolor = yellow
|
||||
} else if (nodecolor === red) {
|
||||
branchcolor = red
|
||||
}
|
||||
|
||||
if (index === 0) {
|
||||
startBranchColor = branchcolor
|
||||
} else if (index !== 0 && index !== relevantactions.length - 1) {
|
||||
// FIXME: This doesn't work yet
|
||||
middleBranchColor = branchcolor
|
||||
}
|
||||
|
||||
if (lastitem) {
|
||||
if (middleError.length === 0) {
|
||||
branchcolor = startBranchColor
|
||||
} else {
|
||||
//branchcolor = middleBranchColor
|
||||
}
|
||||
|
||||
if (founderror === "") {
|
||||
nodecolor = green
|
||||
}
|
||||
}
|
||||
|
||||
// FIXME: This could mean the workflow hasn't ran yet
|
||||
if (workflow.validation.valid === false && (workflow.validation.errors === undefined || workflow.validation.errors === null || workflow.validation.errors.length == 0) && (workflow.validation.subflow_apps === undefined || workflow.validation.subflow_apps === null || workflow.validation.subflow_apps.length == 0)) {
|
||||
nodecolor = grey
|
||||
branchcolor = grey
|
||||
}
|
||||
|
||||
const branchTooltip = branchcolor === yellow ? "Check nodes for errors" : ""
|
||||
const appname = action.app_name.replaceAll('_', ' ').slice(0, 16)
|
||||
|
||||
const chipBackground = nodecolor === green ? "rgba(2,203,112, 0.2)" : nodecolor === grey ? "#494949": "rgba(245,52,52,0.8)"
|
||||
const chipColor = nodecolor === green ? "#02cb70" : nodecolor === grey ? "#CDCDCD" : "white"
|
||||
|
||||
//const ballcolor = lastitem ? nodecolor : branchcolor
|
||||
const ballcolor = branchcolor
|
||||
const ballsize = 8
|
||||
const topMargin = 20
|
||||
|
||||
const chipStyle = {
|
||||
height: 40,
|
||||
minWidth: 125,
|
||||
maxWidth: 125,
|
||||
borderRadius: 50,
|
||||
color: chipColor,
|
||||
backgroundColor: chipBackground,
|
||||
|
||||
//border: `2px solid ${chipBackground}`,
|
||||
//backgroundColor: "rgba(0,0,0,0.0)",
|
||||
}
|
||||
|
||||
if (image === "") {
|
||||
console.log("MISSING IMAGE: ", appname, image, action)
|
||||
}
|
||||
|
||||
if (decidedColor === grey && nodecolor === green) {
|
||||
setDecidedColor(red)
|
||||
}
|
||||
|
||||
if (decidedColor !== red && nodecolor === red) {
|
||||
setDecidedColor(red)
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{display: "flex", flex: flex, justifyContent: "right", }}>
|
||||
{lastitem ?
|
||||
<Tooltip title={branchTooltip}>
|
||||
<div style={{display: "flex", width: "100%", position: "relative",}}>
|
||||
<div style={{marginLeft: 0, marginRight: 0, marginTop: topMargin, height: 3, width: "100%", backgroundColor: branchcolor, }} />
|
||||
<div style={{position: "absolute", right: -3, top: topMargin-2.5, backgroundColor: ballcolor, width: ballsize, height: ballsize, borderRadius: 10, }}/>
|
||||
</div>
|
||||
</Tooltip>
|
||||
: null}
|
||||
|
||||
{appgroup.length > 0 ?
|
||||
<AvatarGroup max={4} style={{height: itemHeight+8, }}>
|
||||
{appgroup.map((subflowApp, subflowIndex) => {
|
||||
var appimage = ""
|
||||
if (apps !== undefined && apps !== null && apps.length > 0) {
|
||||
for (var key in apps) {
|
||||
const app = apps[key]
|
||||
if (app.name === subflowApp.app_name) {
|
||||
appimage = apps[key].large_image
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Tooltip title={`App: ${subflowApp.app_name} - subflow app`}>
|
||||
<Avatar
|
||||
style={{
|
||||
border: `4px solid ${nodecolor}`,
|
||||
}}
|
||||
src={appimage}
|
||||
/>
|
||||
</Tooltip>
|
||||
)
|
||||
})}
|
||||
</AvatarGroup>
|
||||
:
|
||||
<Tooltip title={
|
||||
<Typography variant="body1" style={{margin: 5, color: "white", }}>
|
||||
{founderror.length > 0 ? founderror : `App: ${appname} - Action: ${action.label}`}
|
||||
</Typography>
|
||||
} placement="top">
|
||||
|
||||
<Chip label={`${appname}`} style={chipStyle} icon={
|
||||
<Avatar
|
||||
variant="round"
|
||||
sx={{ backgroundColor: nodecolor, width: itemHeight, height: itemHeight, borderRadius: 50, border: `1px solid ${nodecolor}`,}}
|
||||
>
|
||||
{image !== "" ?
|
||||
<img
|
||||
src={image}
|
||||
style={{
|
||||
width: itemHeight,
|
||||
height: itemHeight,
|
||||
}}
|
||||
/>
|
||||
: null}
|
||||
</Avatar>
|
||||
} />
|
||||
|
||||
</Tooltip>
|
||||
}
|
||||
|
||||
{lastitem ? null :
|
||||
<Tooltip title={branchTooltip}>
|
||||
<div style={{display: "flex", width: "100%", position: "relative",}}>
|
||||
<div style={{position: "absolute", left: -3, top: topMargin-2.5, backgroundColor: ballcolor, color: ballcolor, width: ballsize, height: ballsize, borderRadius: 10, }}/>
|
||||
<div style={{marginLeft: 0, marginRight: 0, marginTop: 20, height: 3, width: "100%", backgroundColor: branchcolor, }} />
|
||||
</div>
|
||||
</Tooltip>
|
||||
}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default WorkflowValidationTimeline
|
||||
@@ -0,0 +1,37 @@
|
||||
import { createContext, useState, useEffect } from 'react';
|
||||
export const Context = createContext();
|
||||
|
||||
export const AppContext =(props) => {
|
||||
|
||||
// Left side bar global states
|
||||
const [searchBarModalOpen, setSearchBarModalOpen] = useState(false);
|
||||
const [leftSideBarOpenByClick, setLeftSideBarOpenByClick] = useState(false);
|
||||
const [windowWidth, setWindowWidth] = useState(window.innerWidth);
|
||||
|
||||
|
||||
//Calculate window width
|
||||
useEffect(() => {
|
||||
const handleResize = () => {
|
||||
setWindowWidth(window?.innerWidth);
|
||||
};
|
||||
|
||||
window.addEventListener('resize', handleResize);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('resize', handleResize);
|
||||
};
|
||||
}, []);
|
||||
|
||||
|
||||
return (
|
||||
<Context.Provider value={{
|
||||
searchBarModalOpen,
|
||||
setSearchBarModalOpen,
|
||||
leftSideBarOpenByClick,
|
||||
setLeftSideBarOpenByClick,
|
||||
windowWidth
|
||||
}}>
|
||||
{props.children}
|
||||
</Context.Provider>
|
||||
)
|
||||
}
|
||||
@@ -13,6 +13,30 @@ const data = [
|
||||
return elementname
|
||||
},
|
||||
"text-valign": "center",
|
||||
"text-margin-x": function(element) {
|
||||
// Attempt at bottom-positioning
|
||||
// Required text-valign: bottom
|
||||
// FIXME: Disabled for now.
|
||||
return "15px"
|
||||
|
||||
|
||||
|
||||
const name = element.data("label")
|
||||
console.log("Name: ", name)
|
||||
if (name === null || name === undefined || name == "" || document=== undefined || document === null) {
|
||||
return "0px"
|
||||
}
|
||||
|
||||
const canvas = document.createElement('canvas');
|
||||
const context = canvas.getContext('2d')
|
||||
|
||||
context.font = '18px Segoe UI, Tahoma, Geneva, Verdana, sans-serif, sans-serif'
|
||||
|
||||
const textWidth = context.measureText(name).width
|
||||
return textWidth + "px"
|
||||
//return -1*(textWidth) + "px"
|
||||
},
|
||||
|
||||
"font-family": "Segoe UI, Tahoma, Geneva, Verdana, sans-serif, sans-serif",
|
||||
"font-weight": "lighter",
|
||||
"font-size": "18px",
|
||||
@@ -23,7 +47,6 @@ const data = [
|
||||
padding: "10px",
|
||||
margin: "5px",
|
||||
"border-width": "1px",
|
||||
"text-margin-x": "10px",
|
||||
"z-index": 5001,
|
||||
},
|
||||
},
|
||||
@@ -35,7 +58,7 @@ const data = [
|
||||
"curve-style": "unbundled-bezier",
|
||||
label: "data(label)",
|
||||
"text-margin-y": "-15px",
|
||||
width: "5px",
|
||||
width: "3px",
|
||||
color: "white",
|
||||
"line-fill": "linear-gradient",
|
||||
"line-gradient-stop-positions": ["0.0", "100"],
|
||||
@@ -119,10 +142,10 @@ const data = [
|
||||
},
|
||||
},
|
||||
{
|
||||
selector: `node[app_name="Shuffle Tools"]`,
|
||||
selector: `node[app_name="Shuffle Tools"], node[app_name="email"], node[app_name="http"]`,
|
||||
css: {
|
||||
width: "30px",
|
||||
height: "30px",
|
||||
width: "35px",
|
||||
height: "35px",
|
||||
"z-index": 5000,
|
||||
"font-size": "0px",
|
||||
"background-width": "75%",
|
||||
@@ -258,7 +281,9 @@ const data = [
|
||||
{
|
||||
selector: "node[?isStartNode]",
|
||||
css: {
|
||||
shape: "ellipse",
|
||||
shape: function(element) {
|
||||
return "ellipse"
|
||||
},
|
||||
"border-color": "#80deea",
|
||||
width: "80px",
|
||||
height: "80px",
|
||||
@@ -270,8 +295,8 @@ const data = [
|
||||
{
|
||||
selector: "node[!is_valid]",
|
||||
css: {
|
||||
"border-color": "red",
|
||||
"border-width": "10px",
|
||||
"border-color": "#f53434",
|
||||
"border-width": "5px",
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -357,7 +382,7 @@ const data = [
|
||||
css: {
|
||||
"background-color": "#f85a3e",
|
||||
"border-color": "#f85a3e",
|
||||
"border-width": "12px",
|
||||
"border-width": "7px",
|
||||
"transition-property": "border-width",
|
||||
"transition-duration": "0.25s",
|
||||
label: "data(label)",
|
||||
@@ -462,13 +487,66 @@ const data = [
|
||||
"font-size": "0px",
|
||||
},
|
||||
},
|
||||
{
|
||||
selector: "node:selected",
|
||||
css: {
|
||||
"border-color": "#f86a3e",
|
||||
"border-width": "7px",
|
||||
},
|
||||
},
|
||||
{
|
||||
selector: "node:selected",
|
||||
css: {
|
||||
"border-color": "#f86a3e",
|
||||
"border-width": "7px",
|
||||
},
|
||||
},
|
||||
{
|
||||
selector: `node[buttonType="condition-drag"]`,
|
||||
css: {
|
||||
"width": "5px",
|
||||
"height": "5px",
|
||||
"background-color": "#f85a3e",
|
||||
},
|
||||
},
|
||||
{
|
||||
selector: `node[name="switch"]`,
|
||||
css: {
|
||||
label: function(element) {
|
||||
// Load from the actual element
|
||||
var nodeheight = 400
|
||||
var conditions = [{
|
||||
"name": "Condition 1",
|
||||
"check": "X equals Y",
|
||||
},
|
||||
{
|
||||
"name": "Condition 2",
|
||||
"check": "X2 equals Y2",
|
||||
},
|
||||
{
|
||||
"name": "Condition 3",
|
||||
"check": "X3 equals Y3",
|
||||
}]
|
||||
|
||||
conditions.push({
|
||||
"name": "Else",
|
||||
"check": "If all else fails",
|
||||
})
|
||||
|
||||
const newlines = nodeheight / conditions.length
|
||||
console.log("Newlines: ", newlines)
|
||||
|
||||
const label = conditions.map((condition) => {
|
||||
return `${condition.name}\n\n\n`
|
||||
}).join("\n")
|
||||
|
||||
return label
|
||||
},
|
||||
color: "white",
|
||||
"border-color": "#f85a3e",
|
||||
"background-color": "#1f1f1f",
|
||||
"font-size": "19px",
|
||||
"text-margin-x": "-110px",
|
||||
"text-wrap": "wrap",
|
||||
shape: "roundrectangle",
|
||||
width: "100",
|
||||
height: "300",
|
||||
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
//{
|
||||
|
||||
@@ -18,11 +18,9 @@ code {
|
||||
margin-right: -13px;
|
||||
}
|
||||
|
||||
.toc:hover {
|
||||
background-color: gray;
|
||||
color: white;
|
||||
table th, table td {
|
||||
border: 1px solid;
|
||||
}
|
||||
|
||||
/* .cm-string{
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
+19
-7
@@ -1,28 +1,30 @@
|
||||
import React from "react";
|
||||
import { createTheme, adaptV4Theme } from "@mui/material/styles";
|
||||
|
||||
//const theme = createTheme({
|
||||
const theme = createTheme(adaptV4Theme({
|
||||
palette: {
|
||||
theme: "dark",
|
||||
main: "#F86743",
|
||||
main: "#FF8544",
|
||||
primary: {
|
||||
main: "#F86743",
|
||||
main: "#FF8544",
|
||||
contrastText: "#ffffff",
|
||||
},
|
||||
secondary: {
|
||||
main: "#e8eaf6",
|
||||
main: "rgba(255,255,255,0.7)",
|
||||
contrastText: "#000000",
|
||||
},
|
||||
text: {
|
||||
secondary: "rgba(255,255,255,0.7)",
|
||||
},
|
||||
type: "dark",
|
||||
inputColor: "rgba(39,41,45,1)",
|
||||
//inputColor: "#383B40",
|
||||
|
||||
inputColor: "rgba(39,41,45,1)",
|
||||
surfaceColor: "#27292d",
|
||||
platformColor: "#1c1c1d",
|
||||
//platformColor: "#1c1c1d",
|
||||
platformColor: "#212121",
|
||||
backgroundColor: "#1a1a1a",
|
||||
|
||||
green: "#5cc879",
|
||||
borderRadius: 10,
|
||||
defaultBorder: "1px solid rgba(255,255,255,0.3)",
|
||||
@@ -44,10 +46,20 @@ const theme = createTheme(adaptV4Theme({
|
||||
overflowX: "auto",
|
||||
},
|
||||
textFieldStyle: {
|
||||
backgroundColor: "#383B40",
|
||||
backgroundColor: "#212121",
|
||||
borderRadius: 5,
|
||||
height: 40,
|
||||
},
|
||||
DialogStyle: {
|
||||
backgroundColor: "#212121",
|
||||
borderRadius: 2,
|
||||
boxShadow: "0px 0px 10px 0px rgba(0,0,0,0.75)",
|
||||
border: "1px solid #494949",
|
||||
},
|
||||
innerTextfieldStyle: {
|
||||
height: 40,
|
||||
fontSize: 16,
|
||||
backgroundColor: "#212121",
|
||||
// Removed since upgrading to mui 18
|
||||
//color: "white",
|
||||
//minHeight: 50,
|
||||
|
||||
@@ -0,0 +1,138 @@
|
||||
import React from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { Button, Typography } from '@mui/material';
|
||||
import theme from "../theme.jsx";
|
||||
|
||||
const NotFound = () => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const buttonStyle = {
|
||||
borderRadius: 25,
|
||||
height: 50,
|
||||
fontSize: 18,
|
||||
width: "100%",
|
||||
marginBottom: "10px",
|
||||
};
|
||||
|
||||
const primaryButtonStyle = {
|
||||
...buttonStyle,
|
||||
background: "linear-gradient(89.83deg, #FF8444 0.13%, #F2643B 99.84%)",
|
||||
color: "white",
|
||||
'&:hover': {
|
||||
background: "linear-gradient(89.83deg, #FF8444 0.13%, #F2643B 99.84%)",
|
||||
opacity: 0.9,
|
||||
}
|
||||
};
|
||||
|
||||
const secondaryButtonStyle = {
|
||||
...buttonStyle,
|
||||
background: "#383B40",
|
||||
border: "1px solid #494949",
|
||||
color: "white",
|
||||
'&:hover': {
|
||||
background: "#434649",
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{paddingTop: 100, }} className="min-h-screen bg-[#1A1A1A] flex items-center justify-center">
|
||||
<div
|
||||
style={{
|
||||
width: "100%",
|
||||
maxWidth: "532px",
|
||||
background: "#212121",
|
||||
borderRadius: "8px",
|
||||
padding: "40px",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
textAlign: "center",
|
||||
justifySelf: "center",
|
||||
}}
|
||||
>
|
||||
<img
|
||||
src="/images/logos/orange_logo.svg"
|
||||
alt="Shuffle Logo"
|
||||
style={{
|
||||
height: 44,
|
||||
width: 44,
|
||||
marginBottom: 10,
|
||||
}}
|
||||
/>
|
||||
|
||||
<Typography
|
||||
variant="h1"
|
||||
style={{
|
||||
fontSize: "72px",
|
||||
fontWeight: 900,
|
||||
background: "linear-gradient(89.83deg, #FF8444 0.13%, #F2643B 99.84%)",
|
||||
WebkitBackgroundClip: "text",
|
||||
WebkitTextFillColor: "transparent",
|
||||
marginBottom: "10px",
|
||||
textAlign: "center",
|
||||
}}
|
||||
>
|
||||
404
|
||||
</Typography>
|
||||
|
||||
<Typography
|
||||
variant="h4"
|
||||
style={{
|
||||
color: "white",
|
||||
marginBottom: "8px",
|
||||
fontWeight: 600,
|
||||
textAlign: "center",
|
||||
}}
|
||||
>
|
||||
Page Not Found
|
||||
</Typography>
|
||||
|
||||
<Typography
|
||||
variant="body1"
|
||||
style={{
|
||||
color: "#9E9E9E",
|
||||
marginBottom: "32px",
|
||||
fontSize: "16px",
|
||||
textAlign: "center",
|
||||
maxWidth: "400px",
|
||||
}}
|
||||
>
|
||||
Our code doggo couldn't find the page you were looking for.
|
||||
</Typography>
|
||||
|
||||
<iframe
|
||||
src="https://giphy.com/embed/FY8c5SKwiNf1EtZKGs"
|
||||
width="125"
|
||||
height="165"
|
||||
frameBorder="0"
|
||||
className="giphy-embed"
|
||||
allowFullScreen
|
||||
style={{
|
||||
marginBottom: 32,
|
||||
borderRadius: theme.palette.borderRadius,
|
||||
}}
|
||||
/>
|
||||
|
||||
<div style={{ width: "100%", maxWidth: "400px" }}>
|
||||
<Button
|
||||
variant="contained"
|
||||
onClick={() => navigate('/docs')}
|
||||
sx={primaryButtonStyle}
|
||||
>
|
||||
Documentation
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="contained"
|
||||
onClick={() => navigate('/')}
|
||||
sx={secondaryButtonStyle}
|
||||
>
|
||||
Return Home
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default NotFound;
|
||||
+1436
-865
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,338 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import AdminNavBar from '../components/AdminNavBar.jsx';
|
||||
import { toast } from "react-toastify";
|
||||
|
||||
const Admin2 = (props) => {
|
||||
// Destructure props if needed
|
||||
const { userdata, globalUrl, serverside, checkLogin, notifications, setNotifications, stripeKey, isLoaded, isLoggedIn} = props;
|
||||
const [selectedTab, setSelectedTab] = useState('editdetails');
|
||||
const [selectedStatus, setSelectedStatus] = React.useState([]);
|
||||
const [selectedOrganization, setSelectedOrganization] = useState({});
|
||||
const [organizationFeatures, setOrganizationFeatures] = useState({});
|
||||
const [orgRequest, setOrgRequest] = React.useState(true);
|
||||
const isCloud = window.location.host === "localhost:3002" || window.location.host === "shuffler.io";
|
||||
const handleGetOrg = (orgId) => {
|
||||
if (
|
||||
serverside !== true &&
|
||||
window.location.search !== undefined &&
|
||||
window.location.search !== null
|
||||
) {
|
||||
const urlSearchParams = new URLSearchParams(window.location.search);
|
||||
const params = Object.fromEntries(urlSearchParams.entries());
|
||||
const foundorgid = params["org_id"];
|
||||
if (foundorgid !== undefined && foundorgid !== null) {
|
||||
orgId = foundorgid;
|
||||
}
|
||||
}
|
||||
console.log("getting organization details for: ", orgId);
|
||||
|
||||
// if (orgId === undefined) {
|
||||
// toast(
|
||||
// "Organization ID not defined. Please contact us on https://shuffler.io if this persists logout.",
|
||||
// );
|
||||
// return;
|
||||
// }
|
||||
|
||||
// Just use this one?
|
||||
|
||||
fetch(`${globalUrl}/api/v1/orgs/${orgId}`, {
|
||||
method: "GET",
|
||||
credentials: "include",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
})
|
||||
.then((response) => {
|
||||
if (response.status === 401) {
|
||||
}
|
||||
|
||||
return response.json();
|
||||
})
|
||||
.then((responseJson) => {
|
||||
if (responseJson["success"] === false) {
|
||||
toast(
|
||||
"Failed getting your org. If this persists, please contact support. Redirecting to workflows...",
|
||||
);
|
||||
setTimeout(() => {
|
||||
window.location.href = "/workflows";
|
||||
}, 3000);
|
||||
} else {
|
||||
if (
|
||||
responseJson.sync_features === undefined ||
|
||||
responseJson.sync_features === null
|
||||
) {
|
||||
responseJson.sync_features = {};
|
||||
}
|
||||
|
||||
if (
|
||||
responseJson.lead_info !== undefined &&
|
||||
responseJson.lead_info !== null
|
||||
) {
|
||||
var leads = [];
|
||||
if (responseJson.lead_info.contacted) {
|
||||
leads.push("contacted");
|
||||
}
|
||||
|
||||
if (responseJson.lead_info.customer) {
|
||||
leads.push("customer");
|
||||
}
|
||||
|
||||
if (responseJson.lead_info.old_customer) {
|
||||
leads.push("old customer");
|
||||
}
|
||||
|
||||
if (responseJson.lead_info.old_lead) {
|
||||
leads.push("old lead");
|
||||
}
|
||||
|
||||
if (responseJson.lead_info.tech_partner) {
|
||||
leads.push("tech partner");
|
||||
}
|
||||
|
||||
if (responseJson.lead_info.creator) {
|
||||
leads.push("creator");
|
||||
}
|
||||
|
||||
if (responseJson.lead_info.opensource) {
|
||||
leads.push("open source");
|
||||
}
|
||||
|
||||
if (responseJson.lead_info.demo_done) {
|
||||
leads.push("demo done");
|
||||
}
|
||||
|
||||
if (responseJson.lead_info.pov) {
|
||||
leads.push("pov");
|
||||
}
|
||||
|
||||
if (responseJson.lead_info.lead) {
|
||||
leads.push("lead");
|
||||
}
|
||||
|
||||
if (responseJson.lead_info.student) {
|
||||
leads.push("student");
|
||||
}
|
||||
|
||||
if (responseJson.lead_info.internal) {
|
||||
leads.push("internal");
|
||||
}
|
||||
|
||||
if (responseJson.lead_info.sub_org) {
|
||||
leads.push("sub_org");
|
||||
}
|
||||
|
||||
setSelectedStatus(leads);
|
||||
}
|
||||
|
||||
setSelectedOrganization(responseJson);
|
||||
var lists = {
|
||||
active: {
|
||||
triggers: [],
|
||||
features: [],
|
||||
sync: [],
|
||||
},
|
||||
inactive: {
|
||||
triggers: [],
|
||||
features: [],
|
||||
sync: [],
|
||||
},
|
||||
};
|
||||
|
||||
// FIXME: Set up features
|
||||
//Object.keys(responseJson.sync_features).map(function(key, index) {
|
||||
// //console.log(responseJson.sync_features[key])
|
||||
//})
|
||||
|
||||
//setOrgName(responseJson.name)
|
||||
//setOrgDescription(responseJson.description)
|
||||
setOrganizationFeatures(lists);
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log("Error getting org: ", error);
|
||||
toast("Error getting current organization");
|
||||
});
|
||||
};
|
||||
|
||||
const urlSearchParams = new URLSearchParams(window.location.search);
|
||||
const params = Object.fromEntries(urlSearchParams.entries());
|
||||
const foundOrgID = params["org_id"]
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
if(foundOrgID !== null && foundOrgID !== undefined && userdata?.support && foundOrgID?.length > 0) {
|
||||
handleClickChangeOrg(foundOrgID)
|
||||
}
|
||||
}, [foundOrgID]);
|
||||
|
||||
const handleClickChangeOrg = (orgId) => {
|
||||
// Don't really care about the logout
|
||||
//name: org.name,
|
||||
//orgId = "asd"
|
||||
const data = {
|
||||
org_id: orgId,
|
||||
};
|
||||
|
||||
localStorage.setItem("globalUrl", "");
|
||||
localStorage.setItem("getting_started_sidebar", "open");
|
||||
|
||||
fetch(`${globalUrl}/api/v1/orgs/${orgId}/change`, {
|
||||
mode: "cors",
|
||||
credentials: "include",
|
||||
crossDomain: true,
|
||||
method: "POST",
|
||||
body: JSON.stringify(data),
|
||||
withCredentials: true,
|
||||
headers: {
|
||||
"Content-Type": "application/json; charset=utf-8",
|
||||
},
|
||||
})
|
||||
.then(function (response) {
|
||||
if (response.status !== 200) {
|
||||
console.log("Error in response");
|
||||
} else {
|
||||
localStorage.removeItem("apps")
|
||||
localStorage.removeItem("workflows")
|
||||
localStorage.removeItem("userinfo")
|
||||
}
|
||||
|
||||
return response.json();
|
||||
})
|
||||
.then(function (responseJson) {
|
||||
if (responseJson.success === true) {
|
||||
if (responseJson.region_url !== undefined && responseJson.region_url !== null && responseJson.region_url.length > 0) {
|
||||
localStorage.setItem("globalUrl", responseJson.region_url)
|
||||
//globalUrl = responseJson.region_url
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
window.location.reload()
|
||||
}, 3000);
|
||||
toast("Successfully changed active organization - refreshing!");
|
||||
} else {
|
||||
if (responseJson.reason !== undefined && responseJson.reason !== null) {
|
||||
if (!responseJson.reason.includes("already")) {
|
||||
toast("Failed changing org: " + responseJson.reason);
|
||||
}
|
||||
} else {
|
||||
toast("Failed changing org")
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log("error changing: ", error);
|
||||
//removeCookie("session_token", {path: "/"})
|
||||
});
|
||||
};
|
||||
|
||||
const handleEditOrg = (
|
||||
name,
|
||||
description,
|
||||
orgId,
|
||||
image,
|
||||
defaults,
|
||||
sso_config,
|
||||
lead_info,
|
||||
{ mfa_required } = {}
|
||||
) => {
|
||||
const data = {
|
||||
name: name,
|
||||
description: description,
|
||||
org_id: orgId,
|
||||
image: image,
|
||||
defaults: defaults,
|
||||
sso_config: sso_config,
|
||||
lead_info: lead_info,
|
||||
mfa_required: mfa_required !== undefined ? mfa_required : selectedOrganization?.mfa_required,
|
||||
};
|
||||
|
||||
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 (
|
||||
lead_info === undefined ||
|
||||
lead_info === null ||
|
||||
lead_info === []
|
||||
) {
|
||||
toast("Successfully edited org!");
|
||||
}
|
||||
}
|
||||
}),
|
||||
)
|
||||
.catch((error) => {
|
||||
toast("Err: " + error.toString());
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
const handleStatusChange = (event) => {
|
||||
const { value } = event.target;
|
||||
setSelectedStatus(value);
|
||||
|
||||
handleEditOrg(
|
||||
selectedOrganization?.name,
|
||||
selectedOrganization?.description,
|
||||
selectedOrganization.id,
|
||||
selectedOrganization?.image,
|
||||
{
|
||||
app_download_repo: selectedOrganization?.defaults?.app_download_repo,
|
||||
app_download_branch: selectedOrganization?.defaults?.app_download_branch,
|
||||
workflow_download_repo: selectedOrganization?.defaults?.workflow_download_repo,
|
||||
workflow_download_branch: selectedOrganization?.defaults?.workflow_download_branch,
|
||||
notification_workflow: selectedOrganization?.defaults?.notification_workflow,
|
||||
documentation_reference: selectedOrganization?.defaults?.documentation_reference,
|
||||
workflow_upload_repo: selectedOrganization?.defaults?.workflow_upload_repo,
|
||||
workflow_upload_branch: selectedOrganization?.defaults?.workflow_upload_branch,
|
||||
workflow_upload_username: selectedOrganization?.defaults?.workflow_upload_username,
|
||||
workflow_upload_token: selectedOrganization?.defaults?.workflow_upload_token,
|
||||
newsletter: !selectedOrganization?.defaults?.newsletter,
|
||||
weekly_recommendations: selectedOrganization?.defaults?.weekly_recommendations,
|
||||
},
|
||||
{
|
||||
sso_entrypoint: selectedOrganization?.sso_config?.sso_entrypoint,
|
||||
sso_certificate: selectedOrganization?.sso_config?.sso_certificate,
|
||||
client_id: selectedOrganization?.sso_config?.client_id,
|
||||
client_secret: selectedOrganization?.sso_config?.client_secret,
|
||||
openid_authorization: selectedOrganization?.sso_config?.openid_authorization,
|
||||
openid_token: selectedOrganization?.sso_config?.openid_token,
|
||||
SSORequired: selectedOrganization?.sso_config?.SSORequired,
|
||||
auto_provision: selectedOrganization?.sso_config?.auto_provision,
|
||||
},
|
||||
value.length === 0 ? ["none"] : value,
|
||||
);
|
||||
};
|
||||
|
||||
if (
|
||||
selectedOrganization.id === undefined &&
|
||||
userdata !== undefined &&
|
||||
userdata.active_org !== undefined &&
|
||||
orgRequest
|
||||
) {
|
||||
const orgId = userdata.active_org.id
|
||||
|
||||
setOrgRequest(false);
|
||||
handleGetOrg(orgId);
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ display: 'flex', justifyContent: 'center', paddingTop: 29, zoom: 0.8, }}>
|
||||
<AdminNavBar userdata={userdata} isLoaded={isLoaded} selectedStatus={selectedStatus} setSelectedStatus={setSelectedStatus} selectedTab={selectedTab} orgId={selectedOrganization.id} handleStatusChange={handleStatusChange} handleEditOrg={handleEditOrg} handleGetOrg={handleGetOrg} setSelectedOrganization={setSelectedOrganization} selectedOrganization={selectedOrganization} setNotifications={setNotifications} stripeKey={stripeKey} notifications={notifications} checkLogin={checkLogin} globalUrl={globalUrl} isCloud={isCloud} serverside={serverside} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Admin2;
|
||||
+5164
-3290
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+142
-111
@@ -37,14 +37,15 @@ import {
|
||||
AttachFile as AttachFileIcon,
|
||||
Apps as AppsIcon,
|
||||
ErrorOutline as ErrorOutlineIcon,
|
||||
AddAPhoto as AddAPhotoIcon,
|
||||
AddAPhotoOutlined as AddAPhotoOutlinedIcon,
|
||||
ZoomInOutlined as ZoomInOutlinedIcon,
|
||||
ZoomOutOutlined as ZoomOutOutlinedIcon,
|
||||
Loop as LoopIcon,
|
||||
AddPhotoAlternate as AddPhotoAlternateIcon,
|
||||
CallMerge as CallMergeIcon,
|
||||
AddAPhoto as AddAPhotoIcon,
|
||||
AddAPhotoOutlined as AddAPhotoOutlinedIcon,
|
||||
ZoomInOutlined as ZoomInOutlinedIcon,
|
||||
ZoomOutOutlined as ZoomOutOutlinedIcon,
|
||||
Loop as LoopIcon,
|
||||
AddPhotoAlternate as AddPhotoAlternateIcon,
|
||||
CallMerge as CallMergeIcon,
|
||||
CloudDownload as CloudDownloadIcon,
|
||||
OpenInNew as OpenInNewIcon,
|
||||
} from "@mui/icons-material";
|
||||
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
@@ -228,6 +229,8 @@ const parseCurl = (s) => {
|
||||
};
|
||||
|
||||
// Basically CRUD for each category + special
|
||||
// These are already tracked in the shuffle/shuffle-shared/blobs.go file
|
||||
// as backend should be used for managing this long-term
|
||||
export const appCategories = [
|
||||
{
|
||||
"name": "Communication",
|
||||
@@ -542,7 +545,7 @@ const AppCreator = (defaultprops) => {
|
||||
};
|
||||
|
||||
|
||||
const isCloud = window.location.host === "localhost:3002" || window.location.host === "shuffler.io";
|
||||
const isCloud = (window.location.host === "localhost:3002" || window.location.host === "shuffler.io") ? true : (process.env.IS_SSR === "true");
|
||||
|
||||
useEffect(() => {
|
||||
if (window.location.pathname.includes("apps/edit")) {
|
||||
@@ -895,8 +898,31 @@ const AppCreator = (defaultprops) => {
|
||||
};
|
||||
|
||||
if (methodvalue["x-label"] !== undefined && methodvalue["x-label"] !== null) {
|
||||
console.log("LABEL: ", methodvalue["x-label"])
|
||||
|
||||
var correctlabel = ""
|
||||
const labels = methodvalue["x-label"].split(",")
|
||||
for (let labelkey in labels) {
|
||||
var label = labels[labelkey].trim()
|
||||
if (label.toLowerCase() === "no label") {
|
||||
continue
|
||||
}
|
||||
|
||||
// Remove quotes and escapes
|
||||
label = label.replace(/['"]+/g, '')
|
||||
label = label.replace(/\\/g, '')
|
||||
|
||||
//label = label.replace("_", " ", -1)
|
||||
//label = label.charAt(0).toUpperCase() + label.slice(1)
|
||||
|
||||
correctlabel = label
|
||||
break
|
||||
}
|
||||
|
||||
console.log("LABEL: ", correctlabel)
|
||||
// FIX: Map labels only if they're actually in the category list
|
||||
newaction.action_label = methodvalue["x-label"]
|
||||
//newaction.action_label = methodvalue["x-label"]
|
||||
newaction.action_label = correctlabel
|
||||
}
|
||||
|
||||
if (methodvalue["x-required-fields"] !== undefined && methodvalue["x-required-fields"] !== null) {
|
||||
@@ -3122,15 +3148,14 @@ const AppCreator = (defaultprops) => {
|
||||
Scopes for Oauth2
|
||||
</Typography>
|
||||
<MuiChipsInput
|
||||
style={{border: "2px solid #f86a3e", borderRadius: theme.palette.borderRadius,}}
|
||||
required
|
||||
required
|
||||
InputProps={{
|
||||
style: {
|
||||
color: "white",
|
||||
maxHeight: 50,
|
||||
maxHeight: 160,
|
||||
},
|
||||
}}
|
||||
style={{ maxHeight: 80, overflowX: "hidden", overflowY: "auto" }}
|
||||
style={{ minHeight: 160, maxHeight: 160, overflowX: "hidden", overflowY: "auto" }}
|
||||
placeholder="Available Oauth2 Scopes"
|
||||
color="primary"
|
||||
fullWidth
|
||||
@@ -3148,7 +3173,7 @@ const AppCreator = (defaultprops) => {
|
||||
<div style={{ color: "white", marginTop: 20 }}>
|
||||
<Typography variant="body1">API key authentication</Typography>
|
||||
<Typography variant="body2" color="textSecondary">
|
||||
Add the name of the field used for authentication, e.g. "X-APIKEY". Should NOT be your actual API-key.
|
||||
<b>Do NOT put your actual API-key.</b> Add the name of the field used for authentication, e.g. "X-APIKEY".
|
||||
</Typography>
|
||||
<div style={{display: "flex", marginTop: 10, }}>
|
||||
<div style={{flex: 4,}}>
|
||||
@@ -3157,7 +3182,7 @@ const AppCreator = (defaultprops) => {
|
||||
required
|
||||
style={{ marginTop: 0, backgroundColor: inputColor }}
|
||||
fullWidth={true}
|
||||
placeholder="Field Name (key, NOT your actual API-key)"
|
||||
placeholder="The Key to use as the header/query - NOT your actual API-key"
|
||||
type="name"
|
||||
id="standard-required"
|
||||
margin="normal"
|
||||
@@ -3824,7 +3849,7 @@ const AppCreator = (defaultprops) => {
|
||||
required
|
||||
style={{
|
||||
flex: "1",
|
||||
marginTop: "5px",
|
||||
marginTop: 5,
|
||||
marginRight: "15px",
|
||||
backgroundColor: inputColor,
|
||||
}}
|
||||
@@ -4317,7 +4342,8 @@ const AppCreator = (defaultprops) => {
|
||||
>
|
||||
<ErrorOutlineIcon />
|
||||
</Tooltip>
|
||||
) : (
|
||||
) : null
|
||||
{/*
|
||||
<Tooltip
|
||||
color="secondary"
|
||||
title={data.errors.join("\n")}
|
||||
@@ -4325,7 +4351,7 @@ const AppCreator = (defaultprops) => {
|
||||
>
|
||||
<CheckCircleIcon style={{ marginTop: 6 }} />
|
||||
</Tooltip>
|
||||
);
|
||||
*/}
|
||||
|
||||
var bgColor = "#61afee";
|
||||
if (data.method === "POST") {
|
||||
@@ -4439,7 +4465,7 @@ const AppCreator = (defaultprops) => {
|
||||
setUpdate(Math.random())
|
||||
}
|
||||
}}
|
||||
value={data.action_label}
|
||||
value={data?.action_label?.replace(" ", "_").toLowerCase()}
|
||||
style={{
|
||||
border: data.action_label === undefined || data.action_label === "No Label" ? "" : `2px solid ${bgColor}`,
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
@@ -4461,7 +4487,7 @@ const AppCreator = (defaultprops) => {
|
||||
return (
|
||||
<MenuItem
|
||||
key={labelindex}
|
||||
value={label}
|
||||
value={label.replace(" ", "_").toLowerCase()}
|
||||
style={{
|
||||
}}
|
||||
>
|
||||
@@ -4693,7 +4719,7 @@ const AppCreator = (defaultprops) => {
|
||||
<TextField
|
||||
style={{
|
||||
backgroundColor: inputColor,
|
||||
borderRadius: theme.palette.borderRadius,
|
||||
borderRadius: theme.palette?.borderRadius,
|
||||
}}
|
||||
InputProps={{
|
||||
style: {
|
||||
@@ -5042,7 +5068,7 @@ const AppCreator = (defaultprops) => {
|
||||
<TextField
|
||||
style={{
|
||||
backgroundColor: inputColor,
|
||||
borderRadius: theme.palette.borderRadius,
|
||||
borderRadius: theme.palette?.borderRadius,
|
||||
}}
|
||||
InputProps={{
|
||||
style: {
|
||||
@@ -5082,7 +5108,7 @@ const AppCreator = (defaultprops) => {
|
||||
<TextField
|
||||
style={{
|
||||
backgroundColor: inputColor,
|
||||
borderRadius: theme.palette.borderRadius,
|
||||
borderRadius: theme.palette?.borderRadius,
|
||||
}}
|
||||
InputProps={{
|
||||
style: {
|
||||
@@ -5776,19 +5802,35 @@ const AppCreator = (defaultprops) => {
|
||||
General information
|
||||
</h2>
|
||||
</div>
|
||||
<div style={{flex: 1, itemAlign: "right", textAlign: "right",}}>
|
||||
<Tooltip title="Merge with another API (coming soon)" placement="bottom">
|
||||
<IconButton
|
||||
disabled
|
||||
<div style={{flex: 1, itemAlign: "right", textAlign: "right", marginRight: 10, }}>
|
||||
<Tooltip title="Try the API" placement="bottom">
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="secondary"
|
||||
onClick={() => {
|
||||
setOpenApiModal(true)
|
||||
var urlParams = new URLSearchParams(window.location.search);
|
||||
if (!urlParams.has("id")) {
|
||||
window.open(`/apis/${app.id}`, "_blank")
|
||||
} else {
|
||||
toast.error("Build the app first.")
|
||||
}
|
||||
|
||||
}}
|
||||
>
|
||||
<OpenInNewIcon style={{marginRight: 5, }}/>
|
||||
Try the API
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<Tooltip title="Manage forks and Merge with another API (coming soon)" placement="bottom">
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
//setOpenApiModal(true)
|
||||
toast.info("Action merging & fork management coming soon")
|
||||
}}
|
||||
style={{marginLeft: 10, }}
|
||||
>
|
||||
<CallMergeIcon
|
||||
style={{}}
|
||||
onClick={() => {
|
||||
setOpenApiModal(true)
|
||||
}}
|
||||
/>
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
@@ -5815,18 +5857,13 @@ const AppCreator = (defaultprops) => {
|
||||
flex: "1",
|
||||
margin: 10,
|
||||
border: "1px solid #f85a3e",
|
||||
borderRadius: theme.palette?.borderRadius,
|
||||
cursor: "pointer",
|
||||
backgroundColor: inputColor,
|
||||
maxWidth: 174,
|
||||
maxHeight: 174,
|
||||
}}
|
||||
onClick={() => {
|
||||
/*
|
||||
if (fileBase64.length === 0) {
|
||||
upload.click()
|
||||
}
|
||||
*/
|
||||
|
||||
setOpenImageModal(true);
|
||||
}}
|
||||
>
|
||||
@@ -5839,7 +5876,7 @@ const AppCreator = (defaultprops) => {
|
||||
/>
|
||||
</div>
|
||||
</Tooltip>
|
||||
<div style={{ flex: "3", color: "white" }}>
|
||||
<div style={{ flex: "3", color: "white", marginLeft: 20, }}>
|
||||
<div style={{ marginTop: "10px" }} />
|
||||
Name
|
||||
<TextField
|
||||
@@ -5893,35 +5930,28 @@ const AppCreator = (defaultprops) => {
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<div style={{ marginTop: "10px" }} />
|
||||
<div style={{ marginTop: 10, }} />
|
||||
Description
|
||||
<TextField
|
||||
required
|
||||
style={{
|
||||
flex: "1",
|
||||
marginTop: "5px",
|
||||
marginRight: "15px",
|
||||
paddingTop: 5,
|
||||
marginTop: 5,
|
||||
marginRight: 15,
|
||||
backgroundColor: inputColor,
|
||||
maxHeight: 250,
|
||||
overflow: "auto",
|
||||
maxHeight: 250,
|
||||
overflowY: "auto"
|
||||
}}
|
||||
fullWidth={true}
|
||||
type="name"
|
||||
id="outlined-with-placeholder"
|
||||
margin="normal"
|
||||
multiline
|
||||
multiline
|
||||
variant="outlined"
|
||||
placeholder="A description for the service"
|
||||
value={description}
|
||||
onChange={(e) => setDescription(e.target.value)}
|
||||
InputProps={{
|
||||
classes: {
|
||||
notchedOutline: classes.notchedOutline,
|
||||
},
|
||||
style: {
|
||||
color: "white",
|
||||
},
|
||||
}}
|
||||
defaultValue={description}
|
||||
//onChange={(e) => setDescription(e.target.value)}
|
||||
onBlur={(e) => setDescription(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -5994,7 +6024,7 @@ const AppCreator = (defaultprops) => {
|
||||
setBaseUrl(tmpstring);
|
||||
}}
|
||||
/>
|
||||
<div style={{padding: 25, border: "2px solid rgba(255,255,255,0.7)", borderRadius: theme.palette.borderRadius, }}>
|
||||
<div style={{padding: 25, border: "2px solid rgba(255,255,255,0.7)", borderRadius: theme.palette?.borderRadius, }}>
|
||||
<span style={{display: "flex", }}>
|
||||
<FormControl style={{ }} variant="outlined">
|
||||
<Typography variant="h6">Authentication</Typography>
|
||||
@@ -6161,62 +6191,63 @@ const AppCreator = (defaultprops) => {
|
||||
{testView}
|
||||
*/}
|
||||
|
||||
<div style={{display: "flex", marginTop: 35, }}>
|
||||
{appDownloadData.length > 0 ?
|
||||
<Tooltip title="Download the OpenAPI specification for the App" placement="bottom">
|
||||
<IconButton
|
||||
style={{marginRight: 25, }}
|
||||
onClick={() => {
|
||||
toast(`Downloading OpenAPI JSON data for for ${name}`)
|
||||
// Download as file
|
||||
var blob = new Blob([appDownloadData], {
|
||||
type: "application/octet-stream",
|
||||
});
|
||||
<div style={{height: 50, padding: 25, display: "flex", marginTop: 35, position: "fixed", bottom: 0, left: 0, width: "100%", backgroundColor: theme.palette?.backgroundColor, borderTop: "1px solid rgba(255,255,255,0.3)",}}>
|
||||
<div style={{width: 450, margin: "auto", display: "flex", textAlign: "center", }}>
|
||||
{appDownloadData.length > 0 ?
|
||||
<Tooltip title="Download the OpenAPI specification for the App" placement="bottom">
|
||||
<IconButton
|
||||
style={{marginRight: 25, }}
|
||||
onClick={() => {
|
||||
toast(`Downloading OpenAPI JSON data for for ${name}`)
|
||||
// Download as file
|
||||
var blob = new Blob([appDownloadData], {
|
||||
type: "application/octet-stream",
|
||||
});
|
||||
|
||||
var url = URL.createObjectURL(blob);
|
||||
var link = document.createElement("a");
|
||||
link.setAttribute("href", url);
|
||||
link.setAttribute("download", `${name}.json`);
|
||||
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);
|
||||
}}
|
||||
>
|
||||
<CloudDownloadIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
: null}
|
||||
<Button
|
||||
disabled={appBuilding}
|
||||
color="primary"
|
||||
variant="contained"
|
||||
fullWidth
|
||||
style={{ height: "50px", flex: 1, }}
|
||||
onClick={() => {
|
||||
submitApp();
|
||||
}}
|
||||
>
|
||||
{appBuilding ? <CircularProgress /> : "Save"}
|
||||
</Button>
|
||||
{appDownloadData.length > 0 ?
|
||||
<div style={{width: 50, }}/>
|
||||
: null}
|
||||
var url = URL.createObjectURL(blob);
|
||||
var link = document.createElement("a");
|
||||
link.setAttribute("href", url);
|
||||
link.setAttribute("download", `${name}.json`);
|
||||
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);
|
||||
}}
|
||||
>
|
||||
<CloudDownloadIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
: null}
|
||||
<Button
|
||||
disabled={appBuilding}
|
||||
color="primary"
|
||||
variant="contained"
|
||||
style={{ height: 50, flex: 1, minWidth: 400, maxWidth: 400, }}
|
||||
onClick={() => {
|
||||
submitApp();
|
||||
}}
|
||||
>
|
||||
{appBuilding ? <CircularProgress /> : "Save API"}
|
||||
</Button>
|
||||
{appDownloadData.length > 0 ?
|
||||
<div style={{width: 50, }}/>
|
||||
: null}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Typography style={{ marginTop: 25, textAlign: "center", }}>
|
||||
|
||||
+122
-80
@@ -1,4 +1,4 @@
|
||||
import React, { useEffect } from "react";
|
||||
import React, { useEffect, useContext, memo } from "react";
|
||||
|
||||
import { useInterval } from "react-powerhooks";
|
||||
import theme from '../theme.jsx';
|
||||
@@ -49,6 +49,7 @@ import {
|
||||
Folder as FolderIcon,
|
||||
LibraryBooks as LibraryBooksIcon,
|
||||
} from "@mui/icons-material";
|
||||
import { Context } from "../context/ContextApi.jsx";
|
||||
|
||||
import {
|
||||
ForkRight as ForkRightIcon,
|
||||
@@ -280,6 +281,7 @@ const searchClient = algoliasearch("JNSS5CFDZZ", "db08e40265e2941b9a7d8f644b6e52
|
||||
const Apps = (props) => {
|
||||
const { globalUrl, isLoggedIn, isLoaded, userdata, serverside, } = props;
|
||||
|
||||
|
||||
//const [workflows, setWorkflows] = React.useState([]);
|
||||
const baseRepository = "https://github.com/frikky/shuffle-apps";
|
||||
//const alert = useAlert();
|
||||
@@ -334,6 +336,11 @@ const Apps = (props) => {
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
console.log("APPVALID: ", appValidation)
|
||||
redirectOpenApi()
|
||||
}, [appValidation])
|
||||
|
||||
const getUserProfile = (username) => {
|
||||
if (serverside === true || !isCloud) {
|
||||
setCreatorProfile({})
|
||||
@@ -418,7 +425,7 @@ const Apps = (props) => {
|
||||
minWidth: "100%",
|
||||
maxWidth: 612.5,
|
||||
marginBottom: 5,
|
||||
borderRadius: theme.palette.borderRadius,
|
||||
borderRadius: theme.palette?.borderRadius,
|
||||
color: "white",
|
||||
backgroundColor: surfaceColor,
|
||||
cursor: "pointer",
|
||||
@@ -870,7 +877,7 @@ const Apps = (props) => {
|
||||
minWidth: viewWidth,
|
||||
maxWidth: viewWidth,
|
||||
color: "white",
|
||||
borderRadius: theme.palette.borderRadius,
|
||||
borderRadius: theme.palette?.borderRadius,
|
||||
backgroundColor: surfaceColor,
|
||||
//display: "flex",
|
||||
marginBottom: 10,
|
||||
@@ -955,7 +962,6 @@ const Apps = (props) => {
|
||||
</Link>
|
||||
: null
|
||||
|
||||
console.log("Sharing config: ", sharingConfiguration);
|
||||
const activateButton =
|
||||
selectedApp.generated && !selectedApp.activated ? (
|
||||
<div>
|
||||
@@ -1281,10 +1287,15 @@ const Apps = (props) => {
|
||||
{isCloud && !internalIds.includes(selectedApp.name.toLowerCase()) ?
|
||||
<Tooltip title="Deactivates this app for the current organisation. This means the app will not be usable again until you re-activate it." placement="top">
|
||||
<Button
|
||||
variant="contained"
|
||||
variant={selectedApp.reference_org === userdata.active_org.id ? "outlined" : "contained"}
|
||||
component="label"
|
||||
color="primary"
|
||||
onClick={() => {
|
||||
if (selectedApp.reference_org === userdata.active_org.id) {
|
||||
toast.info("Can't deactivate apps made in this org. Please contact support if you want to deactivate this app.")
|
||||
return
|
||||
}
|
||||
|
||||
const tmpurl = new URL(window.location.href);
|
||||
const searchParams = tmpurl.searchParams;
|
||||
const queryID = searchParams.get("queryID");
|
||||
@@ -1316,7 +1327,7 @@ const Apps = (props) => {
|
||||
console.log("No query to handle when activating");
|
||||
}
|
||||
|
||||
activateApp(selectedApp.id, true)
|
||||
activateApp(selectedApp.id, true, true)
|
||||
}}
|
||||
style={{ height: 35, marginTop: 0, marginLeft: 10, }}
|
||||
>
|
||||
@@ -1529,22 +1540,36 @@ const Apps = (props) => {
|
||||
|
||||
const [hover, setHover] = React.useState(false);
|
||||
|
||||
const makeFancy = text?.includes("Generate")
|
||||
|
||||
var parsedStyle = {
|
||||
flex: 1,
|
||||
padding: 15,
|
||||
margin: 10,
|
||||
paddingTop: 25,
|
||||
backgroundColor: hover ? theme.palette.surfaceColor : "transparent",
|
||||
cursor: hover ? "pointer" : "default",
|
||||
textAlign: "center",
|
||||
minHeight: 150,
|
||||
maxHeight: 150,
|
||||
|
||||
borderRadius: theme.palette?.borderRadius,
|
||||
}
|
||||
|
||||
if (!makeFancy) {
|
||||
parsedStyle.border = hover ? "1px solid #f85a3e" : "1px solid rgba(255,255,255,0.3)"
|
||||
} else {
|
||||
parsedStyle.border = "1px solid transparent"
|
||||
parsedStyle.borderImage = "linear-gradient(45deg, red, orange, yellow, green, blue, indigo, violet) 1"
|
||||
parsedStyle.borderRadius = 0 // This doesn't work. Try to hover with a high one, and it's weird due to borderImage
|
||||
}
|
||||
|
||||
return (
|
||||
<Paper
|
||||
onMouseEnter={() => setHover(true)}
|
||||
onMouseLeave={() => setHover(false)}
|
||||
onClick={func}
|
||||
style={{
|
||||
flex: 1,
|
||||
padding: 15,
|
||||
margin: 10,
|
||||
backgroundColor: hover ? theme.palette.surfaceColor : "transparent",
|
||||
border: hover ? "1px solid #f85a3e" : "1px solid rgba(255,255,255,0.3)",
|
||||
cursor: hover ? "pointer" : "default",
|
||||
textAlign: "center",
|
||||
minHeight: 150,
|
||||
maxHeight: 150,
|
||||
}}
|
||||
style={parsedStyle}
|
||||
>
|
||||
{icon}
|
||||
<Typography>
|
||||
@@ -1565,7 +1590,7 @@ const Apps = (props) => {
|
||||
</h2>
|
||||
<div style={{display: "flex"}}>
|
||||
<AppCreateButton
|
||||
text="Generate from OpenAPI/Swagger"
|
||||
text="Upload OpenAPI or Swagger"
|
||||
func={() => {
|
||||
setOpenApiModal(true)
|
||||
}}
|
||||
@@ -1806,7 +1831,7 @@ const Apps = (props) => {
|
||||
}}>
|
||||
<TextField
|
||||
fullWidth
|
||||
style={{ backgroundColor: theme.palette.inputColor, borderRadius: theme.palette.borderRadius, maxWidth: leftBarSize - 20, }}
|
||||
style={{ backgroundColor: theme.palette.inputColor, borderRadius: theme.palette?.borderRadius, maxWidth: leftBarSize - 20, }}
|
||||
InputProps={{
|
||||
style: {
|
||||
color: "white",
|
||||
@@ -1845,9 +1870,12 @@ const Apps = (props) => {
|
||||
)
|
||||
}
|
||||
|
||||
const activateApp = (appid, refresh) => {
|
||||
const activateApp = (appid, refresh, deactivate) => {
|
||||
const appExists = userdata.active_apps !== undefined && userdata.active_apps !== null && userdata.active_apps.includes(appid)
|
||||
const url = appExists ? `${globalUrl}/api/v1/apps/${appid}/deactivate` : `${globalUrl}/api/v1/apps/${appid}/activate`
|
||||
const url = deactivate === true ?
|
||||
`${globalUrl}/api/v1/apps/${appid}/deactivate`
|
||||
:
|
||||
appExists ? `${globalUrl}/api/v1/apps/${appid}/deactivate` : `${globalUrl}/api/v1/apps/${appid}/activate`
|
||||
|
||||
fetch(url, {
|
||||
method: 'GET',
|
||||
@@ -1876,7 +1904,7 @@ const Apps = (props) => {
|
||||
if (appExists) {
|
||||
toast("App deactivated for your organization! Existing workflows with the app will continue to work.")
|
||||
} else {
|
||||
toast("App activated for your organization!")
|
||||
toast("App activation changed for your organization!")
|
||||
}
|
||||
|
||||
if (refresh === true) {
|
||||
@@ -2021,8 +2049,6 @@ const Apps = (props) => {
|
||||
userToken: userdata === undefined || userdata === null || userdata.id === undefined ? "unauthenticated" : userdata.id,
|
||||
}
|
||||
])
|
||||
} else {
|
||||
console.log("No query to handle when activating")
|
||||
}
|
||||
|
||||
activateApp(hit.objectID, true)
|
||||
@@ -2161,7 +2187,7 @@ const Apps = (props) => {
|
||||
</div>
|
||||
<div style={{ height: 50 }}>
|
||||
<TextField
|
||||
style={{ backgroundColor: inputColor, borderRadius: theme.palette.borderRadius, }}
|
||||
style={{ backgroundColor: inputColor, borderRadius: theme.palette?.borderRadius, }}
|
||||
InputProps={{
|
||||
style: {
|
||||
},
|
||||
@@ -2212,7 +2238,7 @@ const Apps = (props) => {
|
||||
minWidth: viewWidth,
|
||||
maxWidth: viewWidth,
|
||||
color: "white",
|
||||
borderRadius: theme.palette.borderRadius,
|
||||
borderRadius: theme.palette?.borderRadius,
|
||||
//display: "flex",
|
||||
marginBottom: 10,
|
||||
overflow: "hidden",
|
||||
@@ -2349,6 +2375,7 @@ const Apps = (props) => {
|
||||
toast("Hotloading apps from location in .env");
|
||||
setIsLoading(true);
|
||||
fetch(globalUrl + "/api/v1/apps/run_hotload", {
|
||||
method: "POST",
|
||||
mode: "cors",
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
@@ -2556,7 +2583,8 @@ const Apps = (props) => {
|
||||
}
|
||||
|
||||
//fetch("http://localhost:8080/doc_to_openapi", {
|
||||
fetch("https://doc-to-openapi-stbuwivzoq-nw.a.run.app/doc_to_openapi", {
|
||||
//fetch("https://doc-to-openapi-stbuwivzoq-nw.a.run.app/doc_to_openapi", {
|
||||
fetch("https://doc-to-openapi-stbuwivzoq-nw.a.run.app/api/v1/doc_to_openapi", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Accept": "application/json",
|
||||
@@ -2576,12 +2604,14 @@ const Apps = (props) => {
|
||||
})
|
||||
.then((responseJson) => {
|
||||
// Check if openapi or swagger in string of the json
|
||||
var parsedtext = responseJson
|
||||
try {
|
||||
const parsedtext = JSON.stringify(responseJson);
|
||||
parsedtext = JSON.stringify(responseJson);
|
||||
if (parsedtext.indexOf("openapi") === -1 && parsedtext.indexOf("swagger") === -1) {
|
||||
setValidation(false);
|
||||
setOpenApiError("Error in generation: "+parsedtext);
|
||||
return;
|
||||
setValidation(false)
|
||||
setOpenApiError("Error in generation: "+parsedtext)
|
||||
|
||||
return
|
||||
}
|
||||
} catch (e) {
|
||||
setValidation(false);
|
||||
@@ -2590,7 +2620,7 @@ const Apps = (props) => {
|
||||
}
|
||||
|
||||
console.log("Validating response!");
|
||||
validateOpenApi(responseJson);
|
||||
validateOpenApi(parsedtext)
|
||||
})
|
||||
.catch((error) => {
|
||||
setValidation(false);
|
||||
@@ -2679,10 +2709,11 @@ const Apps = (props) => {
|
||||
body: openApidata,
|
||||
credentials: "include",
|
||||
})
|
||||
.then((response) => {
|
||||
.then((response) => {
|
||||
|
||||
setValidation(false);
|
||||
return response.json();
|
||||
})
|
||||
})
|
||||
.then((responseJson) => {
|
||||
if (responseJson.success) {
|
||||
setAppValidation(responseJson.id);
|
||||
@@ -2701,8 +2732,16 @@ const Apps = (props) => {
|
||||
};
|
||||
|
||||
const redirectOpenApi = () => {
|
||||
if (appValidation === undefined || appValidation === null || appValidation.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
toast.success("Successfully validated OpenAPI. Redirecting to app creation. Remember to save the app to be able to use it.", {
|
||||
// Disable autoclose
|
||||
autoClose: 10000,
|
||||
})
|
||||
navigate(`/apps/new?id=${appValidation}`)
|
||||
};
|
||||
}
|
||||
|
||||
const handleGithubValidation = (forceUpdate) => {
|
||||
getSpecificApps(openApi, forceUpdate);
|
||||
@@ -2962,10 +3001,12 @@ const Apps = (props) => {
|
||||
}}
|
||||
PaperProps={{
|
||||
style: {
|
||||
backgroundColor: surfaceColor,
|
||||
backgroundColor: theme.palette.platformColor,
|
||||
borderRadius: theme.palette?.borderRadius,
|
||||
color: "white",
|
||||
minWidth: "800px",
|
||||
minHeight: "320px",
|
||||
padding: 50,
|
||||
},
|
||||
}}
|
||||
>
|
||||
@@ -2997,14 +3038,14 @@ const Apps = (props) => {
|
||||
height: "50px",
|
||||
}}
|
||||
variant="contained"
|
||||
disabled={openApi.length === 0 || appValidation.length > 0}
|
||||
disabled={openApi.length === 0 || appValidation.length > 0 || validation}
|
||||
color="primary"
|
||||
onClick={() => {
|
||||
setOpenApiError("");
|
||||
validateDocumentationUrl();
|
||||
}}
|
||||
>
|
||||
Validate
|
||||
Generate
|
||||
</Button>
|
||||
),
|
||||
}}
|
||||
@@ -3019,22 +3060,6 @@ const Apps = (props) => {
|
||||
placeholder="API Documentation URL"
|
||||
fullWidth
|
||||
/>
|
||||
<p>Or upload document with the content (coming soon)</p>
|
||||
<input
|
||||
hidden
|
||||
type="file"
|
||||
ref={upload}
|
||||
multiple={false}
|
||||
onChange={uploadFileDocumentation}
|
||||
/>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
disabled
|
||||
onClick={() => upload.current.click()}
|
||||
>
|
||||
Upload
|
||||
</Button>
|
||||
{errorText}
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
@@ -3053,17 +3078,6 @@ const Apps = (props) => {
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
variant="contained"
|
||||
style={{ borderRadius: "0px" }}
|
||||
disabled={appValidation.length === 0}
|
||||
onClick={() => {
|
||||
redirectOpenApi();
|
||||
}}
|
||||
color="primary"
|
||||
>
|
||||
Continue
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</FormControl>
|
||||
</Dialog>
|
||||
@@ -3079,10 +3093,12 @@ const Apps = (props) => {
|
||||
}}
|
||||
PaperProps={{
|
||||
style: {
|
||||
backgroundColor: surfaceColor,
|
||||
backgroundColor: theme.palette.platformColor,
|
||||
borderRadius: theme.palette?.borderRadius,
|
||||
color: "white",
|
||||
minWidth: "800px",
|
||||
minHeight: "320px",
|
||||
padding: 50,
|
||||
},
|
||||
}}
|
||||
>
|
||||
@@ -3189,22 +3205,48 @@ const Apps = (props) => {
|
||||
</Dialog>
|
||||
) : null;
|
||||
|
||||
const loadedCheck =
|
||||
isLoaded && !firstrequest ? (
|
||||
<div>
|
||||
{appView}
|
||||
{modalView}
|
||||
{publishModal}
|
||||
{generateAppView}
|
||||
{appsModalLoad}
|
||||
{deleteModal}
|
||||
</div>
|
||||
) : (
|
||||
<div></div>
|
||||
);
|
||||
const loadedCheck = isLoaded && !firstrequest ? (
|
||||
<SidebarAdjustWrapper userdata={userdata}>
|
||||
<AppsWrapper
|
||||
userdata={userdata}
|
||||
appView={appView}
|
||||
modalView={modalView}
|
||||
publishModal={publishModal}
|
||||
generateAppView={generateAppView}
|
||||
appsModalLoad={appsModalLoad}
|
||||
deleteModal={deleteModal}
|
||||
/>
|
||||
</SidebarAdjustWrapper>
|
||||
) : (
|
||||
<div></div>
|
||||
);
|
||||
|
||||
// Maybe use gridview or something, idk
|
||||
return loadedCheck;
|
||||
};
|
||||
|
||||
export default Apps;
|
||||
|
||||
|
||||
const AppsWrapper = memo(({ appView, modalView, userdata, publishModal, generateAppView, appsModalLoad, deleteModal }) => (
|
||||
<>
|
||||
{appView}
|
||||
{modalView}
|
||||
{publishModal}
|
||||
{generateAppView}
|
||||
{appsModalLoad}
|
||||
{deleteModal}
|
||||
</>
|
||||
));
|
||||
|
||||
|
||||
const SidebarAdjustWrapper = memo(({ children }) => {
|
||||
|
||||
const {leftSideBarOpenByClick } = useContext(Context)
|
||||
|
||||
|
||||
return (
|
||||
<div style={{ marginLeft: leftSideBarOpenByClick ? 250 : 80, transition: 'margin-left 0.3s ease' }}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,467 @@
|
||||
import React, { useState, useRef, useEffect } from 'react';
|
||||
import { toast } from 'react-toastify';
|
||||
import { Button, Box, Typography, Paper, Toolbar, Divider, CircularProgress } from '@mui/material';
|
||||
|
||||
import {
|
||||
PlayArrow as PlayArrowIcon,
|
||||
Save as SaveIcon,
|
||||
History as HistoryIcon
|
||||
} from '@mui/icons-material';
|
||||
|
||||
import ExecutionPanel from '../components/ExecutionPanel.jsx';
|
||||
import ShuffleCodeEditor from "../components/ShuffleCodeEditor1.jsx";
|
||||
|
||||
const CodeWorkflow = (defaultprops) => {
|
||||
const { serverside, userdata, globalUrl, isLoaded, isLoggedIn, surfaceColor, inputColor, ...props } = defaultprops;
|
||||
|
||||
const [workflow, setWorkflow] = useState({});
|
||||
|
||||
const [showExecutions, setShowExecutions] = useState(false);
|
||||
// In CodeWorkflow, add this state
|
||||
const [panelHeight, setPanelHeight] = useState(400);
|
||||
const [executions, setExecutions] = useState([]);
|
||||
const [currentExecution, setCurrentExecution] = useState(null);
|
||||
const [mainAction, setMainAction] = useState(null);
|
||||
const editorRef = useRef(null);
|
||||
const [apiKey, setApiKey] = useState("");
|
||||
|
||||
const [editorData, setEditorData] = React.useState({
|
||||
"name": "",
|
||||
"value": "",
|
||||
"field_number": -1,
|
||||
"actionlist": [],
|
||||
"field_id": "",
|
||||
})
|
||||
|
||||
const getSettings = async () => {
|
||||
try {
|
||||
const response = await fetch(`${globalUrl}/api/v1/getsettings`, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Accept: "application/json",
|
||||
},
|
||||
credentials: "include",
|
||||
});
|
||||
|
||||
if (response.status !== 200) {
|
||||
console.log("Status not 200 for getsettings :O!");
|
||||
if (response.status >= 500) {
|
||||
toast("Something went wrong while loading the settings. Please reload.")
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
const responseJson = await response.json();
|
||||
setApiKey(responseJson.apikey);
|
||||
console.log("API Key: ", responseJson.apikey);
|
||||
|
||||
return responseJson.apikey;
|
||||
} catch (error) {
|
||||
console.log("Get settings error: ", error.toString());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
// Calculate editor height based on execution panel visibility
|
||||
const getEditorHeight = () => {
|
||||
return `calc(100vh - ${showExecutions ? `${panelHeight + 40}px` : '40px'})`;
|
||||
};
|
||||
|
||||
let url = window.location.pathname;
|
||||
const workflowId = url.split("/")[2];
|
||||
|
||||
const getWorkflow = async (workflow_id, sourcenode) => {
|
||||
try {
|
||||
const response = await fetch(`${globalUrl}/api/v1/workflows/${workflow_id}`, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Accept: "application/json",
|
||||
},
|
||||
credentials: "include",
|
||||
});
|
||||
|
||||
if (response.status !== 200) {
|
||||
console.log("Status not 200 for workflows :O!");
|
||||
if (response.status >= 500) {
|
||||
toast("Something went wrong while loading the workflow. Please reload.")
|
||||
}
|
||||
}
|
||||
|
||||
const responseJson = await response.json();
|
||||
setWorkflow(responseJson);
|
||||
|
||||
for (let i = 0; i < responseJson.actions.length; i++) {
|
||||
if (responseJson.actions[i].app_id === "3e320a20966d33c9b7e6790b2705f0bf") {
|
||||
console.log("Setting code to: ", responseJson.actions[i].parameters[0].value);
|
||||
setCode(responseJson.actions[i].parameters[0].value);
|
||||
setMainAction(responseJson.actions[i]);
|
||||
|
||||
if (responseJson.actions[i].parameters[0].value.length === 0) {
|
||||
// fetch API key of the user
|
||||
const result = await getSettings();
|
||||
|
||||
console.log("accessible result: ", result);
|
||||
|
||||
// await setCode(`
|
||||
// from shufflepy import Shuffle
|
||||
|
||||
// shuffle = Shuffle(
|
||||
// "${result}",
|
||||
// url='https://shuffler.io',
|
||||
// )
|
||||
// `
|
||||
// );
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.log("Get workflows error: ", error.toString());
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
getWorkflow(workflowId);
|
||||
}, []);
|
||||
|
||||
// In CodeWorkflow component, add this effect:
|
||||
useEffect(() => {
|
||||
if (workflow.id) {
|
||||
// Check URL for execution_id parameter
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const executionId = urlParams.get('execution_id');
|
||||
if (executionId) {
|
||||
setShowExecutions(true); // Show the panel if execution_id is present
|
||||
}
|
||||
}
|
||||
}, [workflow]);
|
||||
|
||||
const [code, setCode] = useState("");
|
||||
const [testResult, setTestResult] = useState("");
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState("");
|
||||
|
||||
const saveLatestWorkflow = () => {
|
||||
let newWorkflow = workflow;
|
||||
|
||||
console.log("Workflow actions: ", newWorkflow.actions);
|
||||
|
||||
// find the latest "Shuffle tools fork" node
|
||||
for (let i = 0; i < newWorkflow.actions.length; i++) {
|
||||
console.log("Actios: ", newWorkflow.actions[i]);
|
||||
if (newWorkflow.actions[i].app_id === "3e320a20966d33c9b7e6790b2705f0bf") {
|
||||
// update the code
|
||||
console.log("Updating code: ", code);
|
||||
newWorkflow.actions[i].parameters[0].value = code;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
fetch(`${globalUrl}/api/v1/workflows/${workflow.id}`, {
|
||||
method: "PUT",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Accept: "application/json",
|
||||
},
|
||||
credentials: "include",
|
||||
body: JSON.stringify(newWorkflow),
|
||||
}).then((response) => {
|
||||
if (response.status !== 200) {
|
||||
console.log("Status not 200 for workflows :O!");
|
||||
toast("Something went wrong while saving the workflow. Please try again.", { type: "error" });
|
||||
} else if (response.status === 200) {
|
||||
toast("Workflow saved successfully!");
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const getParents = async () => {
|
||||
return [
|
||||
{
|
||||
"label": "Execution Argument",
|
||||
"type": "INTERNAL"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
const handleRunCode = async () => {
|
||||
saveLatestWorkflow();
|
||||
setLoading(true);
|
||||
setError("");
|
||||
|
||||
let start_node = "";
|
||||
|
||||
for (let i = 0; i < workflow.actions.length; i++) {
|
||||
if (workflow.actions[i].isStartNode) {
|
||||
start_node = workflow.actions[i].id;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`${globalUrl}/api/v1/workflows/${workflow.id}/execute`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Accept: "application/json",
|
||||
},
|
||||
credentials: "include",
|
||||
body: JSON.stringify({
|
||||
"start": start_node,
|
||||
"execution_arguments": "",
|
||||
}),
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const responseJson = await response.json();
|
||||
const newExecution = {
|
||||
execution_id: responseJson.execution_id,
|
||||
authorization: responseJson.authorization,
|
||||
status: 'EXECUTING',
|
||||
started_at: new Date().toISOString()
|
||||
};
|
||||
setShowExecutions(true);
|
||||
setCurrentExecution(newExecution);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.log("Error: ", error);
|
||||
setError(error.toString());
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const handleSaveCode = async () => {
|
||||
saveLatestWorkflow();
|
||||
};
|
||||
|
||||
const handleEditorDidMount = (editor, monaco) => {
|
||||
if (monaco.languages && monaco.languages.python) {
|
||||
if (monaco.languages.python.pythonDefaults) {
|
||||
monaco.languages.python.pythonDefaults.setCompilerOptions({
|
||||
target: monaco.languages.typescript.ScriptTarget.ES2020,
|
||||
allowNonTsExtensions: true
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
monaco.languages.setLanguageConfiguration('python', {
|
||||
autoClosingPairs: [
|
||||
{ open: '{', close: '}' },
|
||||
{ open: '[', close: ']' },
|
||||
{ open: '(', close: ')' },
|
||||
{ open: '"', close: '"' },
|
||||
{ open: "'", close: "'" }
|
||||
],
|
||||
surroundingPairs: [
|
||||
{ open: '{', close: '}' },
|
||||
{ open: '[', close: ']' },
|
||||
{ open: '(', close: ')' },
|
||||
{ open: '"', close: '"' },
|
||||
{ open: "'", close: "'" }
|
||||
]
|
||||
});
|
||||
};
|
||||
|
||||
const handleEditorChange = (value) => {
|
||||
setCode(value);
|
||||
};
|
||||
|
||||
return (
|
||||
<Box sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
height: '100vh', // Set to full viewport height
|
||||
backgroundColor: '#252526',
|
||||
marginLeft: '100px',
|
||||
}}>
|
||||
{/* IDE-like toolbar */}
|
||||
<Toolbar
|
||||
variant="dense"
|
||||
sx={{
|
||||
backgroundColor: '#333333',
|
||||
borderBottom: '1px solid #454545',
|
||||
minHeight: '40px',
|
||||
px: 1,
|
||||
flex: '0 0 auto' // Prevent toolbar from flexing
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
variant="subtitle2"
|
||||
sx={{
|
||||
color: '#CCCCCC',
|
||||
marginRight: 2
|
||||
}}
|
||||
>
|
||||
{workflow.name || 'Untitled Workflow'}
|
||||
</Typography>
|
||||
<Box sx={{ display: 'flex', gap: 1 }}>
|
||||
<Button
|
||||
size="small"
|
||||
variant="contained"
|
||||
onClick={handleRunCode}
|
||||
disabled={loading}
|
||||
startIcon={<PlayArrowIcon />}
|
||||
sx={{
|
||||
backgroundColor: '#0098FF',
|
||||
'&:hover': {
|
||||
backgroundColor: '#0076CE'
|
||||
},
|
||||
textTransform: 'none',
|
||||
minWidth: '80px'
|
||||
}}
|
||||
>
|
||||
{loading ? 'Running...' : 'Run'}
|
||||
</Button>
|
||||
<Button
|
||||
size="small"
|
||||
variant="outlined"
|
||||
startIcon={<HistoryIcon />}
|
||||
onClick={() => setShowExecutions(true)}
|
||||
sx={{
|
||||
color: '#CCCCCC',
|
||||
borderColor: '#454545',
|
||||
'&:hover': {
|
||||
borderColor: '#666666',
|
||||
backgroundColor: 'rgba(255,255,255,0.1)'
|
||||
},
|
||||
textTransform: 'none'
|
||||
}}
|
||||
>
|
||||
Show Executions
|
||||
</Button>
|
||||
<Button
|
||||
size="small"
|
||||
variant="outlined"
|
||||
onClick={handleSaveCode}
|
||||
startIcon={<SaveIcon />}
|
||||
sx={{
|
||||
color: '#CCCCCC',
|
||||
borderColor: '#454545',
|
||||
'&:hover': {
|
||||
borderColor: '#666666',
|
||||
backgroundColor: 'rgba(255,255,255,0.1)'
|
||||
},
|
||||
textTransform: 'none',
|
||||
marginLeft: 1
|
||||
}}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
</Box>
|
||||
</Toolbar>
|
||||
|
||||
{/* Editor container */}
|
||||
<Box sx={{
|
||||
height: getEditorHeight(),
|
||||
// flex: '1 1 auto', // Allow editor to grow and shrink
|
||||
// minHeight: 0, // Important for flex child
|
||||
transition: 'height 0.2s ease-in-out',
|
||||
position: 'relative' // Needed for Editor's absolute positioning
|
||||
}}>
|
||||
{workflow && mainAction ? (
|
||||
<ShuffleCodeEditor
|
||||
expansionModalOpen={true}
|
||||
setExpansionModalOpen={true}
|
||||
isCloud={true}
|
||||
globalUrl={globalUrl}
|
||||
workflowExecutions={executions}
|
||||
getParents={getParents}
|
||||
selectedAction={mainAction}
|
||||
aiSubmit={() => { }}
|
||||
toolsAppId={mainAction.app_id}
|
||||
codedata={code}
|
||||
setcodedata={setCode}
|
||||
parameterName={editorData.name}
|
||||
fieldCount={editorData.field_number}
|
||||
actionlist={editorData.actionlist}
|
||||
fieldname={editorData.field_id}
|
||||
changeActionParameterCodeMirror={() => { }}
|
||||
activeDialog={() => { }}
|
||||
setActiveDialog={() => { }}
|
||||
fullScreenMode={true}
|
||||
/>
|
||||
) : (
|
||||
<Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center', height: '100%' }}>
|
||||
<CircularProgress />
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* <Editor
|
||||
height="100%" // Take full height of container
|
||||
language="python"
|
||||
theme="vs-dark"
|
||||
value={code}
|
||||
onChange={handleEditorChange}
|
||||
onMount={handleEditorDidMount}
|
||||
options={{
|
||||
minimap: { enabled: false },
|
||||
fontSize: 14,
|
||||
lineHeight: 24,
|
||||
fontFamily: "'JetBrains Mono', Consolas, monospace",
|
||||
fontLigatures: true,
|
||||
scrollBeyondLastLine: false,
|
||||
automaticLayout: true,
|
||||
contextmenu: false,
|
||||
wordWrap: 'on',
|
||||
snippetSuggestions: 'top',
|
||||
quickSuggestions: {
|
||||
other: true,
|
||||
comments: false,
|
||||
strings: false
|
||||
},
|
||||
intellisenseOptions: {
|
||||
includeAutoCompletionsFromLanguageFile: true,
|
||||
maxCompletionItems: 10
|
||||
}
|
||||
}}
|
||||
/> */}
|
||||
</Box>
|
||||
|
||||
{/* Create an input element called "copy_element_shuffle" that is not visible */}
|
||||
<input type="text" id="copy_element_shuffle" style={{ position: "absolute", left: "-9999px" }} />
|
||||
|
||||
{/* Results Panel */}
|
||||
{(error || testResult) && (
|
||||
<Box sx={{
|
||||
height: '200px',
|
||||
backgroundColor: '#1E1E1E',
|
||||
borderTop: '1px solid #454545',
|
||||
overflow: 'auto',
|
||||
flex: '0 0 auto' // Prevent panel from flexing
|
||||
}}>
|
||||
{error && (
|
||||
<Box sx={{ p: 2, color: '#f44336' }}>
|
||||
<Typography variant="subtitle2">Error:</Typography>
|
||||
<pre style={{ margin: 0 }}>{error}</pre>
|
||||
</Box>
|
||||
)}
|
||||
{testResult && (
|
||||
<Box sx={{ p: 2, color: '#4caf50' }}>
|
||||
<Typography variant="subtitle2">Test Result:</Typography>
|
||||
<pre style={{ margin: 0 }}>{JSON.stringify(testResult, null, 2)}</pre>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* In CodeWorkflow component */}
|
||||
{showExecutions && (
|
||||
<ExecutionPanel
|
||||
workflow={workflow}
|
||||
globalUrl={globalUrl}
|
||||
onClose={() => setShowExecutions(false)}
|
||||
currentExecution={currentExecution}
|
||||
height={panelHeight}
|
||||
onHeightChange={setPanelHeight} // Add this prop to handle height updates
|
||||
mainAction={mainAction}
|
||||
/>
|
||||
)}
|
||||
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default CodeWorkflow;
|
||||
@@ -11,6 +11,7 @@ import { useNavigate, Link, useParams } from "react-router-dom";
|
||||
//import { useAlert
|
||||
import { ToastContainer, toast } from "react-toastify"
|
||||
import Draggable from "react-draggable";
|
||||
import DashboardBarchart, { LoadStats } from '../components/DashboardBarchart.jsx';
|
||||
|
||||
import {
|
||||
Autocomplete,
|
||||
@@ -18,6 +19,8 @@ import {
|
||||
TextField,
|
||||
IconButton,
|
||||
Button,
|
||||
Select,
|
||||
MenuItem,
|
||||
Typography,
|
||||
Grid,
|
||||
Paper,
|
||||
@@ -146,28 +149,8 @@ const inputdata = [
|
||||
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}}>
|
||||
@@ -211,11 +194,10 @@ const LineChartWrapper = ({keys, height, width}) => {
|
||||
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",}}>
|
||||
<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>
|
||||
@@ -299,7 +281,7 @@ const RadialChart = ({keys, setSelectedCategory}) => {
|
||||
}}
|
||||
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",}}>
|
||||
<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>
|
||||
@@ -350,12 +332,47 @@ const Dashboard = (props) => {
|
||||
const [frameworkData, setFrameworkData] = useState(undefined);
|
||||
|
||||
const [widgetData, setWidgetData] = useState([]);
|
||||
const [newWidgetData, setNewWidgetData] = useState([]);
|
||||
|
||||
const [, setUpdate] = useState(0);
|
||||
|
||||
let navigate = useNavigate();
|
||||
const isCloud =
|
||||
window.location.host === "localhost:3002" ||
|
||||
window.location.host === "shuffler.io";
|
||||
const isCloud = window.location.host === "localhost:3002" || window.location.host === "shuffler.io";
|
||||
|
||||
const path = window.location.pathname
|
||||
if (path !== undefined && path !== null) {
|
||||
if (path.includes("/automate")) {
|
||||
navigate("/usecases")
|
||||
}
|
||||
|
||||
if (path.includes("/security")) {
|
||||
navigate("/forms")
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
// Look for the path. If it is automation, go to usecases
|
||||
// Handles redirect
|
||||
|
||||
const widgetnames = ["app_executions_cloud"]
|
||||
for (let widgetkey in widgetnames) {
|
||||
const widgetName = widgetnames[widgetkey]
|
||||
|
||||
console.log("NAME: ", widgetName)
|
||||
|
||||
const resp = LoadStats(globalUrl, widgetName)
|
||||
if (resp !== undefined) {
|
||||
resp.then((data) => {
|
||||
console.log("Got data in parent: ", data)
|
||||
if (data === undefined) {
|
||||
} else {
|
||||
newWidgetData.push(data)
|
||||
setNewWidgetData(newWidgetData)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedUsecaseCategory.length === 0) {
|
||||
@@ -368,6 +385,7 @@ const Dashboard = (props) => {
|
||||
}
|
||||
}, [selectedUsecaseCategory])
|
||||
|
||||
|
||||
const checkSelectedParams = () => {
|
||||
const urlSearchParams = new URLSearchParams(window.location.search)
|
||||
const params = Object.fromEntries(urlSearchParams.entries())
|
||||
@@ -409,23 +427,22 @@ const Dashboard = (props) => {
|
||||
}, [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!");
|
||||
}
|
||||
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)
|
||||
return response.json();
|
||||
})
|
||||
.then((responseJson) => {
|
||||
if (responseJson.success === false) {
|
||||
if (responseJson.reason !== undefined) {
|
||||
//toast("Failed loading: " + responseJson.reason)
|
||||
@@ -441,14 +458,12 @@ const Dashboard = (props) => {
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
})
|
||||
@@ -480,7 +495,7 @@ const Dashboard = (props) => {
|
||||
useEffect(() => {
|
||||
getWidget("main", "Overall")
|
||||
getWidget("main", "Overall2")
|
||||
}, []);
|
||||
}, [])
|
||||
|
||||
const fetchdata = (stats_id) => {
|
||||
fetch(globalUrl + "/api/v1/stats/" + stats_id, {
|
||||
@@ -646,7 +661,6 @@ const Dashboard = (props) => {
|
||||
stats["workflow_executions"].data !== undefined
|
||||
) {
|
||||
setStatsRan(true);
|
||||
//console.log("NEW DATA?: ", stats)
|
||||
console.log("SET WORKFLOW: ", stats["workflow_executions"]);
|
||||
//var curday = startDate.getDate()
|
||||
|
||||
@@ -729,6 +743,100 @@ const Dashboard = (props) => {
|
||||
</div>
|
||||
) : null;
|
||||
|
||||
const WidgetController = (props) => {
|
||||
const { data, index, availableStats, } = props
|
||||
const [hovering, setHovering] = useState(false)
|
||||
|
||||
const newname = data.key !== undefined ? data.key.replaceAll("_", " ") : ""
|
||||
|
||||
console.log("KEYDATA: ", data)
|
||||
|
||||
const loadNewStats = (newkey) => {
|
||||
const resp = LoadStats(globalUrl, newkey)
|
||||
if (resp !== undefined) {
|
||||
resp.then((respdata) => {
|
||||
if (respdata === undefined || respdata === null) {
|
||||
toast("Failed to laod data. Please try again, or contact support@shuffler.io if this persists.")
|
||||
} else {
|
||||
newWidgetData[index] = respdata
|
||||
setNewWidgetData(newWidgetData)
|
||||
|
||||
setUpdate(Math.random())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Draggable>
|
||||
<Paper
|
||||
style={{
|
||||
height: "100%", width: "100%", maxWidth: 500, margin: 15, padding: "15px 15px 15px 15px", textAlign: "left",
|
||||
backgroundColor: hovering ? theme.palette.inputColor : theme.palette.backgroundColor,
|
||||
cursor: hovering ? "pointer" : "default",
|
||||
}}
|
||||
onMouseEnter={() => {
|
||||
setHovering(true)
|
||||
}}
|
||||
onMouseLeave={() => {
|
||||
setHovering(false)
|
||||
}}
|
||||
>
|
||||
<div style={{display: "flex", justifyContent: "space-between", alignItems: "center",}}>
|
||||
<Typography variant="h6">
|
||||
{newname}
|
||||
</Typography>
|
||||
|
||||
{data.available_keys === undefined || data.available_keys === null || data.available_keys.length === 0 ? null :
|
||||
<Select
|
||||
MenuProps={{
|
||||
disableScrollLock: true,
|
||||
}}
|
||||
labelId="Response Action"
|
||||
value={data.key}
|
||||
SelectDisplayProps={{
|
||||
style: {
|
||||
},
|
||||
}}
|
||||
fullWidth
|
||||
onChange={(e) => {
|
||||
loadNewStats(e.target.value)
|
||||
}}
|
||||
style={{
|
||||
backgroundColor: theme.palette.inputColor,
|
||||
color: "white",
|
||||
height: 40,
|
||||
maxWidth: 150,
|
||||
borderRadius: theme.palette?.borderRadius,
|
||||
}}
|
||||
>
|
||||
{data.available_keys.map((foundKey, index) => {
|
||||
const parsedKeyName = foundKey.replaceAll("_", " ")
|
||||
|
||||
return (
|
||||
<MenuItem
|
||||
style={{
|
||||
backgroundColor: theme.palette.inputColor,
|
||||
color: "white",
|
||||
}}
|
||||
value={foundKey}
|
||||
>
|
||||
<em>{parsedKeyName}</em>
|
||||
</MenuItem>
|
||||
)
|
||||
})}
|
||||
</Select>
|
||||
}
|
||||
</div>
|
||||
<DashboardBarchart
|
||||
timelineData={data}
|
||||
height={50}
|
||||
/>
|
||||
</Paper>
|
||||
</Draggable>
|
||||
)
|
||||
}
|
||||
|
||||
const data = (
|
||||
<div className="content" style={{width: 1000, margin: "auto", paddingBottom: 200, textAlign: "center",}}>
|
||||
<div style={{width: 500, margin: "auto"}}>
|
||||
@@ -739,18 +847,31 @@ const Dashboard = (props) => {
|
||||
: null}
|
||||
</div>
|
||||
|
||||
{widgetData === undefined || widgetData === null || widgetData === [] || widgetData.length === 0 ? null :
|
||||
{/*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>
|
||||
*/}
|
||||
|
||||
{newWidgetData === undefined || newWidgetData === null || newWidgetData === [] ? null :
|
||||
newWidgetData.map((data, index) => {
|
||||
|
||||
return (
|
||||
<WidgetController
|
||||
key={index}
|
||||
index={index}
|
||||
data={data}
|
||||
/>
|
||||
)
|
||||
})
|
||||
}
|
||||
</div>
|
||||
);
|
||||
|
||||
const dataWrapper = (
|
||||
<div style={{ maxWidth: 1366, margin: "auto" }}>{data}</div>
|
||||
<div style={{ maxWidth: 1366, margin: "auto", paddingTop: 10, }}>{data}</div>
|
||||
);
|
||||
|
||||
return dataWrapper;
|
||||
|
||||
@@ -0,0 +1,210 @@
|
||||
import React, { useState } from "react";
|
||||
import {
|
||||
Container,
|
||||
Box,
|
||||
TextField,
|
||||
Switch,
|
||||
Typography,
|
||||
Button,
|
||||
CircularProgress,
|
||||
Paper,
|
||||
} from "@mui/material";
|
||||
|
||||
import { toast } from "react-toastify";
|
||||
import theme from '../theme.jsx';
|
||||
import DetectionRuleCard from "../components/DetectionRuleCard.jsx";
|
||||
|
||||
const handleDirectoryChange = (folderDisabled, setFolderDisabled, globalUrl, isTenzirActive) => {
|
||||
|
||||
if (!isTenzirActive) {
|
||||
toast("connect to siem first for global enable/disable to work");
|
||||
return;
|
||||
}
|
||||
|
||||
const action = folderDisabled ? "enable_folder" : "disable_folder";
|
||||
const url = `${globalUrl}/api/v1/detection/${action}`;
|
||||
|
||||
fetch(url, {
|
||||
method: "PUT",
|
||||
credentials: "include",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
})
|
||||
.then((response) =>
|
||||
response.json().then((responseJson) => {
|
||||
if (responseJson["success"] === true) {
|
||||
if (action === "enable_folder") setFolderDisabled(false);
|
||||
else setFolderDisabled(true);
|
||||
} else {
|
||||
//toast(`failed to disable rule`);
|
||||
}
|
||||
})
|
||||
)
|
||||
.catch((error) => {
|
||||
console.log(`Error in ${action} the rule: `, error);
|
||||
toast(`An error occurred while ${action} the rule`);
|
||||
});
|
||||
};
|
||||
|
||||
const Detection = (props) => {
|
||||
const { globalUrl, ruleInfo, folderDisabled, setFolderDisabled, isTenzirActive } = props;
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const handleConnectClick = () => {
|
||||
if (!isTenzirActive) {
|
||||
setLoading(true);
|
||||
const url = `${globalUrl}/api/v1/detection/siem/connect`;
|
||||
|
||||
fetch(url, {
|
||||
method: "GET",
|
||||
credentials: "include",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
})
|
||||
.then((response) =>
|
||||
response.json().then((responseJson) => {
|
||||
if (responseJson["success"] === true) {
|
||||
setTimeout(() => {
|
||||
setLoading(false);
|
||||
window.location.reload();
|
||||
}, 15000);
|
||||
} else {
|
||||
setLoading(false);
|
||||
toast("Failed to connect to SIEM");
|
||||
}
|
||||
})
|
||||
)
|
||||
.catch((error) => {
|
||||
setLoading(false);
|
||||
console.log(`Error in connecting to SIEM: `, error);
|
||||
toast("An error occurred while connecting to SIEM");
|
||||
});
|
||||
} else {
|
||||
console.log("Already connected to SIEM");
|
||||
}
|
||||
};
|
||||
|
||||
const filteredRules = ruleInfo?.filter((rule) =>
|
||||
rule.title.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
rule.description.toLowerCase().includes(searchQuery.toLowerCase())
|
||||
);
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<Paper
|
||||
style={{
|
||||
marginTop: 50,
|
||||
width: "100%",
|
||||
padding: 50,
|
||||
backgroundColor: theme.palette.backgroundColor,
|
||||
borderRadius: theme.palette.borderRadius,
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
mb: 2,
|
||||
}}
|
||||
>
|
||||
<Typography variant="h6" component="div">
|
||||
Sigma Detection Rules
|
||||
</Typography>
|
||||
<Button
|
||||
variant="contained"
|
||||
onClick={handleConnectClick}
|
||||
disabled={loading} // Disable the button while loading
|
||||
color={isTenzirActive ? "primary" : "secondary"}
|
||||
style={{ }}
|
||||
>
|
||||
{loading ? <CircularProgress size={24} /> : isTenzirActive ? "Connected to siem" : "Connect to siem"}
|
||||
</Button>
|
||||
</Box>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
mb: 2,
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
}}
|
||||
>
|
||||
<TextField
|
||||
label="Search rules"
|
||||
variant="outlined"
|
||||
size="small"
|
||||
sx={{ mr: 2 }}
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
/>
|
||||
{/* <Button
|
||||
color="primary"
|
||||
variant="contained"
|
||||
onClick={() => uploadRef.current.click()}
|
||||
>
|
||||
<PublishIcon /> Upload sigma file
|
||||
</Button>
|
||||
<input
|
||||
hidden
|
||||
type="file"
|
||||
multiple
|
||||
ref={uploadRef}
|
||||
onChange={(event) => {
|
||||
uploadFiles(event.target.files);
|
||||
}}
|
||||
/> */}
|
||||
</Box>
|
||||
<Box sx={{ display: "flex", alignItems: "center" }}>
|
||||
<Typography variant="body2" sx={{ mr: 1 }}>
|
||||
Global disable/enable
|
||||
</Typography>
|
||||
<Switch
|
||||
checked={!folderDisabled}
|
||||
onChange={() =>
|
||||
handleDirectoryChange(folderDisabled, setFolderDisabled, globalUrl, isTenzirActive)
|
||||
}
|
||||
disabled={!isTenzirActive}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
<Box
|
||||
sx={{
|
||||
height: "500px",
|
||||
width: "100%",
|
||||
overflowY: "auto",
|
||||
p: 1,
|
||||
}}
|
||||
>
|
||||
{filteredRules?.length > 0 ?
|
||||
filteredRules.map((card) => {
|
||||
console.log("RULE CARD: ", card);
|
||||
|
||||
return (
|
||||
<DetectionRuleCard
|
||||
key={card.file_id}
|
||||
ruleName={card.title}
|
||||
description={card.description}
|
||||
file_id={card.file_id}
|
||||
globalUrl={globalUrl}
|
||||
folderDisabled={folderDisabled}
|
||||
isTenzirActive={isTenzirActive}
|
||||
{...card}
|
||||
/>
|
||||
)
|
||||
})
|
||||
: null }
|
||||
</Box>
|
||||
</Paper>
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
||||
export default Detection;
|
||||
@@ -0,0 +1,162 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { Container, CircularProgress, Typography } from "@mui/material";
|
||||
import { toast } from "react-toastify";
|
||||
import Detection from "./Detection";
|
||||
|
||||
const DetectionDashBoard = (props) => {
|
||||
const { globalUrl } = props;
|
||||
const [ruleInfo, setRuleInfo] = useState(null);
|
||||
const [, setSelectedRule] = useState(null);
|
||||
const [, setFileData] = useState("");
|
||||
const [isTenzirActive, setIsTenzirActive] = useState(false);
|
||||
const [folderDisabled, setFolderDisabled] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [importAttempts, setImportAttempts] = useState(0);
|
||||
const maxImportAttempts = 2;
|
||||
|
||||
useEffect(() => {
|
||||
const fetchTimeout = setTimeout(() => {
|
||||
fetchSigmaInfo();
|
||||
}, 1000); // Delay by 1 second
|
||||
|
||||
return () => clearTimeout(fetchTimeout);
|
||||
}, [globalUrl]);
|
||||
|
||||
useEffect(() => {
|
||||
if (ruleInfo && ruleInfo.length === 0 && importAttempts < maxImportAttempts) {
|
||||
importSigmaFromUrl();
|
||||
}
|
||||
}, [ruleInfo]);
|
||||
|
||||
const openEditBar = (rule) => {
|
||||
setSelectedRule(rule);
|
||||
fetchFileContent(rule.file_id);
|
||||
};
|
||||
|
||||
const handleSave = (updatedContent) => {
|
||||
toast("This will be saved");
|
||||
};
|
||||
|
||||
const fetchFileContent = (file_id) => {
|
||||
setFileData("");
|
||||
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) => {
|
||||
if (respdata.length === 0) {
|
||||
toast("Failed getting file. Is it deleted?");
|
||||
return;
|
||||
}
|
||||
setFileData(respdata);
|
||||
})
|
||||
.catch((error) => {
|
||||
toast(error.toString());
|
||||
});
|
||||
};
|
||||
|
||||
const fetchSigmaInfo = () => {
|
||||
const url = `${globalUrl}/api/v1/files/detection/sigma_rules`;
|
||||
setIsLoading(true);
|
||||
|
||||
fetch(url, {
|
||||
method: "GET",
|
||||
credentials: "include",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
})
|
||||
.then((response) => response.json())
|
||||
.then((responseJson) => {
|
||||
if (responseJson["success"] === false) {
|
||||
toast("Failed to get sigma rules");
|
||||
} else {
|
||||
setRuleInfo(responseJson.sigma_info || []);
|
||||
setFolderDisabled(responseJson.folder_disabled);
|
||||
setIsTenzirActive(responseJson.is_tenzir_active);
|
||||
}
|
||||
setIsLoading(false);
|
||||
})
|
||||
.catch((error) => {
|
||||
setIsLoading(false);
|
||||
console.log("Error in getting sigma files: ", error);
|
||||
toast("An error occurred while fetching sigma rules");
|
||||
setRuleInfo([]);
|
||||
});
|
||||
};
|
||||
|
||||
const importSigmaFromUrl = () => {
|
||||
setIsLoading(true);
|
||||
setImportAttempts((prevAttempts) => prevAttempts + 1);
|
||||
|
||||
const url = "https://github.com/satti-hari-krishna-reddy/shuffle_sigma";
|
||||
const folder = "sigma";
|
||||
|
||||
const parsedData = {
|
||||
url: url,
|
||||
path: folder,
|
||||
field_3: "main",
|
||||
};
|
||||
|
||||
toast(`Getting files from url ${url}. This may take a while if the repository is large. Please wait...`);
|
||||
fetch(`${globalUrl}/api/v1/files/download_remote_enhanced`, {
|
||||
method: "POST",
|
||||
mode: "cors",
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
},
|
||||
body: JSON.stringify(parsedData),
|
||||
credentials: "include",
|
||||
})
|
||||
.then((response) => response.json())
|
||||
.then((responseJson) => {
|
||||
if (responseJson.success) {
|
||||
toast("Successfully loaded files from " + url);
|
||||
fetchSigmaInfo(); // Fetch again after successful import
|
||||
} else {
|
||||
toast(responseJson.reason ? `Failed loading: ${responseJson.reason}` : "Failed loading");
|
||||
}
|
||||
setIsLoading(false);
|
||||
})
|
||||
.catch((error) => {
|
||||
toast(error.toString());
|
||||
setIsLoading(false);
|
||||
});
|
||||
};
|
||||
|
||||
if (isLoading && (!ruleInfo || ruleInfo.length === 0)) {
|
||||
return (
|
||||
<Container style={{ display: "flex", justifyContent: "center", alignItems: "center", height: "100vh" }}>
|
||||
<div>
|
||||
<CircularProgress />
|
||||
<Typography variant="h6" style={{ marginTop: 20 }}>Downloading rules, please wait...</Typography>
|
||||
</div>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Container style={{ display: "flex" }}>
|
||||
<Detection
|
||||
globalUrl={globalUrl}
|
||||
ruleInfo={ruleInfo}
|
||||
folderDisabled={folderDisabled}
|
||||
setFolderDisabled={setFolderDisabled}
|
||||
isTenzirActive={isTenzirActive}
|
||||
/>
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
||||
export default DetectionDashBoard;
|
||||
@@ -0,0 +1,225 @@
|
||||
import React, { useState } from "react";
|
||||
import {
|
||||
Container,
|
||||
Box,
|
||||
TextField,
|
||||
Switch,
|
||||
Typography,
|
||||
Button,
|
||||
CircularProgress,
|
||||
Paper,
|
||||
} from "@mui/material";
|
||||
|
||||
import { toast } from "react-toastify";
|
||||
import theme from '../theme.jsx';
|
||||
import DetectionRuleCard from "../components/DetectionRuleCard.jsx";
|
||||
|
||||
const handleDirectoryChange = (folderDisabled, setFolderDisabled, globalUrl, isTenzirActive) => {
|
||||
if (!isTenzirActive) {
|
||||
toast.warn("Connect to siem first for global enable/disable to work");
|
||||
return;
|
||||
}
|
||||
|
||||
const action = folderDisabled ? "enable_folder" : "disable_folder";
|
||||
const url = `${globalUrl}/api/v1/detections/${action}`;
|
||||
|
||||
fetch(url, {
|
||||
method: "PUT",
|
||||
credentials: "include",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
})
|
||||
.then((response) =>
|
||||
response.json().then((responseJson) => {
|
||||
if (responseJson["success"] === true) {
|
||||
if (action === "enable_folder") setFolderDisabled(false);
|
||||
else setFolderDisabled(true);
|
||||
} else {
|
||||
//toast(`failed to disable rule`);
|
||||
}
|
||||
})
|
||||
)
|
||||
.catch((error) => {
|
||||
console.log(`Error in ${action} the rule: `, error);
|
||||
toast(`An error occurred while ${action} the rule`);
|
||||
});
|
||||
};
|
||||
|
||||
const Detection = (props) => {
|
||||
const { globalUrl, ruleInfo, folderDisabled, setFolderDisabled, isTenzirActive, detectionInfo } = props;
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const handleConnectClick = () => {
|
||||
if (isTenzirActive) {
|
||||
return
|
||||
}
|
||||
|
||||
if (detectionInfo.category === undefined || detectionInfo.category === null) {
|
||||
toast.warn("Detection category not found. Please try again or contact support@shuffler.io if you think this is a bug.")
|
||||
return
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
const url = `${globalUrl}/api/v1/detections/${detectionInfo?.category}/connect`;
|
||||
|
||||
fetch(url, {
|
||||
method: "GET",
|
||||
credentials: "include",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
})
|
||||
.then((response) =>
|
||||
response.json().then((responseJson) => {
|
||||
if (responseJson["success"] === true) {
|
||||
setTimeout(() => {
|
||||
setLoading(false);
|
||||
window.location.reload();
|
||||
}, 15000);
|
||||
} else {
|
||||
if (responseJson.reason !== undefined && responseJson.reason !== null) {
|
||||
toast(responseJson.reason)
|
||||
} else {
|
||||
toast(`Failed to connect to ${detectionInfo?.category}`);
|
||||
}
|
||||
|
||||
if (responseJson.action !== undefined && responseJson.actio !== null && responseJson.action.length > 0) {
|
||||
//if (responseJson.action === "environment_create") {
|
||||
// navigate("/admin?tab=environments")
|
||||
//}
|
||||
}
|
||||
|
||||
setLoading(false);
|
||||
}
|
||||
})
|
||||
)
|
||||
.catch((error) => {
|
||||
setLoading(false);
|
||||
console.log(`Error in connecting to ${detectionInfo?.category}: `, error);
|
||||
toast(`An error occurred while connecting to ${detectionInfo?.category}`);
|
||||
});
|
||||
}
|
||||
|
||||
const filteredRules = ruleInfo === "default" ? [] : ruleInfo?.filter((rule) =>
|
||||
rule.title.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
rule.description.toLowerCase().includes(searchQuery.toLowerCase())
|
||||
)
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<Paper
|
||||
style={{
|
||||
marginTop: 50,
|
||||
width: "100%",
|
||||
padding: 50,
|
||||
backgroundColor: theme.palette.backgroundColor,
|
||||
borderRadius: theme.palette.borderRadius,
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
mb: 2,
|
||||
}}
|
||||
>
|
||||
<Typography variant="h6" component="div">
|
||||
{detectionInfo?.name} ({filteredRules?.length})
|
||||
</Typography>
|
||||
<Button
|
||||
variant="contained"
|
||||
onClick={handleConnectClick}
|
||||
disabled={loading} // Disable the button while loading
|
||||
color={isTenzirActive ? "primary" : "secondary"}
|
||||
style={{ }}
|
||||
>
|
||||
{loading ? <CircularProgress size={24} /> : isTenzirActive ? `Connected to ${detectionInfo?.category}` : `Connect to ${detectionInfo?.category}`}
|
||||
</Button>
|
||||
</Box>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
mb: 2,
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
}}
|
||||
>
|
||||
<TextField
|
||||
label="Search rules"
|
||||
variant="outlined"
|
||||
size="small"
|
||||
sx={{ mr: 2 }}
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
/>
|
||||
{/* <Button
|
||||
color="primary"
|
||||
variant="contained"
|
||||
onClick={() => uploadRef.current.click()}
|
||||
>
|
||||
<PublishIcon /> Upload sigma file
|
||||
</Button>
|
||||
<input
|
||||
hidden
|
||||
type="file"
|
||||
multiple
|
||||
ref={uploadRef}
|
||||
onChange={(event) => {
|
||||
uploadFiles(event.target.files);
|
||||
}}
|
||||
/> */}
|
||||
</Box>
|
||||
<Box sx={{ display: "flex", alignItems: "center" }}>
|
||||
<Typography variant="body2" sx={{ mr: 1 }}>
|
||||
Global disable/enable
|
||||
</Typography>
|
||||
<Switch
|
||||
checked={!folderDisabled}
|
||||
onChange={() =>
|
||||
handleDirectoryChange(folderDisabled, setFolderDisabled, globalUrl, isTenzirActive)
|
||||
}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
<Box
|
||||
sx={{
|
||||
height: "500px",
|
||||
width: "100%",
|
||||
overflowY: "auto",
|
||||
p: 1,
|
||||
}}
|
||||
>
|
||||
{filteredRules?.length > 0 ?
|
||||
filteredRules.map((rule, index) => {
|
||||
console.log("RULE CARD: ", rule);
|
||||
|
||||
return (
|
||||
<DetectionRuleCard
|
||||
key={index}
|
||||
ruleName={rule.file_name}
|
||||
description={rule.description}
|
||||
|
||||
file_id={rule.file_id}
|
||||
globalUrl={globalUrl}
|
||||
folderDisabled={folderDisabled}
|
||||
isTenzirActive={isTenzirActive}
|
||||
{...rule}
|
||||
/>
|
||||
)
|
||||
})
|
||||
: null }
|
||||
</Box>
|
||||
</Paper>
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
||||
export default Detection;
|
||||
@@ -0,0 +1,162 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { Container, CircularProgress, Typography } from "@mui/material";
|
||||
import { toast } from "react-toastify";
|
||||
import Detection from "../views/Detection";
|
||||
|
||||
const DetectionDashBoard = (props) => {
|
||||
const { globalUrl } = props;
|
||||
const [ruleInfo, setRuleInfo] = useState(null);
|
||||
const [, setSelectedRule] = useState(null);
|
||||
const [, setFileData] = useState("");
|
||||
const [isTenzirActive, setIsTenzirActive] = useState(false);
|
||||
const [folderDisabled, setFolderDisabled] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [importAttempts, setImportAttempts] = useState(0);
|
||||
const maxImportAttempts = 2;
|
||||
|
||||
useEffect(() => {
|
||||
const fetchTimeout = setTimeout(() => {
|
||||
fetchSigmaInfo();
|
||||
}, 1000); // Delay by 1 second
|
||||
|
||||
return () => clearTimeout(fetchTimeout);
|
||||
}, [globalUrl]);
|
||||
|
||||
useEffect(() => {
|
||||
if (ruleInfo && ruleInfo.length === 0 && importAttempts < maxImportAttempts) {
|
||||
importSigmaFromUrl();
|
||||
}
|
||||
}, [ruleInfo]);
|
||||
|
||||
const openEditBar = (rule) => {
|
||||
setSelectedRule(rule);
|
||||
fetchFileContent(rule.file_id);
|
||||
};
|
||||
|
||||
const handleSave = (updatedContent) => {
|
||||
toast("This will be saved");
|
||||
};
|
||||
|
||||
const fetchFileContent = (file_id) => {
|
||||
setFileData("");
|
||||
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) => {
|
||||
if (respdata.length === 0) {
|
||||
toast("Failed getting file. Is it deleted?");
|
||||
return;
|
||||
}
|
||||
setFileData(respdata);
|
||||
})
|
||||
.catch((error) => {
|
||||
toast(error.toString());
|
||||
});
|
||||
};
|
||||
|
||||
const fetchSigmaInfo = () => {
|
||||
const url = `${globalUrl}/api/v1/files/detection/sigma_rules`;
|
||||
setIsLoading(true);
|
||||
|
||||
fetch(url, {
|
||||
method: "GET",
|
||||
credentials: "include",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
})
|
||||
.then((response) => response.json())
|
||||
.then((responseJson) => {
|
||||
if (responseJson["success"] === false) {
|
||||
toast("Failed to get sigma rules");
|
||||
} else {
|
||||
setRuleInfo(responseJson.sigma_info || []);
|
||||
setFolderDisabled(responseJson.folder_disabled);
|
||||
setIsTenzirActive(responseJson.is_tenzir_active);
|
||||
}
|
||||
setIsLoading(false);
|
||||
})
|
||||
.catch((error) => {
|
||||
setIsLoading(false);
|
||||
console.log("Error in getting sigma files: ", error);
|
||||
toast("An error occurred while fetching sigma rules");
|
||||
setRuleInfo([]);
|
||||
});
|
||||
};
|
||||
|
||||
const importSigmaFromUrl = () => {
|
||||
setIsLoading(true);
|
||||
setImportAttempts((prevAttempts) => prevAttempts + 1);
|
||||
|
||||
const url = "https://github.com/satti-hari-krishna-reddy/shuffle_sigma";
|
||||
const folder = "sigma";
|
||||
|
||||
const parsedData = {
|
||||
url: url,
|
||||
path: folder,
|
||||
field_3: "main",
|
||||
};
|
||||
|
||||
toast(`Getting files from url ${url}. This may take a while if the repository is large. Please wait...`);
|
||||
fetch(`${globalUrl}/api/v1/files/download_remote_enhanced`, {
|
||||
method: "POST",
|
||||
mode: "cors",
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
},
|
||||
body: JSON.stringify(parsedData),
|
||||
credentials: "include",
|
||||
})
|
||||
.then((response) => response.json())
|
||||
.then((responseJson) => {
|
||||
if (responseJson.success) {
|
||||
toast("Successfully loaded files from " + url);
|
||||
fetchSigmaInfo(); // Fetch again after successful import
|
||||
} else {
|
||||
toast(responseJson.reason ? `Failed loading: ${responseJson.reason}` : "Failed loading");
|
||||
}
|
||||
setIsLoading(false);
|
||||
})
|
||||
.catch((error) => {
|
||||
toast(error.toString());
|
||||
setIsLoading(false);
|
||||
});
|
||||
};
|
||||
|
||||
if (isLoading && (!ruleInfo || ruleInfo.length === 0)) {
|
||||
return (
|
||||
<Container style={{ display: "flex", justifyContent: "center", alignItems: "center", height: "100vh" }}>
|
||||
<div>
|
||||
<CircularProgress />
|
||||
<Typography variant="h6" style={{ marginTop: 20 }}>Downloading rules, please wait...</Typography>
|
||||
</div>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Container style={{ display: "flex" }}>
|
||||
<Detection
|
||||
globalUrl={globalUrl}
|
||||
ruleInfo={ruleInfo}
|
||||
folderDisabled={folderDisabled}
|
||||
setFolderDisabled={setFolderDisabled}
|
||||
isTenzirActive={isTenzirActive}
|
||||
/>
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
||||
export default DetectionDashBoard;
|
||||
+102
-27
@@ -1,15 +1,14 @@
|
||||
import React, { useEffect, useLayoutEffect, useRef, useState } from "react"
|
||||
|
||||
import React, { useEffect, useLayoutEffect, useRef, useState, useContext, memo } from "react"
|
||||
import { toast } from 'react-toastify';
|
||||
import Markdown from 'react-markdown'
|
||||
|
||||
import theme from '../theme.jsx';
|
||||
import ReactJson from "react-json-view";
|
||||
import ReactJson from "react-json-view-ssr";
|
||||
import { isMobile } from "react-device-detect";
|
||||
import { BrowserView, MobileView } from "react-device-detect";
|
||||
import { useParams, useNavigate, Link } from "react-router-dom";
|
||||
import { validateJson, GetIconInfo } from "../views/Workflows.jsx";
|
||||
|
||||
import remarkGfm from 'remark-gfm'
|
||||
import { Context } from "../context/ContextApi.jsx";
|
||||
import {
|
||||
Grid,
|
||||
TextField,
|
||||
@@ -26,7 +25,6 @@ import {
|
||||
ListItemButton,
|
||||
ListItemText
|
||||
} from "@mui/material";
|
||||
|
||||
import {
|
||||
Link as LinkIcon,
|
||||
Edit as EditIcon,
|
||||
@@ -35,7 +33,6 @@ import {
|
||||
FileCopy as FileCopyIcon
|
||||
} from "@mui/icons-material";
|
||||
import { fontGrid } from "@mui/material/styles/cssUtils.js";
|
||||
import { active } from "d3";
|
||||
|
||||
const Body = {
|
||||
//maxWidth: 1000,
|
||||
@@ -46,6 +43,7 @@ const Body = {
|
||||
height: "100%",
|
||||
color: "white",
|
||||
position: "relative",
|
||||
paddingTop: 40,
|
||||
//textAlign: "center",
|
||||
};
|
||||
|
||||
@@ -125,7 +123,7 @@ export const Paragrah = (props) => {
|
||||
|
||||
|
||||
return (
|
||||
<div class="sdf">
|
||||
<div>
|
||||
{element}
|
||||
</div>
|
||||
)
|
||||
@@ -155,9 +153,28 @@ export const OuterLink = (props) => {
|
||||
|
||||
|
||||
export const Img = (props) => {
|
||||
return <img style={{ borderRadius: theme.palette.borderRadius, width: 750, maxWidth: "100%", marginTop: 15, marginBottom: 15, }} alt={props.alt} src={props.src} />;
|
||||
// Find parent container and check width
|
||||
|
||||
var height = "auto"
|
||||
var width = 750
|
||||
if (props.height !== undefined && props.height !== null) {
|
||||
height = props.height
|
||||
}
|
||||
|
||||
if (props.width !== undefined && props.width !== null) {
|
||||
width = props.width
|
||||
}
|
||||
|
||||
return(
|
||||
<img
|
||||
style={{border: "1px solid rgba(255,255,255,0.3)", borderRadius: theme.palette?.borderRadius, width: width, maxWidth: width, margin: "auto", marginTop: 10, marginBottom: 10, }}
|
||||
alt={props.alt}
|
||||
src={props.src}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
export const CodeHandler = (props) => {
|
||||
const propvalue = props.value !== undefined && props.value !== null ? props.value : props.children !== undefined && props.children !== null && props.children.length > 0 ? props.children[0] : ""
|
||||
|
||||
@@ -197,7 +214,7 @@ export const CodeHandler = (props) => {
|
||||
backgroundColor: theme.palette.inputColor,
|
||||
overflowY: "auto",
|
||||
// Have it inline
|
||||
borderRadius: theme.palette.borderRadius,
|
||||
borderRadius: theme.palette?.borderRadius,
|
||||
}}
|
||||
>
|
||||
{validate.valid === true ?
|
||||
@@ -234,10 +251,8 @@ export const CodeHandler = (props) => {
|
||||
}
|
||||
|
||||
const Docs = (defaultprops) => {
|
||||
const { globalUrl, selectedDoc, serverside, serverMobile } = defaultprops;
|
||||
|
||||
const { globalUrl, selectedDoc, serverside, serverMobile, isLoggedIn, isLoaded } = defaultprops;
|
||||
let navigate = useNavigate();
|
||||
|
||||
// Quickfix for react router 5 -> 6
|
||||
const params = useParams();
|
||||
//var props = JSON.parse(JSON.stringify(defaultprops))
|
||||
@@ -369,8 +384,7 @@ const Docs = (defaultprops) => {
|
||||
hash = hash.split('?')[0]
|
||||
}
|
||||
if (hash) {
|
||||
console.log("HASH: ", hash)
|
||||
const element = document.getElementById(hash)
|
||||
const element = document.getElementById(hash.toLowerCase())
|
||||
if (element) {
|
||||
element.scrollIntoView({
|
||||
behavior: "instant",
|
||||
@@ -454,6 +468,30 @@ const Docs = (defaultprops) => {
|
||||
minHeight: "80vh",
|
||||
};
|
||||
|
||||
const noteLabelStyle = {
|
||||
fontWeight: "bold",
|
||||
color: "#f86a3e",
|
||||
display: "block",
|
||||
marginBottom: "5px",
|
||||
};
|
||||
|
||||
const Blockquote = ({ children }) => {
|
||||
|
||||
const textContent = children.map(child =>
|
||||
child.props && child.props.children ? child.props.children.join('') : child
|
||||
).join('').trim();
|
||||
|
||||
// Maybe some more contents....
|
||||
const isNote = textContent.startsWith("[!TIP]");
|
||||
return (
|
||||
<blockquote style={isNote ? alertNote : {}}>
|
||||
{isNote && <span style={noteLabelStyle}>Tips:</span>}
|
||||
{isNote ? textContent.replace("[!TIP]", "").trim() : children}
|
||||
</blockquote>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
const Heading = (props) => {
|
||||
const [hover, setHover] = useState(false);
|
||||
var id = props.children[0].toLowerCase().toString()
|
||||
@@ -487,7 +525,7 @@ const Docs = (defaultprops) => {
|
||||
style={{
|
||||
backgroundColor: theme.palette.inputColor,
|
||||
padding: 15,
|
||||
borderRadius: theme.palette.borderRadius,
|
||||
borderRadius: theme.palette?.borderRadius,
|
||||
marginBottom: 30,
|
||||
display: "flex",
|
||||
}}
|
||||
@@ -837,6 +875,12 @@ const Docs = (defaultprops) => {
|
||||
fontSize: isMobile ? "1.3rem" : "1.1rem",
|
||||
};
|
||||
|
||||
const alertNote = {
|
||||
padding: "10px",
|
||||
borderLeft: "5px solid #f86a3e",
|
||||
backgroundColor: "rgb(26,26,26)",
|
||||
};
|
||||
|
||||
const CustomButton = (props) => {
|
||||
const { title, icon, link } = props
|
||||
|
||||
@@ -847,7 +891,7 @@ const Docs = (defaultprops) => {
|
||||
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, }}
|
||||
<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()
|
||||
@@ -887,7 +931,7 @@ const Docs = (defaultprops) => {
|
||||
|
||||
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, }}
|
||||
<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)
|
||||
}}
|
||||
@@ -923,8 +967,8 @@ const Docs = (defaultprops) => {
|
||||
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" />
|
||||
<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" }}>
|
||||
@@ -971,9 +1015,11 @@ const Docs = (defaultprops) => {
|
||||
h6: Heading,
|
||||
a: OuterLink,
|
||||
p: Paragrah,
|
||||
blockquote: Blockquote,
|
||||
}
|
||||
|
||||
|
||||
|
||||
// PostDataBrowser Section
|
||||
const postDataBrowser =
|
||||
list === undefined || list === null ? null : (
|
||||
@@ -1022,8 +1068,10 @@ const Docs = (defaultprops) => {
|
||||
<Markdown
|
||||
components={markdownComponents}
|
||||
id="markdown_wrapper"
|
||||
className={"style.reactMarkdown"}
|
||||
escapeHtml={false}
|
||||
skipHtml={false}
|
||||
remarkPlugins={[remarkGfm]}
|
||||
style={{
|
||||
maxWidth: "100%", minWidth: "100%",
|
||||
}}
|
||||
@@ -1217,14 +1265,41 @@ const Docs = (defaultprops) => {
|
||||
);
|
||||
|
||||
// Padding and zIndex etc set because of footer in cloud.
|
||||
const loadedCheck = (
|
||||
<div style={{ minHeight: 1000, zIndex: 50000, maxWidth: 1920, minWidth: isMobile ? null : 1366, margin: "auto", }}>
|
||||
<BrowserView>{postDataBrowser}</BrowserView>
|
||||
<MobileView>{postDataMobile}</MobileView>
|
||||
</div>
|
||||
);
|
||||
const loadedCheck = (
|
||||
<DocsWrapper isLoggedIn={isLoggedIn} isLoaded={isLoaded}>
|
||||
<DocsContent postDataBrowser={postDataBrowser} postDataMobile={postDataMobile}/>
|
||||
</DocsWrapper>
|
||||
);
|
||||
|
||||
return <div>{loadedCheck}</div>;
|
||||
|
||||
return <div style={{}}>{loadedCheck}</div>;
|
||||
};
|
||||
|
||||
export default Docs;
|
||||
|
||||
const DocsContent = memo(({postDataBrowser, postDataMobile}) => {
|
||||
return(
|
||||
<div>
|
||||
<BrowserView>{postDataBrowser}</BrowserView>
|
||||
<MobileView>{postDataMobile}</MobileView>
|
||||
</div>
|
||||
)})
|
||||
|
||||
const DocsWrapper = memo(({isLoggedIn, isLoaded, children })=>{
|
||||
|
||||
const { leftSideBarOpenByClick, windowWidth } = useContext(Context);
|
||||
|
||||
return (
|
||||
<div style={{
|
||||
minHeight: 1000, zIndex: 1,
|
||||
maxWidth: Math.min(!(isLoggedIn && isLoaded) ? 1920 : leftSideBarOpenByClick ? windowWidth - 300 : windowWidth - 200, 1920),
|
||||
minWidth: isMobile ? null : (isLoggedIn && isLoaded) ? leftSideBarOpenByClick ? 800 : 900 : null, margin: "auto",
|
||||
position: (isLoggedIn && isLoaded) && leftSideBarOpenByClick ? "relative" : "static",
|
||||
left: (isLoggedIn && isLoaded) && leftSideBarOpenByClick ? 120 : (isLoggedIn && isLoaded) && !leftSideBarOpenByClick ? 80 : 0,
|
||||
marginLeft: windowWidth < 1920 ? leftSideBarOpenByClick && (isLoggedIn && isLoaded) ? 160 : (isLoggedIn && isLoaded) && !leftSideBarOpenByClick ? 80 : 0 : "auto", width: "100%",
|
||||
transition: "left 0.3s ease-in-out, min-width 0.3s ease-in-out, max-width 0.3s ease-in-out, position 0.3s ease-in-out, margin 0.3s ease-in-out, margin-left 0.3s ease"
|
||||
}}>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
})
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
import React from 'react';
|
||||
import { Box, Typography, Button, TextField } from '@mui/material';
|
||||
|
||||
const EditComponent = ({ ruleName, description, content, setContent, lastEdited, editedBy, onSave }) => {
|
||||
|
||||
const handleSave = () => {
|
||||
onSave(content);
|
||||
};
|
||||
|
||||
return (
|
||||
<Box sx={{ p: 2, border: '1px solid #ccc', borderRadius: 2, height: '100%', width: '100%', marginTop:'30px'}}>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<Typography variant="h6">{ruleName}</Typography>
|
||||
</Box>
|
||||
<Typography variant="body2" style={{ marginTop: '2%' }}>
|
||||
{description}
|
||||
</Typography>
|
||||
<Typography variant="body2" sx={{ mt: 1 }}>
|
||||
Last edited: {lastEdited}
|
||||
</Typography>
|
||||
<Typography variant="body2">
|
||||
Edited By: {editedBy}
|
||||
</Typography>
|
||||
<Box sx={{ mt: 2 }}>
|
||||
<TextField
|
||||
multiline
|
||||
rows={12}
|
||||
value={content}
|
||||
onChange={(e) => setContent(e.target.value)}
|
||||
variant="outlined"
|
||||
fullWidth
|
||||
/>
|
||||
</Box>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'flex-end', mt: 2 }}>
|
||||
<Button variant="contained" color="primary" onClick={handleSave}>
|
||||
Save
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default EditComponent;
|
||||
@@ -2467,7 +2467,7 @@ const GettingStarted = (props) => {
|
||||
maxWidth: 1024,
|
||||
zIndex: 11,
|
||||
border: "1px solid rgba(255,255,255,0.1)",
|
||||
borderRadius: theme.palette.borderRadius,
|
||||
borderRadius: theme.palette?.borderRadius,
|
||||
textAlign: "center",
|
||||
overflow: "auto",
|
||||
}}
|
||||
|
||||
@@ -166,7 +166,7 @@ const LoginDialog = (props) => {
|
||||
} else {
|
||||
if (responseJson["reason"] === "MFA_REDIRECT") {
|
||||
setLoginInfo(
|
||||
"MFA required. Please the 6-digit code from your authenticator"
|
||||
"MFA required. Please enter the 6-digit code from your authenticator"
|
||||
);
|
||||
setMFAField(true);
|
||||
return;
|
||||
@@ -183,7 +183,10 @@ const LoginDialog = (props) => {
|
||||
|
||||
if (responseJson.tutorials === undefined || responseJson.tutorials === null || !responseJson.tutorials.includes("welcome")) {
|
||||
console.log("RUN Welcome!!")
|
||||
window.location.pathname = "/welcome"
|
||||
setTimeout(() => {
|
||||
navigate("/welcome?tab=2")
|
||||
},200)
|
||||
// window.location.pathname = ""
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
+1217
-434
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,4 @@
|
||||
import React, { useState, useEffect, useMemo } from "react";
|
||||
import React, { useState, useEffect, useMemo, useContext } from "react";
|
||||
|
||||
import theme from "../theme.jsx";
|
||||
import { isMobile } from "react-device-detect";
|
||||
@@ -7,28 +7,29 @@ import WorkflowGrid from "../components/WorkflowGrid.jsx";
|
||||
import CreatorGrid from "../components/CreatorGrid.jsx";
|
||||
import DocsGrid from "../components/DocsGrid.jsx";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import Typography from "@material-ui/core/Typography";
|
||||
import { Tabs, Tab, setRef } from "@mui/material";
|
||||
import { styled } from "@mui/material/styles";
|
||||
import { makeStyles } from '@mui/styles';
|
||||
import DiscordChat from "../components/DiscordChat.jsx";
|
||||
import { Context } from "../context/ContextApi.jsx";
|
||||
|
||||
import {
|
||||
Apps as AppsIcon,
|
||||
Code as CodeIcon,
|
||||
EmojiObjects as EmojiObjectsIcon,
|
||||
Chat as ChatIcon,
|
||||
BorderBottom,
|
||||
PeopleAltOutlined as PeopleAltOutlinedIcon,
|
||||
DescriptionOutlined as DescriptionOutlinedIcon,
|
||||
} from "@mui/icons-material";
|
||||
|
||||
import PeopleAltOutlinedIcon from '@mui/icons-material/PeopleAltOutlined';
|
||||
import DescriptionOutlinedIcon from '@mui/icons-material/DescriptionOutlined';
|
||||
import {Typography} from "@mui/material";
|
||||
|
||||
// Should be different if logged in :|
|
||||
const Search = (props) => {
|
||||
const { globalUrl, isLoaded, serverside, userdata, hidemargins, isHeader } =
|
||||
props;
|
||||
let navigate = useNavigate();
|
||||
const { leftSideBarOpenByClick } = useContext(Context);
|
||||
|
||||
const [curTab, setCurTab] = useState(0);
|
||||
const iconStyle = { marginRight: isHeader ? null : 10 };
|
||||
@@ -43,11 +44,10 @@ const Search = (props) => {
|
||||
const params = Object.fromEntries(urlSearchParams.entries());
|
||||
const foundTab = params["tab"];
|
||||
if (foundTab !== null && foundTab !== undefined) {
|
||||
for (var key in Object.keys(views)) {
|
||||
for (let key in views) {
|
||||
const value = views[key];
|
||||
console.log(key, value);
|
||||
if (value === foundTab) {
|
||||
setConfig("", key);
|
||||
setConfig(null, key);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -91,7 +91,7 @@ const Search = (props) => {
|
||||
userdata={userdata}
|
||||
/>, [curTab]);
|
||||
|
||||
const MemoizedDiscordChat = useMemo(() => <DiscordChat isMobile={isMobile} />)
|
||||
const MemoizedDiscordChat = useMemo(() => <DiscordChat isMobile={isMobile} />, [curTab])
|
||||
|
||||
const useStyles = makeStyles({
|
||||
hideIndicator: {
|
||||
@@ -123,7 +123,6 @@ const Search = (props) => {
|
||||
flex: "1",
|
||||
marginLeft: isHeader ? null : 10,
|
||||
marginRight: isHeader ? null : 10,
|
||||
paddingLeft: isHeader ? null : 30,
|
||||
paddingRight: isHeader ? null : 30,
|
||||
paddingBottom: isHeader ? null : 30,
|
||||
paddingTop: hidemargins === true ? 0 : isHeader ? null : 30,
|
||||
@@ -131,6 +130,8 @@ const Search = (props) => {
|
||||
flexDirection: "column",
|
||||
overflowX: "hidden",
|
||||
minHeight: 400,
|
||||
paddingLeft: leftSideBarOpenByClick ? 270 : 80,
|
||||
transition: "padding-left 0.3s ease",
|
||||
};
|
||||
|
||||
const views = {
|
||||
@@ -138,7 +139,9 @@ const Search = (props) => {
|
||||
1: "workflows",
|
||||
2: "docs",
|
||||
3: "creators",
|
||||
4: "discord"
|
||||
};
|
||||
|
||||
const setConfig = (event, inputValue) => {
|
||||
const newValue = parseInt(inputValue);
|
||||
|
||||
@@ -216,19 +219,19 @@ const Search = (props) => {
|
||||
<div style={boxStyle}>
|
||||
<Tabs
|
||||
style={{
|
||||
width: 741,
|
||||
width: 757,
|
||||
margin: isHeader ? null : "auto",
|
||||
marginTop: hidemargins === true ? 0 : isHeader ? null : 25,
|
||||
backgroundColor: "rgba(33, 33, 33, 1)",
|
||||
borderRadius: 8
|
||||
borderRadius: 8,
|
||||
}}
|
||||
value={curTab}
|
||||
indicatorColor="primary"
|
||||
textColor="secondary"
|
||||
onChange={setConfig}
|
||||
aria-label="disabled tabs example"
|
||||
variant="scrollable"
|
||||
scrollButtons="auto"
|
||||
// variant="scrollable"
|
||||
// scrollButtons="auto"
|
||||
classes={{ indicator: classes.hideIndicator, root: classes.customTab }}
|
||||
>
|
||||
<StyledTab
|
||||
|
||||
@@ -3,6 +3,8 @@ import React, { useState } from "react";
|
||||
import { Typography, CircularProgress } from "@mui/material";
|
||||
import theme from '../theme.jsx';
|
||||
|
||||
import { red, } from "../views/AngularWorkflow.jsx"
|
||||
|
||||
const SetAuthentication = (props) => {
|
||||
const { globalUrl } = props;
|
||||
|
||||
@@ -300,7 +302,7 @@ const SetAuthentication = (props) => {
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ maringTop: 50, padding: 50, border: "1px solid rgba(255,255,255,0.6)", borderRadius: theme.palette.borderRadius, width: 500, margin: "auto", itemAlign: "center", textAlign: "center",}}>
|
||||
<div style={{ padding: 50, border: failed === true ? `1px solid ${red}` : "1px solid rgba(255,255,255,0.6)", borderRadius: theme.palette?.borderRadius, width: 500, margin: "auto", paddingTop: 50, itemAlign: "center", textAlign: "center",}}>
|
||||
<Typography
|
||||
variant="h4"
|
||||
style={{ marginLeft: "auto", marginRight: "auto", marginTop: 50}}
|
||||
@@ -311,18 +313,22 @@ const SetAuthentication = (props) => {
|
||||
variant="h6"
|
||||
style={{ marginLeft: "auto", marginRight: "auto", marginTop: 50}}
|
||||
>
|
||||
{!finished ? (
|
||||
{!finished ?
|
||||
failed ?
|
||||
null :
|
||||
<CircularProgress />
|
||||
) : (
|
||||
"Done - this window should close within 3 seconds."
|
||||
)}
|
||||
<div />
|
||||
{failed ? "Failed setup. Error: " : ""} {response}
|
||||
:
|
||||
failed ?
|
||||
null
|
||||
:
|
||||
"Done - this window should close within 3 seconds."
|
||||
}
|
||||
|
||||
<div style={{marginTop: 10, }} />
|
||||
<b>{failed ? "Failed auth. Error: " : ""}</b> {response}
|
||||
<br/>
|
||||
<br/>
|
||||
{failed ? "If the error persists, try to use fewer scopes. Contact our support at support@shuffler.io if you need further assistance. You may close this window." : ""}
|
||||
{failed ? "If the error persists, try to use fewer scopes. Contact support@shuffler.io if you need further assistance, and include the current URL and a screenshot. You may now close this window." : ""}
|
||||
</Typography>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
Divider,
|
||||
TextField,
|
||||
Modal,
|
||||
Switch,
|
||||
} from "@mui/material";
|
||||
//import { useAlert
|
||||
import { ToastContainer, toast } from "react-toastify";
|
||||
@@ -19,6 +20,9 @@ import { FileCopy, Visibility, VisibilityOff } from "@mui/icons-material";
|
||||
import IconButton from "@mui/material/IconButton";
|
||||
import { Tooltip } from "@mui/material";
|
||||
import CloseIcon from "@mui/icons-material/Close";
|
||||
import { Box } from "@mui/system";
|
||||
import CircularProgress from "@mui/material/CircularProgress";
|
||||
|
||||
|
||||
const Settings = (props) => {
|
||||
const { globalUrl, isLoaded, userdata, setUserData } = props;
|
||||
@@ -32,6 +36,11 @@ const Settings = (props) => {
|
||||
const [currentPassword, setCurrentPassword] = useState("");
|
||||
const [newPassword, setNewPassword] = useState("");
|
||||
const [newPassword2, setNewPassword2] = useState("");
|
||||
const [selectedOrganization, setSelectedOrganization] = useState({});
|
||||
const [MFARequired, setMFARequired] = React.useState(false);
|
||||
const [image2FA, setImage2FA] = React.useState("");
|
||||
const [value2FA, setValue2FA] = React.useState("");
|
||||
|
||||
// const [file, setFile] = React.useState("");
|
||||
// const [fileBase64, setFileBase64] = React.useState(
|
||||
// userdata.image === undefined || userdata.image === null
|
||||
@@ -87,6 +96,82 @@ const Settings = (props) => {
|
||||
flexDirection: "column",
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (userdata.active_org === undefined || userdata.active_org === null || userdata.active_org.id === undefined || userdata.active_org.id === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
fetch(`${globalUrl}/api/v1/orgs/${userdata.active_org.id}`, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Accept: "application/json",
|
||||
},
|
||||
credentials: "include",
|
||||
})
|
||||
.then((response) => {
|
||||
console.log("Received response:", response);
|
||||
if (response.status !== 200) {
|
||||
console.error("Status not 200:", response.status);
|
||||
throw new Error(`Status not 200: ${response.status}`);
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then((responseJson) => {
|
||||
setSelectedOrganization(responseJson);
|
||||
setMFARequired(responseJson.mfa_required);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("Error fetching organization:", error);
|
||||
});
|
||||
}, [userdata]);
|
||||
|
||||
const UpdateMFAInUserOrg = (org_id) => {
|
||||
|
||||
if (MFARequired === false) {
|
||||
toast("Making MFA required for your organization. Please wait...");
|
||||
} else {
|
||||
toast("Making MFA optional for your organization. Please wait...");
|
||||
}
|
||||
|
||||
const data = {
|
||||
mfa_required: !selectedOrganization.mfa_required,
|
||||
org_id: selectedOrganization.id,
|
||||
}
|
||||
|
||||
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) => {
|
||||
console.log(responseJson)
|
||||
if (responseJson["success"] === false) {
|
||||
toast.error("Failed updating org: ", responseJson.reason);
|
||||
} else {
|
||||
if (MFARequired === false) {
|
||||
setMFARequired(true)
|
||||
toast.success("Successfully make MFA required for your organization!");
|
||||
} else {
|
||||
setMFARequired(false)
|
||||
toast.success("Successfully make MFA optional for your organization!")
|
||||
}
|
||||
}
|
||||
}),
|
||||
)
|
||||
.catch((error) => {
|
||||
toast("Err: " + error.toString());
|
||||
});
|
||||
}
|
||||
|
||||
const checkOwner = (data, userdata) => {
|
||||
var currentOwner = false;
|
||||
if (data.owner.address === userdata.eth_info.account) {
|
||||
@@ -542,7 +627,7 @@ const Settings = (props) => {
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
padding: "0px 0px 12px 0px",
|
||||
borderRadius: theme.palette.borderRadius,
|
||||
borderRadius: theme.palette?.borderRadius,
|
||||
};
|
||||
|
||||
const currentOwner = checkOwner(data, userdata);
|
||||
@@ -551,7 +636,7 @@ const Settings = (props) => {
|
||||
}
|
||||
|
||||
return (
|
||||
<Grid item xs={4} style={{ borderRadius: theme.palette.borderRadius }}>
|
||||
<Grid item xs={4} style={{ borderRadius: theme.palette?.borderRadius }}>
|
||||
<Paper style={innerPaperStyle}>
|
||||
<img
|
||||
src={data.image}
|
||||
@@ -607,7 +692,7 @@ const Settings = (props) => {
|
||||
);
|
||||
|
||||
const landingpageData = (
|
||||
<div style={{ display: "flex", marginTop: 120 }}>
|
||||
<div style={{ display: "flex", paddingTop: 120 }}>
|
||||
<Paper style={boxStyle}>
|
||||
{imageInfo}
|
||||
<h2>Settings</h2>
|
||||
@@ -938,8 +1023,7 @@ const Settings = (props) => {
|
||||
>
|
||||
Submit password change
|
||||
</Button>
|
||||
<h3>{passwordFormMessage}</h3>
|
||||
|
||||
<h3>{passwordFormMessage}</h3>
|
||||
|
||||
{isCloud && (
|
||||
<>
|
||||
@@ -981,7 +1065,7 @@ const Settings = (props) => {
|
||||
<Paper
|
||||
square
|
||||
style={{
|
||||
borderRadius: theme.palette.borderRadius,
|
||||
borderRadius: theme.palette?.borderRadius,
|
||||
padding: 50,
|
||||
backgroundColor: theme.palette.inputColor,
|
||||
}}
|
||||
@@ -1016,7 +1100,7 @@ const Settings = (props) => {
|
||||
<Paper
|
||||
square
|
||||
style={{
|
||||
borderRadius: theme.palette.borderRadius,
|
||||
borderRadius: theme.palette?.borderRadius,
|
||||
padding: 50,
|
||||
backgroundColor: theme.palette.inputColor,
|
||||
}}
|
||||
|
||||
@@ -24,7 +24,7 @@ const SetAuthentication = (props) => {
|
||||
const [loadFail, setLoadFail] = useState("");
|
||||
const [appAuthentication, setAppAuthentication] = React.useState([]);
|
||||
|
||||
const isCloud = window.location.host === "localhost:3002" || window.location.host === "shuffler.io";
|
||||
const isCloud = (window.location.host === "localhost:3002" || window.location.host === "shuffler.io") ? true : (process.env.IS_SSR === "true");
|
||||
//const alert = useAlert();
|
||||
|
||||
const parseIncomingOpenapiData = (data) => {
|
||||
@@ -142,8 +142,8 @@ const SetAuthentication = (props) => {
|
||||
// 3. Help them set info for the app
|
||||
// Make sure to test both private and public apps
|
||||
|
||||
const appname = app.name !== undefined ? app.name : "";
|
||||
const appLink = "/apps/" + app.id || "";
|
||||
const appname = app.name !== undefined ? app.name : ""
|
||||
const appLink = "/apps/" + app.id || ""
|
||||
|
||||
console.log("App: ", app)
|
||||
|
||||
@@ -154,21 +154,16 @@ const SetAuthentication = (props) => {
|
||||
:
|
||||
<><div>
|
||||
<Typography variant="h4" style={{ marginBottom: 20, }}>
|
||||
A Shuffle Organization has invited you to: Configure <a href={appLink} target="_blank" style={{ color: '#FF8444', textDecoration: 'none' }}>{appname}</a> Authentication
|
||||
You are invited to: Configure <a href={appLink} target="_blank" style={{ color: '#FF8444', textDecoration: 'none' }}>{appname}</a> Authentication
|
||||
</Typography>
|
||||
|
||||
{/* What does this mean box */}
|
||||
<Typography variant="h6" style={{ marginBottom: 20, }}>
|
||||
What does this mean?
|
||||
</Typography>
|
||||
<Typography variant="body1" style={{ marginBottom: 20, }}>
|
||||
<Typography variant="body1" style={{ marginBottom: 20, color: "rgba(255,255,255,0.4)", }}>
|
||||
A Shuffle Organization has invited you to configure authentication for this app so that they can use this authentication in one of their workflows.
|
||||
</Typography>
|
||||
|
||||
<Typography variant="h6">
|
||||
Authenticate Here:
|
||||
</Typography>
|
||||
|
||||
<Typography variant="body1" style={{ marginBottom: 20, }}>
|
||||
{app.authentication === undefined || app.authentication === null || app.authentication.length === 0 ?
|
||||
null
|
||||
@@ -195,7 +190,7 @@ const SetAuthentication = (props) => {
|
||||
appAuthentication={appAuthentication} />}
|
||||
</Typography>
|
||||
|
||||
<Typography variant="h6" style={{ marginBottom: 20, }}>
|
||||
<Typography variant="h6" style={{ marginTop: 50, marginBottom: 20, }}>
|
||||
What can they do with this?
|
||||
</Typography>
|
||||
|
||||
@@ -203,12 +198,11 @@ const SetAuthentication = (props) => {
|
||||
You can check the actions they want to use <a href={appLink} target="_blank" style={{ color: '#FF8444', textDecoration: 'none' }}>here</a>.
|
||||
</Typography>
|
||||
|
||||
<Typography variant="body1" style={{ marginBottom: 20, }}>
|
||||
{/* Add a box below */}
|
||||
<Typography variant="body1" style={{ marginBottom: 20, color: "rgba(255,255,255,0.4)",}}>
|
||||
<div className="collapsible-container">
|
||||
<div className="collapsible-list">
|
||||
{app.actions?.map((item, index) => (
|
||||
<div key={index} className="collapsible-item">
|
||||
<div key={index} className="collapsible-item" style={{cursor: "pointer", }}>
|
||||
<div className="collapsible-label" onClick={() => handleToggle(index)}>
|
||||
{item.label}
|
||||
</div>
|
||||
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
Paper,
|
||||
Chip,
|
||||
Checkbox,
|
||||
Fade,
|
||||
} from "@mui/material";
|
||||
|
||||
import {
|
||||
@@ -62,6 +63,7 @@ import {
|
||||
TreeMapLabel,
|
||||
TreeMapRect,
|
||||
} from 'reaviz';
|
||||
import { isMobile } from "react-device-detect"
|
||||
|
||||
const useStyles = makeStyles({
|
||||
notchedOutline: {
|
||||
@@ -381,7 +383,7 @@ const UsecaseListComponent = (props) => {
|
||||
<Typography variant="h6">
|
||||
{usecase.name}
|
||||
</Typography>
|
||||
<Grid container spacing={3} style={{marginTop: 25}}>
|
||||
<Grid container spacing={3} style={{marginTop: isMobile ? null:25, padding: isMobile?20:null}}>
|
||||
{usecase.list.map((subcase, subindex) => {
|
||||
const selectedItem = subindex === expandedItem && index === expandedIndex
|
||||
|
||||
@@ -426,7 +428,7 @@ const UsecaseListComponent = (props) => {
|
||||
const fixedName = subcase.name.toLowerCase().replace("_", " ")
|
||||
|
||||
return (
|
||||
<Grid id={fixedName} item xs={selectedItem ? 12 : 4} key={subindex} style={{minHeight: 110,}} onClick={() => {
|
||||
<Grid id={fixedName} item xs={isMobile ? 12 : selectedItem ? 12 : 4} key={subindex} style={{minHeight: 110,}} onClick={() => {
|
||||
if (fixedName === "reporting") {
|
||||
getUsecase(subcase, index, subindex)
|
||||
return
|
||||
@@ -439,11 +441,11 @@ const UsecaseListComponent = (props) => {
|
||||
navigate(`/usecases?selected_object=${fixedName}`)
|
||||
}
|
||||
}}>
|
||||
<Paper style={{padding: 25, minHeight: isCloud ? 75 : 122, cursor: !selectedItem ? "pointer" : "default", border: itemBorder, backgroundColor: backgroundColor, borderRadius: theme.palette.borderRadius, }} onClick={() => {
|
||||
<Paper style={{padding: 25, minHeight: isCloud ? 75 : 122, cursor: !selectedItem ? "pointer" : "default", border: itemBorder, backgroundColor: backgroundColor, borderRadius: theme.palette?.borderRadius, }} onClick={() => {
|
||||
}}>
|
||||
{!selectedItem ?
|
||||
<div style={{textAlign: "left", position: "relative",}}>
|
||||
<Typography variant="h6" style={{maxWidth: 215}}>
|
||||
<Typography variant="h6" style={{maxWidth: isMobile? null:215}}>
|
||||
<b>{subcase.name}</b>
|
||||
</Typography>
|
||||
{finished ?
|
||||
@@ -478,7 +480,7 @@ const UsecaseListComponent = (props) => {
|
||||
<IconButton
|
||||
style={{
|
||||
position: "absolute",
|
||||
bottom: -25,
|
||||
bottom: isMobile ? 11 :-20,
|
||||
right: -15,
|
||||
}}
|
||||
onClick={(e) => {
|
||||
@@ -503,7 +505,7 @@ const UsecaseListComponent = (props) => {
|
||||
<IconButton
|
||||
style={{
|
||||
position: "absolute",
|
||||
bottom: -65,
|
||||
bottom: isMobile ? -22 : 10,
|
||||
right: -15,
|
||||
}}
|
||||
onClick={(e) => {
|
||||
@@ -618,7 +620,7 @@ const UsecaseListComponent = (props) => {
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div style={{marginTop: 25, display: "flex", minHeight: 400, maxHeight: 400, marginRight: 15, }}>
|
||||
<div style={{marginTop: isMobile? null:25, padding: isMobile?20:null, width: isMobile?"100%":null, display: isMobile ? null:"flex", minHeight: isMobile ? "auto":400, maxHeight: isMobile ? null:400, marginRight: isMobile?null:15 }}>
|
||||
{editing ?
|
||||
<div style={{flex: 1, marginRight: 50, }}>
|
||||
<TextField
|
||||
@@ -797,7 +799,7 @@ const UsecaseListComponent = (props) => {
|
||||
style={{
|
||||
backgroundColor: theme.palette.inputColor,
|
||||
height: 50,
|
||||
borderRadius: theme.palette.borderRadius,
|
||||
borderRadius: theme.palette?.borderRadius,
|
||||
}}
|
||||
onChange={(event, newValue) => {
|
||||
//handleWorkflowSelectionUpdate({ target: { value: newValue} })
|
||||
@@ -854,7 +856,7 @@ const UsecaseListComponent = (props) => {
|
||||
<Tooltip arrow placement="left" title={
|
||||
<span style={{}}>
|
||||
{data.image !== undefined && data.image !== null && data.image.length > 0 ?
|
||||
<img src={data.image} alt={newname} style={{backgroundColor: theme.palette.surfaceColor, maxHeight: 200, minHeigth: 200, borderRadius: theme.palette.borderRadius, }} />
|
||||
<img src={data.image} alt={newname} style={{backgroundColor: theme.palette.surfaceColor, maxHeight: 200, minHeigth: 200, borderRadius: theme.palette?.borderRadius, }} />
|
||||
: null}
|
||||
<Typography>
|
||||
Choose {newname}
|
||||
@@ -879,7 +881,7 @@ const UsecaseListComponent = (props) => {
|
||||
<TextField
|
||||
style={{
|
||||
backgroundColor: theme.palette.inputColor,
|
||||
borderRadius: theme.palette.borderRadius,
|
||||
borderRadius: theme.palette?.borderRadius,
|
||||
}}
|
||||
{...params}
|
||||
label="Find your workflows"
|
||||
@@ -950,8 +952,8 @@ const UsecaseListComponent = (props) => {
|
||||
target="_blank"
|
||||
style={{ textDecoration: "none", color: "rgba(255,255,255,0.7)", marginRight: 5, }}
|
||||
>
|
||||
<div style={{width: 160, display: "flex", borderRadius: theme.palette.borderRadius, cursor: "pointer", border: highlight ? "2px solid #f86a3e" : "1px solid rgba(255,255,255,0.7)", backgroundColor: theme.palette.inputColor, padding: "0px 0px 15px 15px", overflow: "hidden",}}>
|
||||
<img src={subdata.image} style={{width: 40, height: 40, borderRadius: theme.palette.borderRadius, marginTop: 15, }} />
|
||||
<div style={{width: 160, display: "flex", borderRadius: theme.palette?.borderRadius, cursor: "pointer", border: highlight ? "2px solid #f86a3e" : "1px solid rgba(255,255,255,0.7)", backgroundColor: theme.palette.inputColor, padding: "0px 0px 15px 15px", overflow: "hidden",}}>
|
||||
<img src={subdata.image} style={{width: 40, height: 40, borderRadius: theme.palette?.borderRadius, marginTop: 15, }} />
|
||||
<Typography variant="body1" style={{lineHeight: "95%", marginLeft: 12, marginTop: marginTop === 0 ? 19 : 25, maxHeight: 34, }}>
|
||||
{subdata.name}
|
||||
</Typography>
|
||||
@@ -979,8 +981,9 @@ const UsecaseListComponent = (props) => {
|
||||
}
|
||||
<div style={{
|
||||
height: 350,
|
||||
width: 350,
|
||||
borderRadius: theme.palette.borderRadius,
|
||||
width: isMobile? null:350,
|
||||
marginTop: isMobile ? 50:null,
|
||||
borderRadius: theme.palette?.borderRadius,
|
||||
border: "1px solid rgba(255,255,255,0.3)",
|
||||
padding: 5,
|
||||
backgroundColor: theme.palette.backgroundColor,
|
||||
@@ -1066,8 +1069,8 @@ const RadialChart = ({keys, setSelectedCategory}) => {
|
||||
}}>
|
||||
<RadialAreaChart
|
||||
id="workflow_categories"
|
||||
height={500}
|
||||
width={500}
|
||||
height={isMobile ? 400:500}
|
||||
width={isMobile ? null:500}
|
||||
data={keys}
|
||||
axis={<RadialAxis type="category" />}
|
||||
series={
|
||||
@@ -1110,7 +1113,7 @@ const RadialChart = ({keys, setSelectedCategory}) => {
|
||||
}}
|
||||
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",}}>
|
||||
<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>
|
||||
@@ -1675,15 +1678,15 @@ const Dashboard = (props) => {
|
||||
console.log("USECASES: ", usecases)
|
||||
|
||||
const data =
|
||||
<div className="content" style={{width: 1000, margin: "auto", paddingBottom: 200, textAlign: "center",}}>
|
||||
<div style={{width: 500, margin: "auto"}}>
|
||||
<div className="content" style={{width: isMobile ? "100%": 1000, margin: "auto", paddingBottom: 200, textAlign: "center",}}>
|
||||
<div style={{width: isMobile ? null:500, margin: "auto"}}>
|
||||
{keys.length > 0 ?
|
||||
<RadialChart keys={keys} setSelectedCategory={setSelectedUsecaseCategory} />
|
||||
: null}
|
||||
</div>
|
||||
|
||||
{usecases !== null && usecases !== undefined && usecases.length > 0 ?
|
||||
<div style={{ display: "flex", marginLeft: 180, }}>
|
||||
<div style={{ display: isMobile ? null:"flex", marginLeft: isMobile? null :180, }}>
|
||||
{usecases.map((usecase, index) => {
|
||||
return (
|
||||
<Chip
|
||||
@@ -1693,6 +1696,7 @@ const Dashboard = (props) => {
|
||||
marginRight: 10,
|
||||
paddingLeft: 5,
|
||||
paddingRight: 5,
|
||||
marginTop: isMobile? 10:null,
|
||||
height: 28,
|
||||
cursor: "pointer",
|
||||
border: `1px solid ${usecase.color}`,
|
||||
@@ -1738,9 +1742,11 @@ const Dashboard = (props) => {
|
||||
</div>
|
||||
|
||||
const dataWrapper =
|
||||
<div style={{ maxWidth: 1366, margin: "auto" }}>
|
||||
{data}
|
||||
</div>
|
||||
<Fade in={true} timeout={1250}>
|
||||
<div style={{ maxWidth: 1366, margin: "auto" }}>
|
||||
{data}
|
||||
</div>
|
||||
</Fade>
|
||||
|
||||
|
||||
return dataWrapper
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -32,7 +32,7 @@ const Welcome = (props) => {
|
||||
const [inputUsecase, setInputUsecase] = useState({});
|
||||
const [frameworkData, setFrameworkData] = useState(undefined);
|
||||
const [discoveryWrapper, setDiscoveryWrapper] = useState(undefined);
|
||||
const [activeStep, setActiveStep] = React.useState(1);
|
||||
const [activeStep, setActiveStep] = React.useState(0);
|
||||
const [apps, setApps] = React.useState([]);
|
||||
const [defaultSearch, setDefaultSearch] = React.useState("")
|
||||
const [selectionOpen, setSelectionOpen] = React.useState(false)
|
||||
@@ -47,7 +47,7 @@ const Welcome = (props) => {
|
||||
}
|
||||
}, [activeStep])
|
||||
|
||||
const isCloud = window.location.host === "localhost:3002" || window.location.host === "shuffler.io";
|
||||
const isCloud = (window.location.host === "localhost:3002" || window.location.host === "shuffler.io") ? true : (process.env.IS_SSR === "true");
|
||||
const [steps, setSteps] = useState([
|
||||
"Help us get to know you",
|
||||
"Find your Apps",
|
||||
@@ -285,6 +285,9 @@ const Welcome = (props) => {
|
||||
}
|
||||
|
||||
setActiveStep(foundTab-1)
|
||||
|
||||
//setRenderDone(
|
||||
|
||||
}
|
||||
}
|
||||
}, [])
|
||||
@@ -301,14 +304,14 @@ const Welcome = (props) => {
|
||||
minWidth: isMobile ? null : 275,
|
||||
backgroundColor: theme.palette.surfaceColor,
|
||||
color: "white",
|
||||
borderRadius: theme.palette.borderRadius,
|
||||
borderRadius: theme.palette?.borderRadius,
|
||||
}
|
||||
|
||||
const actionObject = {
|
||||
padding: "25px",
|
||||
maxHeight: 280,
|
||||
minHeight: 280,
|
||||
borderRadius: theme.palette.borderRadius,
|
||||
borderRadius: theme.palette?.borderRadius,
|
||||
}
|
||||
|
||||
const imageStyle = {
|
||||
@@ -337,7 +340,7 @@ const Welcome = (props) => {
|
||||
const defaultImage = "/images/experienced.png"
|
||||
const experienced_image = userdata !== undefined && userdata !== null && userdata.active_org !== undefined && userdata.active_org.image !== undefined && userdata.active_org.image !== null && userdata.active_org.image !== "" ? userdata.active_org.image : defaultImage
|
||||
return (
|
||||
<div style={{ margin: "auto", paddingBottom: 150, minHeight: 1500, marginTop: 50, }}>
|
||||
<div style={{ margin: "auto", paddingBottom: 150, minHeight: 1500, paddingTop: 50, }}>
|
||||
{/*
|
||||
<div style={{position: "fixed", bottom: 110, right: 110, display: "flex", }}>
|
||||
<img src="/images/Arrow.png" style={{width: 250, height: "100%",}} />
|
||||
@@ -351,7 +354,7 @@ const Welcome = (props) => {
|
||||
color="primary"
|
||||
style={{
|
||||
backgroundColor: theme.palette.platformColor,
|
||||
borderRadius: theme.palette.borderRadius,
|
||||
borderRadius: theme.palette?.borderRadius,
|
||||
padding: 12,
|
||||
border: "1px solid rgba(255,255,255,0.3)",
|
||||
maxWidth: 500,
|
||||
@@ -398,6 +401,7 @@ const Welcome = (props) => {
|
||||
discoveryWrapper={discoveryWrapper}
|
||||
setDiscoveryWrapper={setDiscoveryWrapper}
|
||||
appFramework={frameworkData}
|
||||
setAppFramework={setFrameworkData}
|
||||
getFramework={getFramework}
|
||||
steps={steps}
|
||||
skipped={skipped}
|
||||
@@ -430,7 +434,7 @@ const Welcome = (props) => {
|
||||
We will use this information to personalize your automation
|
||||
</Typography> */}
|
||||
<div style={{display: isMobile ? null : "flex", marginTop: 70, width: isMobile ? 280 : 700, margin: "auto",}}>
|
||||
<div style={{border: "2px solid #806BFF", borderRadius: theme.palette.borderRadius, }}>
|
||||
<div style={{border: "2px solid #806BFF", borderRadius: theme.palette?.borderRadius, }}>
|
||||
<Card style={paperObject} onClick={() => {
|
||||
if (isCloud) {
|
||||
ReactGA.event({
|
||||
@@ -462,7 +466,7 @@ const Welcome = (props) => {
|
||||
OR
|
||||
</Typography> */}
|
||||
</div>
|
||||
<Card style={paperObject} onClick={() => {
|
||||
<Card disabled style={paperObject} onClick={() => {
|
||||
if (isCloud) {
|
||||
ReactGA.event({
|
||||
category: "welcome",
|
||||
@@ -474,7 +478,7 @@ const Welcome = (props) => {
|
||||
navigate("/workflows?message=Skipped intro")
|
||||
}}>
|
||||
<CardActionArea style={actionObject}>
|
||||
<img src={experienced_image} style={{padding: experienced_image === defaultImage ? 2 : 10, objectFit: "scale-down", minHeight: experienced_image === defaultImage ? 40 : 70, maxHeight: experienced_image === defaultImage ? 40 : 70, bordeRadius: theme.palette.borderRadius*2, marginBottom: experienced_image === defaultImage ? 24 : 2 }} />
|
||||
<img src={experienced_image} style={{padding: experienced_image === defaultImage ? 2 : 10, objectFit: "scale-down", minHeight: experienced_image === defaultImage ? 40 : 70, maxHeight: experienced_image === defaultImage ? 40 : 70, bordeRadius: theme.palette?.borderRadius*2, marginBottom: experienced_image === defaultImage ? 24 : 2 }} />
|
||||
<Typography variant="h4" style={{color: "#F1F1F1"}}>
|
||||
Experienced
|
||||
</Typography>
|
||||
|
||||
+1738
-1511
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user