Merge branch '2.0.0'
@@ -1,6 +1,8 @@
|
||||
# Build environment
|
||||
FROM node:21 as builder
|
||||
|
||||
ENV NODE_OPTIONS="--max-old-space-size=4096"
|
||||
|
||||
RUN mkdir /usr/src/app
|
||||
WORKDIR /usr/src/app
|
||||
ENV PATH /usr/src/app/node_modules/.bin:$PATH
|
||||
@@ -11,7 +13,7 @@ COPY package.json /usr/src/app/package.json
|
||||
#RUN yarn config set "strict-ssl" false -g
|
||||
#RUN yarn install --network-timeout 1000000
|
||||
|
||||
RUN npm install --legacy-peer-deps
|
||||
RUN npm install --timeout=60000 --legacy-peer-deps
|
||||
|
||||
# copy only required files to not trigger rebuilding every time
|
||||
COPY ./certs /usr/src/app/certs/
|
||||
@@ -25,28 +27,33 @@ COPY ./*.json /usr/src/app/
|
||||
RUN npm run build --loglevel verbose 2>&1
|
||||
|
||||
# Production environment
|
||||
FROM nginx:1.21.5
|
||||
FROM nginx:1.26.0
|
||||
|
||||
RUN mkdir -p /usr/share/nginx/html/build
|
||||
RUN mkdir -p /usr/share/nginx/html/css
|
||||
RUN mkdir -p /usr/share/nginx/html/js
|
||||
RUN mkdir -p /usr/share/nginx/html/img
|
||||
|
||||
COPY --from=builder /usr/src/app/build /usr/share/nginx/html
|
||||
|
||||
#Localhost certificate challenge: Y#XwrJ#DoZGz2w6x
|
||||
# Localhost certificate challenge: Y#XwrJ#DoZGz2w6x
|
||||
# Cert challenge doesn't matter to be here or not, as ALL production setups should be using their own certificates + reverse proxy: https://shuffler.io/docs/configuration#using-the-nginx-reverse-proxy-for-tls/ssl
|
||||
COPY --from=builder /usr/src/app/build /usr/share/nginx/html
|
||||
COPY --from=builder /usr/src/app/certs/fullchain.pem /etc/nginx/fullchain.cert.pem
|
||||
COPY --from=builder /usr/src/app/certs/privkey.pem /etc/nginx/privkey.pem
|
||||
|
||||
# install CONFD
|
||||
ENV CONFD_VERSION 0.16.0
|
||||
RUN apt-get update && apt-get install -y curl && apt-get clean
|
||||
RUN curl -sSL https://github.com/kelseyhightower/confd/releases/download/v${CONFD_VERSION}/confd-${CONFD_VERSION}-linux-amd64 -o /usr/local/bin/confd && \
|
||||
chmod +x /usr/local/bin/confd
|
||||
COPY ./confd /etc/confd
|
||||
COPY ./confd/templates/nginx.conf /etc/nginx/nginx.conf.tmpl
|
||||
|
||||
## OLD CONFD THINGS (not compatible with arm)
|
||||
#ENV CONFD_VERSION 0.16.0
|
||||
#RUN curl -sSL https://github.com/kelseyhightower/confd/releases/download/v${CONFD_VERSION}/confd-${CONFD_VERSION}-linux-amd64 -o /usr/local/bin/confd && \
|
||||
# chmod +x /usr/local/bin/confd
|
||||
#COPY ./confd /etc/confd
|
||||
# rewrite command & entrypoint with ours
|
||||
|
||||
COPY ./entrypoint.sh /
|
||||
ENV BACKEND_HOSTNAME="shuffle-backend"
|
||||
ENTRYPOINT [ "/entrypoint.sh" ]
|
||||
CMD ["nginx", "-g", "daemon off;"]
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
## Lalits frontend magic
|
||||
|
||||
## Localhost Certificate info:
|
||||
|
||||
|
||||
|
||||
@@ -71,7 +71,7 @@ http {
|
||||
}
|
||||
|
||||
location ~ /api/v(1|2) {
|
||||
proxy_pass http://{{ getenv "BACKEND_HOSTNAME" "shuffle-backend" }}:5001;
|
||||
proxy_pass http://${BACKEND_HOSTNAME}:5001;
|
||||
proxy_buffering off;
|
||||
proxy_http_version 1.1;
|
||||
|
||||
@@ -113,7 +113,8 @@ http {
|
||||
|
||||
# Get the hostname from environment here?
|
||||
location ~ /api/v(1|2) {
|
||||
proxy_pass http://{{ getenv "BACKEND_HOSTNAME" "shuffle-backend" }}:5001;
|
||||
proxy_pass http://${BACKEND_HOSTNAME}:5001;
|
||||
|
||||
proxy_buffering off;
|
||||
proxy_http_version 1.1;
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
#!/bin/bash
|
||||
#!/usr/bin/env sh
|
||||
set -eu
|
||||
|
||||
# generate configs
|
||||
/usr/local/bin/confd -backend="env" -confdir="/etc/confd" -onetime
|
||||
envsubst '${BACKEND_HOSTNAME}' < /etc/nginx/nginx.conf.tmpl > /etc/nginx/nginx.conf
|
||||
|
||||
# run main command
|
||||
exec "$@"
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
{
|
||||
"name": "shuffler",
|
||||
"homepage": "https://shuffler.io",
|
||||
"version": "1.4.0",
|
||||
"version": "2.0.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@babel/plugin-proposal-class-properties": "^7.18.6",
|
||||
"@codemirror/commands": "^6.2.4",
|
||||
"@codemirror/lang-python": "^6.1.3",
|
||||
"@emotion/react": "^11.11.1",
|
||||
@@ -13,7 +12,7 @@
|
||||
"@metamask/detect-provider": "^1.2.0",
|
||||
"@mui/icons-material": "^5.14.0",
|
||||
"@mui/material": "^5.14.0",
|
||||
"@mui/styles": "^5.14.0",
|
||||
"@mui/styles": "^6.1.4",
|
||||
"@mui/x-data-grid": "^5.17.11",
|
||||
"@mui/x-date-pickers": "^6.11.1",
|
||||
"@types/algoliasearch": "^3.34.11",
|
||||
@@ -21,7 +20,6 @@
|
||||
"@uiw/codemirror-theme-vscode": "^4.21.20",
|
||||
"@uiw/codemirror-themes": "^4.21.9",
|
||||
"@uiw/react-codemirror": "^4.21.21",
|
||||
"@use-it/interval": "^0.1.3",
|
||||
"algoliasearch": "^4.8.3",
|
||||
"class-transformer": "^0.2.0",
|
||||
"codemirror": "^6.0.1",
|
||||
@@ -49,7 +47,6 @@
|
||||
"i18next-localstorage-backend": "^4.1.0",
|
||||
"i18next-xhr-backend": "^3.2.2",
|
||||
"import": "0.0.6",
|
||||
"interweave": "^11.2.0",
|
||||
"is-plain-obj": "^4.1.0",
|
||||
"json-bigint": "^1.0.0",
|
||||
"match-sorter": "^6.3.1",
|
||||
@@ -58,12 +55,12 @@
|
||||
"moment": "~2.29.4",
|
||||
"mui-chips-input": "^2.1.3",
|
||||
"mui-nested-menu": "^3.2.1",
|
||||
"react": "^18.2.0",
|
||||
"react": "^18.3.1",
|
||||
"react-ace": "^10.1.0",
|
||||
"react-alice-carousel": "^2.6.4",
|
||||
"react-avatar-editor": "^11.1.0",
|
||||
"react-beforeunload": "^2.2.1",
|
||||
"react-chartjs-2": "^2.11.1",
|
||||
"react-chartjs-2": "^2.11.2",
|
||||
"react-cookie": "^4.0.1",
|
||||
"react-cytoscapejs": "^2.0.0",
|
||||
"react-device-detect": "^2.2.3",
|
||||
@@ -73,21 +70,18 @@
|
||||
"react-dropzone": "^14.2.3",
|
||||
"react-ga4": "^2.0.0",
|
||||
"react-hotkeys": "^2.0.0",
|
||||
"react-i18next": "^13.1.2",
|
||||
"react-instantsearch-dom": "^6.28.0",
|
||||
"react-json-pretty": "^2.2.0",
|
||||
"react-json-view": "^1.21.3",
|
||||
"react-json-view-ssr": "^1.19.1",
|
||||
"react-markdown": "^8.0.7",
|
||||
"react-markdown-github": "^3.3.1",
|
||||
"react-powerhooks": "^0.0.7",
|
||||
"react-router": "^6.14.1",
|
||||
"react-router-dom": "^6.14.1",
|
||||
"react-scripts": "^5.0.1",
|
||||
"react-social-icons": "^5.15.0",
|
||||
"react-stripe-elements": "^6.1.2",
|
||||
"react-toastify": "^9.1.3",
|
||||
"reaviz": "^14.9.7",
|
||||
"rehype-raw": "^7.0.0",
|
||||
"remark-gfm": "^3.0.1",
|
||||
"remark-html": "^16.0.1",
|
||||
"remark-images": "^4.0.0",
|
||||
@@ -134,7 +128,6 @@
|
||||
"babel-preset-es2015": "^6.24.1",
|
||||
"postcss": "^8.4.38",
|
||||
"promise-window": "^1.2.1",
|
||||
"react-hot-loader": "^4.13.0",
|
||||
"webpack-cli": "^5.1.4"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="24" height="24" fill="#212121" fill-opacity="0.02"/>
|
||||
<path d="M14 4H7.6C7.17565 4 6.76869 4.16857 6.46863 4.46863C6.16857 4.76869 6 5.17565 6 5.6V18.4C6 18.8243 6.16857 19.2313 6.46863 19.5314C6.76869 19.8314 7.17565 20 7.6 20H17.2C17.6243 20 18.0313 19.8314 18.3314 19.5314C18.6314 19.2313 18.8 18.8243 18.8 18.4V8.8L14 4Z" stroke="#F1F1F1" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M14 4V8.8H18.8" stroke="#F1F1F1" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 598 B |
@@ -0,0 +1,4 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M5 7.20001H6.6H19.4" stroke="#FD4C62" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M17.7996 7.2V18.4C17.7996 18.8243 17.631 19.2313 17.331 19.5314C17.0309 19.8314 16.624 20 16.1996 20H8.19961C7.77526 20 7.3683 19.8314 7.06824 19.5314C6.76818 19.2313 6.59961 18.8243 6.59961 18.4V7.2M8.99961 7.2V5.6C8.99961 5.17565 9.16818 4.76869 9.46824 4.46863C9.7683 4.16857 10.1753 4 10.5996 4H13.7996C14.224 4 14.6309 4.16857 14.931 4.46863C15.231 4.76869 15.3996 5.17565 15.3996 5.6V7.2" stroke="#FD4C62" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 669 B |
@@ -0,0 +1,4 @@
|
||||
<svg width="16" height="11" viewBox="0 0 16 11" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M7.99984 6.83329C8.73622 6.83329 9.33317 6.23634 9.33317 5.49996C9.33317 4.76358 8.73622 4.16663 7.99984 4.16663C7.26346 4.16663 6.6665 4.76358 6.6665 5.49996C6.6665 6.23634 7.26346 6.83329 7.99984 6.83329Z" stroke="#C8C8C8" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M10.8269 2.67329C11.1988 3.04478 11.4938 3.48593 11.6951 3.97153C11.8964 4.45712 12 4.97763 12 5.50329C12 6.02895 11.8964 6.54946 11.6951 7.03505C11.4938 7.52064 11.1988 7.9618 10.8269 8.33329M5.17354 8.32662C4.80163 7.95513 4.5066 7.51398 4.3053 7.02838C4.104 6.54279 4.00039 6.02228 4.00039 5.49662C4.00039 4.97096 4.104 4.45045 4.3053 3.96486C4.5066 3.47927 4.80163 3.03811 5.17354 2.66662M12.7135 0.786621C13.9633 2.03681 14.6654 3.73219 14.6654 5.49995C14.6654 7.26772 13.9633 8.9631 12.7135 10.2133M3.28687 10.2133C2.03706 8.9631 1.33496 7.26772 1.33496 5.49995C1.33496 3.73219 2.03706 2.03681 3.28687 0.786621" stroke="#C8C8C8" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<svg width="800px" height="800px" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="512" cy="512" r="512" style="fill:#0091e2"/>
|
||||
<path d="M827.3 461.5c-1.6-1.3-16.1-12.2-46.7-12.2-8.1 0-16.2.6-24.2 2.1-5.9-40.7-39.5-60.5-41-61.4l-8.2-4.8-5.4 7.8c-6.8 10.5-11.7 22-14.6 34.2-5.5 23.2-2.2 45 9.6 63.6-14.2 7.9-37.1 9.9-41.7 10H277c-9.9 0-17.9 8-17.9 17.9-.4 33.1 5.2 66 16.5 97.1 13 34.2 32.4 59.3 57.6 74.7 28.2 17.3 74.1 27.2 126.2 27.2 23.5.1 47-2.1 70.1-6.4 32.1-5.9 63-17.1 91.4-33.2 23.4-13.6 44.5-30.8 62.4-51.1 29.9-33.9 47.8-71.7 61.1-105.2h5.3c32.8 0 53-13.1 64.1-24.1 7.4-7 13.2-15.5 16.9-25l2.3-6.9-5.7-4.3zM312 489.9h50.7c2.4 0 4.4-2 4.4-4.4v-45.1c0-2.4-2-4.4-4.4-4.5H312c-2.4 0-4.4 2-4.4 4.4v45.2c0 2.5 2 4.4 4.4 4.4m69.9 0h50.7c2.4 0 4.4-2 4.4-4.4v-45.1c0-2.4-2-4.4-4.4-4.5h-50.7c-2.5 0-4.5 2-4.5 4.5v45.1c0 2.5 2 4.4 4.5 4.4m70.8.1h50.7c2.4 0 4.4-2 4.4-4.4v-45.1c0-2.4-2-4.4-4.4-4.5h-50.7c-2.4 0-4.4 2-4.4 4.4v45.2c0 2.4 2 4.3 4.4 4.4m70.1 0h50.7c2.4 0 4.4-2 4.5-4.4v-45.1c0-2.5-2-4.5-4.5-4.5h-50.7c-2.4 0-4.4 2-4.4 4.4v45.2c0 2.4 1.9 4.4 4.4 4.4m-141-65h50.7c2.4 0 4.4-2 4.4-4.5v-45.1c0-2.4-2-4.4-4.4-4.4h-50.7c-2.5 0-4.4 2-4.5 4.4v45.1c.1 2.5 2.1 4.5 4.5 4.5m70.9 0h50.7c2.4 0 4.4-2 4.4-4.5v-45.1c0-2.4-2-4.4-4.4-4.4h-50.7c-2.4 0-4.4 2-4.4 4.4v45.1c0 2.5 2 4.5 4.4 4.5m70.1 0h50.7c2.5 0 4.4-2 4.5-4.5v-45.1c0-2.5-2-4.4-4.5-4.4h-50.7c-2.4 0-4.4 2-4.4 4.4v45.1c0 2.5 1.9 4.5 4.4 4.5m0-64.9h50.7c2.5 0 4.5-2 4.5-4.5v-45.2c0-2.4-2-4.4-4.5-4.4h-50.7c-2.4 0-4.4 2-4.4 4.4v45.2c0 2.5 1.9 4.5 4.4 4.5M593.4 490h50.7c2.4 0 4.4-2 4.4-4.4v-45.1c0-2.5-2-4.4-4.4-4.5h-50.7c-2.4 0-4.4 2-4.4 4.4v45.2c0 2.4 2 4.4 4.4 4.4" style="fill:#fff"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.8 KiB |
@@ -0,0 +1,4 @@
|
||||
<svg width="14" height="17" viewBox="0 0 14 17" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M7.75 1H2.5C2.10218 1 1.72064 1.15804 1.43934 1.43934C1.15804 1.72064 1 2.10218 1 2.5V14.5C1 14.8978 1.15804 15.2794 1.43934 15.5607C1.72064 15.842 2.10218 16 2.5 16H11.5C11.8978 16 12.2794 15.842 12.5607 15.5607C12.842 15.2794 13 14.8978 13 14.5V6.25L7.75 1Z" stroke="#C8C8C8" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M7.75 1V6.25H13" stroke="#C8C8C8" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 569 B |
@@ -0,0 +1,6 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="24" height="24" fill="#212121" fill-opacity="0.02"/>
|
||||
<path d="M8.22559 16.4467L11.7788 20L15.3321 16.4467" stroke="#F1F1F1" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M11.7793 12.0052V20" stroke="#F1F1F1" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M19.6676 17.415C20.4399 16.8719 21.019 16.0968 21.321 15.2023C21.6229 14.3078 21.632 13.3403 21.3468 12.4403C21.0617 11.5402 20.4971 10.7545 19.7352 10.197C18.9732 9.6396 18.0534 9.33948 17.1092 9.34021H15.99C15.7228 8.299 15.2229 7.33196 14.5279 6.5119C13.8329 5.69184 12.961 5.04013 11.9777 4.60583C10.9944 4.17153 9.92534 3.96596 8.85109 4.00459C7.77684 4.04322 6.72535 4.32505 5.77578 4.82886C4.82621 5.33267 4.00331 6.04534 3.36902 6.9132C2.73474 7.78106 2.30559 8.78151 2.11391 9.83922C1.92222 10.8969 1.97297 11.9844 2.26236 13.0196C2.55174 14.0549 3.07221 15.011 3.78459 15.816" stroke="#F1F1F1" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.0 KiB |
@@ -0,0 +1,3 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M16.1038 4.66848C16.3158 4.45654 16.5674 4.28843 16.8443 4.17373C17.1212 4.05903 17.418 4 17.7177 4C18.0174 4 18.3142 4.05903 18.5911 4.17373C18.868 4.28843 19.1196 4.45654 19.3315 4.66848C19.5435 4.88041 19.7116 5.13201 19.8263 5.40891C19.941 5.68582 20 5.9826 20 6.28232C20 6.58204 19.941 6.87882 19.8263 7.15573C19.7116 7.43263 19.5435 7.68423 19.3315 7.89617L8.43807 18.7896L4 20L5.21038 15.5619L16.1038 4.66848Z" stroke="#F1F1F1" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 596 B |
@@ -0,0 +1,4 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="12" cy="12" r="12" transform="matrix(-1 0 0 1 24 0)" fill="#2F2F2F"/>
|
||||
<path d="M8 6L14 12L8 18" stroke="#F1F1F1" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 296 B |
@@ -0,0 +1,3 @@
|
||||
<svg width="22" height="20" viewBox="0 0 22 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M21 10H17L14 19L8 1L5 10H1" stroke="#F1F1F1" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 225 B |
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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 />
|
||||
|
||||
@@ -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)">
|
||||
|
||||
@@ -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")
|
||||
|
||||
|
||||
@@ -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;
|
||||
@@ -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,
|
||||
},
|
||||
[],
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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", }}>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||