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

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

View File

@ -0,0 +1,54 @@
# Build environment
FROM node:18 as builder
RUN mkdir /usr/src/app
WORKDIR /usr/src/app
ENV PATH /usr/src/app/node_modules/.bin:$PATH
COPY package.json /usr/src/app/package.json
# Nocache yarn install
RUN yarn config set "strict-ssl" false -g
RUN yarn install --network-timeout 1000000
#RUN npm install
# copy only required files to not trigger rebuilding every time
COPY ./certs /usr/src/app/certs/
COPY ./public /usr/src/app/public/
COPY ./src /usr/src/app/src/
COPY ./*.sh /usr/src/app/
COPY ./*.json /usr/src/app/
#RUN rm -rf /usr/src/app/node_modules/webpack
RUN yarn build
# Production environment
FROM nginx:1.21.5
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
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
# rewrite command & entrypoint with ours
COPY ./entrypoint.sh /
ENTRYPOINT [ "/entrypoint.sh" ]
CMD ["nginx", "-g", "daemon off;"]
EXPOSE 80
EXPOSE 443

View File

@ -0,0 +1,20 @@
## Localhost Certificate info:
Creating a localhost certificate:
```
openssl genrsa -out privkey.pem 2048
openssl req -new -key privkey.pem -out certreq.csr
openssl x509 -req -days 3650 -in certreq.csr -signkey privkey.pem -out fullchain.pem
```
## Using your own certificate
If you have your own .crt and .key file, you can do it like this:
```
openssl x509 -in mycert.crt -out fullchain.cert.pem -outform PEM
```
The KEY file has to be named privkey.pem
```
mv cert.key privkey.pem
```

View File

@ -0,0 +1,3 @@
npm run build
rm -rf ../backend/go-app/build
cp -r build/ ../backend/go-app/build

View File

@ -0,0 +1,19 @@
-----BEGIN CERTIFICATE REQUEST-----
MIIDBTCCAe0CAQAwgYYxCzAJBgNVBAYTAkpQMQ4wDAYDVQQIDAVUb2t5bzEOMAwG
A1UEBwwFVG9reW8xEDAOBgNVBAoMB1NodWZmbGUxEDAOBgNVBAsMB1NodWZmbGUx
EDAOBgNVBAMMB1NodWZmbGUxITAfBgkqhkiG9w0BCQEWEmZyaWtreUBzaHVmZmxl
ci5pbzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANLYQupj6hNpfKzy
km3qFkTvuPFed2s6HkBBqKwgpZ6C7wdDuUrMd9SXguw9yhtgxezg+ui1O735ZL4W
6ZpT/Pf3Staj7SHgXAGDnPwcTeWgbd9rZO02UlD70Bhj6SBKYdS8FHRFJvbCe6V0
tY+pJhaTUogwPr1jJTvuHIqRhXHmUl88jVkMIj27FPTnUDEEbr+E8Q6LDYUfSxpw
beZqpv8FRGP88H+WGb/CnbFsvJMbNkHbM2Wj9b6yYiHuw8WRuTODX5YOVefGSYn/
h3JgniT2AdQAsacdB3avdE/b4Mq1FUVqsYaUGSXZ+Iysdo9/7zhYmb0UtaFMSQDA
zsOlIrMCAwEAAaA5MBYGCSqGSIb3DQEJAjEJDAdTaHVmZmxlMB8GCSqGSIb3DQEJ
BzESDBBZI1h3ckojRG9aR3oydzZ4MA0GCSqGSIb3DQEBCwUAA4IBAQCinafabm9w
EqhWrgi0lXQ+iqcklvigSegaEZx5g8lr4qYADPqRWRnzZcq8Dzifx6HUsDs6Dv6/
7INF6cwGxvPFv2CBEvS/KkPtWxHaoATxRxwGw9Vh88OQy/cE6/Jiu8t94QtGMLRL
V0bUnLfc3OKqNM5YvRf8Z1jbO+tzZ4ul3lApHF7UGq01BIcna0D4JblnwP62M4MW
vY++wzYqqsHAOrUN94tJQaTP1nc4u03lme+4duKY7G5yxMYn1A0aDp0IFeDvela9
nylRCM3V5FK1aubXuQnVYoFnnwaM2hTBVTKJbaCCWQhM0e3XYd8mCQYmgf+qlXBN
OG4enppZCsAJ
-----END CERTIFICATE REQUEST-----

View File

@ -0,0 +1,21 @@
-----BEGIN CERTIFICATE-----
MIIDijCCAnICCQCJ9DUcfuAjwzANBgkqhkiG9w0BAQsFADCBhjELMAkGA1UEBhMC
SlAxDjAMBgNVBAgMBVRva3lvMQ4wDAYDVQQHDAVUb2t5bzEQMA4GA1UECgwHU2h1
ZmZsZTEQMA4GA1UECwwHU2h1ZmZsZTEQMA4GA1UEAwwHU2h1ZmZsZTEhMB8GCSqG
SIb3DQEJARYSZnJpa2t5QHNodWZmbGVyLmlvMB4XDTIwMDUyMzA4MjA1MVoXDTMw
MDUyMTA4MjA1MVowgYYxCzAJBgNVBAYTAkpQMQ4wDAYDVQQIDAVUb2t5bzEOMAwG
A1UEBwwFVG9reW8xEDAOBgNVBAoMB1NodWZmbGUxEDAOBgNVBAsMB1NodWZmbGUx
EDAOBgNVBAMMB1NodWZmbGUxITAfBgkqhkiG9w0BCQEWEmZyaWtreUBzaHVmZmxl
ci5pbzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANLYQupj6hNpfKzy
km3qFkTvuPFed2s6HkBBqKwgpZ6C7wdDuUrMd9SXguw9yhtgxezg+ui1O735ZL4W
6ZpT/Pf3Staj7SHgXAGDnPwcTeWgbd9rZO02UlD70Bhj6SBKYdS8FHRFJvbCe6V0
tY+pJhaTUogwPr1jJTvuHIqRhXHmUl88jVkMIj27FPTnUDEEbr+E8Q6LDYUfSxpw
beZqpv8FRGP88H+WGb/CnbFsvJMbNkHbM2Wj9b6yYiHuw8WRuTODX5YOVefGSYn/
h3JgniT2AdQAsacdB3avdE/b4Mq1FUVqsYaUGSXZ+Iysdo9/7zhYmb0UtaFMSQDA
zsOlIrMCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAix08M+pdohlyL9/zeR20mGKp
njmJrako1vBrV2tTb2p/nv7GJ9jP6NPrsVMlphe/Eu0iezTF44GWa1tUI3Y1zN7K
4a21QGkHIyOUBy/uQCrRqM/C6kEP/vBheebCEtGxmF+J81aDBx9IOOoAFSWOjMHN
Yx1iSk4DMp5kfLhxtui7rlSrVqt9Xrm1i/aUly7BuYIvT42J6tnpEF0g/1phnudz
N0bgSb29+PtzyqbqPhVzxaUT4T9vD3qLIbiDSi1I5Zx1vX1LI222ceR1zrIWU+l7
X1p/CBkFpORaK3NCK7y3L8gReO6kO1eE81WR0993m1pwdmxoOiVlIQ7R1cfYGg==
-----END CERTIFICATE-----

View File

@ -0,0 +1,27 @@
-----BEGIN RSA PRIVATE KEY-----
MIIEowIBAAKCAQEA0thC6mPqE2l8rPKSbeoWRO+48V53azoeQEGorCClnoLvB0O5
Ssx31JeC7D3KG2DF7OD66LU7vflkvhbpmlP89/dK1qPtIeBcAYOc/BxN5aBt32tk
7TZSUPvQGGPpIEph1LwUdEUm9sJ7pXS1j6kmFpNSiDA+vWMlO+4cipGFceZSXzyN
WQwiPbsU9OdQMQRuv4TxDosNhR9LGnBt5mqm/wVEY/zwf5YZv8KdsWy8kxs2Qdsz
ZaP1vrJiIe7DxZG5M4Nflg5V58ZJif+HcmCeJPYB1ACxpx0Hdq90T9vgyrUVRWqx
hpQZJdn4jKx2j3/vOFiZvRS1oUxJAMDOw6UiswIDAQABAoIBACvEQH+vJdPJvduY
rtSqFt1Qda+E0H0tn0HvXzf7vuVcgImdgUUJlIZIvSCU4vMz72HwgaT0meYhcswS
rYMflA9VAe/0LzEtBWw7Ccc7iN/1oVkTTev/rq6o1tV5R9cwGYazU/ueryvhyxDZ
XSbpEcL16dfjS+K8RepezwXklzLBIB6wfZ0aulbCoNYcqtD5Jyo4SLzqYPH95cQi
nN09xSnrS3tc9tMj0R2/5avGWEKUHXqKam+NIEc7y6AQJ/qjDaFn13emIB1mDVls
DiEwAyQBOW+1m77raFYFWCuHe30QgdP7SdB0I8XbPATUssnLLfBPj3K9Hh17n/vx
zAJKkiECgYEA8/EpPTpimOmOM3XsaWvFVbnWSFLAEXgn5c0Ch+uIDHr9DbYufn6K
hVXqgTcO83qVLyRD4WIJ7lgqKajHmCxXB4RsyCYaP80jKW0cYAD8sBokWfQTtbry
+Ka45AEPt1Mp1w45vry4RO4HFhfcFwxGRqADZE5Dtg1JNttkDM5FCW8CgYEA3URJ
upl/6+g3k7njxoXxfV8doS8NCdGF9yQsUVITaOrYUHJ71K8GOwlATG6y6uAjRRb4
GgzLCL+nGoSF79r1vWrwfMz1q+d8nBh+v+uqwS4KFSbs2AICVAkjYnNi3/3BAhBn
tmzcbUigyx7qRhlbFZk1OQCuzw3YNqk0kDO6MP0CgYApcl0eYRAliPE3Px723m+9
3ABTc3Pcw/yLZ+S5MUSBUlgyfzSxG1DvzKQ2ZiNtLPOx+chqv9yOGX64a0vWSBpV
VaOh8g9drb3+qOI8UY6dYSOyAO1kYCouIy2g16lS7ZdbSbh39tqcI5EiqNUlOVmr
YD6TSVTp1qIM5wO9xUInkwKBgD6H4vI6GR25NaOpAAcFqXaN39jCbEPfE6YBcgjV
UijvXYx2nipAAFnExogTLLsV9sG6uQjbnrFtQDNNSnC7h4EtbKNIZRFczSlr/r4M
QuhvM2hA5OQyxSesoXRcOZAlrVsA+d5jK3Qy90YQCZMf7U7QSms+lyhquDTSYslx
5OedAoGBAI/6DX5lNUNBcMNpVr/VnS4/A87Vj9vKW2kfTUNehbfSC1ix8oAdlsJF
UfzdH4KOLP80tOUn1L7d4agxqZ+SWomLZ/RVfiMhaiVxQLH+xTbNFObrvePnAXy/
1Bu/wbF4TGlPfcugYecRV1MhKV0uGRD/OQCqRQBUENJ7zMdCGstl
-----END RSA PRIVATE KEY-----

View File

@ -0,0 +1,9 @@
[template]
src = "nginx.conf"
dest = "/etc/nginx/nginx.conf"
uid = 0
gid = 0
mode = "0644"
keys = [
"/",
]

View File

@ -0,0 +1,125 @@
user nobody nogroup;
worker_processes auto; # auto-detect number of logical CPU cores
events {
worker_connections 512; # set the max number of simultaneous connections (per worker process)
}
http {
client_max_body_size 250M;
include mime.types;
# thanks stackoverflow http://stackoverflow.com/a/5132440/2406040
gzip on;
gzip_http_version 1.1;
gzip_vary on;
gzip_comp_level 6;
gzip_proxied any;
gzip_types text/plain text/css application/json application/javascript application/x-javascript text/javascript text/xml application/xml application/rss+xml application/atom+xml application/rdf+xml;
# make sure gzip does not lose large gzipped js or css files
# see http://blog.leetsoft.com/2007/07/25/nginx-gzip-ssl.html
gzip_buffers 16 8k;
# Disable gzip for certain browsers.
gzip_disable "MSIE [1-6].(?!.*SV1)";
server {
listen 80;
server_name "localhost";
#location /static/js/* {
# # avoid clickjacking
# add_header X-Frame-Options DENY;
# add_header X-Content-Type-Options nosniff;
# add_header ;
# # block MIME sniffing
# # security headers
# add_header X-XSS-Protection "1; mode=block";
# # add_header Content-Security-Policy "default-src 'self'";
# add_header Referrer-Policy "no-referrer";
# server_tokens off;
# root /usr/share/nginx/html;
# gzip_static on;
# expires 1y;
# add_header Cache-Control public;
# add_header ETag "";
# try_files $uri /index.html;
#}
location / {
# avoid clickjacking
add_header X-Frame-Options DENY;
# block MIME sniffing
add_header X-Content-Type-Options nosniff;
# security headers
add_header X-XSS-Protection "1; mode=block";
# add_header Content-Security-Policy "default-src 'self'";
add_header Referrer-Policy "no-referrer";
server_tokens off;
root /usr/share/nginx/html;
gzip_static on;
expires 1y;
add_header Cache-Control public;
add_header ETag "";
try_files $uri /index.html;
}
location ~ /api/v(1|2) {
proxy_pass http://{{ getenv "BACKEND_HOSTNAME" "shuffle-backend" }}:5001;
proxy_buffering off;
proxy_http_version 1.1;
proxy_connect_timeout 900;
proxy_send_timeout 900;
proxy_read_timeout 900;
send_timeout 900;
}
}
server {
listen 443 ssl;
server_name "localhost";
ssl_certificate fullchain.cert.pem;
ssl_certificate_key privkey.pem;
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
ssl_ciphers HIGH:!aNULL:!MD5;
location / {
# avoid clickjacking
add_header X-Frame-Options DENY;
# block MIME sniffing
add_header X-Content-Type-Options nosniff;
# security headers
add_header X-XSS-Protection "1; mode=block";
# add_header Content-Security-Policy "default-src 'self'";
add_header Referrer-Policy "no-referrer";
server_tokens off;
root /usr/share/nginx/html;
gzip_static on;
expires 1y;
add_header Cache-Control public;
add_header ETag "";
try_files $uri /index.html;
}
# Get the hostname from environment here?
location ~ /api/v(1|2) {
proxy_pass http://{{ getenv "BACKEND_HOSTNAME" "shuffle-backend" }}:5001;
proxy_buffering off;
proxy_http_version 1.1;
proxy_connect_timeout 900;
proxy_send_timeout 900;
proxy_read_timeout 900;
send_timeout 900;
}
}
}

View File

@ -0,0 +1,7 @@
#!/bin/bash
# generate configs
/usr/local/bin/confd -backend="env" -confdir="/etc/confd" -onetime
# run main command
exec "$@"

View File

@ -0,0 +1,116 @@
{
"name": "shuffler",
"homepage": "https://shuffler.io",
"version": "1.3.0",
"private": true,
"dependencies": {
"@codemirror/commands": "^6.2.4",
"@emotion/is-prop-valid": "^1.1.1",
"@emotion/react": "^11.11.1",
"@emotion/styled": "^11.11.0",
"@lezer/highlight": "^1.1.3",
"@mui/icons-material": "^5.14.0",
"@mui/material": "^5.14.0",
"@mui/styles": "^5.14.0",
"@mui/x-data-grid": "^5.17.11",
"@mui/x-date-pickers": "^6.11.1",
"@uiw/codemirror-themes": "^4.21.9",
"@uiw/react-codemirror": "^4.21.9",
"@use-it/interval": "^1.0.0",
"algoliasearch": "^4.13.1",
"calculate-size": "^1.1.1",
"class-transformer": "^0.4.0",
"create-react-app": "^5.0.1",
"cytoscape": "^3.15.1",
"cytoscape-edgehandles": "^3.6.0",
"cytoscape-node-html-label": "^1.1.5",
"d3": "^7.1.1",
"dayjs": "^1.11.9",
"dotenv": "^6.1.0",
"downshift": "^3.3.5",
"github-markdown-css": "^3.0.1",
"import": "0.0.6",
"interweave": "^11.2.0",
"jss": "^10.10.0",
"jss-camel-case": "^6.1.0",
"jss-default-unit": "^8.0.2",
"jss-global": "^3.0.0",
"jss-nested": "^6.0.1",
"jss-props-sort": "^6.0.0",
"jss-vendor-prefixer": "^8.0.1",
"md5-file": "^4.0.0",
"mdbreact": "^4.21.1",
"mime": "^3.0.0",
"moment": "^2.29.1",
"mui-chips-input": "^2.1.3",
"mui-nested-menu": "^3.2.1",
"process": "^0.11.10",
"react": "^18.2.0",
"react-alert": "^7.0.3",
"react-alert-template-basic": "^1.0.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-cookie": "^4.0.1",
"react-cytoscapejs": "^2.0.0",
"react-device-detect": "^2.2.3",
"react-dom": "^18.2.0",
"react-draggable": "^3.3.2",
"react-driftjs": "^1.2.2",
"react-dropzone": "^14.2.3",
"react-ga4": "^2.0.0",
"react-instantsearch-dom": "^6.28.0",
"react-json-pretty": "^2.2.0",
"react-json-view": "^1.21.3",
"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-toastify": "^9.1.3",
"reaviz": "^14.9.4",
"remark-gfm": "^3.0.1",
"search-insights": "^2.2.1",
"shellwords": "^0.1.1",
"simplebar": "^4.2.3",
"styled-components": "^4.4.0",
"webpack": "^5.88.2",
"yaml": "^1.7.2",
"yamljs": "^0.3.0",
"zone.js": "^0.13.1"
},
"scripts": {
"start": "HTTPS=false&&PORT=3000 GENERATE_SOURCEMAP=false react-scripts --openssl-legacy-provider start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject",
"lint": "eslint 'src/**/*.{tsx,ts,js,jsx}'",
"lint_file": "eslint 'src/views/AngularWorkflow.jsx'"
},
"eslintConfig": {
"extends": "react-app",
"rules": {
"jsx-a11y/img-redundant-alt": "off",
"no-redeclare": "off",
"no-loop-func": "off"
}
},
"browserslist": [
">0.2%",
"not dead",
"not ie <= 11",
"not op_mini all"
],
"devDependencies": {
"@babel/core": "^7.15.8",
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
"babel-eslint": "^10.1.0",
"prettier": "2.4.1",
"promise-window": "^1.2.1",
"react-16": "npm:react@16.13.1",
"react-dom-16": "npm:react-dom@16.13.1",
"react-error-overlay": "6.0.9"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 217 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

View File

@ -0,0 +1,44 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg width="46px" height="46px" viewBox="0 0 46 46" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:sketch="http://www.bohemiancoding.com/sketch/ns">
<!-- Generator: Sketch 3.3.3 (12081) - http://www.bohemiancoding.com/sketch -->
<title>btn_google_light_focus_ios</title>
<desc>Created with Sketch.</desc>
<defs>
<filter x="-50%" y="-50%" width="200%" height="200%" filterUnits="objectBoundingBox" id="filter-1">
<feOffset dx="0" dy="1" in="SourceAlpha" result="shadowOffsetOuter1"></feOffset>
<feGaussianBlur stdDeviation="0.5" in="shadowOffsetOuter1" result="shadowBlurOuter1"></feGaussianBlur>
<feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.168 0" in="shadowBlurOuter1" type="matrix" result="shadowMatrixOuter1"></feColorMatrix>
<feOffset dx="0" dy="0" in="SourceAlpha" result="shadowOffsetOuter2"></feOffset>
<feGaussianBlur stdDeviation="0.5" in="shadowOffsetOuter2" result="shadowBlurOuter2"></feGaussianBlur>
<feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.084 0" in="shadowBlurOuter2" type="matrix" result="shadowMatrixOuter2"></feColorMatrix>
<feMerge>
<feMergeNode in="shadowMatrixOuter1"></feMergeNode>
<feMergeNode in="shadowMatrixOuter2"></feMergeNode>
<feMergeNode in="SourceGraphic"></feMergeNode>
</feMerge>
</filter>
<rect id="path-2" x="0" y="0" width="40" height="40" rx="2"></rect>
</defs>
<g id="Google-Button" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" sketch:type="MSPage">
<g id="9-PATCH" sketch:type="MSArtboardGroup" transform="translate(-668.000000, -160.000000)"></g>
<g id="btn_google_light_focus" sketch:type="MSArtboardGroup" transform="translate(-1.000000, -1.000000)">
<rect id="Rectangle-14" fill-opacity="0.3" fill="#4285F4" sketch:type="MSShapeGroup" x="1" y="1" width="46" height="46"></rect>
<g id="button" sketch:type="MSLayerGroup" transform="translate(4.000000, 4.000000)" filter="url(#filter-1)">
<g id="button-bg">
<use fill="#FFFFFF" fill-rule="evenodd" sketch:type="MSShapeGroup" xlink:href="#path-2"></use>
<use fill="none" xlink:href="#path-2"></use>
<use fill="none" xlink:href="#path-2"></use>
<use fill="none" xlink:href="#path-2"></use>
</g>
</g>
<g id="logo_googleg_48dp" sketch:type="MSLayerGroup" transform="translate(15.000000, 15.000000)">
<path d="M17.64,9.20454545 C17.64,8.56636364 17.5827273,7.95272727 17.4763636,7.36363636 L9,7.36363636 L9,10.845 L13.8436364,10.845 C13.635,11.97 13.0009091,12.9231818 12.0477273,13.5613636 L12.0477273,15.8195455 L14.9563636,15.8195455 C16.6581818,14.2527273 17.64,11.9454545 17.64,9.20454545 L17.64,9.20454545 Z" id="Shape" fill="#4285F4" sketch:type="MSShapeGroup"></path>
<path d="M9,18 C11.43,18 13.4672727,17.1940909 14.9563636,15.8195455 L12.0477273,13.5613636 C11.2418182,14.1013636 10.2109091,14.4204545 9,14.4204545 C6.65590909,14.4204545 4.67181818,12.8372727 3.96409091,10.71 L0.957272727,10.71 L0.957272727,13.0418182 C2.43818182,15.9831818 5.48181818,18 9,18 L9,18 Z" id="Shape" fill="#34A853" sketch:type="MSShapeGroup"></path>
<path d="M3.96409091,10.71 C3.78409091,10.17 3.68181818,9.59318182 3.68181818,9 C3.68181818,8.40681818 3.78409091,7.83 3.96409091,7.29 L3.96409091,4.95818182 L0.957272727,4.95818182 C0.347727273,6.17318182 0,7.54772727 0,9 C0,10.4522727 0.347727273,11.8268182 0.957272727,13.0418182 L3.96409091,10.71 L3.96409091,10.71 Z" id="Shape" fill="#FBBC05" sketch:type="MSShapeGroup"></path>
<path d="M9,3.57954545 C10.3213636,3.57954545 11.5077273,4.03363636 12.4404545,4.92545455 L15.0218182,2.34409091 C13.4631818,0.891818182 11.4259091,0 9,0 C5.48181818,0 2.43818182,2.01681818 0.957272727,4.95818182 L3.96409091,7.29 C4.67181818,5.16272727 6.65590909,3.57954545 9,3.57954545 L9,3.57954545 Z" id="Shape" fill="#EA4335" sketch:type="MSShapeGroup"></path>
<path d="M0,0 L18,0 L18,18 L0,18 L0,0 Z" id="Shape" sketch:type="MSShapeGroup"></path>
</g>
<g id="handles_square" sketch:type="MSLayerGroup"></g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 109 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 154 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 5.2 KiB

View File

@ -0,0 +1,5 @@
<svg width="22" height="22" viewBox="0 0 22 22" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0 0L-1.48522e-08 13.3913L4.40052 13.3913L4.40052 4.46465L22 4.46465L22 2.44001e-08L0 0Z" fill="#FF8444"/>
<path d="M17.5995 8.60864L17.5995 17.5353L-9.90052e-09 17.5353L-1.48522e-08 22L22 22L22 8.60864L17.5995 8.60864Z" fill="#FF8444"/>
<path d="M13.3915 8.60864L8.60889 8.60864L8.60889 13.3913L13.3915 13.3913L13.3915 8.60864Z" fill="#FF8444"/>
</svg>

After

Width:  |  Height:  |  Size: 459 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

View File

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 80 80">
<defs>
<style>
.cls-1 {
fill: #fff;
}
.cls-2 {
fill-rule: evenodd;
}
</style>
</defs>
<path class="cls-1" d="m23.82.13l32.97.16,23.2,23.42-.16,32.97-23.42,23.2-32.97-.16L.24,56.31l.16-32.97L23.82.13Z"/>
<g>
<path class="cls-2" d="m52.34,43.3c0-.05-.02-.1-.03-.15,0-.05.03-.09.03-.14,0-1.03-.36-1.86-.96-2.43-.59-.56-1.36-.84-2.12-.84-.59,0-1.19.17-1.7.51-.1-.8-.42-1.46-.91-1.94-.59-.56-1.36-.84-2.12-.84-.66,0-1.32.22-1.87.64-.16-.46-.41-.86-.74-1.18-.59-.56-1.36-.84-2.12-.84-.57,0-1.15.16-1.66.48v-3.12c0-1.67-1.42-2.95-3.08-2.95s-3.08,1.28-3.08,2.95v11.68s-.03.04-.05.08c-.11.17-.27.4-.46.68-.36.56-.82,1.31-1.2,2.05-.35.71-.71,1.49-.77,2.33-.06.89.23,1.76.99,2.63.65.75,1.81,2.14,2.81,3.35.5.6.96,1.16,1.3,1.57l.55.66s.04.05.06.07c.74.69,1.76,1.07,2.81,1.07h7.29c4.12,0,7.02-2.98,7.02-6.13h-.71.71v-10.2Zm-1.42,0v10.2c0,2.27-2.15,4.71-5.6,4.71h-7.29c-.7,0-1.35-.25-1.81-.66l-.52-.63c-.34-.41-.8-.97-1.3-1.57-1-1.21-2.18-2.61-2.83-3.37-.55-.63-.67-1.14-.64-1.61.03-.52.26-1.07.62-1.79.13-.27.29-.53.44-.8v2.11c0,.39.32.71.71.71s.71-.32.71-.71v-16.44c0-.81.7-1.53,1.66-1.53s1.66.72,1.66,1.53v5.91s0,0,0,0v5.38c0,.39.32.71.71.71,0,0,0,0,0,0h0s0,0,0,0c.1,0,.19-.02.27-.06.06-.02.11-.07.15-.1.02-.02.05-.03.07-.05.05-.05.08-.1.11-.16.01-.02.03-.04.04-.06.04-.09.06-.18.06-.28v-5.38c0-.67.23-1.12.52-1.4.3-.29.71-.44,1.14-.44s.84.15,1.13.44c.29.28.52.73.52,1.41v1.37s0,0,0,0v1.7s0,0,0,0v2.3c0,.39.32.71.71.71s.71-.32.71-.71v-4.01c0-.67.23-1.12.52-1.4.3-.29.71-.44,1.14-.44s.84.15,1.14.44c.29.28.52.73.52,1.41v2.26s0,0,0,0v.85s0,0,0,0v1.07c0,.39.32.71.71.71s.71-.32.71-.71v-1.93c0-.67.23-1.12.52-1.4.3-.29.71-.44,1.14-.44s.84.15,1.14.44c.29.28.52.73.52,1.41,0,.05.02.1.03.15,0,.05-.03.09-.03.14Z"/>
<path class="cls-2" d="m51.35,22.81c-2.53,0-4.6,1.88-4.95,4.31h-4.84c-.82-3.03-3.18-5.57-6.42-6.44-4.85-1.3-9.84,1.59-11.14,6.43-1.29,4.83.86,9.61,5.72,10.91.41.11.83-.14.94-.54.11-.41-.14-.83-.54-.94-4.01-1.07-5.72-4.99-4.63-9.03,1.08-4.03,5.23-6.43,9.26-5.35,4.03,1.08,6.43,5.23,5.35,9.26-.11.41.14.83.54.94.41.11.83-.14.94-.54.29-1.1.36-2.2.25-3.27h4.58c.35,2.43,2.42,4.31,4.95,4.31,2.78,0,5.03-2.25,5.03-5.03s-2.25-5.03-5.03-5.03Zm-26.02,4.66c-1.1,4.1.63,8.11,4.74,9.21.33.09.53.44.44.77-.07.25-.28.42-.52.46.24-.03.45-.21.52-.46.09-.33-.11-.68-.44-.77-4.1-1.1-5.83-5.1-4.74-9.21.76-2.82,2.99-4.86,5.65-5.5-2.66.64-4.89,2.68-5.65,5.5Zm9.76-6.65c-1.39-.37-2.8-.39-4.12-.11,1.32-.27,2.73-.26,4.12.11,0,0,0,0,.02,0,0,0-.01,0-.02,0Zm6.31,6.3h0s0,0,0-.01c0,0,0,0,0,.01Zm.02,4.67c-.07.25-.28.42-.52.46.24-.03.45-.21.52-.46.08-.28.13-.56.18-.85-.05.28-.11.56-.18.85Zm.25-3.23h0s0,.01,0,.02c0,0,0-.01,0-.02Zm9.68,2.87c-1.98,0-3.59-1.61-3.59-3.59s1.61-3.59,3.59-3.59,3.59,1.61,3.59,3.59-1.61,3.59-3.59,3.59Z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.8 KiB

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="80px" height="80px" viewBox="0 0 80 80" version="1.1">
<g id="surface1">
<path style=" stroke:none;fill-rule:nonzero;fill:rgb(100%,100%,100%);fill-opacity:1;" d="M 23.820312 0.128906 L 56.789062 0.289062 L 79.988281 23.710938 L 79.828125 56.679688 L 56.410156 79.878906 L 23.441406 79.71875 L 0.238281 56.308594 L 0.398438 23.339844 Z M 23.820312 0.128906 "/>
<path style=" stroke:none;fill-rule:evenodd;fill:rgb(0%,0%,0%);fill-opacity:1;" d="M 52.339844 43.300781 C 52.339844 43.25 52.320312 43.199219 52.308594 43.148438 C 52.308594 43.101562 52.339844 43.058594 52.339844 43.011719 C 52.339844 41.980469 51.980469 41.148438 51.378906 40.578125 C 50.789062 40.019531 50.019531 39.738281 49.261719 39.738281 C 48.671875 39.738281 48.070312 39.910156 47.558594 40.25 C 47.460938 39.449219 47.140625 38.789062 46.648438 38.308594 C 46.058594 37.75 45.289062 37.46875 44.53125 37.46875 C 43.871094 37.46875 43.210938 37.691406 42.660156 38.109375 C 42.5 37.648438 42.25 37.25 41.921875 36.929688 C 41.328125 36.371094 40.558594 36.089844 39.800781 36.089844 C 39.230469 36.089844 38.648438 36.25 38.140625 36.570312 L 38.140625 33.449219 C 38.140625 31.78125 36.71875 30.5 35.058594 30.5 C 33.398438 30.5 31.980469 31.78125 31.980469 33.449219 L 31.980469 45.128906 C 31.980469 45.128906 31.949219 45.171875 31.929688 45.210938 C 31.820312 45.378906 31.660156 45.609375 31.46875 45.890625 C 31.109375 46.449219 30.648438 47.199219 30.269531 47.941406 C 29.921875 48.648438 29.558594 49.429688 29.5 50.269531 C 29.441406 51.160156 29.730469 52.03125 30.488281 52.898438 C 31.140625 53.648438 32.300781 55.039062 33.300781 56.25 C 33.800781 56.851562 34.261719 57.410156 34.601562 57.820312 L 35.148438 58.480469 C 35.148438 58.480469 35.191406 58.53125 35.210938 58.550781 C 35.949219 59.238281 36.96875 59.621094 38.019531 59.621094 L 45.308594 59.621094 C 49.429688 59.621094 52.328125 56.640625 52.328125 53.488281 L 51.621094 53.488281 L 52.328125 53.488281 L 52.328125 43.289062 Z M 50.921875 43.300781 L 50.921875 53.5 C 50.921875 55.769531 48.769531 58.210938 45.320312 58.210938 L 38.03125 58.210938 C 37.328125 58.210938 36.679688 57.960938 36.21875 57.550781 L 35.699219 56.921875 C 35.359375 56.511719 34.898438 55.949219 34.398438 55.351562 C 33.398438 54.140625 32.21875 52.738281 31.570312 51.980469 C 31.019531 51.351562 30.898438 50.839844 30.929688 50.371094 C 30.960938 49.851562 31.191406 49.300781 31.550781 48.578125 C 31.679688 48.308594 31.839844 48.050781 31.988281 47.78125 L 31.988281 49.890625 C 31.988281 50.28125 32.308594 50.601562 32.699219 50.601562 C 33.089844 50.601562 33.410156 50.28125 33.410156 49.890625 L 33.410156 33.449219 C 33.410156 32.640625 34.109375 31.921875 35.070312 31.921875 C 36.03125 31.921875 36.730469 32.640625 36.730469 33.449219 L 36.730469 44.738281 C 36.730469 45.128906 37.050781 45.449219 37.441406 45.449219 C 37.539062 45.449219 37.628906 45.429688 37.710938 45.390625 C 37.769531 45.371094 37.820312 45.320312 37.859375 45.289062 C 37.878906 45.269531 37.910156 45.261719 37.929688 45.238281 C 37.980469 45.191406 38.011719 45.140625 38.039062 45.078125 C 38.050781 45.058594 38.070312 45.039062 38.078125 45.019531 C 38.121094 44.929688 38.140625 44.839844 38.140625 44.738281 L 38.140625 39.359375 C 38.140625 38.691406 38.371094 38.238281 38.660156 37.960938 C 38.960938 37.671875 39.371094 37.519531 39.800781 37.519531 C 40.230469 37.519531 40.640625 37.671875 40.929688 37.960938 C 41.21875 38.238281 41.449219 38.691406 41.449219 39.371094 L 41.449219 44.738281 C 41.449219 45.128906 41.769531 45.449219 42.160156 45.449219 C 42.550781 45.449219 42.871094 45.128906 42.871094 44.738281 L 42.871094 40.730469 C 42.871094 40.058594 43.101562 39.609375 43.390625 39.328125 C 43.691406 39.039062 44.101562 38.890625 44.53125 38.890625 C 44.960938 38.890625 45.371094 39.039062 45.671875 39.328125 C 45.960938 39.609375 46.191406 40.058594 46.191406 40.738281 L 46.191406 44.921875 C 46.191406 45.308594 46.511719 45.628906 46.898438 45.628906 C 47.289062 45.628906 47.609375 45.308594 47.609375 44.921875 L 47.609375 42.988281 C 47.609375 42.320312 47.839844 41.871094 48.128906 41.589844 C 48.429688 41.300781 48.839844 41.148438 49.269531 41.148438 C 49.699219 41.148438 50.109375 41.300781 50.410156 41.589844 C 50.699219 41.871094 50.929688 42.320312 50.929688 43 C 50.929688 43.050781 50.949219 43.101562 50.960938 43.148438 C 50.960938 43.199219 50.929688 43.238281 50.929688 43.289062 Z M 50.921875 43.300781 "/>
<path style=" stroke:none;fill-rule:evenodd;fill:rgb(0%,0%,0%);fill-opacity:1;" d="M 51.351562 22.808594 C 48.820312 22.808594 46.75 24.691406 46.398438 27.121094 L 41.558594 27.121094 C 40.738281 24.089844 38.378906 21.550781 35.140625 20.679688 C 30.289062 19.378906 25.300781 22.269531 24 27.109375 C 22.710938 31.941406 24.859375 36.71875 29.71875 38.019531 C 30.128906 38.128906 30.550781 37.878906 30.660156 37.480469 C 30.769531 37.070312 30.519531 36.648438 30.121094 36.539062 C 26.109375 35.46875 24.398438 31.550781 25.488281 27.511719 C 26.570312 23.480469 30.71875 21.078125 34.75 22.160156 C 38.78125 23.238281 41.179688 27.390625 40.101562 31.421875 C 39.988281 31.828125 40.238281 32.25 40.640625 32.359375 C 41.050781 32.46875 41.46875 32.21875 41.578125 31.820312 C 41.871094 30.71875 41.941406 29.621094 41.828125 28.550781 L 46.410156 28.550781 C 46.761719 30.980469 48.828125 32.859375 51.359375 32.859375 C 54.140625 32.859375 56.390625 30.609375 56.390625 27.828125 C 56.390625 25.050781 54.140625 22.800781 51.359375 22.800781 Z M 25.328125 27.46875 C 24.230469 31.570312 25.960938 35.578125 30.070312 36.679688 C 30.398438 36.769531 30.601562 37.121094 30.511719 37.449219 C 30.441406 37.699219 30.230469 37.871094 29.988281 37.910156 C 30.230469 37.878906 30.441406 37.699219 30.511719 37.449219 C 30.601562 37.121094 30.398438 36.769531 30.070312 36.679688 C 25.96875 35.578125 24.238281 31.578125 25.328125 27.46875 C 26.089844 24.648438 28.320312 22.609375 30.980469 21.96875 C 28.320312 22.609375 26.089844 24.648438 25.328125 27.46875 Z M 35.089844 20.820312 C 33.699219 20.449219 32.289062 20.429688 30.96875 20.710938 C 32.289062 20.441406 33.699219 20.449219 35.089844 20.820312 C 35.089844 20.820312 35.089844 20.820312 35.109375 20.820312 C 35.109375 20.820312 35.101562 20.820312 35.089844 20.820312 Z M 41.398438 27.121094 C 41.398438 27.121094 41.398438 27.121094 41.398438 27.109375 C 41.398438 27.109375 41.398438 27.109375 41.398438 27.121094 Z M 41.421875 31.789062 C 41.351562 32.039062 41.140625 32.210938 40.898438 32.25 C 41.140625 32.21875 41.351562 32.039062 41.421875 31.789062 C 41.5 31.511719 41.550781 31.230469 41.601562 30.941406 C 41.550781 31.21875 41.488281 31.5 41.421875 31.789062 Z M 41.671875 28.558594 C 41.671875 28.558594 41.671875 28.570312 41.671875 28.578125 C 41.671875 28.578125 41.671875 28.570312 41.671875 28.558594 Z M 51.351562 31.429688 C 49.371094 31.429688 47.761719 29.820312 47.761719 27.839844 C 47.761719 25.859375 49.371094 24.25 51.351562 24.25 C 53.328125 24.25 54.941406 25.859375 54.941406 27.839844 C 54.941406 29.820312 53.328125 31.429688 51.351562 31.429688 Z M 51.351562 31.429688 "/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 7.1 KiB

View File

@ -0,0 +1,17 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico" />
<meta
name="viewport"
content="width=device-width, initial-scale=1, shrink-to-fit=no"
/>
<meta name="theme-color" content="#000000" />
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<title>Shuffle</title>
</head>
<body>
<div id="root"></div>
</body>
</html>

View File

@ -0,0 +1,14 @@
{
"short_name": "Shuffle",
"name": "Shuffle webapp",
"icons": [
{
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
}
],
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
}

18
shuffle/frontend/run.sh Normal file
View File

@ -0,0 +1,18 @@
#!/bin/sh
docker stop shuffle-frontend
docker rm shuffle-frontend
#docker rmi ghcr.io/frikky/shuffle-frontend:nightly
echo "Running build for website"
#sudo npm run build
docker build . -t ghcr.io/frikky/shuffle-frontend:nightly
docker tag ghcr.io/frikky/shuffle-frontend:nightly ghcr.io/shuffle/shuffle-frontend:nightly
echo "Starting server"
# Rerun build locally for it to update :)
docker run -it \
-p 3001:80 \
-p 3002:443 \
-v $(pwd)/build:/usr/share/nginx/html:ro \
--rm \
ghcr.io/frikky/shuffle-frontend:nightly

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 14 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 20 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 449 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 431 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

View File

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

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 143 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 440 KiB

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,385 @@
import React, {useEffect, useState} from 'react';
import theme from '../theme.jsx';
import ReactGA from 'react-ga4';
import {Link} from 'react-router-dom';
import { removeQuery } from '../components/ScrollToTop.jsx';
import {
Search as SearchIcon,
CloudQueue as CloudQueueIcon,
Code as CodeIcon
} from '@mui/icons-material';
import algoliasearch from 'algoliasearch/lite';
import { InstantSearch, Configure, connectSearchBox, connectHits, connectHitInsights } from 'react-instantsearch-dom';
import aa from 'search-insights'
import {
Zoom,
Grid,
Paper,
TextField,
ButtonBase,
InputAdornment,
Typography,
Button,
Tooltip
} from '@mui/material';
const searchClient = algoliasearch("JNSS5CFDZZ", "db08e40265e2941b9a7d8f644b6e5240")
//const searchClient = algoliasearch("L55H18ZINA", "a19be455e7e75ee8f20a93d26b9fc6d6")
const AppGrid = props => {
const { maxRows, showName, showSuggestion, isMobile, globalUrl, parsedXs, userdata } = props
const isCloud =
window.location.host === "localhost:3002" ||
window.location.host === "shuffler.io";
const rowHandler = maxRows === undefined || maxRows === null ? 50 : maxRows
const xs = parsedXs === undefined || parsedXs === null ? isMobile ? 6 : 2 : parsedXs
//const [apps, setApps] = React.useState([]);
//const [filteredApps, setFilteredApps] = React.useState([]);
const [formMail, setFormMail] = React.useState("");
const [message, setMessage] = React.useState("");
const [formMessage, setFormMessage] = React.useState("");
const buttonStyle = {borderRadius: 30, height: 50, width: 220, margin: isMobile ? "15px auto 15px auto" : 20, fontSize: 18,}
const innerColor = "rgba(255,255,255,0.65)"
const borderRadius = 3
window.title = "Shuffle | Apps | Find and integrate any app"
const submitContact = (email, message) => {
const data = {
"firstname": "",
"lastname": "",
"title": "",
"companyname": "",
"email": email,
"phone": "",
"message": message,
}
const errorMessage = "Something went wrong. Please contact frikky@shuffler.io directly."
fetch(globalUrl+"/api/v1/contact", {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(data),
})
.then(response => response.json())
.then(response => {
if (response.success === true) {
setFormMessage(response.reason)
//toast("Thanks for submitting!")
} else {
setFormMessage(errorMessage)
}
setFormMail("")
setMessage("")
})
.catch(error => {
setFormMessage(errorMessage)
console.log(error)
});
}
const SearchBox = ({currentRefinement, refine, isSearchStalled} ) => {
var defaultSearch = ""
//useEffect(() => {
if (window !== undefined && window.location !== undefined && window.location.search !== undefined && window.location.search !== null) {
const urlSearchParams = new URLSearchParams(window.location.search)
const params = Object.fromEntries(urlSearchParams.entries())
const foundQuery = params["q"]
if (foundQuery !== null && foundQuery !== undefined) {
console.log("Got query: ", foundQuery)
refine(foundQuery)
defaultSearch = foundQuery
}
}
//}, [])
return (
<form noValidate action="" role="search">
<TextField
defaultValue={defaultSearch}
fullWidth
style={{backgroundColor: theme.palette.inputColor, borderRadius: borderRadius, margin: 10, width: "100%",}}
InputProps={{
style:{
color: "white",
fontSize: "1em",
height: 50,
},
startAdornment: (
<InputAdornment position="start">
<SearchIcon style={{marginLeft: 5}}/>
</InputAdornment>
),
}}
autoComplete='off'
type="search"
color="primary"
placeholder="Find Apps..."
id="shuffle_search_field"
onChange={(event) => {
// Remove "q" from URL
removeQuery("q")
refine(event.currentTarget.value)
}}
limit={5}
/>
{/*isSearchStalled ? 'My search is stalled' : ''*/}
</form>
)
}
var workflowDelay = -50
const Hits = ({ hits, insights }) => {
const [mouseHoverIndex, setMouseHoverIndex] = useState(-1)
var counted = 0
//console.log(hits)
//var curhits = hits
//if (hits.length > 0 && defaultApps.length === 0) {
// setDefaultApps(hits)
//}
//const [defaultApps, setDefaultApps] = React.useState([])
//console.log(hits)
//if (hits.length > 0 && hits.length !== innerHits.length) {
// setInnerHits(hits)
//}
return (
<Grid container spacing={2}>
{hits.map((data, index) => {
workflowDelay += 50
const paperStyle = {
backgroundColor: index === mouseHoverIndex ? "rgba(255,255,255,0.8)" : theme.palette.inputColor,
color: index === mouseHoverIndex ? theme.palette.inputColor : "rgba(255,255,255,0.8)",
border: `1px solid ${innerColor}`,
padding: 15,
cursor: "pointer",
position: "relative",
minHeight: 116,
}
if (counted === 12/xs*rowHandler) {
return null
}
counted += 1
var parsedname = ""
for (var key = 0; key < data.name.length; key++) {
var character = data.name.charAt(key)
if (character === character.toUpperCase()) {
//console.log(data.name[key], data.name[key+1])
if (data.name.charAt(key+1) !== undefined && data.name.charAt(key+1) === data.name.charAt(key+1).toUpperCase()) {
} else {
parsedname += " "
}
}
parsedname += character
}
parsedname = (parsedname.charAt(0).toUpperCase()+parsedname.substring(1)).replaceAll("_", " ")
const appUrl = isCloud ? `/apps/${data.objectID}?queryID=${data.__queryID}` : `https://shuffler.io/apps/${data.objectID}?queryID=${data.__queryID}`
return (
<Zoom key={index} in={true} style={{ transitionDelay: `${workflowDelay}ms` }}>
<Grid item xs={xs} key={index}>
<a href={appUrl} rel="noopener noreferrer" target="_blank" style={{textDecoration: "none", color: "#f85a3e"}}>
<Paper elevation={0} style={paperStyle} onMouseOver={() => {
setMouseHoverIndex(index)
/*
ReactGA.event({
category: "app_grid_view",
action: `search_bar_click`,
label: "",
})
*/
}} onMouseOut={() => {
setMouseHoverIndex(-1)
}} onClick={() => {
if (isCloud) {
ReactGA.event({
category: "app_grid_view",
action: `app_${parsedname}_${data.id}_click`,
label: "",
})
}
//const searchClient = algoliasearch("L55H18ZINA", "a19be455e7e75ee8f20a93d26b9fc6d6")
console.log(searchClient)
aa('init', {
appId: searchClient.appId,
apiKey: searchClient.transporter.queryParameters["x-algolia-api-key"]
})
const timestamp = new Date().getTime()
aa('sendEvents', [
{
eventType: 'click',
eventName: 'Product Clicked',
index: 'appsearch',
objectIDs: [data.objectID],
timestamp: timestamp,
queryID: data.__queryID,
positions: [data.__position],
userToken: userdata === undefined || userdata === null || userdata.id === undefined ? "unauthenticated" : userdata.id,
}
])
}}>
<ButtonBase style={{padding: 5, borderRadius: 3, minHeight: 100, minWidth: 100,}}>
<img alt={data.name} src={data.image_url} style={{width: "100%", maxWidth: 100, minWidth: 100, minHeight: 100, maxHeight: 100, display: "block", margin: "0 auto"}} />
</ButtonBase>
<div/>
{index === mouseHoverIndex || showName === true ?
parsedname
:
null
}
{data.generated ?
<Tooltip title={"Created with App editor"} style={{marginTop: "28px", width: "100%"}} aria-label={data.name}>
{data.invalid ?
<CloudQueueIcon style={{position: "absolute", top: 1, left: 3, height: 16, width: 16, color: theme.palette.primary.main }}/>
:
<CloudQueueIcon style={{position: "absolute", top: 1, left: 3, height: 16, width: 16, color: "rgba(255,255,255,0.95)",}}/>
}
</Tooltip>
:
<Tooltip title={"Created with python (custom app)"} style={{marginTop: "28px", width: "100%"}} aria-label={data.name}>
<CodeIcon style={{position: "absolute", top: 1, left: 3, height: 16, width: 16, color: "rgba(255,255,255,0.95)",}}/>
</Tooltip>
}
</Paper>
</a>
</Grid>
</Zoom>
)
})}
</Grid>
)
}
const CustomSearchBox = connectSearchBox(SearchBox)
const CustomHits = connectHits(Hits)
//const CustomHits = connectHitInsights(aa)(Hits)
const selectButtonStyle = {
minWidth: 150,
maxWidth: 150,
minHeight: 50,
}
return (
<div style={{width: "100%", textAlign: "center", position: "relative", height: "100%", display: "flex"}}>
{/*
<div style={{padding: 10, }}>
<Button
style={selectButtonStyle}
variant="outlined"
onClick={() => {
const searchField = document.createElement("shuffle_search_field")
console.log("Field: ", searchField)
if (searchField !== null & searchField !== undefined) {
console.log("Set field.")
searchField.value = "WHAT WABALABA"
searchField.setAttribute("value", "WHAT WABALABA")
}
}}
>
Cases
</Button>
</div>
*/}
<div style={{width: "100%", position: "relative", height: "100%",}}>
<InstantSearch searchClient={searchClient} indexName="appsearch">
<div style={{maxWidth: 450, margin: "auto", marginTop: 15, marginBottom: 15, }}>
<CustomSearchBox />
</div>
<CustomHits hitsPerPage={5}/>
<Configure clickAnalytics />
</InstantSearch>
{showSuggestion === true ?
<div style={{paddingTop: 0, maxWidth: isMobile ? "100%" : "60%", margin: "auto"}}>
<Typography variant="h6" style={{color: "white", marginTop: 50,}}>
Can't find what you're looking for?
</Typography>
<div style={{flex: "1", display: "flex", flexDirection: "row", textAlign: "center",}}>
<TextField
required
style={{flex: "1", marginRight: "15px", backgroundColor: theme.palette.inputColor}}
InputProps={{
style:{
color: "#ffffff",
},
}}
color="primary"
fullWidth={true}
placeholder="Email (optional)"
type="email"
id="email-handler"
autoComplete="email"
margin="normal"
variant="outlined"
onChange={e => setFormMail(e.target.value)}
/>
<TextField
required
style={{flex: "1", backgroundColor: theme.palette.inputColor}}
InputProps={{
style:{
color: "#ffffff",
},
}}
color="primary"
fullWidth={true}
placeholder="What apps do you want to see?"
type=""
id="standard-required"
margin="normal"
variant="outlined"
autoComplete="off"
onChange={e => setMessage(e.target.value)}
/>
</div>
<Button
variant="contained"
color="primary"
style={buttonStyle}
disabled={message.length === 0}
onClick={() => {
submitContact(formMail, message)
}}
>
Submit
</Button>
<Typography style={{color: "white"}} variant="body2">{formMessage}</Typography>
</div>
: null
}
<span style={{position: "absolute", display: "flex", textAlign: "right", float: "right", right: 0, bottom: isMobile?"":120, }}>
<Typography variant="body2" color="textSecondary" style={{}}>
Search by
</Typography>
<a rel="noopener noreferrer" href="https://www.algolia.com/" target="_blank" style={{textDecoration: "none", color: "white"}}>
<img src={"/images/logo-algolia-nebula-blue-full.svg"} alt="Algolia logo" style={{height: 17, marginLeft: 5, marginTop: 3,}} />
</a>
</span>
</div>
</div>
)
}
export default AppGrid;

View File

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

View File

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

View File

@ -0,0 +1,220 @@
import React, { useState, useEffect } from 'react';
import ReactGA from 'react-ga4';
import theme from '../theme.jsx';
import {Link} from 'react-router-dom';
import { Search as SearchIcon, CloudQueue as CloudQueueIcon, Code as CodeIcon } from '@mui/icons-material';
import { toast } from 'react-toastify';
//import algoliasearch from 'algoliasearch/lite';
import algoliasearch from 'algoliasearch';
import { InstantSearch, connectSearchBox, connectHits } from 'react-instantsearch-dom';
import {
Grid,
Paper,
TextField,
ButtonBase,
InputAdornment,
Typography,
Button,
Tooltip
} from '@mui/material';
import aa from 'search-insights'
const searchClient = algoliasearch("JNSS5CFDZZ", "db08e40265e2941b9a7d8f644b6e5240")
const Appsearch = props => {
const { maxRows, showName, showSuggestion, isMobile, globalUrl, parsedXs, newSelectedApp, setNewSelectedApp, defaultSearch, showSearch, ConfiguredHits, userdata, cy, isCreatorPage, actionImageList, setActionImageList, setUserSpecialzedApp } = props
const isCloud = window.location.host === "localhost:3002" || window.location.host === "shuffler.io";
const rowHandler = maxRows === undefined || maxRows === null ? 50 : maxRows
const xs = parsedXs === undefined || parsedXs === null ? 12 : parsedXs
//const theme = useTheme();
//const [apps, setApps] = React.useState([]);
//const [filteredApps, setFilteredApps] = React.useState([]);
const [formMail, setFormMail] = React.useState("");
const [message, setMessage] = React.useState("");
const [formMessage, setFormMessage] = React.useState("");
const [selectedApp, setSelectedApp] = React.useState({});
const buttonStyle = {borderRadius: 30, height: 50, width: 220, margin: isMobile ? "15px auto 15px auto" : 20, fontSize: 18,}
const innerColor = "rgba(255,255,255,0.65)"
const borderRadius = 3
window.title = "Shuffle | Apps | Find and integration any app"
// value={currentRefinement}
const SearchBox = ({currentRefinement, refine, isSearchStalled} ) => {
useEffect(() => {
//console.log("FIRST LOAD ONLY? RUN REFINEMENT: !", currentRefinement)
if (defaultSearch !== undefined && defaultSearch !== null) {
refine(defaultSearch)
}
}, [])
return (
<form noValidate action="" role="search">
<TextField
fullWidth
style={{backgroundColor: "#2F2F2F", borderRadius: borderRadius, width: "100%",}}
InputProps={{
style:{
color: "white",
fontSize: "1em",
height: 50,
},
endAdornment: (
<InputAdornment position="start">
<SearchIcon style={{marginLeft: 5}}/>
</InputAdornment>
),
}}
autoComplete='on'
type="search"
color="primary"
defaultValue={defaultSearch}
// placeholder={`Find ${defaultSearch} Apps...`}
placeholder= {defaultSearch ? `${defaultSearch}` : "Search Cases "}
id="shuffle_workflow_search_field"
onChange={(event) => {
refine(event.currentTarget.value)
}}
limit={5}
/>
{/*isSearchStalled ? 'My search is stalled' : ''*/}
</form>
)
//value={currentRefinement}
}
const Hits = ({ hits }) => {
const [mouseHoverIndex, setMouseHoverIndex] = useState(-1)
var counted = 0
return (
<Grid container spacing={0} style={{border: "1px solid rgba(255,255,255,0.2)", maxHeight: 250, minHeight: 250, overflowY: "auto", overflowX: "hidden", }}>
{hits.map((data, index) => {
const paperStyle = {
backgroundColor: index === mouseHoverIndex ? "rgba(255,255,255,0.8)" : "#2F2F2F",
color: index === mouseHoverIndex ? theme.palette.inputColor : "rgba(255,255,255,0.8)",
// border: newSelectedApp.objectID !== data.objectID ? `1px solid rgba(255,255,255,0.2)` : "2px solid #f86a3e",
textAlign: "left",
padding: 10,
cursor: "pointer",
position: "relative",
overflow: "hidden",
width: "100%",
minHeight: 37,
maxHeight: 52,
}
if (counted === 12/xs*rowHandler) {
return null
}
counted += 1
var parsedname = data.name.valueOf()
//for (var key = 0; key < data.name.length; key++) {
// var character = data.name.charAt(key)
// if (character === character.toUpperCase()) {
// //console.log(data.name[key], data.name[key+1])
// if (data.name.charAt(key+1) !== undefined && data.name.charAt(key+1) === data.name.charAt(key+1).toUpperCase()) {
// } else {
// parsedname += " "
// }
// }
// parsedname += character
//}
parsedname = (parsedname.charAt(0).toUpperCase()+parsedname.substring(1)).replaceAll("_", " ")
return (
<Paper key={index} elevation={0} style={paperStyle} onMouseOver={() => {
setMouseHoverIndex(index)
/*
ReactGA.event({
category: "app_grid_view",
action: `search_bar_click`,
label: "",
})
*/
}} onMouseOut={() => {
setMouseHoverIndex(-1)
}} onClick={() => {
if(isCreatorPage === true){
if (setNewSelectedApp !== undefined && setUserSpecialzedApp !== undefined) {
setUserSpecialzedApp(userdata.id, data)
}
}
if (setNewSelectedApp !== undefined) {
setNewSelectedApp(data)
}
if (isCloud) {
ReactGA.event({
category: "app_search",
action: `app_${parsedname}_${data.id}_personalize_click`,
label: "",
})
}
const queryID = ""
if (queryID !== undefined && queryID !== null) {
try {
aa('init', {
appId: searchClient.appId,
apiKey: searchClient.transporter.headers["x-algolia-api-key"]
})
const timestamp = new Date().getTime()
aa('sendEvents', [
{
eventType: 'conversion',
eventName: 'App Framework Activation',
index: 'appsearch',
objectIDs: [data.objectID],
timestamp: timestamp,
queryID: queryID,
userToken: userdata === undefined || userdata === null || userdata.id === undefined ? "unauthenticated" : userdata.id,
}
])
} catch (e) {
console.log("Failed algolia search update: ", e)
}
}
}}>
<div style={{display: "flex"}}>
<img alt={data.name} src={data.image_url} style={{width: "100%", maxWidth: 30, minWidth: 30, minHeight: 30, borderRadius: 40, maxHeight: 30, display: "block", }} />
<Typography variant="body1" style={{marginTop: 2, marginLeft: 10, }}>
{parsedname}
</Typography>
</div>
</Paper>
)
})}
</Grid>
)
}
const InputHits = ConfiguredHits === undefined ? Hits : ConfiguredHits
const CustomSearchBox = connectSearchBox(SearchBox)
const CustomHits = connectHits(InputHits)
return (
<div style={{width: 287, height: 295, padding: "16px 16px 267px 16px", alignItems: "center", gap: 138,}}>
<InstantSearch searchClient={searchClient} indexName="appsearch">
{/* showSearch === false ? null :
<div style={{maxWidth: 450, margin: "auto", }}>
<CustomSearchBox />
</div>
*/}
<div style={{maxWidth: 450, margin: "auto", }}>
<CustomSearchBox />
</div>
<CustomHits hitsPerPage={5}/>
</InstantSearch>
</div>
)
}
export default Appsearch;

View File

@ -0,0 +1,190 @@
import React, { useState, useEffect } from 'react';
import theme from '../theme.jsx';
import AppSearch from './Appsearch.jsx';
import {
Paper,
Typography,
Divider,
IconButton,
Badge,
CircularProgress,
Tooltip,
Button,
} from "@mui/material";
import {
Close as CloseIcon,
Delete as DeleteIcon,
} from "@mui/icons-material";
const AppSearchPopout = (props) => {
const {
cy,
paperTitle,
setPaperTitle,
newSelectedApp,
setNewSelectedApp,
selectionOpen,
setSelectionOpen,
discoveryData,
setDiscoveryData,
userdata,
} = props;
const [defaultSearch, setDefaultSearch] = React.useState(paperTitle !== undefined ? paperTitle : "")
if (selectionOpen !== true) {
return null
}
// <Paper style={{width: 275, maxHeight: 400, zIndex: 100000, padding: 25, paddingRight: 35, backgroundColor: theme.palette.surfaceColor, border: "1px solid rgba(255,255,255,0.2)", position: "absolute", top: -15, left: 50, }}>
return (
<Paper style={{minWidth: 275, width: 275, minHeight: 400, maxHeight: 400, zIndex: 100000, padding: 25, paddingRight: 35, backgroundColor: theme.palette.surfaceColor, border: "1px solid rgba(255,255,255,0.2)", position: "absolute", top: -15, left: 50, }}>
{paperTitle !== undefined && paperTitle.length > 0 ?
<span>
<Typography variant="h6" style={{textAlign: "center"}}>
{paperTitle}
</Typography>
<Divider style={{marginTop: 5, marginBottom: 5 }} />
</span>
: null}
<Tooltip
title="Close window"
placement="top"
style={{ zIndex: 10011 }}
>
<IconButton
style={{ zIndex: 12501, position: "absolute", top: 10, right: 10}}
onClick={(e) => {
//cy.elements().unselectify();
if (cy !== undefined) {
cy.elements().unselect()
}
e.preventDefault();
setSelectionOpen(false)
}}
>
<CloseIcon style={{ color: "white", height: 15, width: 15, }} />
</IconButton>
</Tooltip>
{/* {/*Causes errors in Cytoscape. Removing for now.}
<Tooltip
title="Unselect app"
placement="top"
style={{ zIndex: 10011 }}
>
<IconButton
style={{ zIndex: 12501, position: "absolute", top: 32, right: 10}}
onClick={(e) => {
e.preventDefault();
setDiscoveryData({
"id": discoveryData.id,
"label": discoveryData.label,
"name": ""
})
setNewSelectedApp({
"image_url": "",
"name": "",
"id": "",
"objectID": "remove",
})
setSelectionOpen(true)
setDefaultSearch("")
const foundelement = cy.getElementById(discoveryData.id)
if (foundelement !== undefined && foundelement !== null) {
console.log("element: ", foundelement)
foundelement.data("large_image", discoveryData.large_image)
foundelement.data("text_margin_y", "14px")
foundelement.data("margin_x", "32px")
foundelement.data("margin_y", "19x")
foundelement.data("width", "45px")
foundelement.data("height", "45px")
}
setTimeout(() => {
setDiscoveryData({})
setNewSelectedApp({})
}, 1000)
}}
>
<DeleteIcon style={{ color: "white", height: 15, width: 15, }} />
</IconButton>
</Tooltip>
*/}
<div style={{display: "flex"}}>
{discoveryData.name !== undefined && discoveryData.name !== null && discoveryData.name.length > 0 ?
<div style={{border: "1px solid rgba(255,255,255,0.2)", borderRadius: 25, height: 40, width: 40, textAlign: "center", overflow: "hidden",}}>
<img alt={discoveryData.id} src={newSelectedApp.image_url !== undefined && newSelectedApp.image_url !== null && newSelectedApp.image_url.length > 0 ? newSelectedApp.image_url : discoveryData.large_image} style={{height: 40, width: 40, margin: "auto",}}/>
</div>
:
<img alt={discoveryData.id} src={discoveryData.large_image} style={{height: 40,}}/>
}
<Typography variant="body1" style={{marginLeft: 10, marginTop: 6}}>
{discoveryData.name !== undefined && discoveryData.name !== null && discoveryData.name.length > 0 ?
discoveryData.name
:
newSelectedApp.name !== undefined && newSelectedApp.name !== null && newSelectedApp.name.length > 0 ?
newSelectedApp.name
:
`No ${discoveryData.label} app chosen`
}
</Typography>
</div>
<div>
{discoveryData !== undefined && discoveryData.name !== undefined && discoveryData.name !== null && discoveryData.name.length > 0 ?
<span>
<Typography variant="body2" color="textSecondary" style={{marginTop: 10, marginBottom: 10, maxHeight: 75, overflowY: "auto", overflowX: "hidden", }}>
{discoveryData.description}
</Typography>
{/*isCloud && defaultSearch !== undefined && defaultSearch.length > 0 ?
{<
newSelectedApp={newSelectedApp}
setNewSelectedApp={setNewSelectedApp}
defaultSearch={defaultSearch}
/>}
:
null
*/}
</span>
:
selectionOpen
?
<span>
<Typography variant="body2" color="textSecondary" style={{marginTop: 10}}>
Click an app below to select it
</Typography>
</span>
:
<Button
variant="contained"
color="primary"
style={{marginTop: 10, }}
onClick={() => {
setSelectionOpen(true)
setDefaultSearch(discoveryData.label)
}}
>
Choose {discoveryData.label} app
</Button>
}
</div>
<div style={{marginTop: 10}}>
{selectionOpen ?
<AppSearch
defaultSearch={defaultSearch}
newSelectedApp={newSelectedApp}
setNewSelectedApp={setNewSelectedApp}
userdata={userdata}
/>
: null}
</div>
</Paper>
)
}
export default AppSearchPopout;

View File

@ -0,0 +1,278 @@
import React, { useState, useEffect } from "react";
import theme from '../theme.jsx';
import { toast } from 'react-toastify';
import {
Tooltip,
IconButton,
ListItem,
ListItemText,
FormGroup,
FormControl,
InputLabel,
FormLabel,
FormControlLabel,
Select,
MenuItem,
Grid,
Paper,
Typography,
Zoom,
} from "@mui/material";
import {
Edit as EditIcon,
Delete as DeleteIcon,
SelectAll as SelectAllIcon,
} from "@mui/icons-material";
const AuthenticationItem = (props) => {
const { data, index, globalUrl, getAppAuthentication } = props
const [selectedAuthentication, setSelectedAuthentication] = React.useState({})
const [selectedAuthenticationModalOpen, setSelectedAuthenticationModalOpen] = React.useState(false);
const [authenticationFields, setAuthenticationFields] = React.useState([]);
//const alert = useAlert();
var bgColor = "#27292d";
if (index % 2 === 0) {
bgColor = "#1f2023";
}
//console.log("Auth data: ", data)
if (data.type === "oauth2") {
data.fields = [
{
key: "url",
value: "Secret. Replaced during app execution!",
},
{
key: "client_id",
value: "Secret. Replaced during app execution!",
},
{
key: "client_secret",
value: "Secret. Replaced during app execution!",
},
{
key: "scope",
value: "Secret. Replaced during app execution!",
},
];
}
const deleteAuthentication = (data) => {
toast("Deleting auth " + data.label);
// Just use this one?
const url = globalUrl + "/api/v1/apps/authentication/" + data.id;
console.log("URL: ", url);
fetch(url, {
method: "DELETE",
credentials: "include",
headers: {
"Content-Type": "application/json",
},
})
.then((response) =>
response.json().then((responseJson) => {
console.log("RESP: ", responseJson);
if (responseJson["success"] === false) {
toast("Failed deleting auth");
} else {
// Need to wait because query in ES is too fast
setTimeout(() => {
getAppAuthentication();
}, 1000);
//toast("Successfully deleted authentication!")
}
})
)
.catch((error) => {
console.log("Error in userdata: ", error);
});
}
const editAuthenticationConfig = (id) => {
const data = {
id: id,
action: "assign_everywhere",
};
const url = globalUrl + "/api/v1/apps/authentication/" + id + "/config";
fetch(url, {
mode: "cors",
method: "POST",
body: JSON.stringify(data),
credentials: "include",
crossDomain: true,
withCredentials: true,
headers: {
"Content-Type": "application/json; charset=utf-8",
},
})
.then((response) =>
response.json().then((responseJson) => {
if (responseJson["success"] === false) {
toast("Failed overwriting appauth in workflows");
} else {
toast("Successfully updated auth everywhere!");
//setSelectedUserModalOpen(false);
setTimeout(() => {
getAppAuthentication();
}, 1000);
}
})
)
.catch((error) => {
toast("Err: " + error.toString());
});
};
const updateAppAuthentication = (field) => {
setSelectedAuthenticationModalOpen(true);
setSelectedAuthentication(field);
//{selectedAuthentication.fields.map((data, index) => {
var newfields = [];
for (var key in field.fields) {
newfields.push({
key: field.fields[key].key,
value: "",
});
}
setAuthenticationFields(newfields);
}
return (
<ListItem key={index} style={{ backgroundColor: bgColor }}>
<ListItemText
primary=<img
alt=""
src={data.app.large_image}
style={{
maxWidth: 50,
borderRadius: theme.palette.borderRadius,
}}
/>
style={{ minWidth: 75, maxWidth: 75 }}
/>
<ListItemText
primary={data.label}
style={{
minWidth: 225,
maxWidth: 225,
overflow: "hidden",
}}
/>
<ListItemText
primary={data.app.name}
style={{ minWidth: 175, maxWidth: 175, marginLeft: 10 }}
/>
{/*
<ListItemText
primary={data.defined === false ? "No" : "Yes"}
style={{ minWidth: 100, maxWidth: 100, }}
/>
*/}
<ListItemText
primary={
data.workflow_count === null ? 0 : data.workflow_count
}
style={{
minWidth: 100,
maxWidth: 100,
textAlign: "center",
overflow: "hidden",
}}
/>
{/*
<ListItemText
primary={data.node_count}
style={{
minWidth: 110,
maxWidth: 110,
textAlign: "center",
overflow: "hidden",
}}
/>
*/}
<ListItemText
primary={
data.fields === null || data.fields === undefined
? ""
: data.fields
.map((data) => {
return data.key;
})
.join(", ")
}
style={{
minWidth: 125,
maxWidth: 125,
overflow: "hidden",
}}
/>
<ListItemText
style={{
maxWidth: 230,
minWidth: 230,
overflow: "hidden",
}}
primary={new Date(data.created * 1000).toISOString()}
/>
<ListItemText>
<IconButton
onClick={() => {
updateAppAuthentication(data);
}}
>
<EditIcon color="primary" />
</IconButton>
{data.defined ? (
<Tooltip
color="primary"
title="Set in EVERY workflow"
placement="top"
>
<IconButton
style={{ marginRight: 10 }}
disabled={data.defined === false}
onClick={() => {
editAuthenticationConfig(data.id);
}}
>
<SelectAllIcon
color={data.defined ? "primary" : "secondary"}
/>
</IconButton>
</Tooltip>
) : (
<Tooltip
color="primary"
title="Must edit before you can set in all workflows"
placement="top"
>
<IconButton
style={{ marginRight: 10 }}
onClick={() => {}}
>
<SelectAllIcon
color={data.defined ? "primary" : "secondary"}
/>
</IconButton>
</Tooltip>
)}
<IconButton
onClick={() => {
deleteAuthentication(data);
}}
>
<DeleteIcon color="primary" />
</IconButton>
</ListItemText>
</ListItem>
)
}
export default AuthenticationItem

View File

@ -0,0 +1,366 @@
import React, { useState, useEffect } from "react";
import theme from '../theme.jsx';
import { v4 as uuidv4 } from "uuid";
import { toast } from 'react-toastify';
import {
Button,
Divider,
Select,
MenuItem,
TextField,
DialogActions,
DialogTitle,
DialogContent,
Typography,
} from "@mui/material";
import {
LockOpen as LockOpenIcon,
} from "@mui/icons-material";
const AuthenticationData = (props) => {
const {
globalUrl,
saveWorkflow,
selectedApp,
workflow,
selectedAction,
authenticationType,
getAppAuthentication,
appAuthentication,
setSelectedAction,
setAuthenticationModalOpen,
isCloud,
} = props;
const setNewAppAuth = (appAuthData) => {
console.log("DAta: ", appAuthData);
fetch(globalUrl + "/api/v1/apps/authentication", {
method: "PUT",
headers: {
"Content-Type": "application/json",
Accept: "application/json",
},
body: JSON.stringify(appAuthData),
credentials: "include",
})
.then((response) => {
if (response.status !== 200) {
console.log("Status not 200 for setting app auth :O!");
}
return response.json();
})
.then((responseJson) => {
if (!responseJson.success) {
toast("Failed to set app auth: " + responseJson.reason);
} else {
if (getAppAuthentication !== undefined) {
getAppAuthentication()
}
if (setAuthenticationModalOpen !== undefined) {
setAuthenticationModalOpen(false)
}
// Needs a refresh with the new authentication..
//toast("Successfully saved new app auth")
}
})
.catch((error) => {
//toast(error.toString());
console.log("New auth error: ", error.toString());
});
}
const [authenticationOption, setAuthenticationOptions] = React.useState({
app: JSON.parse(JSON.stringify(selectedApp)),
fields: {},
label: "",
usage: [
{
workflow_id: workflow.id,
},
],
id: uuidv4(),
active: true,
});
if (
selectedApp.authentication === undefined ||
selectedApp.authentication.parameters === null ||
selectedApp.authentication.parameters === undefined ||
selectedApp.authentication.parameters.length === 0
) {
return (
<DialogContent style={{ textAlign: "center", marginTop: 50 }}>
<Typography variant="h4" id="draggable-dialog-title" style={{cursor: "move",}}>
{selectedApp.name} does not require authentication
</Typography>
</DialogContent>
);
}
authenticationOption.app.actions = [];
for (var key in selectedApp.authentication.parameters) {
if (
authenticationOption.fields[
selectedApp.authentication.parameters[key].name
] === undefined
) {
authenticationOption.fields[
selectedApp.authentication.parameters[key].name
] = "";
}
}
const handleSubmitCheck = () => {
console.log("NEW AUTH: ", authenticationOption);
if (authenticationOption.label.length === 0) {
authenticationOption.label = `Auth for ${selectedApp.name}`;
}
// Automatically mapping fields that already exist (predefined).
// Warning if fields are NOT filled
for (var key in selectedApp.authentication.parameters) {
if (
authenticationOption.fields[
selectedApp.authentication.parameters[key].name
].length === 0
) {
if (
selectedApp.authentication.parameters[key].value !== undefined &&
selectedApp.authentication.parameters[key].value !== null &&
selectedApp.authentication.parameters[key].value.length > 0
) {
authenticationOption.fields[
selectedApp.authentication.parameters[key].name
] = selectedApp.authentication.parameters[key].value;
} else {
if (
selectedApp.authentication.parameters[key].schema.type === "bool"
) {
authenticationOption.fields[
selectedApp.authentication.parameters[key].name
] = "false";
} else {
toast(
"Field " +
selectedApp.authentication.parameters[key].name +
" can't be empty"
);
return;
}
}
}
}
console.log("Action: ", selectedAction);
selectedAction.authentication_id = authenticationOption.id;
selectedAction.selectedAuthentication = authenticationOption;
if (
selectedAction.authentication === undefined ||
selectedAction.authentication === null
) {
selectedAction.authentication = [authenticationOption];
} else {
selectedAction.authentication.push(authenticationOption);
}
setSelectedAction(selectedAction);
var newAuthOption = JSON.parse(JSON.stringify(authenticationOption));
var newFields = [];
for (const key in newAuthOption.fields) {
const value = newAuthOption.fields[key];
newFields.push({
key: key,
value: value,
});
}
console.log("FIELDS: ", newFields);
newAuthOption.fields = newFields;
setNewAppAuth(newAuthOption);
//if (configureWorkflowModalOpen) {
// setSelectedAction({});
//}
//setUpdate(authenticationOption.id);
};
if (
authenticationOption.label === null ||
authenticationOption.label === undefined
) {
authenticationOption.label = selectedApp.name + " authentication";
}
return (
<div>
<DialogTitle id="draggable-dialog-title" style={{cursor: "move",}}>
<div style={{ color: "white" }}>
Authentication for {selectedApp.name}
</div>
</DialogTitle>
<DialogContent>
<a
target="_blank"
rel="noopener noreferrer"
href="https://shuffler.io/docs/apps#authentication"
style={{ textDecoration: "none", color: "#f85a3e" }}
>
What is app authentication?
</a>
<div />
These are required fields for authenticating with {selectedApp.name}
<div style={{ marginTop: 15 }} />
<b>Name - what is this used for?</b>
<TextField
style={{
backgroundColor: theme.palette.inputColor,
borderRadius: theme.palette.borderRadius,
}}
InputProps={{
style: {
color: "white",
marginLeft: "5px",
maxWidth: "95%",
height: 50,
fontSize: "1em",
},
}}
fullWidth
color="primary"
placeholder={"Auth july 2020"}
defaultValue={`Auth for ${selectedApp.name}`}
onChange={(event) => {
authenticationOption.label = event.target.value;
}}
/>
<Divider
style={{
marginTop: 15,
marginBottom: 15,
backgroundColor: "rgb(91, 96, 100)",
}}
/>
<div />
{selectedApp.authentication.parameters.map((data, index) => {
return (
<div key={index} style={{ marginTop: 10 }}>
<LockOpenIcon style={{ marginRight: 10 }} />
<b>{data.name}</b>
{data.schema !== undefined &&
data.schema !== null &&
data.schema.type === "bool" ? (
<Select
MenuProps={{
disableScrollLock: true,
}}
SelectDisplayProps={{
style: {
marginLeft: 10,
},
}}
defaultValue={"false"}
fullWidth
onChange={(e) => {
console.log("Value: ", e.target.value);
authenticationOption.fields[data.name] = e.target.value;
}}
style={{
backgroundColor: theme.palette.surfaceColor,
color: "white",
height: 50,
}}
>
<MenuItem
key={"false"}
style={{
backgroundColor: theme.palette.inputColor,
color: "white",
}}
value={"false"}
>
false
</MenuItem>
<MenuItem
key={"true"}
style={{
backgroundColor: theme.palette.inputColor,
color: "white",
}}
value={"true"}
>
true
</MenuItem>
</Select>
) : (
<TextField
style={{
backgroundColor: theme.palette.inputColor,
borderRadius: theme.palette.borderRadius,
}}
InputProps={{
style: {
color: "white",
marginLeft: "5px",
maxWidth: "95%",
height: 50,
fontSize: "1em",
},
}}
fullWidth
type={
data.example !== undefined && data.example.includes("***")
? "password"
: "text"
}
color="primary"
defaultValue={
data.value !== undefined && data.value !== null
? data.value
: ""
}
placeholder={data.example}
onChange={(event) => {
authenticationOption.fields[data.name] =
event.target.value;
}}
/>
)}
</div>
);
})}
</DialogContent>
<DialogActions>
<Button
style={{ borderRadius: "0px" }}
onClick={() => {
setAuthenticationModalOpen(false);
}}
color="primary"
>
Cancel
</Button>
<Button
style={{ borderRadius: "0px" }}
onClick={() => {
setAuthenticationOptions(authenticationOption);
handleSubmitCheck();
}}
color="primary"
>
Submit
</Button>
</DialogActions>
</div>
);
};
export default AuthenticationData

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

@ -0,0 +1,504 @@
import React, { useState, useEffect } from "react";
import theme from "../theme.jsx";
import { toast } from 'react-toastify';
import {
Tooltip,
Divider,
TextField,
Button,
Tabs,
Tab,
Grid,
List,
ListItem,
ListItemText,
IconButton,
Dialog,
DialogTitle,
DialogActions,
} from "@mui/material";
import {
Edit as EditIcon,
FileCopy as FileCopyIcon,
SelectAll as SelectAllIcon,
OpenInNew as OpenInNewIcon,
CloudDownload as CloudDownloadIcon,
Description as DescriptionIcon,
Polymer as PolymerIcon,
CheckCircle as CheckCircleIcon,
Close as CloseIcon,
Apps as AppsIcon,
Image as ImageIcon,
Delete as DeleteIcon,
Cached as CachedIcon,
AccessibilityNew as AccessibilityNewIcon,
Lock as LockIcon,
Eco as EcoIcon,
Schedule as ScheduleIcon,
Cloud as CloudIcon,
Business as BusinessIcon,
Visibility as VisibilityIcon,
VisibilityOff as VisibilityOffIcon,
} from "@mui/icons-material";
const scrollStyle1 = {
height: 100,
width: 225,
overflow: "hidden",
position: "relative",
}
const scrollStyle2 = {
position: "absolute",
top: 0,
left: 0,
bottom: "-20px",
right: "-20px",
overflow: "scroll",
}
const CacheView = (props) => {
const { globalUrl, userdata, serverside, orgId } = props;
const [orgCache, setOrgCache] = React.useState("");
const [listCache, setListCache] = React.useState([]);
const [addCache, setAddCache] = React.useState("");
const [editedCache, setEditedCache] = React.useState("");
const [modalOpen, setModalOpen] = React.useState(false);
const [key, setKey] = React.useState("");
const [value, setValue] = React.useState("");
const [cacheInput, setCacheInput] = React.useState("");
const [cacheCursor, setCacheCursor] = React.useState("");
const [dataValue, setDataValue] = React.useState({});
const [editCache, setEditCache] = React.useState(false);
const [show, setShow] = useState({});
useEffect(() => {
listOrgCache(orgId);
console.log("orgid", orgId);
}, []);
const listOrgCache = (orgId) => {
fetch(globalUrl + `/api/v1/orgs/${orgId}/list_cache`, {
method: "GET",
headers: {
"Content-Type": "application/json",
Accept: "application/json",
},
credentials: "include",
})
.then((response) => {
if (response.status !== 200) {
console.log("Status not 200 for apps :O!");
return;
}
return response.json();
})
.then((responseJson) => {
if (responseJson.success === true) {
setListCache(responseJson.keys);
}
if (responseJson.cursor !== undefined && responseJson.cursor !== null && responseJson.cursor !== "") {
setCacheCursor(responseJson.cursor);
}
})
.catch((error) => {
toast(error.toString());
});
};
// const getCacheList = (orgId) => {
// fetch(`${globalUrl}/api/v1/orgs/${orgId}/get_cache`, {
// method: "GET",
// headers: {
// "Content-Type": "application/json",
// Accept: "application/json",
// },
// credentials: "include",
// })
// .then((response) => {
// if (response.status !== 200) {
// console.log("Status not 200 for WORKFLOW EXECUTION :O!");
// }
// return response.json();
// })
// .then((responseJson) => {
// if (responseJson.success !== false) {
// console.log("Found cache: ", responseJson)
// setListCache(responseJson)
// } else {
// console.log("Couldn't find the creator profile (rerun?): ", responseJson)
// // If the current user is any of the Shuffle Creators
// // AND the workflow doesn't have an owner: allow editing.
// // else: Allow suggestions?
// //console.log("User: ", userdata)
// //if (rerun !== true) {
// // getUserProfile(userdata.id, true)
// //}
// }
// })
// .catch((error) => {
// console.log("Get userprofile error: ", error);
// })
// }
const deleteCache = (orgId, key) => {
toast("Attempting to delete Cache");
fetch(globalUrl + `/api/v1/orgs/${orgId}/cache/${key}`, {
method: "DELETE",
headers: {
Accept: "application/json",
},
credentials: "include",
})
.then((response) => {
if (response.status === 200) {
toast("Successfully deleted Cache");
setTimeout(() => {
listOrgCache(orgId);
}, 1000);
} else {
toast("Failed deleting Cache. Does it still exist?");
}
})
.catch((error) => {
toast(error.toString());
});
};
const editOrgCache = (orgId) => {
const cache = { key: dataValue.key , value: value };
setCacheInput([cache]);
console.log("cache:", cache)
console.log("cache input: ", cacheInput)
fetch(globalUrl + `/api/v1/orgs/${orgId}/set_cache`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Accept: "application/json",
},
credentials: "include",
body: JSON.stringify(cache),
})
.then((response) => {
if (response.status !== 200) {
console.log("Status not 200 for Cache :O!");
return;
}
return response.json();
})
.then((responseJson) => {
setAddCache(responseJson);
toast("Cache Edited Successfully!");
listOrgCache(orgId);
setModalOpen(false);
})
.catch((error) => {
toast(error.toString());
});
};
const addOrgCache = (orgId) => {
const cache = { key: key, value: value };
setCacheInput([cache]);
console.log("cache input:", cacheInput)
fetch(globalUrl + `/api/v1/orgs/${orgId}/set_cache`, {
method: "POST",
body: JSON.stringify(cache),
headers: {
"Content-Type": "application/json",
Accept: "application/json",
},
credentials: "include",
})
.then((response) => {
if (response.status !== 200) {
console.log("Status not 200 for apps :O!");
return;
}
return response.json();
})
.then((responseJson) => {
setAddCache(responseJson);
toast("New Cache Added Successfully!");
listOrgCache(orgId);
setModalOpen(false);
})
.catch((error) => {
toast(error.toString());
});
};
const modalView = (
// console.log("key:", dataValue.key),
//console.log("value:",dataValue.value),
<Dialog
open={modalOpen}
onClose={() => {
setModalOpen(false);
}}
PaperProps={{
style: {
backgroundColor: theme.palette.surfaceColor,
color: "white",
minWidth: "800px",
minHeight: "320px",
},
}}
>
<DialogTitle>
<span style={{ color: "white" }}>
{ editCache ? "Edit Cache" : "Add Cache" }
</span>
</DialogTitle>
<div style={{ paddingLeft: "30px", paddingRight: '30px' }}>
Key
<TextField
color="primary"
style={{ backgroundColor: theme.palette.inputColor }}
autoFocus
InputProps={{
style: {
height: "50px",
color: "white",
fontSize: "1em",
},
}}
required
fullWidth={true}
autoComplete="Key"
placeholder="abc"
id="keyfield"
margin="normal"
variant="outlined"
value={editCache ? dataValue.key : key}
onChange={(e) => setKey(e.target.value)}
/>
</div>
<div style={{ paddingLeft: "30px", paddingRight: '30px' }}>
Value
<TextField
color="primary"
style={{ backgroundColor: theme.palette.inputColor }}
InputProps={{
style: {
height: "50px",
color: "white",
fontSize: "1em",
},
}}
required
fullWidth={true}
autoComplete="Value"
placeholder="123"
id="Valuefield"
margin="normal"
variant="outlined"
defaultValue={editCache ? dataValue.value : ""}
onChange={(e) => setValue(e.target.value)}
/>
</div>
<DialogActions style={{ paddingLeft: "30px", paddingRight: '30px' }}>
<Button
style={{ borderRadius: "0px" }}
onClick={() => setModalOpen(false)}
color="primary"
>
Cancel
</Button>
<Button
variant="contained"
style={{ borderRadius: "0px" }}
onClick={() => {
{editCache ? editOrgCache(orgId) : addOrgCache(orgId)}
}}
color="primary"
>
{editCache ? "Edit":"Submit"}
</Button>
</DialogActions>
</Dialog>
);
return (
<div>
{modalView}
<div style={{ marginTop: 20, marginBottom: 20 }}>
<h2 style={{ display: "inline" }}>Shuffle Datastore</h2>
<span style={{ marginLeft: 25 }}>
Datastore is a key-value store for storing data that can be used cross-workflow.&nbsp;
<a
target="_blank"
rel="noopener noreferrer"
href="/docs/organizations#datastore"
style={{ textDecoration: "none", color: "#f85a3e" }}
>
Learn more
</a>
</span>
</div>
<Button
style={{}}
variant="contained"
color="primary"
onClick={() =>{
setEditCache(false)
setModalOpen(true)
}
}
>
Add Cache
</Button>
<Button
style={{ marginLeft: 5, marginRight: 15 }}
variant="contained"
color="primary"
onClick={() => listOrgCache(orgId)}
>
<CachedIcon />
</Button>
<Divider
style={{
marginTop: 20,
marginBottom: 20,
}}
/>
<List>
<ListItem>
<ListItemText
primary="Key"
// style={{ minWidth: 150, maxWidth: 150 }}
/>
<ListItemText
primary="value"
// style={{ minWidth: 150, maxWidth: 150 }}
/>
<ListItemText
primary="Updated"
// style={{ minWidth: 150, maxWidth: 150 }}
/>
<ListItemText
primary="Actions"
// style={{ minWidth: 150, maxWidth: 150 }}
/>
</ListItem>
{listCache === undefined || listCache === null
? null
: listCache.map((data, index) => {
var bgColor = "#27292d";
if (index % 2 === 0) {
bgColor = "#1f2023";
}
return (
<ListItem key={index} style={{ backgroundColor: bgColor }}>
<ListItemText
style={{
maxWidth: 225,
minWidth: 225,
overflow: "hidden",
}}
primary={data.key}
/>
<div style={scrollStyle1}>
<ListItemText
// style={{
// maxWidth: 225,
// maxHeight: 150,
// // overflow: "hidden",
// paddingLeft: "52px",
// overflow: "scroll",
// }}
style={scrollStyle2}
// style={{ maxWidth: 100, minWidth: 100 }}
// onMouseOver={() =>
// setShow((prevState) => ({ ...prevState, [data.value]: true }))
// }
// onMouseLeave={() =>
// setShow((prevState) => ({ ...prevState, [data.value]: false }))
// }
//primary={show[data.value] ? data.value : `${data.value.substring(0, 5)}...`}
primary={data.value}
/>
</div>
<ListItemText
style={{
maxWidth: 225,
minWidth: 225,
overflow: "hidden",
marginLeft: "42px",
}}
primary={new Date(data.edited * 1000).toISOString()}
/>
<ListItemText
style={{
minWidth: 250,
maxWidth: 250,
overflow: "hidden",
paddingLeft: "155px",
}}
primary=<span style={{ display: "inline" }}>
<Tooltip
title="Edit"
style={{}}
aria-label={"Edit"}
>
<span>
<IconButton
style={{ padding: "6px" }}
onClick={() => {
setEditCache(true)
setDataValue({"key":data.key,"value":data.value})
setModalOpen(true)
}}
>
<EditIcon
style={{ color: "white" }}
/>
</IconButton>
</span>
</Tooltip>
<Tooltip
title={"Delete Cache"}
style={{ marginLeft: 15, }}
aria-label={"Delete"}
>
<span>
<IconButton
style={{ padding: "6px" }}
onClick={() => {
deleteCache(orgId, data.key);
//deleteFile(orgId);
}}
>
<DeleteIcon
style={{ color: "white" }}
/>
</IconButton>
</span>
</Tooltip>
</span>
/>
</ListItem>
);
})}
</List>
</div>
);
}
export default CacheView;

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,434 @@
const countries = [
{ code: 'GB', label: 'United Kingdom', phone: '44' },
{
code: 'US',
label: 'United States',
phone: '1',
suggested: true,
},
{ code: 'IN', label: 'India', phone: '91' },
{ code: 'AD', label: 'Andorra', phone: '376' },
{
code: 'AE',
label: 'United Arab Emirates',
phone: '971',
},
{ code: 'AF', label: 'Afghanistan', phone: '93' },
{
code: 'AG',
label: 'Antigua and Barbuda',
phone: '1-268',
},
{ code: 'AI', label: 'Anguilla', phone: '1-264' },
{ code: 'AL', label: 'Albania', phone: '355' },
{ code: 'AM', label: 'Armenia', phone: '374' },
{ code: 'AO', label: 'Angola', phone: '244' },
{ code: 'AQ', label: 'Antarctica', phone: '672' },
{ code: 'AR', label: 'Argentina', phone: '54' },
{ code: 'AS', label: 'American Samoa', phone: '1-684' },
{ code: 'AT', label: 'Austria', phone: '43' },
{
code: 'AU',
label: 'Australia',
phone: '61',
suggested: true,
},
{ code: 'AW', label: 'Aruba', phone: '297' },
{ code: 'AX', label: 'Alland Islands', phone: '358' },
{ code: 'AZ', label: 'Azerbaijan', phone: '994' },
{
code: 'BA',
label: 'Bosnia and Herzegovina',
phone: '387',
},
{ code: 'BB', label: 'Barbados', phone: '1-246' },
{ code: 'BD', label: 'Bangladesh', phone: '880' },
{ code: 'BE', label: 'Belgium', phone: '32' },
{ code: 'BF', label: 'Burkina Faso', phone: '226' },
{ code: 'BG', label: 'Bulgaria', phone: '359' },
{ code: 'BH', label: 'Bahrain', phone: '973' },
{ code: 'BI', label: 'Burundi', phone: '257' },
{ code: 'BJ', label: 'Benin', phone: '229' },
{ code: 'BL', label: 'Saint Barthelemy', phone: '590' },
{ code: 'BM', label: 'Bermuda', phone: '1-441' },
{ code: 'BN', label: 'Brunei Darussalam', phone: '673' },
{ code: 'BO', label: 'Bolivia', phone: '591' },
{ code: 'BR', label: 'Brazil', phone: '55' },
{ code: 'BS', label: 'Bahamas', phone: '1-242' },
{ code: 'BT', label: 'Bhutan', phone: '975' },
{ code: 'BV', label: 'Bouvet Island', phone: '47' },
{ code: 'BW', label: 'Botswana', phone: '267' },
{ code: 'BY', label: 'Belarus', phone: '375' },
{ code: 'BZ', label: 'Belize', phone: '501' },
{
code: 'CA',
label: 'Canada',
phone: '1',
suggested: true,
},
{
code: 'CC',
label: 'Cocos (Keeling) Islands',
phone: '61',
},
{
code: 'CD',
label: 'Congo, Democratic Republic of the',
phone: '243',
},
{
code: 'CF',
label: 'Central African Republic',
phone: '236',
},
{
code: 'CG',
label: 'Congo, Republic of the',
phone: '242',
},
{ code: 'CH', label: 'Switzerland', phone: '41' },
{ code: 'CI', label: "Cote d'Ivoire", phone: '225' },
{ code: 'CK', label: 'Cook Islands', phone: '682' },
{ code: 'CL', label: 'Chile', phone: '56' },
{ code: 'CM', label: 'Cameroon', phone: '237' },
{ code: 'CN', label: 'China', phone: '86' },
{ code: 'CO', label: 'Colombia', phone: '57' },
{ code: 'CR', label: 'Costa Rica', phone: '506' },
{ code: 'CU', label: 'Cuba', phone: '53' },
{ code: 'CV', label: 'Cape Verde', phone: '238' },
{ code: 'CW', label: 'Curacao', phone: '599' },
{ code: 'CX', label: 'Christmas Island', phone: '61' },
{ code: 'CY', label: 'Cyprus', phone: '357' },
{ code: 'CZ', label: 'Czech Republic', phone: '420' },
{
code: 'DE',
label: 'Germany',
phone: '49',
suggested: true,
},
{ code: 'DJ', label: 'Djibouti', phone: '253' },
{ code: 'DK', label: 'Denmark', phone: '45' },
{ code: 'DM', label: 'Dominica', phone: '1-767' },
{
code: 'DO',
label: 'Dominican Republic',
phone: '1-809',
},
{ code: 'DZ', label: 'Algeria', phone: '213' },
{ code: 'EC', label: 'Ecuador', phone: '593' },
{ code: 'EE', label: 'Estonia', phone: '372' },
{ code: 'EG', label: 'Egypt', phone: '20' },
{ code: 'EH', label: 'Western Sahara', phone: '212' },
{ code: 'ER', label: 'Eritrea', phone: '291' },
{ code: 'ES', label: 'Spain', phone: '34' },
{ code: 'ET', label: 'Ethiopia', phone: '251' },
{ code: 'FI', label: 'Finland', phone: '358' },
{ code: 'FJ', label: 'Fiji', phone: '679' },
{
code: 'FK',
label: 'Falkland Islands (Malvinas)',
phone: '500',
},
{
code: 'FM',
label: 'Micronesia, Federated States of',
phone: '691',
},
{ code: 'FO', label: 'Faroe Islands', phone: '298' },
{
code: 'FR',
label: 'France',
phone: '33',
suggested: true,
},
{ code: 'GA', label: 'Gabon', phone: '241' },
{ code: 'GB', label: 'United Kingdom', phone: '44' },
{ code: 'GD', label: 'Grenada', phone: '1-473' },
{ code: 'GE', label: 'Georgia', phone: '995' },
{ code: 'GF', label: 'French Guiana', phone: '594' },
{ code: 'GG', label: 'Guernsey', phone: '44' },
{ code: 'GH', label: 'Ghana', phone: '233' },
{ code: 'GI', label: 'Gibraltar', phone: '350' },
{ code: 'GL', label: 'Greenland', phone: '299' },
{ code: 'GM', label: 'Gambia', phone: '220' },
{ code: 'GN', label: 'Guinea', phone: '224' },
{ code: 'GP', label: 'Guadeloupe', phone: '590' },
{ code: 'GQ', label: 'Equatorial Guinea', phone: '240' },
{ code: 'GR', label: 'Greece', phone: '30' },
{
code: 'GS',
label: 'South Georgia and the South Sandwich Islands',
phone: '500',
},
{ code: 'GT', label: 'Guatemala', phone: '502' },
{ code: 'GU', label: 'Guam', phone: '1-671' },
{ code: 'GW', label: 'Guinea-Bissau', phone: '245' },
{ code: 'GY', label: 'Guyana', phone: '592' },
{ code: 'HK', label: 'Hong Kong', phone: '852' },
{
code: 'HM',
label: 'Heard Island and McDonald Islands',
phone: '672',
},
{ code: 'HN', label: 'Honduras', phone: '504' },
{ code: 'HR', label: 'Croatia', phone: '385' },
{ code: 'HT', label: 'Haiti', phone: '509' },
{ code: 'HU', label: 'Hungary', phone: '36' },
{ code: 'ID', label: 'Indonesia', phone: '62' },
{ code: 'IE', label: 'Ireland', phone: '353' },
{ code: 'IL', label: 'Israel', phone: '972' },
{ code: 'IM', label: 'Isle of Man', phone: '44' },
{ code: 'IN', label: 'India', phone: '91' },
{
code: 'IO',
label: 'British Indian Ocean Territory',
phone: '246',
},
{ code: 'IQ', label: 'Iraq', phone: '964' },
{
code: 'IR',
label: 'Iran, Islamic Republic of',
phone: '98',
},
{ code: 'IS', label: 'Iceland', phone: '354' },
{ code: 'IT', label: 'Italy', phone: '39' },
{ code: 'JE', label: 'Jersey', phone: '44' },
{ code: 'JM', label: 'Jamaica', phone: '1-876' },
{ code: 'JO', label: 'Jordan', phone: '962' },
{
code: 'JP',
label: 'Japan',
phone: '81',
suggested: true,
},
{ code: 'KE', label: 'Kenya', phone: '254' },
{ code: 'KG', label: 'Kyrgyzstan', phone: '996' },
{ code: 'KH', label: 'Cambodia', phone: '855' },
{ code: 'KI', label: 'Kiribati', phone: '686' },
{ code: 'KM', label: 'Comoros', phone: '269' },
{
code: 'KN',
label: 'Saint Kitts and Nevis',
phone: '1-869',
},
{
code: 'KP',
label: "Korea, Democratic People's Republic of",
phone: '850',
},
{ code: 'KR', label: 'Korea, Republic of', phone: '82' },
{ code: 'KW', label: 'Kuwait', phone: '965' },
{ code: 'KY', label: 'Cayman Islands', phone: '1-345' },
{ code: 'KZ', label: 'Kazakhstan', phone: '7' },
{
code: 'LA',
label: "Lao People's Democratic Republic",
phone: '856',
},
{ code: 'LB', label: 'Lebanon', phone: '961' },
{ code: 'LC', label: 'Saint Lucia', phone: '1-758' },
{ code: 'LI', label: 'Liechtenstein', phone: '423' },
{ code: 'LK', label: 'Sri Lanka', phone: '94' },
{ code: 'LR', label: 'Liberia', phone: '231' },
{ code: 'LS', label: 'Lesotho', phone: '266' },
{ code: 'LT', label: 'Lithuania', phone: '370' },
{ code: 'LU', label: 'Luxembourg', phone: '352' },
{ code: 'LV', label: 'Latvia', phone: '371' },
{ code: 'LY', label: 'Libya', phone: '218' },
{ code: 'MA', label: 'Morocco', phone: '212' },
{ code: 'MC', label: 'Monaco', phone: '377' },
{
code: 'MD',
label: 'Moldova, Republic of',
phone: '373',
},
{ code: 'ME', label: 'Montenegro', phone: '382' },
{
code: 'MF',
label: 'Saint Martin (French part)',
phone: '590',
},
{ code: 'MG', label: 'Madagascar', phone: '261' },
{ code: 'MH', label: 'Marshall Islands', phone: '692' },
{
code: 'MK',
label: 'Macedonia, the Former Yugoslav Republic of',
phone: '389',
},
{ code: 'ML', label: 'Mali', phone: '223' },
{ code: 'MM', label: 'Myanmar', phone: '95' },
{ code: 'MN', label: 'Mongolia', phone: '976' },
{ code: 'MO', label: 'Macao', phone: '853' },
{
code: 'MP',
label: 'Northern Mariana Islands',
phone: '1-670',
},
{ code: 'MQ', label: 'Martinique', phone: '596' },
{ code: 'MR', label: 'Mauritania', phone: '222' },
{ code: 'MS', label: 'Montserrat', phone: '1-664' },
{ code: 'MT', label: 'Malta', phone: '356' },
{ code: 'MU', label: 'Mauritius', phone: '230' },
{ code: 'MV', label: 'Maldives', phone: '960' },
{ code: 'MW', label: 'Malawi', phone: '265' },
{ code: 'MX', label: 'Mexico', phone: '52' },
{ code: 'MY', label: 'Malaysia', phone: '60' },
{ code: 'MZ', label: 'Mozambique', phone: '258' },
{ code: 'NA', label: 'Namibia', phone: '264' },
{ code: 'NC', label: 'New Caledonia', phone: '687' },
{ code: 'NE', label: 'Niger', phone: '227' },
{ code: 'NF', label: 'Norfolk Island', phone: '672' },
{ code: 'NG', label: 'Nigeria', phone: '234' },
{ code: 'NI', label: 'Nicaragua', phone: '505' },
{ code: 'NL', label: 'Netherlands', phone: '31' },
{ code: 'NO', label: 'Norway', phone: '47' },
{ code: 'NP', label: 'Nepal', phone: '977' },
{ code: 'NR', label: 'Nauru', phone: '674' },
{ code: 'NU', label: 'Niue', phone: '683' },
{ code: 'NZ', label: 'New Zealand', phone: '64' },
{ code: 'OM', label: 'Oman', phone: '968' },
{ code: 'PA', label: 'Panama', phone: '507' },
{ code: 'PE', label: 'Peru', phone: '51' },
{ code: 'PF', label: 'French Polynesia', phone: '689' },
{ code: 'PG', label: 'Papua New Guinea', phone: '675' },
{ code: 'PH', label: 'Philippines', phone: '63' },
{ code: 'PK', label: 'Pakistan', phone: '92' },
{ code: 'PL', label: 'Poland', phone: '48' },
{
code: 'PM',
label: 'Saint Pierre and Miquelon',
phone: '508',
},
{ code: 'PN', label: 'Pitcairn', phone: '870' },
{ code: 'PR', label: 'Puerto Rico', phone: '1' },
{
code: 'PS',
label: 'Palestine, State of',
phone: '970',
},
{ code: 'PT', label: 'Portugal', phone: '351' },
{ code: 'PW', label: 'Palau', phone: '680' },
{ code: 'PY', label: 'Paraguay', phone: '595' },
{ code: 'QA', label: 'Qatar', phone: '974' },
{ code: 'RE', label: 'Reunion', phone: '262' },
{ code: 'RO', label: 'Romania', phone: '40' },
{ code: 'RS', label: 'Serbia', phone: '381' },
{ code: 'RU', label: 'Russian Federation', phone: '7' },
{ code: 'RW', label: 'Rwanda', phone: '250' },
{ code: 'SA', label: 'Saudi Arabia', phone: '966' },
{ code: 'SB', label: 'Solomon Islands', phone: '677' },
{ code: 'SC', label: 'Seychelles', phone: '248' },
{ code: 'SD', label: 'Sudan', phone: '249' },
{ code: 'SE', label: 'Sweden', phone: '46' },
{ code: 'SG', label: 'Singapore', phone: '65' },
{ code: 'SH', label: 'Saint Helena', phone: '290' },
{ code: 'SI', label: 'Slovenia', phone: '386' },
{
code: 'SJ',
label: 'Svalbard and Jan Mayen',
phone: '47',
},
{ code: 'SK', label: 'Slovakia', phone: '421' },
{ code: 'SL', label: 'Sierra Leone', phone: '232' },
{ code: 'SM', label: 'San Marino', phone: '378' },
{ code: 'SN', label: 'Senegal', phone: '221' },
{ code: 'SO', label: 'Somalia', phone: '252' },
{ code: 'SR', label: 'Suriname', phone: '597' },
{ code: 'SS', label: 'South Sudan', phone: '211' },
{
code: 'ST',
label: 'Sao Tome and Principe',
phone: '239',
},
{ code: 'SV', label: 'El Salvador', phone: '503' },
{
code: 'SX',
label: 'Sint Maarten (Dutch part)',
phone: '1-721',
},
{
code: 'SY',
label: 'Syrian Arab Republic',
phone: '963',
},
{ code: 'SZ', label: 'Swaziland', phone: '268' },
{
code: 'TC',
label: 'Turks and Caicos Islands',
phone: '1-649',
},
{ code: 'TD', label: 'Chad', phone: '235' },
{
code: 'TF',
label: 'French Southern Territories',
phone: '262',
},
{ code: 'TG', label: 'Togo', phone: '228' },
{ code: 'TH', label: 'Thailand', phone: '66' },
{ code: 'TJ', label: 'Tajikistan', phone: '992' },
{ code: 'TK', label: 'Tokelau', phone: '690' },
{ code: 'TL', label: 'Timor-Leste', phone: '670' },
{ code: 'TM', label: 'Turkmenistan', phone: '993' },
{ code: 'TN', label: 'Tunisia', phone: '216' },
{ code: 'TO', label: 'Tonga', phone: '676' },
{ code: 'TR', label: 'Turkey', phone: '90' },
{
code: 'TT',
label: 'Trinidad and Tobago',
phone: '1-868',
},
{ code: 'TV', label: 'Tuvalu', phone: '688' },
{
code: 'TW',
label: 'Taiwan, Province of China',
phone: '886',
},
{
code: 'TZ',
label: 'United Republic of Tanzania',
phone: '255',
},
{ code: 'UA', label: 'Ukraine', phone: '380' },
{ code: 'UG', label: 'Uganda', phone: '256' },
{
code: 'US',
label: 'United States',
phone: '1',
suggested: true,
},
{ code: 'UY', label: 'Uruguay', phone: '598' },
{ code: 'UZ', label: 'Uzbekistan', phone: '998' },
{
code: 'VA',
label: 'Holy See (Vatican City State)',
phone: '379',
},
{
code: 'VC',
label: 'Saint Vincent and the Grenadines',
phone: '1-784',
},
{ code: 'VE', label: 'Venezuela', phone: '58' },
{
code: 'VG',
label: 'British Virgin Islands',
phone: '1-284',
},
{
code: 'VI',
label: 'US Virgin Islands',
phone: '1-340',
},
{ code: 'VN', label: 'Vietnam', phone: '84' },
{ code: 'VU', label: 'Vanuatu', phone: '678' },
{ code: 'WF', label: 'Wallis and Futuna', phone: '681' },
{ code: 'WS', label: 'Samoa', phone: '685' },
{ code: 'XK', label: 'Kosovo', phone: '383' },
{ code: 'YE', label: 'Yemen', phone: '967' },
{ code: 'YT', label: 'Mayotte', phone: '262' },
{ code: 'ZA', label: 'South Africa', phone: '27' },
{ code: 'ZM', label: 'Zambia', phone: '260' },
{ code: 'ZW', label: 'Zimbabwe', phone: '263' },
];
export default countries

View File

@ -0,0 +1,304 @@
import React, { useEffect, useState } from 'react';
import ReactGA from 'react-ga4';
import {Link} from 'react-router-dom';
import theme from '../theme.jsx';
import { removeQuery } from '../components/ScrollToTop.jsx';
import {
SkipNext as SkipNextIcon,
SkipPrevious as SkipPreviousIcon,
PlayArrow as PlayArrowIcon,
VerifiedUser as VerifiedUserIcon,
Search as SearchIcon, CloudQueue as CloudQueueIcon, Code as CodeIcon } from '@mui/icons-material';
import algoliasearch from 'algoliasearch/lite';
import { InstantSearch, Configure, connectSearchBox, connectHits } from 'react-instantsearch-dom';
import {
Grid,
Paper,
TextField,
ButtonBase,
InputAdornment,
Typography,
Button,
Tooltip,
Card,
Box,
CardContent,
IconButton,
Zoom,
CardMedia,
CardActionArea,
} from '@mui/material';
import {
Avatar,
AvatarGroup,
} from "@mui/material"
const searchClient = algoliasearch("JNSS5CFDZZ", "db08e40265e2941b9a7d8f644b6e5240")
const CreatorGrid = props => {
const { maxRows, showName, showSuggestion, isMobile, globalUrl, parsedXs } = props
const rowHandler = maxRows === undefined || maxRows === null ? 50 : maxRows
const xs = parsedXs === undefined || parsedXs === null ? isMobile ? 6 : 4 : parsedXs
//const [apps, setApps] = React.useState([]);
//const [filteredApps, setFilteredApps] = React.useState([]);
const [formMail, setFormMail] = React.useState("");
const [message, setMessage] = React.useState("");
const [formMessage, setFormMessage] = React.useState("");
const buttonStyle = {borderRadius: 30, height: 50, width: 220, margin: isMobile ? "15px auto 15px auto" : 20, fontSize: 18,}
const isCloud =
window.location.host === "localhost:3002" ||
window.location.host === "shuffler.io";
const innerColor = "rgba(255,255,255,0.65)"
const borderRadius = 3
window.title = "Shuffle | Workflows | Discover your use-case"
const submitContact = (email, message) => {
const data = {
"firstname": "",
"lastname": "",
"title": "",
"companyname": "",
"email": email,
"phone": "",
"message": message,
}
const errorMessage = "Something went wrong. Please contact frikky@shuffler.io directly."
fetch(globalUrl+"/api/v1/contact", {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(data),
})
.then(response => response.json())
.then(response => {
if (response.success === true) {
setFormMessage(response.reason)
//toast("Thanks for submitting!")
} else {
setFormMessage(errorMessage)
}
setFormMail("")
setMessage("")
})
.catch(error => {
setFormMessage(errorMessage)
console.log(error)
});
}
// value={currentRefinement}
const SearchBox = ({currentRefinement, refine, isSearchStalled} ) => {
var defaultSearch = ""
if (window !== undefined && window.location !== undefined && window.location.search !== undefined && window.location.search !== null) {
const urlSearchParams = new URLSearchParams(window.location.search)
const params = Object.fromEntries(urlSearchParams.entries())
const foundQuery = params["q"]
if (foundQuery !== null && foundQuery !== undefined) {
refine(foundQuery)
defaultSearch = foundQuery
}
}
return (
<form noValidate action="" role="search">
<TextField
defaultValue={defaultSearch}
fullWidth
style={{backgroundColor: theme.palette.inputColor, borderRadius: borderRadius, margin: 10, width: "100%",}}
InputProps={{
style:{
color: "white",
fontSize: "1em",
height: 50,
},
startAdornment: (
<InputAdornment position="start">
<SearchIcon style={{marginLeft: 5}}/>
</InputAdornment>
),
}}
autoComplete='off'
type="search"
color="primary"
placeholder="Find Creators..."
id="shuffle_search_field"
onChange={(event) => {
removeQuery("q")
refine(event.currentTarget.value)
}}
/>
{/*isSearchStalled ? 'My search is stalled' : ''*/}
</form>
)
}
const paperAppContainer = {
display: "flex",
flexWrap: "wrap",
alignContent: "space-between",
marginTop: 5,
}
const Hits = ({ hits }) => {
const [mouseHoverIndex, setMouseHoverIndex] = useState(-1)
var counted = 0
return (
<Grid container spacing={4} style={paperAppContainer}>
{hits.map((data, index) => {
if (counted === 12/xs*rowHandler) {
return null
}
counted += 1
const creatorUrl = !isCloud ? `https://shuffler.io/creators/${data.username}` : `/creators/${data.username}`
return (
<Zoom key={index} in={true} style={{}}>
<Grid item xs={xs} style={{ padding: "12px 10px 12px 10px", }}>
<Card style={{border: "1px solid rgba(255,255,255,0.3)", minHeight: 177, maxHeight: 177,}}>
<a href={creatorUrl} rel="noopener noreferrer" target={isCloud ? "" : "_blank"} style={{textDecoration: "none", color: "inherit",}}>
<CardActionArea style={{padding: "5px 10px 5px 10px", minHeight: 177, maxHeight: 177,}}>
<CardContent sx={{ flex: '1 0 auto', minWidth: 160, maxWidth: 160, overflow: "hidden", padding: 0, }}>
<div style={{display: "flex"}}>
<img style={{height: 74, width: 74, borderRadius: 100, }} alt={"Creator profile of "+data.username} src={data.image} />
<Typography component="div" variant="body1" style={{marginTop: 20, marginLeft: 15, }}>
@{data.username}
</Typography>
<span style={{marginTop: "auto", marginBottom: "auto", marginLeft: 10, }}>
{data.verified === true ?
<Tooltip title="Verified and earning from Shuffle contributions" placement="top">
<VerifiedUserIcon style={{}}/>
</Tooltip>
:
null
}
</span>
</div>
<Typography variant="body1" color="textSecondary" style={{marginTop: 10, }}>
<b>{data.apps === undefined || data.apps === null ? 0 : data.apps}</b> apps <span style={{marginLeft: 15, }}/><b>{data.workflows === null || data.workflows === undefined ? 0 : data.workflows}</b> workflows
</Typography>
{data.specialized_apps !== undefined && data.specialized_apps !== null && data.specialized_apps.length > 0 ?
<AvatarGroup max={10} style={{flexDirection: "row", padding: 0, margin: 0, itemAlign: "left", textAlign: "left", marginTop: 3,}}>
{data.specialized_apps.map((app, index) => {
// Putting all this in secondary of ListItemText looked weird.
return (
<div
key={index}
style={{
height: 24,
width: 24,
filter: "brightness(0.6)",
cursor: "pointer",
}}
onClick={() => {
console.log("Click")
//navigate("/apps/"+app.id)
}}
>
<Tooltip color="primary" title={app.name} placement="bottom">
<Avatar alt={app.name} src={app.image} style={{width: 24, height: 24}}/>
</Tooltip>
</div>
)
})}
</AvatarGroup>
:
null}
</CardContent>
</CardActionArea>
</a>
</Card>
</Grid>
</Zoom>
)
})}
</Grid>
)
}
const CustomSearchBox = connectSearchBox(SearchBox)
const CustomHits = connectHits(Hits)
return (
<div style={{width: "100%", position: "relative", height: "100%",}}>
<InstantSearch searchClient={searchClient} indexName="creators">
<Configure clickAnalytics />
<div style={{maxWidth: 450, margin: "auto", marginTop: 15, marginBottom: 15, }}>
<CustomSearchBox />
</div>
<CustomHits hitsPerPage={100}/>
</InstantSearch>
{showSuggestion === true ?
<div style={{maxWidth: isMobile ? "100%" : "60%", margin: "auto", paddingTop: 50, textAlign: "center",}}>
<Typography variant="h6" style={{color: "white", marginTop: 50,}}>
Can't find what you're looking for?
</Typography>
<div style={{flex: "1", display: "flex", flexDirection: "row"}}>
<TextField
required
style={{flex: "1", marginRight: "15px", backgroundColor: theme.palette.inputColor}}
InputProps={{
style:{
color: "#ffffff",
},
}}
color="primary"
fullWidth={true}
placeholder="Email (optional)"
type="email"
id="email-handler"
autoComplete="email"
margin="normal"
variant="outlined"
onChange={e => setFormMail(e.target.value)}
/>
<TextField
required
style={{flex: "1", backgroundColor: theme.palette.inputColor}}
InputProps={{
style:{
color: "#ffffff",
},
}}
color="primary"
fullWidth={true}
placeholder="What are we missing?"
type=""
id="standard-required"
margin="normal"
variant="outlined"
autoComplete="off"
onChange={e => setMessage(e.target.value)}
/>
</div>
<Button
variant="contained"
color="primary"
style={buttonStyle}
disabled={message.length === 0}
onClick={() => {
submitContact(formMail, message)
}}
>
Submit
</Button>
<Typography style={{color: "white"}} variant="body2">{formMessage}</Typography>
</div>
: null
}
</div>
)
}
export default CreatorGrid;

View File

@ -0,0 +1,357 @@
import React, {useEffect, useState} from 'react';
import theme from '../theme.jsx';
import ReactGA from 'react-ga4';
import {Link} from 'react-router-dom';
import { removeQuery } from '../components/ScrollToTop.jsx';
import { Search as SearchIcon, CloudQueue as CloudQueueIcon, Code as CodeIcon, Close as CloseIcon, Folder as FolderIcon, LibraryBooks as LibraryBooksIcon } from '@mui/icons-material';
import aa from 'search-insights'
import algoliasearch from 'algoliasearch/lite';
import { InstantSearch, Configure, connectSearchBox, connectHits } from 'react-instantsearch-dom';
import {
Zoom,
Grid,
Paper,
TextField,
Avatar,
ButtonBase,
InputAdornment,
Typography,
Button,
Tooltip,
List,
ListItem,
ListItemAvatar,
ListItemText,
} from '@mui/material';
const searchClient = algoliasearch("JNSS5CFDZZ", "db08e40265e2941b9a7d8f644b6e5240")
const DocsGrid = props => {
const { maxRows, showName, showSuggestion, isMobile, globalUrl, parsedXs, userdata, } = props
const rowHandler = maxRows === undefined || maxRows === null ? 50 : maxRows
const xs = parsedXs === undefined || parsedXs === null ? isMobile ? 6 : 2 : parsedXs
//const [apps, setApps] = React.useState([]);
//const [filteredApps, setFilteredApps] = React.useState([]);
const [formMail, setFormMail] = React.useState("");
const [message, setMessage] = React.useState("");
const [formMessage, setFormMessage] = React.useState("");
const buttonStyle = {borderRadius: 30, height: 50, width: 220, margin: isMobile ? "15px auto 15px auto" : 20, fontSize: 18,}
const innerColor = "rgba(255,255,255,0.65)"
const borderRadius = 3
window.title = "Shuffle | Apps | Find and integrate any app"
const submitContact = (email, message) => {
const data = {
"firstname": "",
"lastname": "",
"title": "",
"companyname": "",
"email": email,
"phone": "",
"message": message,
}
const errorMessage = "Something went wrong. Please contact frikky@shuffler.io directly."
fetch(globalUrl+"/api/v1/contact", {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(data),
})
.then(response => response.json())
.then(response => {
if (response.success === true) {
setFormMessage(response.reason)
//toast("Thanks for submitting!")
} else {
setFormMessage(errorMessage)
}
setFormMail("")
setMessage("")
})
.catch(error => {
setFormMessage(errorMessage)
console.log(error)
});
}
const SearchBox = ({currentRefinement, refine, isSearchStalled} ) => {
var defaultSearch = ""
if (window !== undefined && window.location !== undefined && window.location.search !== undefined && window.location.search !== null) {
const urlSearchParams = new URLSearchParams(window.location.search)
const params = Object.fromEntries(urlSearchParams.entries())
const foundQuery = params["q"]
if (foundQuery !== null && foundQuery !== undefined) {
console.log("Got query: ", foundQuery)
refine(foundQuery)
defaultSearch = foundQuery
}
}
return (
<form noValidate action="" role="search">
<TextField
defaultValue={defaultSearch}
fullWidth
style={{backgroundColor: theme.palette.inputColor, borderRadius: borderRadius, margin: 10, width: "100%",}}
InputProps={{
style:{
color: "white",
fontSize: "1em",
height: 50,
},
startAdornment: (
<InputAdornment position="start">
<SearchIcon style={{marginLeft: 5}}/>
</InputAdornment>
),
}}
autoComplete='off'
type="search"
color="primary"
placeholder="Search our Documentation..."
id="shuffle_search_field"
onChange={(event) => {
removeQuery("q")
refine(event.currentTarget.value)
}}
limit={5}
/>
{/*isSearchStalled ? 'My search is stalled' : ''*/}
</form>
)
}
var workflowDelay = -50
const Hits = ({ hits }) => {
const [mouseHoverIndex, setMouseHoverIndex] = useState(-1)
//console.log(hits)
//var curhits = hits
//if (hits.length > 0 && defaultApps.length === 0) {
// setDefaultApps(hits)
//}
//const [defaultApps, setDefaultApps] = React.useState([])
//console.log(hits)
//if (hits.length > 0 && hits.length !== innerHits.length) {
// setInnerHits(hits)
//}
var counted = 0
return (
<List>
{hits.map((data, index) => {
workflowDelay += 50
const innerlistitemStyle = {
width: "100%",
overflowX: "hidden",
overflowY: "hidden",
borderBottom: "1px solid rgba(255,255,255,0.4)",
backgroundColor: mouseHoverIndex === index ? "#1f2023" : "inherit",
cursor: "pointer",
marginLeft: 5,
marginRight: 5,
maxHeight: 75,
minHeight: 75,
maxWidth: 420,
minWidth: "100%",
}
if (counted >= 12/xs*rowHandler) {
return null
}
counted += 1
var name = data.name === undefined ?
data.filename.charAt(0).toUpperCase() + data.filename.slice(1).replaceAll("_", " ") + " - " + data.title :
(data.name.charAt(0).toUpperCase()+data.name.slice(1)).replaceAll("_", " ")
if (name.length > 96) {
name = name.slice(0, 96)+"..."
}
//const secondaryText = data.data !== undefined ? data.data.slice(0, 100)+"..." : ""
const secondaryText = data.data !== undefined ? data.data.slice(0, 100)+"..." : ""
const baseImage = <CodeIcon/>
const avatar = data.image_url === undefined ?
baseImage
:
<Avatar
src={data.image_url}
variant="rounded"
/>
var parsedUrl = data.urlpath !== undefined ? data.urlpath : ""
parsedUrl += `?queryID=${data.__queryID}`
return (
<Zoom key={index} in={true} style={{ transitionDelay: `${workflowDelay}ms` }}>
<Link key={data.objectID} to={parsedUrl} style={{textDecoration: "none", color: "white",}} onClick={(event) => {
aa('init', {
appId: searchClient.appId,
apiKey: searchClient.transporter.queryParameters["x-algolia-api-key"]
})
const timestamp = new Date().getTime()
aa('sendEvents', [
{
eventType: 'click',
eventName: 'Product Clicked Appgrid',
index: 'documentation',
objectIDs: [data.objectID],
timestamp: timestamp,
queryID: data.__queryID,
positions: [data.__position],
userToken: userdata === undefined || userdata === null || userdata.id === undefined ? "unauthenticated" : userdata.id,
}
])
console.log("CLICK")
}}>
<ListItem key={data.objectID} style={innerlistitemStyle} onMouseOver={() => {
setMouseHoverIndex(index)
}}>
<ListItemAvatar>
{avatar}
</ListItemAvatar>
<ListItemText
primary={name}
secondary={secondaryText}
/>
{/*
<ListItemSecondaryAction>
<IconButton edge="end" aria-label="delete">
<DeleteIcon />
</IconButton>
</ListItemSecondaryAction>
*/}
</ListItem>
</Link>
</Zoom>
)
})}
</List>
)
}
const CustomSearchBox = connectSearchBox(SearchBox)
const CustomHits = connectHits(Hits)
const selectButtonStyle = {
minWidth: 150,
maxWidth: 150,
minHeight: 50,
}
return (
<div style={{width: "100%", textAlign: "center", position: "relative", height: "100%", display: "flex"}}>
{/*
<div style={{padding: 10, }}>
<Button
style={selectButtonStyle}
variant="outlined"
onClick={() => {
const searchField = document.createElement("shuffle_search_field")
console.log("Field: ", searchField)
if (searchField !== null & searchField !== undefined) {
console.log("Set field.")
searchField.value = "WHAT WABALABA"
searchField.setAttribute("value", "WHAT WABALABA")
}
}}
>
Cases
</Button>
</div>
*/}
<div style={{width: "100%", position: "relative", height: "100%",}}>
<InstantSearch searchClient={searchClient} indexName="documentation">
<div style={{maxWidth: 450, margin: "auto", marginTop: 15, marginBottom: 15, }}>
<CustomSearchBox />
</div>
<Configure clickAnalytics />
<CustomHits hitsPerPage={5}/>
</InstantSearch>
{showSuggestion === true ?
<div style={{paddingTop: 0, maxWidth: isMobile ? "100%" : "60%", margin: "auto"}}>
<Typography variant="h6" style={{color: "white", marginTop: 50,}}>
Can't find what you're looking for?
</Typography>
<div style={{flex: "1", display: "flex", flexDirection: "row", textAlign: "center",}}>
<TextField
required
style={{flex: "1", marginRight: "15px", backgroundColor: theme.palette.inputColor}}
InputProps={{
style:{
color: "#ffffff",
},
}}
color="primary"
fullWidth={true}
placeholder="Email (optional)"
type="email"
id="email-handler"
autoComplete="email"
margin="normal"
variant="outlined"
onChange={e => setFormMail(e.target.value)}
/>
<TextField
required
style={{flex: "1", backgroundColor: theme.palette.inputColor}}
InputProps={{
style:{
color: "#ffffff",
},
}}
color="primary"
fullWidth={true}
placeholder="What are we missing?"
type=""
id="standard-required"
margin="normal"
variant="outlined"
autoComplete="off"
onChange={e => setMessage(e.target.value)}
/>
</div>
<Button
variant="contained"
color="primary"
style={buttonStyle}
disabled={message.length === 0}
onClick={() => {
submitContact(formMail, message)
}}
>
Submit
</Button>
<Typography style={{color: "white"}} variant="body2">{formMessage}</Typography>
</div>
: null
}
<span style={{position: "absolute", display: "flex", textAlign: "right", float: "right", right: 0, bottom: 120, }}>
<Typography variant="body2" color="textSecondary" style={{}}>
Search by
</Typography>
<a rel="noopener noreferrer" href="https://www.algolia.com/" target="_blank" style={{textDecoration: "none", color: "white"}}>
<img src={"/images/logo-algolia-nebula-blue-full.svg"} alt="Algolia logo" style={{height: 17, marginLeft: 5, marginTop: 3,}} />
</a>
</span>
</div>
</div>
)
}
export default DocsGrid;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,810 @@
import React, { useState, useEffect } from "react";
import { toast } from 'react-toastify';
import {
IconButton,
List,
ListItem,
ListItemText,
ListItemAvatar,
ListItemSecondaryAction,
Tooltip,
Button,
FormControl,
InputLabel,
TextField,
Divider,
Select,
MenuItem,
} from "@mui/material";
import {
OpenInNew as OpenInNewIcon,
Edit as EditIcon,
CloudDownload as CloudDownloadIcon,
Delete as DeleteIcon,
FileCopy as FileCopyIcon,
Cached as CachedIcon,
Publish as PublishIcon,
Clear as ClearIcon,
Add as AddIcon,
} from "@mui/icons-material";
//import { useAlert
import Dropzone from "../components/Dropzone.jsx";
import CodeEditor from "../components/ShuffleCodeEditor.jsx";
import theme from "../theme.jsx";
const Files = (props) => {
const { globalUrl, userdata, serverside, selectedOrganization, isCloud, } = props;
const [files, setFiles] = React.useState([]);
const [selectedNamespace, setSelectedNamespace] = React.useState("default");
const [openFileId, setOpenFileId] = React.useState(false);
const [fileNamespaces, setFileNamespaces] = React.useState([]);
const [fileContent, setFileContent] = React.useState("");
const [openEditor, setOpenEditor] = React.useState(false);
const [renderTextBox, setRenderTextBox] = React.useState(false);
//const alert = useAlert();
const allowedFileTypes = ["txt", "py", "yaml", "yml","json", "html", "js", "csv", "log"]
var upload = "";
const handleKeyDown = (event) => {
if (event.key === 'Enter') {
console.log('do validate')
console.log("new namespace name->",event.target.value);
fileNamespaces.push(event.target.value);
setSelectedNamespace(event.target.value);
setRenderTextBox(false);
}
if (event.key === 'Escape'){ // not working for some reasons
console.log('escape pressed')
setRenderTextBox(false);
}
}
const runUpdateText = (text) =>{
fetch(`${globalUrl}/api/v1/files/${openFileId}/edit`, {
method: "PUT",
headers: {
"Content-Type": "application/json",
Accept: "application/json",
},
body:text,
credentials: "include",
}).then((response) => {
if (response.status !== 200) {
console.log("Can't update file");
}
return response.json();
})
//console.log(text);
}
const getFiles = () => {
fetch(globalUrl + "/api/v1/files", {
method: "GET",
headers: {
"Content-Type": "application/json",
Accept: "application/json",
},
credentials: "include",
})
.then((response) => {
if (response.status !== 200) {
console.log("Status not 200 for apps :O!");
return;
}
return response.json();
})
.then((responseJson) => {
if (responseJson.files !== undefined && responseJson.files !== null) {
setFiles(responseJson.files);
} else {
setFiles([]);
}
if (responseJson.namespaces !== undefined && responseJson.namespaces !== null) {
setFileNamespaces(responseJson.namespaces);
}
})
.catch((error) => {
toast(error.toString());
});
};
useEffect(() => {
getFiles();
}, []);
const deleteFile = (file) => {
fetch(globalUrl + "/api/v1/files/" + file.id, {
method: "DELETE",
headers: {
"Content-Type": "application/json",
Accept: "application/json",
},
credentials: "include",
})
.then((response) => {
if (response.status !== 200) {
console.log("Status not 200 for file delete :O!");
}
return response.json();
})
.then((responseJson) => {
if (responseJson.success) {
toast("Successfully deleted file " + file.name);
} else if (
responseJson.reason !== undefined &&
responseJson.reason !== null
) {
toast("Failed to delete file: " + responseJson.reason);
}
setTimeout(() => {
getFiles();
}, 1500);
console.log(responseJson);
})
.catch((error) => {
toast(error.toString());
});
};
const readFileData = (file) => {
fetch(globalUrl + "/api/v1/files/" + file.id + "/content", {
method: "GET",
headers: {
"Content-Type": "application/json",
Accept: "application/json",
},
credentials: "include",
})
.then((response) => {
if (response.status !== 200) {
console.log("Status not 200 for file :O!");
return "";
}
return response.text();
})
.then((respdata) => {
// console.log("respdata ->", respdata);
// console.log("respdata type ->", typeof(respdata));
if (respdata.length === 0) {
toast("Failed getting file. Is it deleted?");
return;
}
return respdata
})
.then((responseData) => {
setFileContent(responseData);
//console.log("filecontent state ",fileContent);
})
.catch((error) => {
toast(error.toString());
});
};
const downloadFile = (file) => {
fetch(globalUrl + "/api/v1/files/" + file.id + "/content", {
method: "GET",
credentials: "include",
})
.then((response) => {
if (response.status !== 200) {
console.log("Status not 200 for apps :O!");
return "";
}
console.log("Resp: ", response)
return response.blob()
})
.then((respdata) => {
if (respdata.length === 0) {
toast("Failed getting file. Is it deleted?");
return;
}
var blob = new Blob([respdata], {
type: "application/octet-stream",
});
var url = URL.createObjectURL(blob);
var link = document.createElement("a");
link.setAttribute("href", url);
link.setAttribute("download", `${file.filename}`);
var event = document.createEvent("MouseEvents");
event.initMouseEvent(
"click",
true,
true,
window,
1,
0,
0,
0,
0,
false,
false,
false,
false,
0,
null
);
link.dispatchEvent(event);
//return response.json()
})
.then((responseJson) => {
//console.log(responseJson)
//setSchedules(responseJson)
})
.catch((error) => {
toast(error.toString());
});
};
const handleCreateFile = (filename, file) => {
var data = {
filename: filename,
org_id: selectedOrganization.id,
workflow_id: "global",
};
if (
selectedNamespace !== undefined &&
selectedNamespace !== null &&
selectedNamespace.length > 0 &&
selectedNamespace !== "default"
) {
data.namespace = selectedNamespace;
}
fetch(globalUrl + "/api/v1/files/create", {
method: "POST",
headers: {
"Content-Type": "application/json",
Accept: "application/json",
},
credentials: "include",
body: JSON.stringify(data),
})
.then((response) => {
if (response.status !== 200) {
console.log("Status not 200 for apps :O!");
return;
}
return response.json();
})
.then((responseJson) => {
//console.log("RESP: ", responseJson)
if (responseJson.success === true) {
handleFileUpload(responseJson.id, file);
} else {
toast("Failed to upload file ", filename);
}
})
.catch((error) => {
toast("Failed to upload file ", filename);
console.log(error.toString());
});
};
const handleFileUpload = (file_id, file) => {
//console.log("FILE: ", file_id, file)
fetch(`${globalUrl}/api/v1/files/${file_id}/upload`, {
method: "POST",
credentials: "include",
body: file,
})
.then((response) => {
if (response.status !== 200 && response.status !== 201) {
console.log("Status not 200 for apps :O!");
toast("File was created, but failed to upload.");
return;
}
return response.json();
})
.then((responseJson) => {
//console.log("RESPONSE: ", responseJson)
//setFiles(responseJson)
})
.catch((error) => {
toast(error.toString());
});
};
const uploadFiles = (files) => {
for (var key in files) {
try {
const filename = files[key].name;
var filedata = new FormData();
filedata.append("shuffle_file", files[key]);
if (typeof files[key] === "object") {
handleCreateFile(filename, filedata);
}
/*
reader.addEventListener('load', (e) => {
var data = e.target.result;
setIsDropzone(false)
console.log(filename)
console.log(data)
console.log(files[key])
})
reader.readAsText(files[key])
*/
} catch (e) {
console.log("Error in dropzone: ", e);
}
}
setTimeout(() => {
getFiles();
}, 2500);
};
const uploadFile = (e) => {
const isDropzone =
e.dataTransfer === undefined ? false : e.dataTransfer.files.length > 0;
const files = isDropzone ? e.dataTransfer.files : e.target.files;
//const reader = new FileReader();
//toast("Starting fileupload")
uploadFiles(files);
};
return (
<Dropzone
style={{
maxWidth: window.innerWidth > 1366 ? 1366 : 1200,
margin: "auto",
padding: 20,
}}
onDrop={uploadFile}
>
<div>
<div style={{ marginTop: 20, marginBottom: 20 }}>
<h2 style={{ display: "inline" }}>Files</h2>
<span style={{ marginLeft: 25 }}>
Files from Workflows.{" "}
<a
target="_blank"
rel="noopener noreferrer"
href="https://shuffler.io/docs/organizations#files"
style={{ textDecoration: "none", color: "#f85a3e" }}
>
Learn more
</a>
</span>
</div>
<Button
color="primary"
variant="contained"
onClick={() => {
upload.click();
}}
>
<PublishIcon /> Upload files
</Button>
{/* <FileCategoryInput
isSet={renderTextBox} /> */}
<input
hidden
type="file"
multiple
ref={(ref) => (upload = ref)}
onChange={(event) => {
//const file = event.target.value
//const fileObject = URL.createObjectURL(actualFile)
//setFile(fileObject)
//const files = event.target.files[0]
uploadFiles(event.target.files);
}}
/>
<Button
style={{ marginLeft: 5, marginRight: 15 }}
variant="contained"
color="primary"
onClick={() => getFiles()}
>
<CachedIcon />
</Button>
{fileNamespaces !== undefined &&
fileNamespaces !== null &&
fileNamespaces.length > 1 ? (
<FormControl style={{ minWidth: 150, maxWidth: 150 }}>
<InputLabel id="input-namespace-label">File Category</InputLabel>
<Select
labelId="input-namespace-select-label"
id="input-namespace-select-id"
style={{
color: "white",
minWidth: 150,
maxWidth: 150,
float: "right",
}}
value={selectedNamespace}
onChange={(event) => {
console.log("CHANGE NAMESPACE: ", event.target);
setSelectedNamespace(event.target.value);
}}
>
{fileNamespaces.map((data, index) => {
return (
<MenuItem
key={index}
value={data}
style={{ color: "white" }}
>
{data}
</MenuItem>
);
})}
</Select>
</FormControl>
) : null}
<div style={{display: "inline-flex", position:"relative"}}>
{renderTextBox ?
<Tooltip title={"Close"} style={{}} aria-label={""}>
<Button
style={{ marginLeft: 5, marginRight: 15 }}
color="primary"
onClick={() => {
setRenderTextBox(false);
console.log(" close clicked")
}}
>
<ClearIcon/>
</Button>
</Tooltip>
:
<Tooltip title={"Add new file category"} style={{}} aria-label={""}>
<Button
style={{ marginLeft: 5, marginRight: 15 }}
color="primary"
onClick={() => {
setRenderTextBox(true);
}}
>
<AddIcon/>
</Button>
</Tooltip> }
{renderTextBox && <TextField
onKeyPress={(event)=>{
handleKeyDown(event);
}}
InputProps={{
style: {
color: "white",
},
}}
color="primary"
placeholder="File category name"
required
margin="dense"
defaultValue={""}
autoFocus
/>}</div>
<CodeEditor
isCloud={isCloud}
expansionModalOpen={openEditor}
setExpansionModalOpen={setOpenEditor}
setcodedata = {setFileContent}
codedata={fileContent}
isFileEditor = {true}
key = {fileContent} //https://reactjs.org/docs/reconciliation.html#recursing-on-children
runUpdateText = {runUpdateText}
/>
<Divider
style={{
marginTop: 20,
marginBottom: 20,
backgroundColor: theme.palette.inputColor,
}}
/>
<List>
<ListItem>
<ListItemText
primary="Updated"
style={{ maxWidth: 225, minWidth: 225 }}
/>
<ListItemText
primary="Name"
style={{
maxWidth: 150,
minWidth: 150,
overflow: "hidden",
marginLeft: 10,
}}
/>
<ListItemText
primary="Workflow"
style={{ maxWidth: 100, minWidth: 100, overflow: "hidden" }}
/>
<ListItemText
primary="Md5"
style={{ minWidth: 300, maxWidth: 300, overflow: "hidden" }}
/>
<ListItemText
primary="Status"
style={{ minWidth: 75, maxWidth: 75, marginLeft: 10 }}
/>
<ListItemText
primary="Filesize"
style={{ minWidth: 125, maxWidth: 125 }}
/>
<ListItemText primary="Actions" />
</ListItem>
{files === undefined || files === null || files.length === 0 ? null :
files.map((file, index) => {
if (file.namespace === "") {
file.namespace = "default";
}
if (file.namespace !== selectedNamespace) {
return null;
}
var bgColor = "#27292d";
if (index % 2 === 0) {
bgColor = "#1f2023";
}
const filenamesplit = file.filename.split(".")
const iseditable = file.filesize < 2000000 && file.status === "active" && allowedFileTypes.includes(filenamesplit[filenamesplit.length-1])
return (
<ListItem
key={index}
style={{
backgroundColor: bgColor,
maxHeight: 100,
overflow: "hidden",
}}
>
<ListItemText
style={{
maxWidth: 225,
minWidth: 225,
overflow: "hidden",
}}
primary={new Date(file.updated_at * 1000).toISOString()}
/>
<ListItemText
style={{
maxWidth: 150,
minWidth: 150,
overflow: "hidden",
marginLeft: 10,
}}
primary={file.filename}
/>
<ListItemText
primary={
file.workflow_id === "global" ? (
<IconButton
disabled={file.workflow_id === "global"}
>
<OpenInNewIcon
style={{
color:
file.workflow_id !== "global"
? "white"
: "grey",
}}
/>
</IconButton>
) : (
<Tooltip
title={"Go to workflow"}
style={{}}
aria-label={"Download"}
>
<span>
<a
rel="noopener noreferrer"
style={{
textDecoration: "none",
color: "#f85a3e",
}}
href={`/workflows/${file.workflow_id}`}
target="_blank"
>
<IconButton
disabled={file.workflow_id === "global"}
>
<OpenInNewIcon
style={{
color:
file.workflow_id !== "global"
? "white"
: "grey",
}}
/>
</IconButton>
</a>
</span>
</Tooltip>
)
}
style={{
minWidth: 100,
maxWidth: 100,
overflow: "hidden",
}}
/>
<ListItemText
primary={file.md5_sum}
style={{
minWidth: 300,
maxWidth: 300,
overflow: "hidden",
}}
/>
<ListItemText
primary={file.status}
style={{
minWidth: 75,
maxWidth: 75,
overflow: "hidden",
marginLeft: 10,
}}
/>
<ListItemText
primary={file.filesize}
style={{
minWidth: 125,
maxWidth: 125,
overflow: "hidden",
}}
/>
<ListItemText
primary=<span style={{ display:"inline"}}>
<Tooltip
title={`Edit File (${allowedFileTypes.join(", ")}). Max size 2MB`}
style={{}}
aria-label={"Edit"}
>
<span>
<IconButton
disabled={!iseditable}
style = {{padding: "6px"}}
onClick={() => {
setOpenEditor(true)
setOpenFileId(file.id)
readFileData(file)
}}
>
<EditIcon
style={{color: iseditable ? "white" : "grey",}}
/>
</IconButton>
</span>
</Tooltip>
<Tooltip
title={"Download file"}
style={{}}
aria-label={"Download"}
>
<span>
<IconButton
style = {{padding: "6px"}}
disabled={file.status !== "active"}
onClick={() => {
downloadFile(file);
}}
>
<CloudDownloadIcon
style={{
color:
file.status === "active"
? "white"
: "grey",
}}
/>
</IconButton>
</span>
</Tooltip>
<Tooltip
title={"Copy file ID"}
style={{}}
aria-label={"copy"}
>
<IconButton
style = {{padding: "6px"}}
onClick={() => {
const elementName = "copy_element_shuffle";
var copyText =
document.getElementById(elementName);
if (
copyText !== null &&
copyText !== undefined
) {
const clipboard = navigator.clipboard;
if (clipboard === undefined) {
toast(
"Can only copy over HTTPS (port 3443)"
);
return;
}
navigator.clipboard.writeText(file.id);
copyText.select();
copyText.setSelectionRange(
0,
99999
); /* For mobile devices */
/* Copy the text inside the text field */
document.execCommand("copy");
toast(file.id + " copied to clipboard");
}
}}
>
<FileCopyIcon style={{ color: "white" }} />
</IconButton>
</Tooltip>
<Tooltip
title={"Delete file"}
style={{marginLeft: 15, }}
aria-label={"Delete"}
>
<span>
<IconButton
disabled={file.status !== "active"}
style = {{padding: "6px"}}
onClick={() => {
deleteFile(file);
}}
>
<DeleteIcon
style={{
color:
file.status === "active"
? "white"
: "grey",
}}
/>
</IconButton>
</span>
</Tooltip>
</span>
style={{
minWidth: 250,
maxWidth: 250,
// overflow: "hidden",
}}
/>
</ListItem>
);
})
}
</List>
</div>
</Dropzone>
)
}
export default Files;

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

File diff suppressed because one or more lines are too long

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

@ -0,0 +1,157 @@
import React, { useState, useEffect, useLayoutEffect } from "react";
import * as cytoscape from "cytoscape";
import CytoscapeComponent from "react-cytoscapejs";
import cystyle from "../defaultCytoscapeStyle";
const surfaceColor = "#27292D";
const CytoscapeWrapper = (props) => {
const { globalUrl, inworkflow } = props;
const [elements, setElements] = useState([]);
const [workflow, setWorkflow] = useState(inworkflow);
const [cy, setCy] = React.useState();
const bodyWidth = 200;
const bodyHeight = 150;
const setupGraph = () => {
const actions = workflow.actions.map((action) => {
const node = {};
node.position = action.position;
node.data = action;
node.data._id = action["id"];
node.data.type = "ACTION";
node.isStartNode = action["id"] === workflow.start;
var example = "";
if (
action.example !== undefined &&
action.example !== null &&
action.example.length > 0
) {
example = action.example;
}
node.data.example = example;
return node;
});
const triggers = workflow.triggers.map((trigger) => {
const node = {};
node.position = trigger.position;
node.data = trigger;
node.data._id = trigger["id"];
node.data.type = "TRIGGER";
return node;
});
// FIXME - tmp branch update
var insertedNodes = [].concat(actions, triggers);
const edges = workflow.branches.map((branch, index) => {
//workflow.branches[index].conditions = [{
const edge = {};
var conditions = workflow.branches[index].conditions;
if (conditions === undefined || conditions === null) {
conditions = [];
}
var label = "";
if (conditions.length === 1) {
label = conditions.length + " condition";
} else if (conditions.length > 1) {
label = conditions.length + " conditions";
}
edge.data = {
id: branch.id,
_id: branch.id,
source: branch.source_id,
target: branch.destination_id,
label: label,
conditions: conditions,
hasErrors: branch.has_errors,
};
// This is an attempt at prettier edges. The numbers are weird to work with.
/*
//http://manual.graphspace.org/projects/graphspace-python/en/latest/demos/edge-types.html
const sourcenode = actions.find(node => node.data._id === branch.source_id)
const destinationnode = actions.find(node => node.data._id === branch.destination_id)
if (sourcenode !== undefined && destinationnode !== undefined && branch.source_id !== branch.destination_id) {
//node.data._id = action["id"]
console.log("SOURCE: ", sourcenode.position)
console.log("DESTINATIONNODE: ", destinationnode.position)
var opposite = true
if (sourcenode.position.x > destinationnode.position.x) {
opposite = false
} else {
opposite = true
}
edge.style = {
'control-point-distance': opposite ? ["25%", "-75%"] : ["-10%", "90%"],
'control-point-weight': ['0.3', '0.7'],
}
}
*/
return edge;
});
setWorkflow(workflow);
// Verifies if a branch is valid and skips others
var newedges = [];
for (var key in edges) {
var item = edges[key];
const sourcecheck = insertedNodes.find(
(data) => data.data.id === item.data.source
);
const destcheck = insertedNodes.find(
(data) => data.data.id === item.data.target
);
if (sourcecheck === undefined || destcheck === undefined) {
continue;
}
newedges.push(item);
}
insertedNodes = insertedNodes.concat(newedges);
setElements(insertedNodes);
};
if (elements.length === 0) {
setupGraph();
}
return (
<CytoscapeComponent
elements={elements}
minZoom={0.35}
maxZoom={2.0}
style={{
width: bodyWidth - 15,
height: bodyHeight - 5,
backgroundColor: surfaceColor,
}}
stylesheet={cystyle}
boxSelectionEnabled={true}
autounselectify={false}
showGrid={true}
cy={(incy) => {
// FIXME: There's something specific loading when
// you do the first hover of a node. Why is this different?
//console.log("CY: ", incy)
setCy(incy);
}}
/>
);
};
export default CytoscapeWrapper;

View File

@ -0,0 +1,157 @@
import React, { useState, useEffect, useLayoutEffect } from "react";
import * as cytoscape from "cytoscape";
import CytoscapeComponent from "react-cytoscapejs";
import cystyle from "../defaultCytoscapeStyle.jsx";
const surfaceColor = "#27292D";
const CytoscapeWrapper = (props) => {
const { globalUrl, inworkflow } = props;
const [elements, setElements] = useState([]);
const [workflow, setWorkflow] = useState(inworkflow);
const [cy, setCy] = React.useState();
const bodyWidth = 200;
const bodyHeight = 150;
const setupGraph = () => {
const actions = workflow.actions.map((action) => {
const node = {};
node.position = action.position;
node.data = action;
node.data._id = action["id"];
node.data.type = "ACTION";
node.isStartNode = action["id"] === workflow.start;
var example = "";
if (
action.example !== undefined &&
action.example !== null &&
action.example.length > 0
) {
example = action.example;
}
node.data.example = example;
return node;
});
const triggers = workflow.triggers.map((trigger) => {
const node = {};
node.position = trigger.position;
node.data = trigger;
node.data._id = trigger["id"];
node.data.type = "TRIGGER";
return node;
});
// FIXME - tmp branch update
var insertedNodes = [].concat(actions, triggers);
const edges = workflow.branches.map((branch, index) => {
//workflow.branches[index].conditions = [{
const edge = {};
var conditions = workflow.branches[index].conditions;
if (conditions === undefined || conditions === null) {
conditions = [];
}
var label = "";
if (conditions.length === 1) {
label = conditions.length + " condition";
} else if (conditions.length > 1) {
label = conditions.length + " conditions";
}
edge.data = {
id: branch.id,
_id: branch.id,
source: branch.source_id,
target: branch.destination_id,
label: label,
conditions: conditions,
hasErrors: branch.has_errors,
};
// This is an attempt at prettier edges. The numbers are weird to work with.
/*
//http://manual.graphspace.org/projects/graphspace-python/en/latest/demos/edge-types.html
const sourcenode = actions.find(node => node.data._id === branch.source_id)
const destinationnode = actions.find(node => node.data._id === branch.destination_id)
if (sourcenode !== undefined && destinationnode !== undefined && branch.source_id !== branch.destination_id) {
//node.data._id = action["id"]
console.log("SOURCE: ", sourcenode.position)
console.log("DESTINATIONNODE: ", destinationnode.position)
var opposite = true
if (sourcenode.position.x > destinationnode.position.x) {
opposite = false
} else {
opposite = true
}
edge.style = {
'control-point-distance': opposite ? ["25%", "-75%"] : ["-10%", "90%"],
'control-point-weight': ['0.3', '0.7'],
}
}
*/
return edge;
});
setWorkflow(workflow);
// Verifies if a branch is valid and skips others
var newedges = [];
for (var key in edges) {
var item = edges[key];
const sourcecheck = insertedNodes.find(
(data) => data.data.id === item.data.source
);
const destcheck = insertedNodes.find(
(data) => data.data.id === item.data.target
);
if (sourcecheck === undefined || destcheck === undefined) {
continue;
}
newedges.push(item);
}
insertedNodes = insertedNodes.concat(newedges);
setElements(insertedNodes);
};
if (elements.length === 0) {
setupGraph();
}
return (
<CytoscapeComponent
elements={elements}
minZoom={0.35}
maxZoom={2.0}
style={{
width: bodyWidth - 15,
height: bodyHeight - 5,
backgroundColor: surfaceColor,
}}
stylesheet={cystyle}
boxSelectionEnabled={true}
autounselectify={false}
showGrid={true}
cy={(incy) => {
// FIXME: There's something specific loading when
// you do the first hover of a node. Why is this different?
//console.log("CY: ", incy)
setCy(incy);
}}
/>
);
};
export default CytoscapeWrapper;

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

File diff suppressed because it is too large Load Diff

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